"""Idempotent fixed profile preparation. All Ubuntu prerequisites have an owner.""" import fcntl import grp import hashlib import json import os import platform import shutil import subprocess import sys import time import uuid import zipfile from contextlib import contextmanager from pathlib import Path, PurePosixPath sys.path.insert(0, str(Path(__file__).resolve().parent)) from layout import SHARE, STATE, directory, manifest, trusted, write # noqa: E402 from runtime.http import request # noqa: E402 STEPS = [ ("platform", "Проверка совместимости системы"), ("payload", "Проверка встроенного драйвера"), ("runtime", "Развёртывание драйвера"), ("access", "Настройка доступа к камере"), ("service", "Запуск службы камеры"), ] SOCKET = Path("/run/mission-core-insta360/driver.sock") INSTANCES = Path("/run/mission-core-x4-instances") def run(*args): subprocess.run(args, check=True, timeout=45, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) def assert_safe(): if SOCKET.exists(): if request(SOCKET, "/prepare-safe", timeout=5).get("safe") is not True: raise RuntimeError("Остановите просмотр и запись X4 перед обновлением драйвера.") elif list(INSTANCES.glob("instax4_*")): raise RuntimeError("Состояние X4 неизвестно. Подготовка не изменяла службы камеры.") def entries(archive, bundle): expected = bundle["files"] if len(archive.infolist()) != len(expected) or set(archive.namelist()) != set(expected): raise RuntimeError("Состав встроенного драйвера изменён.") for name, info in expected.items(): path = PurePosixPath(name) entry = archive.getinfo(name) if ( path.is_absolute() or ".." in path.parts or path.as_posix() != name or "\\" in name or entry.is_dir() or entry.file_size != info["bytes"] or entry.file_size > 100 * 1024 * 1024 ): raise RuntimeError("Недопустимый файл драйвера.") data = archive.read(entry) if hashlib.sha256(data).hexdigest() != info["sha256"]: raise RuntimeError("Контрольная сумма драйвера не совпала.") yield name, data def install_runtime(bundle): parent = STATE / "runtime" directory(parent) ident = bundle["revision"] if len(ident) != 24 or any(c not in "0123456789abcdef" for c in ident): raise RuntimeError("Некорректная версия драйвера.") target, stage = parent / ident, parent / (ident + ".partial") source = trusted(SHARE / "payload.zip") if hashlib.sha256(source.read_bytes()).hexdigest() != bundle["payload_sha256"]: raise RuntimeError("Встроенный драйвер повреждён. Переустановите пакет.") if stage.exists(): trusted(stage, True) shutil.rmtree(stage) if not target.exists(): directory(stage) try: with zipfile.ZipFile(source) as archive: for name, data in entries(archive, bundle): path = stage / name path.parent.mkdir(mode=0o755, parents=True, exist_ok=True) path.write_bytes(data) path.chmod(0o644) stage.rename(target) finally: if stage.exists(): shutil.rmtree(stage) trusted(target, True) for name, info in bundle["files"].items(): path = target / name for parent in path.parents: if parent == target.parent: break trusted(parent, True) data = trusted(path).read_bytes() if len(data) != info["bytes"] or hashlib.sha256(data).hexdigest() != info["sha256"]: raise RuntimeError("Установленный драйвер изменён. Нужна переустановка пакета.") return ident def prepare(): directory(STATE) lock = STATE / "prepare.lock" with lock.open("a") as handle: trusted(lock) fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB) with lifecycle_lock(): return prepare_locked() @contextmanager def lifecycle_lock(): directory(STATE) path = STATE / "lifecycle.lock" descriptor = os.open(path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o640) with os.fdopen(descriptor, "r+b") as handle: trusted(path) os.fchown(handle.fileno(), 0, grp.getgrnam("mission-core-node").gr_gid) fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB) yield def prepare_locked(): bundle = manifest() report = { "schema": "missioncore.node.device-preparation/v1", "model_id": "insta360.x4", "revision": bundle["revision"], "run_id": str(uuid.uuid4()), "started_at": time.time(), "monotonic_started": time.monotonic(), "state": "running", "steps": [{"id": key, "label": label, "state": "pending"} for key, label in STEPS], } def publish(): report["updated_at"] = time.time() write(STATE / "preparation.json", (json.dumps(report, ensure_ascii=False) + "\n").encode()) publish() try: assert_safe() for step in report["steps"]: step["state"] = "running" publish() if step["id"] == "platform": release = platform.freedesktop_os_release() if ( release.get("ID"), release.get("VERSION_ID"), platform.machine(), sys.version_info[:2], ) != ("ubuntu", "24.04", "x86_64", (3, 12)): raise RuntimeError("Этот пакет поддерживает Ubuntu 24.04 amd64.") elif step["id"] == "payload": data = trusted(SHARE / "payload.zip").read_bytes() if hashlib.sha256(data).hexdigest() != bundle["payload_sha256"]: raise RuntimeError("Встроенный драйвер повреждён.") elif step["id"] == "runtime": ident = install_runtime(bundle) assert_safe() write(STATE / "active.path", (ident + "\n").encode()) elif step["id"] == "access": run("/usr/bin/udevadm", "control", "--reload-rules") run( "/usr/bin/udevadm", "trigger", "--action=change", "--subsystem-match=usb", "--attr-match=idVendor=2e1a", "--attr-match=idProduct=0002", "--attr-match=product=Insta360 X4", ) run("/usr/bin/udevadm", "settle", "--timeout=10") elif step["id"] == "service": maintenance = STATE / "maintenance" if maintenance.exists(): trusted(maintenance).unlink() run("/usr/bin/systemctl", "daemon-reload") for service in ( "mission-core-insta360.service", "mission-core-insta360-supervisor.service", ): run("/usr/bin/systemctl", "enable", service) run("/usr/bin/systemctl", "start", service) run("/usr/bin/systemctl", "is-active", "--quiet", service) step["state"] = "complete" publish() report["state"] = "complete" except ( OSError, ValueError, RuntimeError, subprocess.SubprocessError, zipfile.BadZipFile, ) as error: report["state"] = "error" message = ( str(error) if isinstance(error, RuntimeError) else "Не удалось подготовить X4. Повторите действие." ) report["message"] = message[:300] for step in report["steps"]: if step["state"] == "running": step.update(state="error", message=message[:300]) elif step["state"] == "pending": step["state"] = "blocked" report["duration_seconds"] = time.monotonic() - report["monotonic_started"] publish() return report["state"] == "complete" if __name__ == "__main__": os.umask(0o022) if os.geteuid() or sys.argv[1:] not in ([], ["--assert-safe"], ["--quiesce"]): sys.exit(1) if sys.argv[1:] == ["--quiesce"]: with lifecycle_lock(): assert_safe() write(STATE / "maintenance", b"package-lifecycle\n") elif sys.argv[1:]: assert_safe() else: sys.exit(0 if prepare() else 1)