233 lines
8.9 KiB
Python
233 lines
8.9 KiB
Python
"""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-wake.service",
|
|
"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)
|