diff --git a/AtlasToolkit.iss b/AtlasToolkit.iss index 2d58d92..9be927f 100644 --- a/AtlasToolkit.iss +++ b/AtlasToolkit.iss @@ -44,8 +44,9 @@ WizardStyle=modern ; installer from running concurrently. AppMutex={#MyAppMutex} SetupMutex=AtlasToolkitSetupMutex -; During a silent self-update, close the running app via Restart Manager; the -; updater cmd handles relaunch, so don't let Inno restart it (avoids double-launch). +; CloseApplications=yes lets silent self-update shut down the running app via +; Restart Manager. RestartApplications=no keeps interactive installs on [Run]; +; self-update passes /RESTARTAPPLICATIONS on the command line to relaunch. CloseApplications=yes RestartApplications=no ; Notify the shell when the .atlas association changes (refreshes icons / Open With). diff --git a/atlas_toolkit/__init__.py b/atlas_toolkit/__init__.py index 5e1d7b3..02385db 100644 --- a/atlas_toolkit/__init__.py +++ b/atlas_toolkit/__init__.py @@ -2,4 +2,4 @@ from __future__ import annotations -__version__ = "0.3.0" +__version__ = "0.3.1" diff --git a/atlas_toolkit/app/bridge.py b/atlas_toolkit/app/bridge.py index e44f2ec..204f6f6 100644 --- a/atlas_toolkit/app/bridge.py +++ b/atlas_toolkit/app/bridge.py @@ -482,11 +482,7 @@ def download_update(self) -> dict[str, Any]: return self._updates.download() def restart_and_install_update(self) -> dict[str, Any]: - def _close_window() -> None: - if self._window: - self._window.destroy() - - return self._updates.restart_and_install(on_success=_close_window) + return self._updates.restart_and_install() def _run_update_check(self) -> None: try: diff --git a/atlas_toolkit/update/controller.py b/atlas_toolkit/update/controller.py index 59f79f8..4163672 100644 --- a/atlas_toolkit/update/controller.py +++ b/atlas_toolkit/update/controller.py @@ -10,7 +10,7 @@ import threading import time from pathlib import Path -from typing import Any, Callable, Optional +from typing import Any, Optional from atlas_toolkit.app.config import get_config_dir from atlas_toolkit.update.updater import ( @@ -23,8 +23,15 @@ log = logging.getLogger(__name__) -_INNO_SILENT_FLAGS = ( - "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /CLOSEAPPLICATIONS /NORESTARTAPPLICATIONS" +# Inno closes the running app (Restart Manager + AppMutex) and relaunches when done. +# /RESTARTAPPLICATIONS overrides RestartApplications=no in AtlasToolkit.iss so the +# interactive [Run] entry (skipifsilent) is not used for silent self-update. +_INNO_SILENT_INSTALL_ARGS = ( + "/VERYSILENT", + "/SUPPRESSMSGBOXES", + "/NORESTART", + "/CLOSEAPPLICATIONS", + "/RESTARTAPPLICATIONS", ) @@ -137,51 +144,16 @@ def is_installed_build() -> bool: return False -def build_update_script( +def build_inno_install_command( installer_path: Path, - target_exe: Path, - pid: int, - relaunch_args: list[str], - release_url: str, inno_log_path: Path, -) -> str: - inst = str(installer_path) - exe = str(target_exe) - log_path = str(inno_log_path) - relaunch_suffix = " ".join(f'"{a}"' for a in relaunch_args) - success_launch = f'start "" "{exe}" {relaunch_suffix}'.rstrip() - - fail_message = "Update failed: the installer reported an error." - fail_args = [ - "--update-install-failed", - "--update-failed-message", f'"{fail_message}"', - "--update-failed-log", f'"{log_path}"', +) -> list[str]: + """Argv to run the downloaded Inno Setup installer for silent self-update.""" + return [ + str(installer_path), + *_INNO_SILENT_INSTALL_ARGS, + f"/LOG={inno_log_path}", ] - if release_url: - fail_args += ["--update-release-url", f'"{release_url}"'] - fail_launch = f'start "" "{exe}" ' + " ".join(fail_args) - - lines = [ - "@echo off", - "setlocal", - ":waitloop", - f'tasklist /FI "PID eq {pid}" 2>nul | find "{pid}" >nul', - "if not errorlevel 1 (", - " ping -n 2 127.0.0.1 >nul", - " goto waitloop", - ")", - f'"{inst}" {_INNO_SILENT_FLAGS} /LOG="{log_path}"', - "if errorlevel 1 goto failed", - success_launch, - "goto cleanup", - ":failed", - fail_launch, - ":cleanup", - f'del /f /q "{inst}" >nul 2>nul', - 'del /f /q "%~f0" >nul 2>nul', - "endlocal", - ] - return "\r\n".join(lines) + "\r\n" class UpdateController: @@ -298,11 +270,8 @@ def _progress(downloaded: int, total: Optional[int]) -> None: progress_cb=_progress, ) - target_exe = get_running_executable_path() metadata = { "installer_path": str(target_installer_path), - "target_exe_path": str(target_exe), - "relaunch_args": sys.argv[1:], "version": latest.latest_version, "release_url": latest.release_url, } @@ -339,10 +308,7 @@ def _progress(downloaded: int, total: Optional[int]) -> None: ) return {"ok": False, "error": msg} - def restart_and_install( - self, - on_success: Callable[[], None], - ) -> dict[str, Any]: + def restart_and_install(self) -> dict[str, Any]: if not is_running_as_exe(): return { "ok": False, @@ -362,42 +328,27 @@ def restart_and_install( "error": "Downloaded installer is missing or invalid.", } - target_exe = get_running_executable_path() - if not target_exe.exists() or not target_exe.is_file(): - return { - "ok": False, - "error": f"Cannot locate executable for relaunch: {target_exe}", - } - update_dir = get_update_dir() timestamp = time.strftime("%Y%m%d_%H%M%S") inno_log_path = update_dir / f"inno_install_{timestamp}.log" - script_text = build_update_script( - installer_path=installer_path, - target_exe=target_exe, - pid=os.getpid(), - relaunch_args=list(sys.argv[1:]), - release_url=self._release_url or "", - inno_log_path=inno_log_path, - ) - script_path = update_dir / f"install_update_{timestamp}_{os.getpid()}.cmd" + install_cmd = build_inno_install_command(installer_path, inno_log_path) try: - script_path.write_text(script_text, encoding="utf-8") - cmd_exe = os.environ.get("COMSPEC") or "cmd" popen_kwargs: dict[str, Any] = { "cwd": str(update_dir), "close_fds": True, } if sys.platform == "win32": popen_kwargs["creationflags"] = ( - subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP + subprocess.DETACHED_PROCESS + | subprocess.CREATE_NEW_PROCESS_GROUP + | subprocess.CREATE_NO_WINDOW ) else: popen_kwargs["start_new_session"] = True - subprocess.Popen([cmd_exe, "/d", "/c", str(script_path)], **popen_kwargs) - on_success() + subprocess.Popen(install_cmd, **popen_kwargs) + log.info("Launched silent installer: %s", installer_path.name) return {"ok": True} except Exception as e: return { diff --git a/atlas_toolkit/update/updater.py b/atlas_toolkit/update/updater.py index a2a80fc..dd6288f 100644 --- a/atlas_toolkit/update/updater.py +++ b/atlas_toolkit/update/updater.py @@ -44,7 +44,12 @@ class UpdateInfo(NamedTuple): def is_running_as_exe() -> bool: """Check if running as Nuitka-compiled executable.""" - return "__compiled__" in globals() + import sys + + if getattr(sys, "frozen", False): + return True + main = sys.modules.get("__main__") + return main is not None and getattr(main, "__compiled__", None) is not None def get_current_version() -> str: diff --git a/pyproject.toml b/pyproject.toml index 806bb0b..f1c63b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "AtlasToolkit" -version = "0.3.0" +version = "0.3.1" description = "Spine Atlas Toolkit - Extract, modify, and repack atlas sprites." readme = "README.md" requires-python = ">=3.11" diff --git a/uv.lock b/uv.lock index 89929d8..52bb156 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.11" [[package]] name = "atlastoolkit" -version = "0.3.0" +version = "0.3.1" source = { editable = "." } dependencies = [ { name = "pillow" },