255 lines
8.4 KiB
Python
255 lines
8.4 KiB
Python
"""
|
|
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 <update_folder> <main_app_executable>")
|
|
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() |