""" Filename: flares_updater.py Description: FLARES updater executable Author: Tyler de Zeeuw License: GPL-3.0 """ # Built-in imports import os import sys import time import shlex import psutil import shutil import platform import subprocess from datetime import datetime PLATFORM_NAME = platform.system().lower() if PLATFORM_NAME == 'darwin': LOG_FILE = os.path.join(os.path.dirname(sys.executable), "../../../flares_updater.log") else: LOG_FILE = os.path.join(os.getcwd(), "flares_updater.log") def log(msg): with open(LOG_FILE, "a", encoding="utf-8") as f: timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") f.write(f"{timestamp} - {msg}\n") def kill_all_processes_by_executable(exe_path): terminated_any = False exe_path = os.path.realpath(exe_path) if PLATFORM_NAME == 'windows': for proc in psutil.process_iter(['pid', 'exe']): try: proc_exe = proc.info.get('exe') if proc_exe and os.path.samefile(os.path.realpath(proc_exe), exe_path): log(f"Terminating process: PID {proc.pid}") _terminate_process(proc) terminated_any = True except Exception as e: log(f"Error terminating process (Windows): {e}") elif PLATFORM_NAME == 'linux': for proc in psutil.process_iter(['pid', 'cmdline']): try: cmdline = proc.info.get('cmdline', []) if cmdline: proc_cmd = os.path.realpath(cmdline[0]) if os.path.samefile(proc_cmd, exe_path): log(f"Terminating process: PID {proc.pid}") _terminate_process(proc) terminated_any = True except Exception as e: log(f"Error terminating process (Linux): {e}") if not terminated_any: log(f"No running processes found for {exe_path}") return terminated_any def _terminate_process(proc): try: proc.terminate() proc.wait(timeout=10) log(f"Process {proc.pid} terminated gracefully.") except psutil.TimeoutExpired: log(f"Process {proc.pid} did not terminate in time. Killing forcefully.") proc.kill() proc.wait(timeout=5) log(f"Process {proc.pid} killed.") def wait_for_unlock(path, timeout=100): start_time = time.time() while time.time() - start_time < timeout: try: if os.path.isdir(path): shutil.rmtree(path) else: os.remove(path) log(f"Deleted (after wait): {path}") return except Exception as e: log(f"Still locked: {path} - {e}") time.sleep(1) log(f"Failed to delete after wait: {path}") def delete_path(path): if os.path.exists(path): try: if os.path.isdir(path): shutil.rmtree(path) log(f"Deleted directory: {path}") else: os.remove(path) log(f"Deleted file: {path}") except Exception as e: log(f"Error deleting {path}: {e}") def copy_update_files(src_folder, dest_folder, updater_name): for item in os.listdir(src_folder): if item.lower() == updater_name.lower(): log(f"Skipping updater executable: {item}") continue s = os.path.join(src_folder, item) d = os.path.join(dest_folder, item) delete_path(d) try: if os.path.isdir(s): shutil.copytree(s, d) log(f"Copied folder: {s} -> {d}") else: shutil.copy2(s, d) log(f"Copied file: {s} -> {d}") except Exception as e: log(f"Error copying {s} -> {d}: {e}") def copy_update_files_darwin(src_folder, dest_folder, updater_name): updater_name = updater_name + ".app" for item in os.listdir(src_folder): if item.lower() == updater_name.lower(): log(f"Skipping updater executable: {item}") continue s = os.path.join(src_folder, item) d = os.path.join(dest_folder, item) delete_path(d) try: if os.path.isdir(s): subprocess.check_call(["ditto", s, d]) log(f"Copied folder with ditto: {s} -> {d}") else: shutil.copy2(s, d) log(f"Copied file: {s} -> {d}") except Exception as e: log(f"Error copying {s} -> {d}: {e}") def remove_quarantine(app_path): script = f''' do shell script "xattr -d -r com.apple.quarantine {shlex.quote(app_path)}" with administrator privileges with prompt "FLARES needs privileges to finish the update. (1/2)" ''' try: subprocess.run(['osascript', '-e', script], check=True) print("✅ Quarantine attribute removed.") except subprocess.CalledProcessError as e: print("❌ Failed to remove quarantine attribute.") print(e) def main(): try: log(f"[Updater] sys.argv: {sys.argv}") if len(sys.argv) != 3: log("Invalid arguments. Usage: flares_updater ") sys.exit(1) update_folder = sys.argv[1] main_exe = sys.argv[2] # Interesting naming convention parent_dir = os.path.dirname(os.path.abspath(main_exe)) pparent_dir = os.path.dirname(parent_dir) ppparent_dir = os.path.dirname(pparent_dir) pppparent_dir = os.path.dirname(ppparent_dir) updater_name = os.path.basename(sys.argv[0]) log("Updater started.") log(f"Update folder: {update_folder}") log(f"Main EXE: {main_exe}") log(f"Updater EXE: {updater_name}") if PLATFORM_NAME == 'darwin': log(f"Main App Folder: {ppparent_dir}") # Kill all instances of main app kill_all_processes_by_executable(main_exe) # Wait until main_exe process is fully gone (polling) for _ in range(20): # wait max 10 seconds running = False for proc in psutil.process_iter(['exe', 'cmdline']): try: if PLATFORM_NAME == 'windows': proc_exe = proc.info.get('exe') if proc_exe and os.path.samefile(os.path.realpath(proc_exe), os.path.realpath(main_exe)): running = True break elif PLATFORM_NAME == 'linux': cmdline = proc.info.get('cmdline', []) if cmdline: proc_cmd = os.path.realpath(cmdline[0]) if os.path.samefile(proc_cmd, os.path.realpath(main_exe)): running = True break except Exception as e: log(f"Polling error: {e}") if not running: break time.sleep(0.5) else: log("Warning: main executable still running after wait timeout.") # Delete old version files if PLATFORM_NAME == 'darwin': log(f'Attempting to delete {ppparent_dir}') delete_path(ppparent_dir) update_folder = os.path.join(sys.argv[1], "flares-darwin") copy_update_files_darwin(update_folder, pppparent_dir, updater_name) else: delete_path(main_exe) wait_for_unlock(os.path.join(parent_dir, "_internal")) # Copy new files excluding the updater itself copy_update_files(update_folder, parent_dir, updater_name) except Exception as e: log(f"Something went wrong: {e}") # Relaunch main app try: if PLATFORM_NAME == 'linux': os.chmod(main_exe, 0o755) log("Added executable bit") if PLATFORM_NAME == 'darwin': os.chmod(ppparent_dir, 0o755) log("Added executable bit") remove_quarantine(ppparent_dir) log(f"Removed the quarantine flag on {ppparent_dir}") subprocess.Popen(['open', ppparent_dir, "--args", "--finish-update"]) else: subprocess.Popen([main_exe, "--finish-update"], cwd=parent_dir) log("Relaunched main app.") except Exception as e: log(f"Failed to relaunch main app: {e}") log("Updater completed. Exiting.") sys.exit(0) if __name__ == "__main__": main()