FrameBridge 0.3.1 community source release
This commit is contained in:
commit
b23819fff1
1354 files changed
+343716
No files matched your search
@@ -0,0 +1,146 @@
|
||||
"""Stop only this package's managed user services before Debian removal."""
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import os
|
||||
import pwd
|
||||
import re
|
||||
import shlex
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
MODULE_ROOT = "/usr/lib/framebridge-expanded/expanded"
|
||||
UNIT_NAME = re.compile(r"framebridge-autostart-[0-9a-f]{16}\.service\Z")
|
||||
|
||||
|
||||
def managed_unit(path, uid):
|
||||
"""Inspect a bounded owned regular file; never execute user configuration."""
|
||||
if not UNIT_NAME.fullmatch(path.name):
|
||||
return False
|
||||
try:
|
||||
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
||||
try:
|
||||
info = os.fstat(fd)
|
||||
if not stat.S_ISREG(info.st_mode) or info.st_uid != uid or info.st_size > 16384:
|
||||
return False
|
||||
raw = os.read(fd, 16385)
|
||||
finally:
|
||||
os.close(fd)
|
||||
lines = raw.decode("utf-8").splitlines()
|
||||
if not lines or lines[0] != "# FrameBridge managed autostart v1":
|
||||
return False
|
||||
if lines.count('Environment="PYTHONPATH=' + MODULE_ROOT + '"') != 1:
|
||||
return False
|
||||
starts = [line[10:] for line in lines if line.startswith("ExecStart=")]
|
||||
if len(starts) != 1:
|
||||
return False
|
||||
args = shlex.split(starts[0])
|
||||
if len(args) != 7 or args[0] not in {"/usr/bin/python3", "/usr/bin/python3.13"}:
|
||||
return False
|
||||
if args[1:6] != ["-B", "-m", "framebridge_expanded.autostart", "_run", "--state"]:
|
||||
return False
|
||||
state = args[6].replace("%%", "%").replace("$$", "$")
|
||||
if not state.startswith("/") or any(ord(c) < 32 for c in state):
|
||||
return False
|
||||
expected = "framebridge-autostart-" + hashlib.sha256(state.encode()).hexdigest()[:16] + ".service"
|
||||
return path.name == expected
|
||||
except (OSError, UnicodeError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def cleanup_user(home=None, *, run=None, bus_available=None):
|
||||
run = run or subprocess.run
|
||||
home = Path(home) if home is not None else Path.home()
|
||||
uid = os.getuid()
|
||||
directory = home / ".config/systemd/user"
|
||||
if not directory.is_dir():
|
||||
return 0
|
||||
units = sorted(p.name for p in directory.glob("framebridge-autostart-*.service") if managed_unit(p, uid))
|
||||
if not units:
|
||||
return 0
|
||||
runtime = Path("/run/user") / str(uid)
|
||||
if bus_available is None:
|
||||
try:
|
||||
bus_available = stat.S_ISSOCK((runtime / "bus").stat().st_mode)
|
||||
except OSError:
|
||||
bus_available = False
|
||||
env = {"HOME": str(home), "PATH": "/usr/bin:/bin", "LANG": "C.UTF-8", "XDG_CONFIG_HOME": str(home / ".config"), "XDG_RUNTIME_DIR": str(runtime), "DBUS_SESSION_BUS_ADDRESS": "unix:path=" + str(runtime / "bus")}
|
||||
failure = False
|
||||
if bus_available:
|
||||
try:
|
||||
result = run(["/usr/bin/systemctl", "--user", "stop", *units], env=env, capture_output=True, timeout=35)
|
||||
if result.returncode:
|
||||
current = run(["/usr/bin/systemctl", "--user", "show", *units, "--property=ActiveState", "--value"], env=env, capture_output=True, timeout=5)
|
||||
values = current.stdout.decode().split() if isinstance(current.stdout, bytes) else current.stdout.split()
|
||||
failure = current.returncode != 0 or len(values) != len(units) or any(value not in {"inactive", "failed"} for value in values)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
failure = True
|
||||
try:
|
||||
result = run(["/usr/bin/systemctl", "--user", "--no-reload", "disable", *units], env=env, capture_output=True, timeout=10)
|
||||
failure = result.returncode != 0 or failure
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
failure = True
|
||||
# Remove only enablement symlinks targeting these exact managed files.
|
||||
# This also works offline: systemctl can delegate file operations to a live
|
||||
# user manager with a different environment, and no bus exists at all for
|
||||
# an offline owner. Never delete profile data, ciphertext, or unit files.
|
||||
for group in directory.iterdir():
|
||||
if not group.name.endswith((".wants", ".requires")) or group.is_symlink() or not group.is_dir():
|
||||
continue
|
||||
for name in units:
|
||||
link = group / name
|
||||
if link.is_symlink():
|
||||
try:
|
||||
if link.resolve() == (directory / name).resolve():
|
||||
link.unlink()
|
||||
except OSError:
|
||||
failure = True
|
||||
if bus_available:
|
||||
try:
|
||||
run(["/usr/bin/systemctl", "--user", "daemon-reload"], env=env, capture_output=True, timeout=3)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
pass # Units are stopped/disabled; an owner may have just logged out.
|
||||
if failure:
|
||||
raise RuntimeError("A managed FrameBridge service could not be stopped or disabled.")
|
||||
return len(units)
|
||||
|
||||
|
||||
def cleanup_all_users(*, run=None, accounts=None):
|
||||
if os.getuid() != 0:
|
||||
raise RuntimeError("Package service cleanup requires the normal package manager's root context.")
|
||||
run = run or subprocess.run
|
||||
accounts = pwd.getpwall() if accounts is None else accounts
|
||||
seen = set()
|
||||
failed = False
|
||||
for account in accounts:
|
||||
identity = (account.pw_uid, account.pw_dir)
|
||||
if identity in seen or account.pw_uid == 65534 or not account.pw_dir.startswith("/") or account.pw_dir in {"/", "/nonexistent", "/dev/null"}:
|
||||
continue
|
||||
seen.add(identity)
|
||||
command = ["/usr/sbin/runuser", "-u", account.pw_name, "--", "/usr/bin/env", "-i", "HOME=" + account.pw_dir, "USER=" + account.pw_name, "LOGNAME=" + account.pw_name, "PATH=/usr/bin:/bin", "LANG=C.UTF-8", "/usr/bin/python3", "-I", "-S", str(Path(__file__).resolve()), "--user"]
|
||||
try:
|
||||
result = run(command, capture_output=True, timeout=55)
|
||||
failed = result.returncode != 0 or failed
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
failed = True
|
||||
if failed:
|
||||
raise RuntimeError("Some FrameBridge services could not be stopped; package removal was paused.")
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
args = list(sys.argv[1:] if argv is None else argv)
|
||||
try:
|
||||
if args == ["--all-users"]:
|
||||
cleanup_all_users()
|
||||
elif args == ["--user"]:
|
||||
cleanup_user()
|
||||
else:
|
||||
return 2
|
||||
return 0
|
||||
except (OSError, RuntimeError, subprocess.TimeoutExpired):
|
||||
print("FrameBridge service cleanup did not complete. Package data was retained.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user