163 lines
4.7 KiB
Python
163 lines
4.7 KiB
Python
"""
|
|
Auto-Updater
|
|
============
|
|
On every launch (after a successful license check), checks the Verimund API
|
|
for a newer version. If found, downloads the new exe via the API and
|
|
self-replaces via a helper batch script.
|
|
|
|
Called after a successful license check on every launch.
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
import requests
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Current app version — kept in sync by push_update.py before each build.
|
|
# ---------------------------------------------------------------------------
|
|
APP_VERSION = "1.3.8"
|
|
|
|
|
|
def _exe_dir() -> str:
|
|
if getattr(sys, "frozen", False):
|
|
return os.path.dirname(sys.executable)
|
|
return os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
def _parse_version(v: str) -> tuple:
|
|
try:
|
|
return tuple(int(x) for x in v.strip().split("."))
|
|
except Exception:
|
|
return (0, 0, 0)
|
|
|
|
|
|
def _log_update_error(msg: str) -> None:
|
|
"""Log update errors silently — never let this crash the app."""
|
|
try:
|
|
from license_check import get_api_headers, _API_URL
|
|
from hwid import get_hwid
|
|
hwid = get_hwid()
|
|
import license_check
|
|
channel = license_check.get_channel()
|
|
requests.post(
|
|
f"{_API_URL}/log",
|
|
headers=get_api_headers(),
|
|
json={
|
|
"hwid": hwid,
|
|
"app_version": APP_VERSION,
|
|
"channel": channel,
|
|
"message": msg,
|
|
},
|
|
timeout=5,
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _self_replace_exe(jwt_token: str, channel: str) -> None:
|
|
"""
|
|
Download the new exe from the API, then launch a helper batch script that
|
|
waits for this process to exit, moves the file into place, and restarts.
|
|
Never returns.
|
|
"""
|
|
if not getattr(sys, "frozen", False):
|
|
return
|
|
|
|
from license_check import _API_URL
|
|
|
|
current_exe = os.path.abspath(sys.executable)
|
|
tmp_dir = tempfile.gettempdir()
|
|
pending_exe = os.path.join(tmp_dir, "StockScreener_pending.exe")
|
|
|
|
try:
|
|
with requests.post(
|
|
f"{_API_URL}/download",
|
|
headers={"Authorization": f"Bearer {jwt_token}", "Content-Type": "application/json"},
|
|
stream=True,
|
|
timeout=120,
|
|
) as r:
|
|
r.raise_for_status()
|
|
with open(pending_exe, "wb") as f:
|
|
for chunk in r.iter_content(chunk_size=65536):
|
|
f.write(chunk)
|
|
except Exception as exc:
|
|
_log_update_error(f"_self_replace_exe download failed: {exc}")
|
|
return
|
|
|
|
helper = os.path.join(tmp_dir, "uit_update.bat")
|
|
with open(helper, "w") as f:
|
|
f.write(
|
|
f"@echo off\n"
|
|
f":retry\n"
|
|
f"timeout /t 1 /nobreak >nul\n"
|
|
f"move /Y \"{pending_exe}\" \"{current_exe}\"\n"
|
|
f"if errorlevel 1 goto retry\n"
|
|
f"start \"\" \"{current_exe}\"\n"
|
|
f"del \"%~f0\"\n"
|
|
)
|
|
|
|
subprocess.Popen(
|
|
["cmd", "/c", helper],
|
|
creationflags=subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS,
|
|
close_fds=True,
|
|
)
|
|
sys.exit(0)
|
|
|
|
|
|
def get_release_notes() -> tuple:
|
|
"""
|
|
Returns (version, release_notes) for the current channel from the API.
|
|
Falls back gracefully on any error.
|
|
"""
|
|
try:
|
|
from license_check import get_api_headers, get_jwt, _API_URL
|
|
resp = requests.get(
|
|
f"{_API_URL}/update",
|
|
headers=get_api_headers(),
|
|
timeout=8,
|
|
)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
version = data.get("version", APP_VERSION)
|
|
notes = data.get("release_notes") or "No release notes available."
|
|
return version, notes
|
|
except Exception:
|
|
return APP_VERSION, "Unable to fetch release notes."
|
|
|
|
|
|
def check_and_apply_update() -> None:
|
|
"""
|
|
Silently check for updates and apply them. Errors are swallowed so a
|
|
failed update check never blocks the app from launching.
|
|
"""
|
|
try:
|
|
from license_check import get_api_headers, get_jwt, get_channel, _API_URL
|
|
|
|
resp = requests.get(
|
|
f"{_API_URL}/update",
|
|
headers=get_api_headers(),
|
|
timeout=8,
|
|
)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
|
|
latest = data.get("version", APP_VERSION)
|
|
channel = data.get("channel", "stable")
|
|
|
|
if _parse_version(latest) <= _parse_version(APP_VERSION):
|
|
return # already up to date
|
|
|
|
jwt_token = get_jwt()
|
|
if not jwt_token:
|
|
_log_update_error("No JWT token available for update download")
|
|
return
|
|
|
|
_self_replace_exe(jwt_token, channel)
|
|
|
|
except Exception as exc:
|
|
_log_update_error(f"check_and_apply_update failed: {exc}")
|