fix to app not updating

This commit is contained in:
2026-08-31 23:37:12 -07:00
parent 2fe6f42c48
commit 8ef352add3
4 changed files with 201 additions and 167 deletions
+143 -135
View File
@@ -18,7 +18,8 @@ import zipfile
import traceback
import subprocess
import configparser
from typing import List
from typing import List, Tuple, Optional, Callable, Any
# External library imports
import psutil
@@ -26,7 +27,7 @@ import requests
from PySide6.QtCore import QThread, Signal, QObject
from PySide6.QtWidgets import QMainWindow, QMessageBox
from src.shared.shareddata import get_app_dir
class UpdateDownloadThread(QThread):
"""
@@ -54,7 +55,7 @@ class UpdateDownloadThread(QThread):
self.platform_name = platform_name
self.app_name = app_name
def run(self):
def run(self) -> None:
try:
local_filename = os.path.basename(self.download_url)
@@ -63,8 +64,8 @@ class UpdateDownloadThread(QThread):
os.makedirs(tmp_dir, exist_ok=True)
local_path = os.path.join(tmp_dir, local_filename)
else:
tmp_dir = os.getcwd()
local_path = os.path.join(os.getcwd(), local_filename)
tmp_dir = get_app_dir()
local_path = os.path.join(tmp_dir, local_filename)
# Download the file
with requests.get(self.download_url, stream=True, timeout=15) as r:
@@ -78,10 +79,9 @@ class UpdateDownloadThread(QThread):
if self.platform_name == 'darwin':
extract_folder = os.path.splitext(local_filename)[0]
extract_path = os.path.join(tmp_dir, extract_folder)
else:
extract_folder = os.path.splitext(local_filename)[0]
extract_path = os.path.join(os.getcwd(), extract_folder)
extract_path = os.path.join(get_app_dir(), extract_folder)
# Create the folder if not exists
os.makedirs(extract_path, exist_ok=True)
@@ -132,10 +132,7 @@ class UpdateCheckThread(QThread):
self.platform_name = platform_name
self.app_name = app_name
def run(self):
# if not getattr(sys, 'frozen', False):
# self.error_occurred.emit("Application is not frozen (Development mode).")
# return
def run(self) -> None:
try:
latest_version, download_url = self.get_latest_release_for_platform()
if not latest_version:
@@ -159,11 +156,10 @@ class UpdateCheckThread(QThread):
return [int(x) for x in v.split(".")]
return (normalize(v1) > normalize(v2)) - (normalize(v1) < normalize(v2))
def get_latest_release_for_platform(self):
def get_latest_release_for_platform(self) -> Tuple[Optional[str], Optional[str]]:
urls = [self.api_url, self.api_url_sec]
for url in urls:
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
releases = response.json()
@@ -219,11 +215,11 @@ class LocalPendingUpdateCheckThread(QThread):
return [int(x) for x in v.split(".")]
return (normalize(v1) > normalize(v2)) - (normalize(v1) < normalize(v2))
def run(self):
def run(self) -> None:
if self.platform_name == 'darwin':
cwd = f'/tmp/{self.app_name}tempupdate'
else:
cwd = os.getcwd()
cwd = get_app_dir()
pattern = re.compile(r".*-(\d+\.\d+\.\d+)" + re.escape(self.platform_suffix) + r"$")
found = False
@@ -239,7 +235,7 @@ class LocalPendingUpdateCheckThread(QThread):
self.pending_update_found.emit(folder_version, folder_path)
found = True
break
except:
except Exception:
pass
if not found:
@@ -272,9 +268,8 @@ class UpdateManager(QObject):
self.platform_suffix = platform_suffix
self.app_name = app_name
self.pending_update_version = None
self.pending_update_path = None
self.pending_update_version: Optional[str] = None
self.pending_update_path: Optional[str] = None
def manual_check_for_updates(self) -> None:
self.local_check_thread = LocalPendingUpdateCheckThread(self.current_version, self.platform_suffix, self.platform_name, self.app_name)
@@ -283,14 +278,15 @@ class UpdateManager(QObject):
self.local_check_thread.start()
def on_pending_update_found(self, version: str, folder_path: str) -> None:
self.main_window.statusBar().showMessage(f"Pending update found: version {version}")
if self.main_window.statusBar():
self.main_window.statusBar().showMessage(f"Pending update found: version {version}")
self.pending_update_version = version
self.pending_update_path = folder_path
self.show_pending_update_popup()
def on_no_pending_update(self) -> None:
# No pending update found locally, start server check directly
self.main_window.statusBar().showMessage("No pending local update found. Checking server...")
if self.main_window.statusBar():
self.main_window.statusBar().showMessage("No pending local update found. Checking server...")
self.start_update_check_thread()
def show_pending_update_popup(self) -> None:
@@ -306,7 +302,6 @@ class UpdateManager(QObject):
else:
if self.main_window.statusBar():
self.main_window.statusBar().showMessage("Pending update available. Install later.")
# After user dismisses, still check the server for new updates
self.start_update_check_thread()
def start_update_check_thread(self) -> None:
@@ -327,13 +322,15 @@ class UpdateManager(QObject):
if pending_version and pending_path:
cmp = self.version_compare(latest_version, pending_version)
if cmp > 0:
# Server version is newer than pending update
self.main_window.statusBar().showMessage(f"Newer version {latest_version} available on server. Removing old pending update...")
if self.main_window.statusBar():
self.main_window.statusBar().showMessage(f"Newer version {latest_version} available on server. Removing old pending update...")
try:
shutil.rmtree(pending_path)
self.main_window.statusBar().showMessage(f"Deleted old update folder: {pending_path}")
if self.main_window.statusBar():
self.main_window.statusBar().showMessage(f"Deleted old update folder: {pending_path}")
except Exception as e:
self.main_window.statusBar().showMessage(f"Failed to delete old update folder: {e}")
if self.main_window.statusBar():
self.main_window.statusBar().showMessage(f"Failed to delete old update folder: {e}")
# Clear pending update info so new download proceeds
self.pending_update_version = None
@@ -342,8 +339,8 @@ class UpdateManager(QObject):
# Download the new update
self.download_update(download_url, latest_version)
elif cmp == 0:
# Versions equal, no download needed
self.main_window.statusBar().showMessage(f"Pending update version {self.pending_update_version} is already latest. No download needed.")
if self.main_window.statusBar():
self.main_window.statusBar().showMessage(f"Pending update version {self.pending_update_version} is already latest. No download needed.")
else:
# Server version older than pending? Unlikely but just keep pending update
self.main_window.statusBar().showMessage(f"Pending update version {self.pending_update_version} is newer than server version. No action.")
@@ -374,24 +371,26 @@ class UpdateManager(QObject):
if msg_box.clickedButton() == install_now_button:
self.install_update(extract_folder)
else:
self.main_window.statusBar().showMessage("Update ready. Install later.")
if self.main_window.statusBar():
self.main_window.statusBar().showMessage("Update ready. Install later.")
def install_update(self, extract_folder: str) -> None:
# Path to updater executable
base_dir = get_app_dir()
# Path to updater executable
if self.platform_name == 'windows':
updater_path = os.path.join(os.getcwd(), f"{self.app_name}_updater.exe")
updater_path = os.path.join(base_dir, f"{self.app_name}_updater.exe")
elif self.platform_name == 'darwin':
if getattr(sys, 'frozen', False):
updater_path = os.path.join(os.path.dirname(sys.executable), f"../../../{self.app_name}_updater.app")
updater_path = os.path.join(base_dir, f"../../../{self.app_name}_updater.app")
else:
updater_path = os.path.join(os.getcwd(), f"../{self.app_name}_updater.app")
updater_path = os.path.join(base_dir, f"../{self.app_name}_updater.app")
elif self.platform_name == 'linux':
updater_path = os.path.join(os.getcwd(), f"{self.app_name}_updater")
updater_path = os.path.join(base_dir, f"{self.app_name}_updater")
else:
updater_path = os.getcwd()
updater_path = base_dir
updater_path = os.path.abspath(updater_path)
if not os.path.exists(updater_path):
QMessageBox.critical(self.main_window, "Error", f"Updater not found at:\n{updater_path}. The absolute path was {os.path.abspath(updater_path)}")
@@ -399,8 +398,7 @@ class UpdateManager(QObject):
# Launch updater with extracted folder path as argument
try:
# Pass current app's executable path for updater to relaunch
main_app_executable = os.path.abspath(sys.argv[0])
main_app_executable = sys.executable if getattr(sys, 'frozen', False) else os.path.abspath(sys.argv[0])
print(f'Launching updater with: "{updater_path}" "{extract_folder}" "{main_app_executable}"')
@@ -457,121 +455,131 @@ def wait_for_process_to_exit(process_name: str, timeout: int = 10) -> bool:
return False
def _remove_readonly(func: Callable[[str], Any], path: str, exc: Any) -> None:
"""Error handler for shutil.rmtree to clear read-only files on Windows."""
import stat
os.chmod(path, stat.S_IWRITE)
func(path)
def finish_update_if_needed(platform_name: str, app_name: str, cfg_path: str, finish_update: bool) -> None:
"""
Completes a pending application update if '--finish-update' is present in the command-line arguments.
"""
if finish_update:
print("Finishing update...")
update_cfg = configparser.ConfigParser()
try:
update_cfg.read(cfg_path)
if not finish_update:
return
print("Finishing update...")
# 1. Reset welcome dialog config flag
update_cfg = configparser.ConfigParser()
try:
if os.path.exists(cfg_path):
update_cfg.read(cfg_path)
if not update_cfg.has_section("Options"):
update_cfg.add_section("Options")
update_cfg.set("Options", "show_welcome_dialog", "true")
with open(cfg_path, "w") as f:
update_cfg.write(f)
print("Welcome dialog flag successfully reset to 'true' for next run.")
except Exception as e:
print(f"Warning: Could not update welcome dialog preference flag: {e}")
except Exception as e:
print(f"Warning: Could not update welcome dialog preference flag: {e}")
if platform_name == 'darwin':
app_dir = f'/tmp/{app_name}tempupdate'
else:
app_dir = os.getcwd()
app_dir = f'/tmp/{app_name}tempupdate' if platform_name == 'darwin' else get_app_dir()
# 1. Find update folder
update_folder = None
if not os.path.exists(app_dir):
print(f"App directory does not exist: {app_dir}. Skipping cleanup.")
return
# 2. Find update folder(s)
update_folders: List[str] = []
try:
for entry in os.listdir(app_dir):
entry_path = os.path.join(app_dir, entry)
if os.path.isdir(entry_path) and entry.startswith(f"{app_name}-") and entry.endswith("-" + platform_name):
update_folder = os.path.join(app_dir, entry)
break
update_folders.append(entry_path)
except Exception as e:
print(f"Error scanning app directory for update folders: {e}")
if update_folder is None:
print("No update folder found. Skipping update steps.")
return
if platform_name == 'darwin':
update_folder = os.path.join(update_folder, f"{app_name}-darwin")
if not update_folders:
print("No update folder found. Skipping update steps.")
return
# 2. Wait for updater to exit
print(f"Waiting for {app_name}_updater to exit...")
for proc in psutil.process_iter(['pid', 'name']):
if proc.info['name'] and f"{app_name}_updater" in proc.info['name'].lower():
try:
proc.wait(timeout=5)
except psutil.TimeoutExpired:
print(f"Force killing lingering {app_name}_updater")
proc.kill()
primary_update_folder = update_folders[0]
target_updater_folder = os.path.join(primary_update_folder, f"{app_name}-darwin") if platform_name == 'darwin' else primary_update_folder
# 3. Replace the updater
if platform_name == 'windows':
new_updater = os.path.join(update_folder, f"{app_name}_updater.exe")
dest_updater = os.path.join(app_dir, f"{app_name}_updater.exe")
elif platform_name == 'darwin':
new_updater = os.path.join(update_folder, f"{app_name}_updater.app")
dest_updater = os.path.abspath(os.path.join(sys.executable, f"../../../../{app_name}_updater.app"))
elif platform_name == 'linux':
new_updater = os.path.join(update_folder, f"{app_name}_updater")
dest_updater = os.path.join(app_dir, f"{app_name}_updater")
else:
print("Unknown Platform")
new_updater = os.getcwd()
dest_updater = os.getcwd()
print(f"New updater is {new_updater}")
print(f"Dest updater is {dest_updater}")
print("Writable?", os.access(dest_updater, os.W_OK))
print("Executable path:", sys.executable)
print("Trying to copy:", new_updater, "->", dest_updater)
if os.path.exists(new_updater):
try:
if os.path.exists(dest_updater):
if platform_name == 'darwin':
try:
if os.path.isdir(dest_updater):
shutil.rmtree(dest_updater)
print(f"Deleted directory: {dest_updater}")
else:
os.remove(dest_updater)
print(f"Deleted file: {dest_updater}")
except Exception as e:
print(f"Error deleting {dest_updater}: {e}")
else:
os.remove(dest_updater)
if platform_name == 'darwin':
wait_for_process_to_exit(f"{app_name}_updater", timeout=10)
subprocess.check_call(["ditto", new_updater, dest_updater])
else:
shutil.copy2(new_updater, dest_updater)
if platform_name in ('linux', 'darwin'):
os.chmod(dest_updater, 0o755)
if platform_name == 'darwin':
remove_quarantine(dest_updater, app_name)
print(f"{app_name}_updater replaced.")
except Exception as e:
print(f"Failed to replace {app_name}_updater: {e}")
# 4. Delete the update folder
# 3. Wait for updater process to exit
print(f"Waiting for {app_name}_updater to exit...")
updater_bin_name = f"{app_name}_updater"
for proc in psutil.process_iter(['pid', 'name']):
try:
if platform_name == 'darwin':
shutil.rmtree(app_dir)
else:
shutil.rmtree(update_folder)
except Exception as e:
print(f"Failed to delete update folder: {e}")
if proc.info['name'] and updater_bin_name.lower() in proc.info['name'].lower():
proc.wait(timeout=5)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
except psutil.TimeoutExpired:
print(f"Force killing lingering {app_name}_updater")
try:
proc.kill()
except Exception:
pass
# 4. Replace the updater executable
target_base = get_app_dir()
if platform_name == 'windows':
new_updater = os.path.join(target_updater_folder, f"{app_name}_updater.exe")
dest_updater = os.path.join(target_base, f"{app_name}_updater.exe")
elif platform_name == 'darwin':
new_updater = os.path.join(target_updater_folder, f"{app_name}_updater.app")
dest_updater = os.path.abspath(os.path.join(sys.executable, f"../../../../{app_name}_updater.app"))
elif platform_name == 'linux':
new_updater = os.path.join(target_updater_folder, f"{app_name}_updater")
dest_updater = os.path.join(target_base, f"{app_name}_updater")
else:
new_updater = target_base
dest_updater = target_base
if os.path.exists(new_updater):
try:
if os.path.exists(dest_updater):
if platform_name == 'darwin' and os.path.isdir(dest_updater):
shutil.rmtree(dest_updater, onexc=_remove_readonly)
else:
os.remove(dest_updater)
if platform_name == 'darwin':
wait_for_process_to_exit(f"{app_name}_updater", timeout=10)
subprocess.check_call(["ditto", new_updater, dest_updater])
else:
shutil.copy2(new_updater, dest_updater)
if platform_name in ('linux', 'darwin'):
os.chmod(dest_updater, 0o755)
if platform_name == 'darwin':
remove_quarantine(dest_updater, app_name)
print(f"{app_name}_updater replaced successfully.")
except Exception as e:
print(f"Failed to replace {app_name}_updater: {e}")
# 5. Clean up all temporary update folders (with retries for Windows file locks)
for folder in update_folders:
for attempt in range(3):
try:
if os.path.exists(folder):
shutil.rmtree(folder, onexc=_remove_readonly)
print(f"Successfully deleted update folder: {folder}")
break
except Exception as e:
if attempt < 2:
time.sleep(1.0)
else:
print(f"Failed to delete update folder '{folder}' after 3 attempts: {e}")
if "--finish-update" in sys.argv:
sys.argv.remove("--finish-update")