update
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
AZA Desktop Updater — Phase 1.
|
||||
|
||||
Prueft Updates ueber das Server-Manifest, laedt bei Bestaetigung herunter,
|
||||
prueft SHA256, sichert vor Installation und bietet Rollback an.
|
||||
Kein stilles Auto-Update.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from pathlib import Path
|
||||
from tkinter import messagebox, ttk
|
||||
from typing import Any
|
||||
|
||||
from aza_update_core import (
|
||||
create_pre_update_backup,
|
||||
download_update_package,
|
||||
evaluate_update,
|
||||
extract_update_zip,
|
||||
fetch_remote_manifest,
|
||||
format_version_label,
|
||||
get_project_root,
|
||||
list_available_backups,
|
||||
load_local_version,
|
||||
rollback_from_backup,
|
||||
save_local_version,
|
||||
)
|
||||
|
||||
|
||||
def _log(msg: str) -> None:
|
||||
print(f"[AZA Updater] {msg}")
|
||||
|
||||
|
||||
def check_update_status() -> dict[str, Any]:
|
||||
"""Nur pruefen — kein Download, keine UI."""
|
||||
local = load_local_version()
|
||||
remote, err = fetch_remote_manifest()
|
||||
if err or not remote:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Manifest nicht erreichbar.",
|
||||
"detail": err,
|
||||
"local": local,
|
||||
}
|
||||
result = evaluate_update(local, remote)
|
||||
result["fetch_error"] = err
|
||||
return result
|
||||
|
||||
|
||||
def _status_title(result: dict[str, Any]) -> str:
|
||||
status = result.get("status")
|
||||
if status == "update_available":
|
||||
return "Update verfuegbar"
|
||||
if status == "current":
|
||||
return "AZA ist aktuell"
|
||||
if status == "channel_mismatch":
|
||||
return "Update-Kanal abweichend"
|
||||
return "Update-Pruefung"
|
||||
|
||||
|
||||
def _status_message(result: dict[str, Any]) -> str:
|
||||
status = result.get("status")
|
||||
local = result.get("local") or load_local_version()
|
||||
local_label = format_version_label(local)
|
||||
|
||||
if status == "error":
|
||||
detail = result.get("detail") or result.get("message") or "Unbekannter Fehler"
|
||||
return (
|
||||
f"Lokal: {local_label}\n\n"
|
||||
f"Das Update-Manifest konnte nicht geladen werden.\n"
|
||||
f"Details: {detail}\n\n"
|
||||
"AZA laeuft weiter — es wurde nichts installiert."
|
||||
)
|
||||
|
||||
if status == "current":
|
||||
remote = result.get("remote") or {}
|
||||
remote_ver, remote_build, _ = (
|
||||
str(remote.get("latest_version") or remote.get("version") or "?"),
|
||||
str(remote.get("latest_build") or remote.get("build") or ""),
|
||||
"",
|
||||
)
|
||||
remote_label = f"v{remote_ver}"
|
||||
if remote_build:
|
||||
remote_label += f" · {remote_build}"
|
||||
return (
|
||||
f"Lokal: {local_label}\n"
|
||||
f"Server: {remote_label}\n\n"
|
||||
"Kein Update erforderlich."
|
||||
)
|
||||
|
||||
if status == "channel_mismatch":
|
||||
return (
|
||||
f"Lokal: {local_label}\n\n"
|
||||
f"{result.get('message', 'Kanal stimmt nicht ueberein.')}\n\n"
|
||||
"Es wurde nichts installiert."
|
||||
)
|
||||
|
||||
if status == "update_available":
|
||||
latest = result.get("latest_version") or "?"
|
||||
latest_build = result.get("latest_build") or ""
|
||||
lines = [
|
||||
f"Lokal: {local_label}",
|
||||
f"Angebot: v{latest}" + (f" · {latest_build}" if latest_build else ""),
|
||||
"",
|
||||
"Ein Update ist verfuegbar.",
|
||||
"Moechten Sie es jetzt herunterladen und installieren?",
|
||||
]
|
||||
notes = result.get("notes") or []
|
||||
if notes:
|
||||
lines.extend(["", "Aenderungen:"])
|
||||
lines.extend(f"• {n}" for n in notes[:8])
|
||||
return "\n".join(lines)
|
||||
|
||||
return str(result.get("message") or "Unbekannter Status.")
|
||||
|
||||
|
||||
def _stop_aza_processes() -> None:
|
||||
if sys.platform != "win32":
|
||||
return
|
||||
for name in ("aza_desktop.exe", "aza_controller.exe", "AZA_EmpfangShell.exe"):
|
||||
try:
|
||||
subprocess.run(
|
||||
["taskkill", "/F", "/IM", name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _apply_downloaded_update(package: Path, result: dict[str, Any]) -> tuple[bool, str]:
|
||||
install_dir = get_project_root()
|
||||
backup_dir = create_pre_update_backup(install_dir)
|
||||
_log(f"Backup erstellt: {backup_dir}")
|
||||
|
||||
suffix = package.suffix.lower()
|
||||
if suffix == ".zip":
|
||||
ok, msg = extract_update_zip(package, install_dir)
|
||||
if not ok:
|
||||
rollback_from_backup(backup_dir, install_dir)
|
||||
return False, f"Installation fehlgeschlagen: {msg}\nRollback durchgefuehrt."
|
||||
elif suffix == ".exe":
|
||||
target = install_dir / package.name
|
||||
try:
|
||||
import shutil
|
||||
|
||||
shutil.copy2(package, target)
|
||||
except Exception as exc:
|
||||
rollback_from_backup(backup_dir, install_dir)
|
||||
return False, f"Installer konnte nicht kopiert werden: {exc}\nRollback durchgefuehrt."
|
||||
else:
|
||||
return False, f"Unbekanntes Update-Format: {package.name}"
|
||||
|
||||
new_version = {
|
||||
"version": result.get("latest_version"),
|
||||
"build": result.get("latest_build"),
|
||||
"channel": (result.get("local") or {}).get("channel", "stable"),
|
||||
}
|
||||
try:
|
||||
save_local_version({k: v for k, v in new_version.items() if v})
|
||||
except Exception as exc:
|
||||
_log(f"version.json konnte nicht geschrieben werden: {exc}")
|
||||
|
||||
return True, f"Update installiert.\nBackup: {backup_dir}"
|
||||
|
||||
|
||||
def perform_confirmed_update(
|
||||
result: dict[str, Any],
|
||||
*,
|
||||
parent: tk.Misc | None = None,
|
||||
) -> tuple[bool, str]:
|
||||
"""Laedt das erste Manifest-Paket herunter und installiert nach Bestaetigung."""
|
||||
files = result.get("files") or []
|
||||
if not files:
|
||||
return False, "Kein Update-Paket im Manifest."
|
||||
|
||||
file_info = files[0]
|
||||
_log(f"Download starten: {file_info.get('url')}")
|
||||
|
||||
package, err = download_update_package(file_info)
|
||||
if not package:
|
||||
return False, f"Download fehlgeschlagen: {err}"
|
||||
|
||||
confirm = messagebox.askyesno(
|
||||
"AZA — Update installieren",
|
||||
(
|
||||
f"Paket: {package.name}\n"
|
||||
f"Groesse: {package.stat().st_size:,} Bytes\n\n"
|
||||
"Laufende AZA-Prozesse werden beendet.\n"
|
||||
"Die aktuelle Installation wird gesichert.\n\n"
|
||||
"Jetzt installieren?"
|
||||
),
|
||||
parent=parent,
|
||||
)
|
||||
if not confirm:
|
||||
return False, "Installation abgebrochen — Download liegt lokal vor, wurde nicht installiert."
|
||||
|
||||
_stop_aza_processes()
|
||||
ok, msg = _apply_downloaded_update(package, result)
|
||||
return ok, msg
|
||||
|
||||
|
||||
def check_updates_interactive(*, parent: tk.Misc | None = None) -> None:
|
||||
"""UI: Manifest pruefen, Status anzeigen, optional Update starten."""
|
||||
result = check_update_status()
|
||||
status = result.get("status")
|
||||
_log(f"Status={status} detail={result.get('detail') or result.get('message')}")
|
||||
|
||||
title = _status_title(result)
|
||||
message = _status_message(result)
|
||||
|
||||
if status == "update_available":
|
||||
if messagebox.askyesno(title, message, parent=parent):
|
||||
ok, install_msg = perform_confirmed_update(result, parent=parent)
|
||||
if ok:
|
||||
messagebox.showinfo("AZA — Update", install_msg, parent=parent)
|
||||
else:
|
||||
messagebox.showwarning("AZA — Update", install_msg, parent=parent)
|
||||
return
|
||||
|
||||
messagebox.showinfo(title, message, parent=parent)
|
||||
|
||||
|
||||
def show_app_info(*, parent: tk.Misc | None = None) -> None:
|
||||
local = load_local_version()
|
||||
root = get_project_root()
|
||||
vf = root / "version.json"
|
||||
lines = [
|
||||
"AZA Desktop — Controller / Launcher",
|
||||
"",
|
||||
f"Version: {format_version_label(local)}",
|
||||
f"Kanal: {local.get('channel', 'stable')}",
|
||||
f"App: {local.get('app', 'AZA Desktop')}",
|
||||
"",
|
||||
f"Installationsordner:\n{root}",
|
||||
"",
|
||||
f"version.json:\n{vf if vf.is_file() else '(nicht gefunden)'}",
|
||||
"",
|
||||
"Updates: api.aza-medwork.ch/downloads/updates/manifest.json",
|
||||
"Benutzerdaten bleiben in %APPDATA%\\AzA getrennt.",
|
||||
]
|
||||
messagebox.showinfo("AZA — Info", "\n".join(lines), parent=parent)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
owns = False
|
||||
parent: tk.Misc | None = None
|
||||
if not tk._default_root: # type: ignore[attr-defined]
|
||||
parent = tk.Tk()
|
||||
parent.withdraw()
|
||||
owns = True
|
||||
check_updates_interactive(parent=parent)
|
||||
if owns and parent is not None:
|
||||
parent.destroy()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user