fix(node): verify D455 access and share sensor progress and recording UI

This commit is contained in:
DCCONSTRUCTIONS
2026-09-05 23:25:11 +03:00
parent b1aaa40508
commit a8647c4d87
25 changed files with 497 additions and 61 deletions
+83 -6
View File
@@ -1,11 +1,14 @@
"""Fixed model job. No paths, packages, URLs or commands are accepted from clients."""
import hashlib
import http.client
import json
import os
import shutil
import socket
import subprocess
import sys
import tempfile
import time
import uuid
import zipfile
@@ -25,11 +28,12 @@ STEPS = [
def publish(value):
ROOT.mkdir(mode=0o755, exist_ok=True)
if ROOT.is_symlink() or ROOT.stat().st_uid != 0:
if ROOT.is_symlink() or ROOT.stat().st_uid != 0 or ROOT.stat().st_mode & 0o022:
raise RuntimeError("Небезопасный каталог драйверов")
ROOT.chmod(0o755)
tmp = ROOT / ".preparation.tmp"
with tmp.open("w") as f:
handle, name = tempfile.mkstemp(prefix=".preparation-", dir=ROOT)
tmp = Path(name)
with os.fdopen(handle, "w") as f:
os.fchmod(f.fileno(), 0o644)
json.dump(value, f, ensure_ascii=False)
f.flush()
@@ -57,12 +61,75 @@ def safe_members(archive):
yield info
def configure_imu_namespace():
# Isolated Python mode deliberately omits the script directory. This fixed,
# root-owned module is the same allowlist used by the udev grant.
import importlib.util
spec = importlib.util.spec_from_file_location(
"realsense_iio_access", Path(__file__).with_name("realsense_iio_access.py")
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
paths = []
for root in Path("/sys/bus/iio/devices").glob("iio:device*"):
resolved = root.resolve()
try:
module.validate(str(resolved)[4:])
except ValueError:
continue
paths.extend(str(p) for p in module.allowed_attributes(resolved))
# No subtree write grant. Only concrete allowlisted IIO attribute files
# belonging to detected D455s are admitted to the service mount namespace.
if any(" " in p or "\n" in p or "%" in p for p in paths):
raise RuntimeError("Неподдерживаемый путь IMU")
content = (
"[Service]\nReadWritePaths=\n"
+ "".join("ReadWritePaths=-" + p + "\n" for p in sorted(paths))
).encode()
folder = Path("/etc/systemd/system/mission-core-realsense.service.d")
folder.mkdir(exist_ok=True, mode=0o755)
destination = folder / "70-imu-access.conf"
fingerprint = ROOT / "imu-config.sha256"
previous = fingerprint.read_text() if fingerprint.exists() else None
if destination.is_symlink() or fingerprint.is_symlink():
raise RuntimeError("Конфликт настроек доступа IMU")
if destination.exists():
old = destination.read_bytes()
if old == content:
return False
if hashlib.sha256(old).hexdigest() != previous:
raise RuntimeError("Настройки IMU изменены в системе. Чужой файл сохранён.")
# Read-only preflight; every entry point shares the board's capture owner.
sock = Path("/run/mission-core-sensors/driver.sock")
if sock.exists():
client = http.client.HTTPConnection("driver", timeout=5)
client.sock = socket.socket(socket.AF_UNIX)
client.sock.settimeout(5)
try:
client.sock.connect(str(sock))
client.request("GET", "/prepare-safe")
response = client.getresponse()
if response.status != 200 or not json.loads(response.read(1024)).get("safe"):
raise RuntimeError("Остановите захват всех камер перед подготовкой драйвера.")
finally:
client.close()
destination.write_bytes(content)
destination.chmod(0o644)
fingerprint.write_text(hashlib.sha256(content).hexdigest())
fingerprint.chmod(0o644)
return True
def prepare():
manifest = json.loads((SHARE / "bundle.json").read_text())
revision = manifest["revision"]
if not revision.isalnum():
raise RuntimeError("Некорректная версия драйвера")
target = ROOT / revision
if target.is_symlink() or (ROOT / "active.path").is_symlink():
raise RuntimeError("Конфликт установленного драйвера")
imu_changed = False
state = {
"schema": "missioncore.node.device-preparation/v1",
"model_id": "realsense.d455",
@@ -129,10 +196,14 @@ def prepare():
(ROOT / "active.path").write_text(str(target))
(ROOT / "active.path").chmod(0o644)
elif step["id"] == "access":
imu_changed = configure_imu_namespace()
source = SHARE / "70-mission-core-realsense.rules"
dest = Path("/etc/udev/rules.d/70-mission-core-realsense.rules")
if dest.is_symlink() or (
dest.exists() and dest.read_bytes() != source.read_bytes()
dest.exists()
and dest.read_bytes() != source.read_bytes()
and hashlib.sha256(dest.read_bytes()).hexdigest()
!= "782eba7935400e688a7eaea53fe50d358a046eb6b2c99187a787cc03b0301449"
):
raise RuntimeError(
"Правила доступа к камере изменены в системе. Чужая конфигурация сохранена."
@@ -156,11 +227,17 @@ def prepare():
"--subsystem-match=video4linux",
)
run("/usr/bin/udevadm", "trigger", "--action=change", "--subsystem-match=hidraw")
run("/usr/bin/udevadm", "trigger", "--action=change", "--subsystem-match=iio")
run("/usr/bin/udevadm", "settle", "--timeout=10")
elif step["id"] == "service":
run("/usr/bin/systemctl", "enable", "mission-core-realsense.service")
# Never restart a running acquisition on repeated preparation.
run("/usr/bin/systemctl", "start", "mission-core-realsense.service")
# The fixed job refuses running capture before changing its namespace.
run("/usr/bin/systemctl", "daemon-reload")
run(
"/usr/bin/systemctl",
"restart" if imu_changed else "start",
"mission-core-realsense.service",
)
run("/usr/bin/systemctl", "is-active", "--quiet", "mission-core-realsense.service")
step["state"] = "complete"
publish(state)