diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c61ddeea5e..be99f53ca3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -18,6 +18,9 @@ jobs: uses: actions/checkout@v2 with: submodules: recursive + - name: Install wget for windows + if: matrix.os == 'windows-latest' + run: choco install wget --no-progress - name: Set up Node.js uses: actions/setup-node@v1 with: @@ -41,29 +44,29 @@ jobs: python -m pip install --upgrade pip pip install pydantic --no-binary pydantic pip install -r requirements-dev.txt + - name: Install Windows dependencies + if: matrix.os == 'windows-latest' + run: pip install -r requirements-windows.txt - name: Fix symlink for windows if: matrix.os == 'windows-latest' run: | rm antareslauncher ln -s antares-launcher\antareslauncher antareslauncher - - name: Generate binary Unix - if: matrix.os != 'windows-latest' - run: | - git log -1 HEAD --format=%H > ./resources/commit_id - pyinstaller -F antarest/gui.py --windowed --icon=resources/webapp/favicon.ico -n AntaresWebServer --hidden-import='cmath' --hidden-import='antarest.dbmodel' --hidden-import='plyer.platforms.linux' --hidden-import='plyer.platforms.linux.notification' --additional-hooks-dir extra-hooks --add-data resources:resources --add-data alembic:alembic --add-data alembic.ini:. - pyinstaller -F antarest/main.py -n TestWebServer --hidden-import='cmath' --hidden-import='antarest.dbmodel' --additional-hooks-dir extra-hooks --add-data resources:resources --add-data alembic:alembic --add-data alembic.ini:. - pyinstaller -F antarest/tools/cli.py -n AntaresTool --hidden-import='cmath' --hidden-import='sqlalchemy.sql.default_comparator' --hidden-import='sqlalchemy.ext.baked' --additional-hooks-dir extra-hooks --add-data resources:resources - dist/TestWebServer -v - dist/AntaresTool --help - - name: Generate binary Windows + - name: Generate Windows binary if: matrix.os == 'windows-latest' run: | git log -1 HEAD --format=%H > .\resources\commit_id - pyinstaller -F antarest\gui.py --windowed --icon=resources/webapp/favicon.ico -n AntaresWebServer --hidden-import='cmath' --hidden-import='antarest.dbmodel' --hidden-import='plyer.platforms.win' --hidden-import='plyer.platforms.win.notification' --additional-hooks-dir extra-hooks --add-data ".\resources;.\resources" --add-data ".\alembic;.\alembic" --add-binary ".\alembic.ini;.\alembic.ini" - pyinstaller -F antarest\tools\cli.py -n AntaresTool --hidden-import='cmath' --hidden-import='sqlalchemy.sql.default_comparator' --hidden-import='sqlalchemy.ext.baked' --additional-hooks-dir extra-hooks --add-data ".\resources;.\resources" - dist\AntaresTool.exe --help + pyinstaller AntaresWebWin.spec + - name: Generate linux binary + if: matrix.os == 'ubuntu-latest' + run: | + git log -1 HEAD --format=%H > .\resources\commit_id + pyinstaller AntaresWebLinux.spec + - name: Packaging + run: bash ./package_antares_web.sh + working-directory: scripts - name: Archive binaries uses: actions/upload-artifact@v2 with: - name: dist-${{ matrix.os }} + name: AntaresWeb-${{ matrix.os }} path: dist/* diff --git a/.gitignore b/.gitignore index ae0f738dfc..c1e83b8272 100644 --- a/.gitignore +++ b/.gitignore @@ -35,7 +35,6 @@ MANIFEST # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest -*.spec # Installer logs pip-log.txt diff --git a/AntaresWebLinux.spec b/AntaresWebLinux.spec new file mode 100644 index 0000000000..7d9d38fc8f --- /dev/null +++ b/AntaresWebLinux.spec @@ -0,0 +1,79 @@ +# -*- mode: python ; coding: utf-8 -*- + + +block_cipher = None + +antares_web_server_a = Analysis(['antarest/gui.py'], + pathex=[], + binaries=[('./alembic.ini', './alembic.ini')], + datas=[('./resources', './resources'), ('./alembic', './alembic')], + hiddenimports=['cmath', 'antarest.dbmodel', 'plyer.platforms.linux', 'plyer.platforms.linux.notification'], + hookspath=['extra-hooks'], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) +antares_web_server_pyz = PYZ(antares_web_server_a.pure, antares_web_server_a.zipped_data, + cipher=block_cipher) +antares_web_server_exe = EXE(antares_web_server_pyz, + antares_web_server_a.scripts, + [], + exclude_binaries=True, + name='AntaresWebServer', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, + disable_windowed_traceback=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None , icon='resources/webapp/favicon.ico') + + +antares_tool_a = Analysis(['antarest/tools/cli.py'], + pathex=[], + binaries=[], + datas=[('./resources', './resources')], + hiddenimports=['cmath', 'sqlalchemy.sql.default_comparator', 'sqlalchemy.ext.baked'], + hookspath=['extra-hooks'], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) +antares_tool_pyz = PYZ(antares_tool_a.pure, antares_tool_a.zipped_data, + cipher=block_cipher) +antares_tool_exe = EXE(antares_tool_pyz, + antares_tool_a.scripts, + [], + exclude_binaries=True, + name='AntaresTool', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True, + disable_windowed_traceback=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None ) + + +coll = COLLECT(antares_web_server_exe, + antares_web_server_a.binaries, + antares_web_server_a.zipfiles, + antares_web_server_a.datas, + antares_tool_exe, + antares_tool_a.binaries, + antares_tool_a.zipfiles, + antares_tool_a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='AntaresWeb') diff --git a/AntaresWebWin.spec b/AntaresWebWin.spec new file mode 100644 index 0000000000..a91c594635 --- /dev/null +++ b/AntaresWebWin.spec @@ -0,0 +1,79 @@ +# -*- mode: python ; coding: utf-8 -*- + + +block_cipher = None + +antares_web_server_a = Analysis(['antarest/gui.py'], + pathex=[], + binaries=[('./alembic.ini', './alembic.ini')], + datas=[('./resources', './resources'), ('./alembic', './alembic')], + hiddenimports=['cmath', 'antarest.dbmodel', 'plyer.platforms.win', 'plyer.platforms.win.notification'], + hookspath=['extra-hooks'], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) +antares_web_server_pyz = PYZ(antares_web_server_a.pure, antares_web_server_a.zipped_data, + cipher=block_cipher) +antares_web_server_exe = EXE(antares_web_server_pyz, + antares_web_server_a.scripts, + [], + exclude_binaries=True, + name='AntaresWebServer', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, + disable_windowed_traceback=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None , icon='resources/webapp/favicon.ico') + + +antares_tool_a = Analysis(['antarest/tools/cli.py'], + pathex=[], + binaries=[], + datas=[('./resources', './resources')], + hiddenimports=['cmath', 'sqlalchemy.sql.default_comparator', 'sqlalchemy.ext.baked'], + hookspath=['extra-hooks'], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) +antares_tool_pyz = PYZ(antares_tool_a.pure, antares_tool_a.zipped_data, + cipher=block_cipher) +antares_tool_exe = EXE(antares_tool_pyz, + antares_tool_a.scripts, + [], + exclude_binaries=True, + name='AntaresTool', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True, + disable_windowed_traceback=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None ) + + +coll = COLLECT(antares_web_server_exe, + antares_web_server_a.binaries, + antares_web_server_a.zipfiles, + antares_web_server_a.datas, + antares_tool_exe, + antares_tool_a.binaries, + antares_tool_a.zipfiles, + antares_tool_a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='AntaresWeb') diff --git a/antarest/core/utils/utils.py b/antarest/core/utils/utils.py index 2a42960e3d..c2e1084592 100644 --- a/antarest/core/utils/utils.py +++ b/antarest/core/utils/utils.py @@ -64,6 +64,10 @@ def get_default_config_path() -> Optional[Path]: if config.exists(): return config + config = Path("../config.yaml") + if config.exists(): + return config + config = Path.home() / ".antares/config.yaml" if config.exists(): return config diff --git a/antarest/gui.py b/antarest/gui.py index 74bab39d78..95126a1c6e 100644 --- a/antarest/gui.py +++ b/antarest/gui.py @@ -1,4 +1,5 @@ import multiprocessing +import platform import sys import time import webbrowser @@ -6,7 +7,6 @@ from pathlib import Path import requests -from plyer import notification # type: ignore from antarest import __version__ @@ -47,13 +47,26 @@ def open_app() -> None: print(__version__) sys.exit() - notification.notify( - title="AntaresWebServer", - message="Antares Web Server started, you can manage the application within the systray app", - app_name="AntaresWebServer", - app_icon=RESOURCE_PATH / "webapp" / "favicon.ico", - timeout=600, - ) + if platform.system() == "Windows": + from win10toast import ToastNotifier # type: ignore + + toaster = ToastNotifier() + toaster.show_toast( + "AntaresWebServer", + "Antares Web Server started, you can manage the application within the systray app", + icon_path=RESOURCE_PATH / "webapp" / "favicon.ico", + threaded=True, + ) + else: + from plyer import notification # type: ignore + + notification.notify( + title="AntaresWebServer", + message="Antares Web Server started, you can manage the application within the systray app", + app_name="AntaresWebServer", + app_icon=RESOURCE_PATH / "webapp" / "favicon.ico", + timeout=600, + ) app = QApplication([]) app.setQuitOnLastWindowClosed(False) @@ -81,6 +94,8 @@ def open_app() -> None: tray.setContextMenu(menu) app.processEvents() # type: ignore + tray.setToolTip("AntaresWebServer") + server = Process( target=run_server, args=(config_file,), diff --git a/antarest/launcher/adapters/local_launcher/local_launcher.py b/antarest/launcher/adapters/local_launcher/local_launcher.py index bebd8aab6d..03ed270f87 100644 --- a/antarest/launcher/adapters/local_launcher/local_launcher.py +++ b/antarest/launcher/adapters/local_launcher/local_launcher.py @@ -24,10 +24,6 @@ logger = logging.getLogger(__name__) -class StudyVersionNotSupported(Exception): - pass - - class LocalLauncher(AbstractLauncher): """ This local launcher is meant to work when using AntaresWeb on a single worker process in local mode @@ -46,6 +42,25 @@ def __init__( ] = {} self.logs: Dict[str, str] = {} + def _select_best_binary(self, version: str) -> Path: + if self.config.launcher.local is None: + raise LauncherInitException() + + if version in self.config.launcher.local.binaries: + antares_solver_path = self.config.launcher.local.binaries[version] + else: + version_int = int(version) + keys = list(map(int, self.config.launcher.local.binaries.keys())) + keys_sup = [k for k in keys if k > version_int] + best_existing_version = min(keys_sup) if keys_sup else max(keys) + antares_solver_path = self.config.launcher.local.binaries[ + str(best_existing_version) + ] + logger.warning( + f"Version {version} is not available. Version {best_existing_version} has been selected instead" + ) + return antares_solver_path + def run_study( self, study_uuid: str, @@ -57,21 +72,19 @@ def run_study( if self.config.launcher.local is None: raise LauncherInitException() - antares_solver_path = self.config.launcher.local.binaries[version] - if antares_solver_path is None: - raise StudyVersionNotSupported() - else: - job = threading.Thread( - target=LocalLauncher._compute, - args=( - self, - antares_solver_path, - study_uuid, - job_id, - launcher_parameters, - ), - ) - job.start() + antares_solver_path = self._select_best_binary(version) + + job = threading.Thread( + target=LocalLauncher._compute, + args=( + self, + antares_solver_path, + study_uuid, + job_id, + launcher_parameters, + ), + ) + job.start() def _compute( self, @@ -84,6 +97,11 @@ def _compute( def stop_reading_output() -> bool: if end and str(uuid) in self.logs: + with open( + self.config.storage.tmp_dir / f"antares_solver-{uuid}.log", + "w", + ) as log_file: + log_file.write(self.logs[str(uuid)]) del self.logs[str(uuid)] return end @@ -158,14 +176,14 @@ def stop_reading_output() -> bool: self.callbacks.update_status( str(uuid), JobStatus.FAILED - if (not process.returncode == 0) or not output_id + if process.returncode != 0 or not output_id else JobStatus.SUCCESS, None, output_id, ) except Exception as e: logger.error( - f"Unexpected error happend during launch {uuid}", exc_info=e + f"Unexpected error happened during launch {uuid}", exc_info=e ) self.callbacks.update_status( str(uuid), diff --git a/antarest/main.py b/antarest/main.py index 41c1ef351a..9b0818450f 100644 --- a/antarest/main.py +++ b/antarest/main.py @@ -117,7 +117,7 @@ def get_default_config_path_or_raise() -> Path: config_path = get_default_config_path() if not config_path: raise ValueError( - "Config file not found. Set it by '-c' with command line or place it at ./config.yaml or ~/.antares/config.yaml" + "Config file not found. Set it by '-c' with command line or place it at ./config.yaml, ../config.yaml or ~/.antares/config.yaml" ) return config_path diff --git a/requirements-windows.txt b/requirements-windows.txt new file mode 100644 index 0000000000..c9500efdd2 --- /dev/null +++ b/requirements-windows.txt @@ -0,0 +1 @@ +win10toast diff --git a/resources/AntaresWebServerShortcut.lnk b/resources/AntaresWebServerShortcut.lnk new file mode 100644 index 0000000000..588d7f8bbe Binary files /dev/null and b/resources/AntaresWebServerShortcut.lnk differ diff --git a/resources/application.yaml b/resources/application.yaml index 99e61c887e..6bec07fb9e 100644 --- a/resources/application.yaml +++ b/resources/application.yaml @@ -79,7 +79,7 @@ server: logging: level: INFO -# logfile: /tmp/antarest.log + logfile: ./tmp/antarest.log # json: false # Uncomment these lines to use redis as a backend for the eventbus diff --git a/resources/deploy/README.md b/resources/deploy/README.md new file mode 100644 index 0000000000..100eb4642b --- /dev/null +++ b/resources/deploy/README.md @@ -0,0 +1,18 @@ +# Antares Web + +| WARNING: AntaresWeb/AntaresWebServer and AntaresWeb/antares_solver/antares--solver must be set as executable on *LINUX* | +|----------------------------------------------------------------------------------------------------------------------------------| + +To launch the Antares Web, run the command +``` +./AntaresWeb/AntaresWebServer +``` +Then go to http://localhost:8080 + +## Variant manager tool + +To use the variant manager tool, run the command +``` +./AntaresWeb/AntaresTool +``` +Further instruction will be provided by the command output. diff --git a/resources/deploy/config.yaml b/resources/deploy/config.yaml new file mode 100644 index 0000000000..82c210ce75 --- /dev/null +++ b/resources/deploy/config.yaml @@ -0,0 +1,78 @@ +security: + disabled: true + jwt: + key: super-secret + login: + admin: + pwd: admin + external_auth: + url: "" + default_group_role: 10 + +db: + url: "sqlite:///database.db" + +storage: + tmp_dir: ./tmp + matrixstore: ./matrices + archive_dir: ./examples/archives + workspaces: + default: # required, no filters applied, this folder is not watched + path: ./examples/internal_studies/ + # other workspaces can be added + # if a directory is to be ignored by the watcher, place a file named AW_NO_SCAN inside + tmp: + path: ./examples/studies/ + # filter_in: ['.*'] # default to '.*' + # filter_out: [] # default to empty + # groups: [] # default empty + +launcher: + default: local + local: + binaries: + 700: path/to/700 +# slurm: +# local_workspace: path/to/workspace +# username: username +# hostname: 0.0.0.0 +# port: 22 +# private_key_file: path/to/key +# key_password: key_password +# password: password_is_optional_but_necessary_if_key_is_absent +# default_wait_time: 900 +# default_time_limit: 172800 +# default_n_cpu: 12 +# default_json_db_name: launcher_db.json +# slurm_script_path: /path/to/launchantares_v1.1.3.sh +# db_primary_key: name +# antares_versions_on_remote_server : +# - "610" +# - "700" +# - "710" +# - "720" +# - "800" + + +debug: false + +root_path: "" + +#tasks: +# max_workers: 5 +server: + worker_threadpool_size: 12 + services: + - watcher + +logging: + level: INFO + logfile: ./tmp/antarest.log +# json: false + +# Uncomment these lines to use redis as a backend for the eventbus +# It is required to use redis when using this application on multiple workers in a preforked model like gunicorn for instance +#eventbus: +# redis: +# host: localhost +# port: 6379 diff --git a/resources/deploy/examples/README.md b/resources/deploy/examples/README.md new file mode 100644 index 0000000000..2d761fd2f3 --- /dev/null +++ b/resources/deploy/examples/README.md @@ -0,0 +1,2 @@ +Examples can be found at https://github.com/AntaresSimulatorTeam/Antares_Simulator_Examples +These can be copied into the studies directory. diff --git a/resources/deploy/examples/archives/.placeholder b/resources/deploy/examples/archives/.placeholder new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/deploy/examples/internal_studies/.placeholder b/resources/deploy/examples/internal_studies/.placeholder new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/deploy/examples/studies/.placeholder b/resources/deploy/examples/studies/.placeholder new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/deploy/examples/studies/example_study.zip b/resources/deploy/examples/studies/example_study.zip new file mode 100644 index 0000000000..8bb0596a35 Binary files /dev/null and b/resources/deploy/examples/studies/example_study.zip differ diff --git a/resources/deploy/matrices/.placeholder b/resources/deploy/matrices/.placeholder new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/deploy/tmp/.placeholder b/resources/deploy/tmp/.placeholder new file mode 100644 index 0000000000..e69de29bb2 diff --git a/scripts/package_antares_web.sh b/scripts/package_antares_web.sh new file mode 100755 index 0000000000..1574ac2b4a --- /dev/null +++ b/scripts/package_antares_web.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +ANTARES_SOLVER_VERSION="8.2" +ANTARES_SOLVER_FULL_VERSION="$ANTARES_SOLVER_VERSION.1" +ANTARES_SOLVER_FULL_VERSION_INT=$(echo $ANTARES_SOLVER_FULL_VERSION | sed 's/\.//g') + + +if [[ "$OSTYPE" == "msys"* ]]; then + ANTARES_SOLVER_FOLDER_NAME="rte-antares-$ANTARES_SOLVER_FULL_VERSION-installer-64bits" + ANTARES_SOLVER_ZIPFILE_NAME="$ANTARES_SOLVER_FOLDER_NAME.zip" +else + ANTARES_SOLVER_FOLDER_NAME="antares-$ANTARES_SOLVER_FULL_VERSION-Ubuntu-20.04" + ANTARES_SOLVER_ZIPFILE_NAME="$ANTARES_SOLVER_FOLDER_NAME.tar.gz" +fi + +LINK="https://github.com/AntaresSimulatorTeam/Antares_Simulator/releases/download/v$ANTARES_SOLVER_FULL_VERSION/$ANTARES_SOLVER_ZIPFILE_NAME" +DESTINATION="../dist/AntaresWeb/antares_solver" + +echo "Downloading AntaresSimulator from $LINK" + wget $LINK + +echo "Unzipping $ANTARES_SOLVER_ZIPFILE_NAME and move Antares solver to $DESTINATION" + 7z x $ANTARES_SOLVER_ZIPFILE_NAME + if [[ "$OSTYPE" != "msys"* ]]; then + 7z x "$ANTARES_SOLVER_FOLDER_NAME.tar" + fi + + mkdir $DESTINATION + + if [[ "$OSTYPE" == "msys"* ]]; then + mv "$ANTARES_SOLVER_FOLDER_NAME/bin/antares-$ANTARES_SOLVER_VERSION-solver.exe" $DESTINATION + mv $ANTARES_SOLVER_FOLDER_NAME/bin/sirius_solver.dll $DESTINATION + mv $ANTARES_SOLVER_FOLDER_NAME/bin/zlib1.dll $DESTINATION + else + mv "$ANTARES_SOLVER_FOLDER_NAME/bin/antares-$ANTARES_SOLVER_VERSION-solver" $DESTINATION + mv "$ANTARES_SOLVER_FOLDER_NAME/bin/libsirius_solver.so" $DESTINATION + fi + + +echo "Copy basic configuration files" + cp -r ../resources/deploy/* ../dist/ + if [[ "$OSTYPE" == "msys"* ]]; then + sed -i "s/700: path\/to\/700/$ANTARES_SOLVER_FULL_VERSION_INT: .\/AntaresWeb\/antares_solver\/antares-$ANTARES_SOLVER_VERSION-solver.exe/g" ../dist/config.yaml + else + sed -i "s/700: path\/to\/700/$ANTARES_SOLVER_FULL_VERSION_INT: .\/AntaresWeb\/antares_solver\/antares-$ANTARES_SOLVER_VERSION-solver/g" ../dist/config.yaml + fi + + + +echo "Creating shortcuts" + if [[ "$OSTYPE" == "msys"* ]]; then + cp ../resources/AntaresWebServerShortcut.lnk ../dist/ + else + ln -s ../dist/AntaresWeb/AntaresWebServer ../dist/AntaresWebServer + fi + +echo "Unzipping example study" + cd ../dist/examples/studies + 7z x example_study.zip + rm example_study.zip + +echo "Cleaning up" + rm $ANTARES_SOLVER_ZIPFILE_NAME + rm -rf $ANTARES_SOLVER_FOLDER_NAME + diff --git a/tests/launcher/test_local_launcher.py b/tests/launcher/test_local_launcher.py index 976294dbcf..e14a7e8c0c 100644 --- a/tests/launcher/test_local_launcher.py +++ b/tests/launcher/test_local_launcher.py @@ -5,7 +5,7 @@ import pytest from sqlalchemy import create_engine -from antarest.core.config import Config +from antarest.core.config import Config, LauncherConfig, LocalConfig from antarest.core.persistence import Base from antarest.core.utils.fastapi_sqlalchemy import DBSessionMiddleware from antarest.launcher.adapters.local_launcher.local_launcher import ( @@ -48,3 +48,23 @@ def test_compute(tmp_path: Path): call(str(uuid), JobStatus.SUCCESS, None, "some output"), ] ) + + +@pytest.mark.unit_test +def test_select_best_binary(tmp_path: Path): + binaries = { + "700": Path("700"), + "800": Path("800"), + "900": Path("900"), + "1000": Path("1000"), + } + local_launcher = LocalLauncher( + Config(launcher=LauncherConfig(local=LocalConfig(binaries=binaries))), + callbacks=Mock(), + event_bus=Mock(), + ) + + assert local_launcher._select_best_binary("600") == binaries["700"] + assert local_launcher._select_best_binary("700") == binaries["700"] + assert local_launcher._select_best_binary("710") == binaries["800"] + assert local_launcher._select_best_binary("1100") == binaries["1000"]