diff --git a/src/antares_web_installer/app.py b/src/antares_web_installer/app.py index e9f038b..ac98895 100644 --- a/src/antares_web_installer/app.py +++ b/src/antares_web_installer/app.py @@ -10,10 +10,11 @@ from pathlib import Path from shutil import copy2, copytree +import httpx + if os.name == "nt": from pythoncom import com_error -import httpx import psutil from antares_web_installer import logger @@ -21,7 +22,7 @@ from antares_web_installer.shortcuts import create_shortcut, get_desktop # List of files and directories to exclude during installation -COMMON_EXCLUDED_FILES = {"config.prod.yaml", "config.yaml", "examples", "logs", "matrices", "tmp"} +COMMON_EXCLUDED_FILES = {"config.prod.yaml", "config.yaml", "examples", "logs", "matrices", "tmp", "*.zip"} POSIX_EXCLUDED_FILES = COMMON_EXCLUDED_FILES | {"AntaresWebWorker"} WINDOWS_EXCLUDED_FILES = COMMON_EXCLUDED_FILES | {"AntaresWebWorker.exe"} EXCLUDED_FILES = POSIX_EXCLUDED_FILES if os.name == "posix" else WINDOWS_EXCLUDED_FILES @@ -29,6 +30,8 @@ SERVER_NAMES = {"posix": "AntaresWebServer", "nt": "AntaresWebServer.exe"} SHORTCUT_NAMES = {"posix": "AntaresWebServer.desktop", "nt": "AntaresWebServer.lnk"} +SERVER_ADDRESS = "http://127.0.0.1:8080" + class InstallError(Exception): """ @@ -46,7 +49,7 @@ class App: server_path: Path = dataclasses.field(init=False) progress: float = dataclasses.field(init=False) nb_steps: int = dataclasses.field(init=False) - completed_step: int = dataclasses.field(init=False) + version: str = dataclasses.field(init=False) def __post_init__(self): # Prepare the path to the executable which is located in the target directory @@ -74,10 +77,14 @@ def run(self) -> None: self.current_step += 1 if self.launch: - self.start_server() - self.current_step += 1 - self.open_browser() - self.current_step += 1 + try: + self.start_server() + except InstallError as e: + raise e + else: + self.current_step += 1 + self.open_browser() + self.current_step += 1 def update_progress(self, progress: float): self.progress = (progress / self.nb_steps) + (self.current_step / self.nb_steps) * 100 @@ -95,6 +102,9 @@ def kill_running_server(self) -> None: # evaluate matching between query process name and existing process name try: matching_ratio = SequenceMatcher(None, "antareswebserver", proc.name().lower()).ratio() + except FileNotFoundError: + logger.warning("The process '{}' does not exist anymore. Skipping its analysis".format(proc.name())) + continue except psutil.NoSuchProcess: logger.warning("The process '{}' was stopped before being analyzed. Skipping.".format(proc.name())) continue @@ -123,15 +133,15 @@ def install_files(self): if self.target_dir.is_dir() and list(self.target_dir.iterdir()): logger.info("Existing files were found. Proceed checking old version...") # check app version - version = self.check_version() - logger.info(f"Old application version : {version}.") + old_version = self.check_version() + logger.info(f"Old application version : {old_version}.") self.update_progress(25) # update config file logger.info("Update configuration file...") src_config_path = self.source_dir.joinpath("config.yaml") target_config_path = self.target_dir.joinpath("config.yaml") - update_config(src_config_path, target_config_path, version) + update_config(src_config_path, target_config_path, old_version) logger.info("Configuration file updated.") self.update_progress(50) @@ -143,8 +153,8 @@ def install_files(self): # check new version of the application logger.info("Check new application version...") - version = self.check_version() - logger.info(f"New application version : {version}.") + self.version = self.check_version() + logger.info(f"New application version : {self.version}.") self.update_progress(100) else: @@ -152,6 +162,7 @@ def install_files(self): logger.info("No existing files found. Starting file copy...") copytree(self.source_dir, self.target_dir, dirs_exist_ok=True) logger.info("Files was successfully copied.") + self.version = self.check_version() self.update_progress(100) def copy_files(self): @@ -217,9 +228,12 @@ def create_shortcuts(self): Create a local server icon and a browser icon on desktop and """ # prepare a shortcut into the desktop directory + desktop_path = Path(get_desktop()) + logger.info("Generating server shortcut on desktop...") - shortcut_name = SHORTCUT_NAMES[os.name] - shortcut_path = Path(get_desktop()).joinpath(shortcut_name) + name, ext = SHORTCUT_NAMES[os.name].split(".") + new_shortcut_name = f"{name}-{self.version}.{ext}" + shortcut_path = desktop_path.joinpath(new_shortcut_name) # if the shortcut already exists, remove it shortcut_path.unlink(missing_ok=True) @@ -227,7 +241,7 @@ def create_shortcuts(self): # shortcut generation logger.info( - f"Shortcut will be created in {shortcut_path}, " + f"Shortcut {new_shortcut_name} will be created in {shortcut_path}, " f"linked to '{self.server_path}' " f"and located in '{self.target_dir}' directory." ) @@ -242,66 +256,66 @@ def create_shortcuts(self): except com_error as e: raise InstallError("Impossible to create a new shortcut: {}\nSkip shortcut creation".format(e)) else: - logger.info("Server shortcut was successfully created.") + assert shortcut_path in list(desktop_path.iterdir()) + logger.info(f"Server shortcut {shortcut_path} was successfully created.") self.update_progress(100) def start_server(self): """ Launch the local server as a background task """ - logger.info("Attempt to start the newly installed server...") - args = [str(self.server_path)] + logger.info(f"Attempt to start the newly installed server located in '{self.target_dir}'...") + logger.debug(f"User permissions: {os.path.exists(self.server_path) and os.access(self.server_path, os.X_OK)}") + + args = [self.server_path] server_process = subprocess.Popen( args=args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - shell=True, cwd=self.target_dir, + shell=True, ) self.update_progress(50) if not server_process.poll(): logger.info("Server is starting up ...") else: - logger.info(f"The server unexpectedly stopped running. (code {server_process.returncode})") + stdout, stderr = server_process.communicate() + msg = f"The server unexpectedly stopped running. (code {server_process.returncode})" + logger.info(msg) + logger.info(f"Server unexpectedly stopped.\nstdout: {stdout}\nstderr: {stderr}") + raise InstallError(msg) nb_attempts = 0 - max_attempts = 5 + max_attempts = 10 while nb_attempts < max_attempts: - attempt_info = f"Attempt #{nb_attempts}: " + logger.info(f"Attempt #{nb_attempts}...") try: - res = httpx.get("http://localhost:8080/", timeout=1) - except httpx.ConnectError: - logger.info(attempt_info + "The server is not accepting request yet. Retry ...") - except httpx.ConnectTimeout: - logger.info(attempt_info + "The server cannot retrieve a response yet. Retry ...") - else: - if res.status_code: - logger.info("Server is now available.") - self.update_progress(100) - logger.debug("Update progress was called.") + res = httpx.get(SERVER_ADDRESS + "/health", timeout=1) + if res.status_code == 200: + logger.info("The server is now running.") break - finally: - nb_attempts += 1 - if nb_attempts == max_attempts: - try: - server_process.wait(timeout=5) - except subprocess.TimeoutExpired as e: - raise InstallError(f"Impossible to launch Antares Web Server after {nb_attempts} attempts: {e}") - time.sleep(5) + except httpx.RequestError: + time.sleep(1) + nb_attempts += 1 + else: + stdout, stderr = server_process.communicate() + msg = "The server didn't start in time" + logger.error(msg) + logger.error(f"stdout: {stdout}\nstderr: {stderr}") + raise InstallError(msg) def open_browser(self): """ Open server URL in default user's browser """ logger.debug("In open browser method.") - url = "http://localhost:8080/" try: - webbrowser.open(url=url, new=2) + webbrowser.open(url=SERVER_ADDRESS, new=2) except webbrowser.Error as e: - raise InstallError(f"Could not open browser at '{url}': {e}") from e + raise InstallError(f"Could not open browser at '{SERVER_ADDRESS}': {e}") from e else: logger.info("Browser was successfully opened.") self.update_progress(100) diff --git a/src/antares_web_installer/gui/controller.py b/src/antares_web_installer/gui/controller.py index f038c13..8afd4a7 100644 --- a/src/antares_web_installer/gui/controller.py +++ b/src/antares_web_installer/gui/controller.py @@ -90,18 +90,36 @@ def init_log_file_handler(self): # initialize file handler logger def init_console_handler(self, callback): + """ + + @param callback: + @return: + """ console_handler = ConsoleHandler(callback) self.logger.addHandler(console_handler) def init_progress_handler(self, callback): + """ + Initialize handler log of progress. + The logs generated will be shown on the window console during installation + @param callback: function that is used to update logs + """ progress_logger = ProgressHandler(callback) self.logger.addHandler(progress_logger) def run(self) -> None: + """ + start program + @return: + """ self.view.update_view() super().run() def install(self, callback: typing.Callable): + """ + Run App.install method + @param callback: function that is used to update logs + """ self.init_log_file_handler() self.logger.debug("file logger initialized.") self.init_console_handler(callback) diff --git a/src/antares_web_installer/gui/widgets/frame.py b/src/antares_web_installer/gui/widgets/frame.py index e53e239..e7234e9 100644 --- a/src/antares_web_installer/gui/widgets/frame.py +++ b/src/antares_web_installer/gui/widgets/frame.py @@ -5,6 +5,7 @@ from antares_web_installer.shortcuts import get_homedir from .button import CancelBtn, BackBtn, NextBtn, FinishBtn, InstallBtn +from ..mvc import ControllerError if TYPE_CHECKING: from antares_web_installer.gui.view import WizardView @@ -152,8 +153,12 @@ def browse(self): title="Choose the target directory", initialdir=get_homedir(), ) - self.window.set_target_dir(dir_path) - self.target_path.set(dir_path) + try: + self.window.set_target_dir(dir_path) + except ControllerError: + pass + else: + self.target_path.set(dir_path) def get_next_frame(self): # Lazy import for typing and testing purposes diff --git a/src/antares_web_installer/shortcuts/_win32_shell.py b/src/antares_web_installer/shortcuts/_win32_shell.py index e1e7c6d..205095a 100644 --- a/src/antares_web_installer/shortcuts/_win32_shell.py +++ b/src/antares_web_installer/shortcuts/_win32_shell.py @@ -56,14 +56,6 @@ def create_shortcut( if isinstance(arguments, str): arguments = [arguments] if arguments else [] - target_parent, target_name = str(target).rsplit("\\", maxsplit=1) - new_target_name = "Antares Web Server.lnk" - new_target = os.path.join(target_parent, new_target_name) - - # remove any existing shortcut - if os.path.exists(new_target): - os.remove(new_target) - wscript = _WSHELL.CreateShortCut(str(target)) wscript.TargetPath = str(exe_path) wscript.Arguments = " ".join(arguments) @@ -74,6 +66,3 @@ def create_shortcut( if icon_path: wscript.IconLocation = str(icon_path) wscript.save() - - # add spaces to shortcut name - os.rename(target, new_target) diff --git a/tests/integration/test_app.py b/tests/integration/test_app.py index fda8f71..b678ec1 100644 --- a/tests/integration/test_app.py +++ b/tests/integration/test_app.py @@ -1,18 +1,23 @@ import os import shutil -from difflib import SequenceMatcher -from pathlib import Path +import time import psutil import pytest +import re + +from difflib import SequenceMatcher +from pathlib import Path +from typing import Any + from _pytest.monkeypatch import MonkeyPatch -from antares_web_installer.app import App +from antares_web_installer.app import App, InstallError from tests.samples import SAMPLES_DIR DOWNLOAD_FOLDER = "Download" DESKTOP_FOLDER = "Desktop" -PROGRAM_FOLDER = "Program Files" +PROGRAM_FOLDER = "ProgramFiles" @pytest.fixture(name="downloaded_dir") @@ -49,17 +54,24 @@ def program_dir_fixture(tmp_path: Path) -> Path: @pytest.fixture() def settings(desktop_dir: Path, monkeypatch: MonkeyPatch): + print("Set up data ...") # Patch the `get_desktop` function according to the current platform platform = {"nt": "_win32_shell", "posix": "_linux_shell"}[os.name] monkeypatch.setattr(f"antares_web_installer.shortcuts.{platform}.get_desktop", lambda: desktop_dir) + print("Data was set up successfully") yield # kill the running servers + print("Starting killing servers.") for proc in psutil.process_iter(): - matching_ratio = SequenceMatcher(None, "antareswebserver", proc.name().lower()).ratio() + try: + matching_ratio = SequenceMatcher(None, "antareswebserver", proc.name().lower()).ratio() + except psutil.NoSuchProcess: + continue if matching_ratio > 0.8: proc.kill() proc.wait(1) break + print("Severs were successfully killed.") class TestApp: @@ -67,7 +79,24 @@ class TestApp: Integration tests for the app """ - def test_run__empty_target(self, downloaded_dir: Path, program_dir: Path, settings: None) -> None: + @staticmethod + def count_shortcut_file(dir_path: Path) -> int: + match = 0 + for file in list(dir_path.iterdir()): + if os.name == "nt": + pattern = re.compile(r"AntaresWebServer-([0-9]*\.){3}lnk") + if pattern.fullmatch(file.name): + match += 1 + else: + pattern = re.compile(r"AntaresWebServer-([0-9]*\.){3}desktop") + if pattern.fullmatch(file.name): + match += 1 + return match + + def kill_running_server(self): + pass + + def test_run__empty_target(self, downloaded_dir: Path, program_dir: Path, settings: Any) -> None: """ The goal of this test is to verify the behavior of the application when: - The Antares server is not running @@ -75,29 +104,43 @@ def test_run__empty_target(self, downloaded_dir: Path, program_dir: Path, settin The test must verify that: - Files are correctly copied to the target directory - - Shortcuts are correctly created on the desktop - The server is correctly started """ - # For each application versions, check if everything is working - for application_dir in downloaded_dir.iterdir(): + for source_dir in downloaded_dir.iterdir(): # Run the application - app = App(source_dir=application_dir, target_dir=program_dir, shortcut=True, launch=True) - app.run() - - def test_shortcut__created(self, downloaded_dir: Path, program_dir: Path, desktop_dir: Path, settings: None): + # Make sure each application is installed in new directory + custom_dir = program_dir.joinpath(source_dir.name) + app = App(source_dir=source_dir, target_dir=custom_dir, shortcut=False, launch=True) + try: + app.run() + except InstallError: + print("Server execution problem") + raise + + # check if application was successfully installed in the target dir (program_dir) + assert custom_dir.is_dir() + assert custom_dir.iterdir() + + # check if all files are identical in both source_dir (application_dir) and target_dir (program_dir) + program_dir_content = list(custom_dir.iterdir()) + source_dir_content = list(source_dir.iterdir()) + assert len(source_dir_content) == len(program_dir_content) + for index, file in enumerate(source_dir.iterdir()): + assert file.name == program_dir_content[index].name + + # give some time for the server to shut down + time.sleep(2) + + def test_shortcut__created(self, downloaded_dir: Path, program_dir: Path, desktop_dir: Path, settings: Any): for application_dir in downloaded_dir.iterdir(): # Run the application - app = App(source_dir=application_dir, target_dir=program_dir, shortcut=True, launch=True) + # Deactivate launch option in order to improve tests speed + app = App(source_dir=application_dir, target_dir=program_dir, shortcut=True, launch=False) app.run() + match = self.count_shortcut_file(desktop_dir) + assert match == 1 - desktop_files = [file_name.name for file_name in list(desktop_dir.iterdir())] - - if os.name != "nt": - assert "AntaresWebServer.desktop" in desktop_files - else: - assert "Antares Web Server.lnk" in desktop_files - - def test_shortcut__not_created(self): + def test_shortcut__not_created(self, downloaded_dir: Path, program_dir: Path, desktop_dir: Path, settings: Any): """ Test if a shortcut was created on the desktop @param downloaded_dir: @@ -105,4 +148,10 @@ def test_shortcut__not_created(self): @param program_dir: @return: """ - pass + for application_dir in downloaded_dir.iterdir(): + # Run the application + # Deactivate launch option in order to improve tests speed + app = App(source_dir=application_dir, target_dir=program_dir, shortcut=False, launch=False) + app.run() + match = self.count_shortcut_file(desktop_dir) + assert match == 0 diff --git a/tests/test_app.py b/tests/test_app.py index 1260cf9..3017eb3 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,5 +1,8 @@ import hashlib from pathlib import Path + +import pytest + from antares_web_installer.app import App, EXCLUDED_FILES @@ -13,7 +16,7 @@ def test_kill_running_server(self) -> None: # 3. Vérifier que le serveur a bien été tué pass - def test_install_files__from_scratch(self, tmp_path: Path) -> None: + def test_install_files__from_scratch(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """ Case where the target directory does not exist. """ @@ -21,6 +24,7 @@ def test_install_files__from_scratch(self, tmp_path: Path) -> None: source_dir = tmp_path / "source" source_dir.mkdir() target_dir = tmp_path / "target" + monkeypatch.setattr("antares_web_installer.app.App.check_version", lambda _: "2.17.0") # Say we have dummy files in the source directory expected_files = ["dummy.txt", "folder/dummy2.txt"]