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
+1
View File
@@ -175,6 +175,7 @@ cython_debug/
.pypirc .pypirc
/individual_images /individual_images
/plugins
*.xlsx *.xlsx
*.csv *.csv
*.snirf *.snirf
+5
View File
@@ -1,3 +1,8 @@
# Version 1.7.1
- Fixed an issue where the application could not automatically update between versions 1.5.1 to 1.7.1. Sorry!
# Version 1.7.0 # Version 1.7.0
- This is potentially a save-changing release due to adding more data into the save file. Please update your project files to ensure compatibility - This is potentially a save-changing release due to adding more data into the save file. Please update your project files to ensure compatibility
+52 -32
View File
@@ -20,21 +20,24 @@ from pathlib import Path
from datetime import datetime from datetime import datetime
# External library imports # External library imports
from src.shared.shareddata import APP_NAME, PLATFORM_NAME from src.shared.shareddata import APP_NAME, PLATFORM_NAME, get_app_dir
if PLATFORM_NAME == 'darwin': if PLATFORM_NAME == 'darwin':
_log_path = os.path.join(os.path.dirname(sys.executable), f"../../../{APP_NAME}_updater.log") _log_path = os.path.join(os.path.dirname(sys.executable), f"../../../{APP_NAME}_updater.log")
else: else:
_log_path = os.path.join(os.getcwd(), f"{APP_NAME}_updater.log") _log_path = os.path.join(get_app_dir(), f"{APP_NAME}_updater.log")
LOG_FILE = _log_path LOG_FILE = _log_path
def log(msg: str) -> None: def log(msg: str) -> None:
with open(LOG_FILE, "a", encoding="utf-8") as f: try:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"{timestamp} - {msg}\n") timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
f.write(f"{timestamp} - {msg}\n")
except Exception:
pass
def kill_all_processes_by_executable(exe_path: Union[str, Path]) -> bool: def kill_all_processes_by_executable(exe_path: Union[str, Path]) -> bool:
@@ -45,22 +48,28 @@ def kill_all_processes_by_executable(exe_path: Union[str, Path]) -> bool:
for proc in psutil.process_iter(['pid', 'exe']): for proc in psutil.process_iter(['pid', 'exe']):
try: try:
proc_exe = proc.info.get('exe') proc_exe = proc.info.get('exe')
if proc_exe and os.path.samefile(os.path.realpath(proc_exe), exe_path): if proc_exe and os.path.exists(proc_exe) and os.path.exists(exe_path):
log(f"Terminating process: PID {proc.pid}") if os.path.samefile(os.path.realpath(proc_exe), exe_path):
_terminate_process(proc) log(f"Terminating process: PID {proc.pid}")
terminated_any = True _terminate_process(proc)
terminated_any = True
except (psutil.NoSuchProcess, psutil.AccessDenied, FileNotFoundError) as e:
continue
except Exception as e: except Exception as e:
log(f"Error terminating process (Windows): {e}") log(f"Error terminating process (Windows): {e}")
elif PLATFORM_NAME == 'linux': elif PLATFORM_NAME == 'linux':
for proc in psutil.process_iter(['pid', 'cmdline']): for proc in psutil.process_iter(['pid', 'cmdline']):
try: try:
cmdline = proc.info.get('cmdline', []) cmdline = proc.info.get('cmdline', [])
if cmdline: if cmdline and os.path.exists(cmdline[0]) and os.path.exists(exe_path):
proc_cmd = os.path.realpath(cmdline[0]) proc_cmd = os.path.realpath(cmdline[0])
if os.path.samefile(proc_cmd, exe_path): if os.path.samefile(proc_cmd, exe_path):
log(f"Terminating process: PID {proc.pid}") log(f"Terminating process: PID {proc.pid}")
_terminate_process(proc) _terminate_process(proc)
terminated_any = True terminated_any = True
except (psutil.NoSuchProcess, psutil.AccessDenied, FileNotFoundError) as e:
continue
except Exception as e: except Exception as e:
log(f"Error terminating process (Linux): {e}") log(f"Error terminating process (Linux): {e}")
@@ -82,6 +91,9 @@ def _terminate_process(proc: psutil.Process) -> None:
def wait_for_unlock(path: Union[str, Path], timeout: Union[int, float] = 100) -> None: def wait_for_unlock(path: Union[str, Path], timeout: Union[int, float] = 100) -> None:
if not os.path.exists(path):
return
start_time = time.time() start_time = time.time()
while time.time() - start_time < timeout: while time.time() - start_time < timeout:
try: try:
@@ -95,7 +107,7 @@ def wait_for_unlock(path: Union[str, Path], timeout: Union[int, float] = 100) ->
log(f"Still locked: {path} - {e}") log(f"Still locked: {path} - {e}")
time.sleep(1) time.sleep(1)
log(f"Failed to delete after wait: {path}") log(f"Failed to delete after wait: {path}")
def delete_path(path: Union[str, Path]) -> None: def delete_path(path: Union[str, Path]) -> None:
if os.path.exists(path): if os.path.exists(path):
@@ -130,8 +142,8 @@ def copy_update_files(src_folder: Union[str, Path], dest_folder: Union[str, Path
def copy_update_files_darwin(src_folder: Union[str, Path], dest_folder: Union[str, Path], updater_name: str) -> None: def copy_update_files_darwin(src_folder: Union[str, Path], dest_folder: Union[str, Path], updater_name: str) -> None:
if not updater_name.endswith(".app"):
updater_name = updater_name + ".app" updater_name = updater_name + ".app"
for item in os.listdir(src_folder): for item in os.listdir(src_folder):
if item.lower() == updater_name.lower(): if item.lower() == updater_name.lower():
@@ -152,10 +164,7 @@ def copy_update_files_darwin(src_folder: Union[str, Path], dest_folder: Union[st
def remove_quarantine(app_path: Union[str, Path]) -> bool: def remove_quarantine(app_path: Union[str, Path]) -> bool:
"""Removes the macOS quarantine extended attribute from an application bundle using osascript. """Removes the macOS quarantine extended attribute from an application bundle using osascript."""
Returns True on success, False on error or cancellation.
"""
clean_path: str = str(app_path) clean_path: str = str(app_path)
escaped_path: str = shlex.quote(clean_path) escaped_path: str = shlex.quote(clean_path)
@@ -172,7 +181,7 @@ def remove_quarantine(app_path: Union[str, Path]) -> bool:
return False return False
def main(): def main() -> None:
main_exe: str = "" main_exe: str = ""
app_dir: Path = Path() app_dir: Path = Path()
bundle_dir: Path = Path() bundle_dir: Path = Path()
@@ -189,17 +198,24 @@ def main():
main_exe = sys.argv[2] main_exe = sys.argv[2]
main_exe_path = Path(main_exe).resolve() main_exe_path = Path(main_exe).resolve()
app_dir = main_exe_path.parent app_dir = main_exe_path.parent
bundle_dir = main_exe_path.parents[2]
parent_bundle_dir = main_exe_path.parents[3]
updater_name = os.path.basename(sys.argv[0]) if PLATFORM_NAME == 'darwin':
# Bundle hierarchy on macOS: App.app/Contents/MacOS/executable
if len(main_exe_path.parents) >= 4:
bundle_dir = main_exe_path.parents[2]
parent_bundle_dir = main_exe_path.parents[3]
else:
bundle_dir = app_dir
parent_bundle_dir = app_dir.parent
updater_name = os.path.basename(sys.executable if getattr(sys, 'frozen', False) else sys.argv[0])
log("Updater started.") log("Updater started.")
log(f"Update folder: {update_folder}") log(f"Update folder: {update_folder}")
log(f"Main EXE: {main_exe}") log(f"Main EXE: {main_exe}")
log(f"Updater EXE: {updater_name}") log(f"Updater EXE: {updater_name}")
if PLATFORM_NAME == 'darwin': if PLATFORM_NAME == 'darwin' and bundle_dir:
log(f"Main App Folder: {bundle_dir}") log(f"Main App Folder: {bundle_dir}")
# Kill all instances of main app # Kill all instances of main app
@@ -212,18 +228,22 @@ def main():
try: try:
if PLATFORM_NAME == 'windows': if PLATFORM_NAME == 'windows':
proc_exe = proc.info.get('exe') proc_exe = proc.info.get('exe')
if proc_exe and os.path.samefile(os.path.realpath(proc_exe), os.path.realpath(main_exe)): if proc_exe and os.path.exists(proc_exe) and os.path.exists(main_exe):
running = True if os.path.samefile(os.path.realpath(proc_exe), os.path.realpath(main_exe)):
break running = True
break
elif PLATFORM_NAME == 'linux': elif PLATFORM_NAME == 'linux':
cmdline = proc.info.get('cmdline', []) cmdline = proc.info.get('cmdline', [])
if cmdline: if cmdline and os.path.exists(cmdline[0]) and os.path.exists(main_exe):
proc_cmd = os.path.realpath(cmdline[0]) proc_cmd = os.path.realpath(cmdline[0])
if os.path.samefile(proc_cmd, os.path.realpath(main_exe)): if os.path.samefile(proc_cmd, os.path.realpath(main_exe)):
running = True running = True
break break
except (psutil.NoSuchProcess, psutil.AccessDenied, FileNotFoundError):
continue
except Exception as e: except Exception as e:
log(f"Polling error: {e}") log(f"Polling error: {e}")
if not running: if not running:
break break
time.sleep(0.5) time.sleep(0.5)
@@ -231,11 +251,11 @@ def main():
log("Warning: main executable still running after wait timeout.") log("Warning: main executable still running after wait timeout.")
# Delete old version files # Delete old version files
if PLATFORM_NAME == 'darwin': if PLATFORM_NAME == 'darwin' and bundle_dir and parent_bundle_dir:
log(f'Attempting to delete {bundle_dir}') log(f'Attempting to delete {bundle_dir}')
delete_path(str(bundle_dir)) delete_path(str(bundle_dir))
update_folder = os.path.join(sys.argv[1], f"{APP_NAME}-darwin") extracted_update_folder = os.path.join(update_folder, f"{APP_NAME}-darwin")
copy_update_files_darwin(update_folder, str(parent_bundle_dir), updater_name) copy_update_files_darwin(extracted_update_folder, str(parent_bundle_dir), updater_name)
else: else:
delete_path(main_exe) delete_path(main_exe)
@@ -252,8 +272,8 @@ def main():
if PLATFORM_NAME == 'linux': if PLATFORM_NAME == 'linux':
os.chmod(main_exe, 0o755) os.chmod(main_exe, 0o755)
log("Added executable bit") log("Added executable bit")
if PLATFORM_NAME == 'darwin': if PLATFORM_NAME == 'darwin' and bundle_dir:
os.chmod(str(bundle_dir), 0o755) os.chmod(str(bundle_dir), 0o755)
log("Added executable bit") log("Added executable bit")
remove_quarantine(str(bundle_dir)) remove_quarantine(str(bundle_dir))
+143 -135
View File
@@ -18,7 +18,8 @@ import zipfile
import traceback import traceback
import subprocess import subprocess
import configparser import configparser
from typing import List from typing import List, Tuple, Optional, Callable, Any
# External library imports # External library imports
import psutil import psutil
@@ -26,7 +27,7 @@ import requests
from PySide6.QtCore import QThread, Signal, QObject from PySide6.QtCore import QThread, Signal, QObject
from PySide6.QtWidgets import QMainWindow, QMessageBox from PySide6.QtWidgets import QMainWindow, QMessageBox
from src.shared.shareddata import get_app_dir
class UpdateDownloadThread(QThread): class UpdateDownloadThread(QThread):
""" """
@@ -54,7 +55,7 @@ class UpdateDownloadThread(QThread):
self.platform_name = platform_name self.platform_name = platform_name
self.app_name = app_name self.app_name = app_name
def run(self): def run(self) -> None:
try: try:
local_filename = os.path.basename(self.download_url) local_filename = os.path.basename(self.download_url)
@@ -63,8 +64,8 @@ class UpdateDownloadThread(QThread):
os.makedirs(tmp_dir, exist_ok=True) os.makedirs(tmp_dir, exist_ok=True)
local_path = os.path.join(tmp_dir, local_filename) local_path = os.path.join(tmp_dir, local_filename)
else: else:
tmp_dir = os.getcwd() tmp_dir = get_app_dir()
local_path = os.path.join(os.getcwd(), local_filename) local_path = os.path.join(tmp_dir, local_filename)
# Download the file # Download the file
with requests.get(self.download_url, stream=True, timeout=15) as r: with requests.get(self.download_url, stream=True, timeout=15) as r:
@@ -78,10 +79,9 @@ class UpdateDownloadThread(QThread):
if self.platform_name == 'darwin': if self.platform_name == 'darwin':
extract_folder = os.path.splitext(local_filename)[0] extract_folder = os.path.splitext(local_filename)[0]
extract_path = os.path.join(tmp_dir, extract_folder) extract_path = os.path.join(tmp_dir, extract_folder)
else: else:
extract_folder = os.path.splitext(local_filename)[0] 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 # Create the folder if not exists
os.makedirs(extract_path, exist_ok=True) os.makedirs(extract_path, exist_ok=True)
@@ -132,10 +132,7 @@ class UpdateCheckThread(QThread):
self.platform_name = platform_name self.platform_name = platform_name
self.app_name = app_name self.app_name = app_name
def run(self): def run(self) -> None:
# if not getattr(sys, 'frozen', False):
# self.error_occurred.emit("Application is not frozen (Development mode).")
# return
try: try:
latest_version, download_url = self.get_latest_release_for_platform() latest_version, download_url = self.get_latest_release_for_platform()
if not latest_version: if not latest_version:
@@ -159,11 +156,10 @@ class UpdateCheckThread(QThread):
return [int(x) for x in v.split(".")] return [int(x) for x in v.split(".")]
return (normalize(v1) > normalize(v2)) - (normalize(v1) < normalize(v2)) 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] urls = [self.api_url, self.api_url_sec]
for url in urls: for url in urls:
try: try:
response = requests.get(url, timeout=5) response = requests.get(url, timeout=5)
response.raise_for_status() response.raise_for_status()
releases = response.json() releases = response.json()
@@ -219,11 +215,11 @@ class LocalPendingUpdateCheckThread(QThread):
return [int(x) for x in v.split(".")] return [int(x) for x in v.split(".")]
return (normalize(v1) > normalize(v2)) - (normalize(v1) < normalize(v2)) return (normalize(v1) > normalize(v2)) - (normalize(v1) < normalize(v2))
def run(self): def run(self) -> None:
if self.platform_name == 'darwin': if self.platform_name == 'darwin':
cwd = f'/tmp/{self.app_name}tempupdate' cwd = f'/tmp/{self.app_name}tempupdate'
else: else:
cwd = os.getcwd() cwd = get_app_dir()
pattern = re.compile(r".*-(\d+\.\d+\.\d+)" + re.escape(self.platform_suffix) + r"$") pattern = re.compile(r".*-(\d+\.\d+\.\d+)" + re.escape(self.platform_suffix) + r"$")
found = False found = False
@@ -239,7 +235,7 @@ class LocalPendingUpdateCheckThread(QThread):
self.pending_update_found.emit(folder_version, folder_path) self.pending_update_found.emit(folder_version, folder_path)
found = True found = True
break break
except: except Exception:
pass pass
if not found: if not found:
@@ -272,9 +268,8 @@ class UpdateManager(QObject):
self.platform_suffix = platform_suffix self.platform_suffix = platform_suffix
self.app_name = app_name self.app_name = app_name
self.pending_update_version = None self.pending_update_version: Optional[str] = None
self.pending_update_path = None self.pending_update_path: Optional[str] = None
def manual_check_for_updates(self) -> 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) 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() self.local_check_thread.start()
def on_pending_update_found(self, version: str, folder_path: str) -> None: 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_version = version
self.pending_update_path = folder_path self.pending_update_path = folder_path
self.show_pending_update_popup() self.show_pending_update_popup()
def on_no_pending_update(self) -> None: def on_no_pending_update(self) -> None:
# No pending update found locally, start server check directly if self.main_window.statusBar():
self.main_window.statusBar().showMessage("No pending local update found. Checking server...") self.main_window.statusBar().showMessage("No pending local update found. Checking server...")
self.start_update_check_thread() self.start_update_check_thread()
def show_pending_update_popup(self) -> None: def show_pending_update_popup(self) -> None:
@@ -306,7 +302,6 @@ class UpdateManager(QObject):
else: else:
if self.main_window.statusBar(): if self.main_window.statusBar():
self.main_window.statusBar().showMessage("Pending update available. Install later.") 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() self.start_update_check_thread()
def start_update_check_thread(self) -> None: def start_update_check_thread(self) -> None:
@@ -327,13 +322,15 @@ class UpdateManager(QObject):
if pending_version and pending_path: if pending_version and pending_path:
cmp = self.version_compare(latest_version, pending_version) cmp = self.version_compare(latest_version, pending_version)
if cmp > 0: if cmp > 0:
# Server version is newer than pending update if self.main_window.statusBar():
self.main_window.statusBar().showMessage(f"Newer version {latest_version} available on server. Removing old pending update...") self.main_window.statusBar().showMessage(f"Newer version {latest_version} available on server. Removing old pending update...")
try: try:
shutil.rmtree(pending_path) 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: 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 # Clear pending update info so new download proceeds
self.pending_update_version = None self.pending_update_version = None
@@ -342,8 +339,8 @@ class UpdateManager(QObject):
# Download the new update # Download the new update
self.download_update(download_url, latest_version) self.download_update(download_url, latest_version)
elif cmp == 0: elif cmp == 0:
# Versions equal, 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.") self.main_window.statusBar().showMessage(f"Pending update version {self.pending_update_version} is already latest. No download needed.")
else: else:
# Server version older than pending? Unlikely but just keep pending update # 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.") 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: if msg_box.clickedButton() == install_now_button:
self.install_update(extract_folder) self.install_update(extract_folder)
else: 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: 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': 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': elif self.platform_name == 'darwin':
if getattr(sys, 'frozen', False): 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: 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': 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: else:
updater_path = os.getcwd() updater_path = base_dir
updater_path = os.path.abspath(updater_path)
if not os.path.exists(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)}") 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 # Launch updater with extracted folder path as argument
try: try:
# Pass current app's executable path for updater to relaunch main_app_executable = sys.executable if getattr(sys, 'frozen', False) else os.path.abspath(sys.argv[0])
main_app_executable = os.path.abspath(sys.argv[0])
print(f'Launching updater with: "{updater_path}" "{extract_folder}" "{main_app_executable}"') 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 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: 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. Completes a pending application update if '--finish-update' is present in the command-line arguments.
""" """
if finish_update: if not finish_update:
print("Finishing update...") return
update_cfg = configparser.ConfigParser() print("Finishing update...")
try:
update_cfg.read(cfg_path) # 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") update_cfg.set("Options", "show_welcome_dialog", "true")
with open(cfg_path, "w") as f: with open(cfg_path, "w") as f:
update_cfg.write(f) update_cfg.write(f)
print("Welcome dialog flag successfully reset to 'true' for next run.") print("Welcome dialog flag successfully reset to 'true' for next run.")
except Exception as e: except Exception as e:
print(f"Warning: Could not update welcome dialog preference flag: {e}") print(f"Warning: Could not update welcome dialog preference flag: {e}")
if platform_name == 'darwin': app_dir = f'/tmp/{app_name}tempupdate' if platform_name == 'darwin' else get_app_dir()
app_dir = f'/tmp/{app_name}tempupdate'
else:
app_dir = os.getcwd()
# 1. Find update folder if not os.path.exists(app_dir):
update_folder = None 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): for entry in os.listdir(app_dir):
entry_path = os.path.join(app_dir, entry) 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): 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) update_folders.append(entry_path)
break except Exception as e:
print(f"Error scanning app directory for update folders: {e}")
if update_folder is None: if not update_folders:
print("No update folder found. Skipping update steps.") print("No update folder found. Skipping update steps.")
return return
if platform_name == 'darwin':
update_folder = os.path.join(update_folder, f"{app_name}-darwin")
# 2. Wait for updater to exit primary_update_folder = update_folders[0]
print(f"Waiting for {app_name}_updater to exit...") target_updater_folder = os.path.join(primary_update_folder, f"{app_name}-darwin") if platform_name == 'darwin' else primary_update_folder
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()
# 3. Replace the updater # 3. Wait for updater process to exit
if platform_name == 'windows': print(f"Waiting for {app_name}_updater to exit...")
new_updater = os.path.join(update_folder, f"{app_name}_updater.exe") updater_bin_name = f"{app_name}_updater"
dest_updater = os.path.join(app_dir, f"{app_name}_updater.exe") for proc in psutil.process_iter(['pid', 'name']):
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
try: try:
if platform_name == 'darwin': if proc.info['name'] and updater_bin_name.lower() in proc.info['name'].lower():
shutil.rmtree(app_dir) proc.wait(timeout=5)
else: except (psutil.NoSuchProcess, psutil.AccessDenied):
shutil.rmtree(update_folder) pass
except Exception as e: except psutil.TimeoutExpired:
print(f"Failed to delete update folder: {e}") 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") sys.argv.remove("--finish-update")