fix to app not updating
This commit is contained in:
@@ -175,6 +175,7 @@ cython_debug/
|
|||||||
.pypirc
|
.pypirc
|
||||||
|
|
||||||
/individual_images
|
/individual_images
|
||||||
|
/plugins
|
||||||
*.xlsx
|
*.xlsx
|
||||||
*.csv
|
*.csv
|
||||||
*.snirf
|
*.snirf
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user