Make self-update backups skip runtime sockets
Treat live usr runtime artifacts as non-blocking during self-update backups. Skip sockets, device nodes, vanished files, and unreadable entries with log messages so update rollback checks are not tripped by active Desktop profile state.
Alessandro committed
May 12, 2026 at 16:46 UTC
7ba1d61e341903a9274f9074c7caea4f3d19f1de
2 files changed
+91
-3
docker/run/fs/exe/self_update_manager.py
+43
-3
@@ -8,6 +8,7 @@ import os
8
import re
9
import shutil
10
import signal
11
+import stat
12
import subprocess
13
import sys
14
import tempfile
@@ -366,11 +367,15 @@ def create_usr_backup(
367
root_path = Path(root)
368
for filename in files:
369
source_file = root_path / filename
369
- if source_file.is_symlink() and not source_file.exists():
370
- logger.log(f"Skipping broken symlink during usr backup: {source_file}")
370
+ if not should_include_usr_backup_entry(source_file, logger):
371
continue
372
archive_name = Path("usr") / source_file.relative_to(usr_dir)
373
- archive.write(source_file, archive_name.as_posix())
373
+ try:
374
+ archive.write(source_file, archive_name.as_posix())
375
+ except FileNotFoundError:
376
+ logger.log(f"Skipping vanished usr backup entry: {source_file}")
377
+ except OSError as exc:
378
+ logger.log(f"Skipping usr backup entry after read error: {source_file}: {exc}")
379
380
destination.parent.mkdir(parents=True, exist_ok=True)
381
shutil.move(str(temporary_backup), str(destination))
@@ -381,6 +386,41 @@ def create_usr_backup(
386
temporary_backup.unlink(missing_ok=True)
387
388
389
+def should_include_usr_backup_entry(source_file: Path, logger: AttemptLogger) -> bool:
390
+ try:
391
+ source_stat = source_file.lstat()
392
+ except FileNotFoundError:
393
+ logger.log(f"Skipping vanished usr backup entry: {source_file}")
394
+ return False
395
+ except OSError as exc:
396
+ logger.log(f"Skipping unreadable usr backup entry: {source_file}: {exc}")
397
+ return False
398
+
399
+ if stat.S_ISLNK(source_stat.st_mode):
400
+ try:
401
+ target_stat = source_file.stat()
402
+ except FileNotFoundError:
403
+ logger.log(f"Skipping broken symlink during usr backup: {source_file}")
404
+ return False
405
+ except OSError as exc:
406
+ logger.log(
407
+ f"Skipping unreadable symlink target during usr backup: {source_file}: {exc}"
408
+ )
409
+ return False
410
+ if not stat.S_ISREG(target_stat.st_mode):
411
+ logger.log(
412
+ f"Skipping non-regular symlink target during usr backup: {source_file}"
413
+ )
414
+ return False
415
+ return True
416
+
417
+ if not stat.S_ISREG(source_stat.st_mode):
418
+ logger.log(f"Skipping non-regular usr backup entry: {source_file}")
419
+ return False
420
+
421
+ return True
422
+
423
+
424
def run_command(
425
command: list[str],
426
*,
tests/test_self_update_tag_filter.py
+48
@@ -1,5 +1,7 @@
1
import importlib.util
2
+import socket
3
import sys
4
+import tempfile
5
import types
6
import zipfile
7
from pathlib import Path
@@ -781,6 +783,52 @@ def test_self_update_manager_usr_backup_skips_broken_symlinks(tmp_path):
783
assert "usr/workdir/reachy-mini-mcp/.venv/bin/python" not in names
784
785
786
+def test_self_update_manager_usr_backup_skips_runtime_sockets():
787
+ manager = load_self_update_manager()
788
+ with tempfile.TemporaryDirectory(prefix="a0su-", dir="/tmp") as temp_root:
789
+ repo_dir = Path(temp_root) / "repo"
790
+ usr_dir = repo_dir / "usr"
791
+ gnupg_dir = (
792
+ usr_dir
793
+ / "plugins"
794
+ / "_desktop"
795
+ / "profiles"
796
+ / "agent-zero-desktop"
797
+ / ".gnupg"
798
+ )
799
+ gnupg_dir.mkdir(parents=True)
800
+ (usr_dir / "settings.json").write_text('{"ok": true}\n', encoding="utf-8")
801
+ socket_path = gnupg_dir / "S.gpg-agent"
802
+ messages = []
803
+
804
+ class ListLogger:
805
+ def log(self, message=""):
806
+ messages.append(message)
807
+
808
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as runtime_socket:
809
+ runtime_socket.bind(str(socket_path))
810
+ backup_path = manager.create_usr_backup(
811
+ repo_dir=repo_dir,
812
+ backup_path=str(Path(temp_root) / "backups"),
813
+ backup_name="usr-backup.zip",
814
+ conflict_policy="rename",
815
+ logger=ListLogger(),
816
+ )
817
+
818
+ with zipfile.ZipFile(backup_path) as archive:
819
+ names = set(archive.namelist())
820
+
821
+ assert "usr/settings.json" in names
822
+ assert (
823
+ "usr/plugins/_desktop/profiles/agent-zero-desktop/.gnupg/S.gpg-agent"
824
+ not in names
825
+ )
826
+ assert any(
827
+ "Skipping non-regular usr backup entry" in message
828
+ for message in messages
829
+ )
830
+
831
+
832
def test_self_update_manager_clean_uv_cache_uses_uv_when_available(monkeypatch):
833
manager = load_self_update_manager()
834
commands = []