|
14 | 14 | def apply_pending_update() -> None: |
15 | 15 | update_zip = os.path.join(DATA_DIR, ".pending_update.zip") |
16 | 16 | if not os.path.exists(update_zip): |
| 17 | + print(f"[Update] No pending update at {update_zip}; skipping.") |
17 | 18 | return |
| 19 | + print(f"[Update] Applying pending update from {update_zip} " |
| 20 | + f"(size {os.path.getsize(update_zip)} bytes)...") |
18 | 21 |
|
19 | 22 | # The update zip mirrors the install layout. Extract one level above |
20 | 23 | # the install dir so the launchscript and binary both get replaced. |
| 24 | + # Linux refuses to overwrite a running binary with O_TRUNC (ETXTBSY on |
| 25 | + # overlay-style filesystems used by ROCKNIX/Knulli/MuOS), so write each |
| 26 | + # entry to a sibling .tmp and os.replace() it onto the target. The |
| 27 | + # inode swap leaves the currently-running process undisturbed and the |
| 28 | + # new file is in place for the next launch. |
21 | 29 | target = os.path.abspath(os.path.join(INSTALL_DIR, "..")) |
22 | 30 | try: |
23 | 31 | with zipfile.ZipFile(update_zip, "r") as zf: |
24 | | - zf.extractall(target) |
| 32 | + for entry in zf.infolist(): |
| 33 | + dest = os.path.join(target, entry.filename) |
| 34 | + if entry.is_dir(): |
| 35 | + os.makedirs(dest, exist_ok=True) |
| 36 | + continue |
| 37 | + os.makedirs(os.path.dirname(dest), exist_ok=True) |
| 38 | + tmp = dest + ".tmp" |
| 39 | + with zf.open(entry, "r") as src, open(tmp, "wb") as dst: |
| 40 | + while True: |
| 41 | + chunk = src.read(64 * 1024) |
| 42 | + if not chunk: |
| 43 | + break |
| 44 | + dst.write(chunk) |
| 45 | + # Preserve original mode bits (e.g. 0o755 on the binary). |
| 46 | + mode = (entry.external_attr >> 16) & 0o777 |
| 47 | + if mode: |
| 48 | + os.chmod(tmp, mode) |
| 49 | + os.replace(tmp, dest) |
25 | 50 | os.remove(update_zip) |
26 | 51 | print(f"[Update] Applied pending update to {target}.") |
27 | 52 | except (zipfile.BadZipFile, OSError) as e: |
28 | 53 | print(f"[Update] Failed to apply pending update: {e}", file=sys.stderr) |
| 54 | + # Drop the zip so we don't keep retrying a broken/partial download. |
| 55 | + try: |
| 56 | + os.remove(update_zip) |
| 57 | + except OSError: |
| 58 | + pass |
29 | 59 |
|
30 | 60 |
|
31 | 61 | apply_pending_update() |
|
0 commit comments