Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions AtlasToolkit.iss
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion atlas_toolkit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

from __future__ import annotations

__version__ = "0.3.0"
__version__ = "0.3.1"
6 changes: 1 addition & 5 deletions atlas_toolkit/app/bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Comment on lines 484 to +485

def _run_update_check(self) -> None:
try:
Expand Down
97 changes: 24 additions & 73 deletions atlas_toolkit/update/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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",
)


Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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,
Expand All @@ -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}
Comment on lines +334 to 352
except Exception as e:
return {
Expand Down
7 changes: 6 additions & 1 deletion atlas_toolkit/update/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading