initial commit
This commit is contained in:
255
flares_updater.py
Normal file
255
flares_updater.py
Normal file
@@ -0,0 +1,255 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user