Files

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)