Files
NODEDC_MISSION_CORE/apps/node-agent/packaging/insta360_profile.py
T
DCCONSTRUCTIONS a3c15e11e9 Add packaged Insta360 X4 integration and recover paired Node channels
Discover independent camera instances and prepare their versioned runtime
from Node or remote Core. Add isolated SDK workers, camera controls, raw
dual-fisheye WebRTC preview, and shared action/region loading states.

Recover existing Node bindings over known Tailscale addresses after a Core
LAN address change. Preserve identities and trust, pin both peers, migrate
endpoints with revision checks, and require real heartbeats for online status.
Fix the Python client certificate profile for Go X509 verification.

Pin Design Guideline 8c53f73 and retain installer/build/acceptance history.
Node 0.8.19 is installed; X4 0.1.3-3 is bundled but hardware activation is pending.

Validation: qualified DG/Node builds and Go race tests; 31 fleet tests;
Python-to-Go certificate interoperability and live tailnet recovery with five
fresh heartbeats; prior 38 X4 tests and bounded remote WebRTC acceptance.
Clean-OS, replug/power autonomy, local X4 video and long-run stability remain open.
2026-09-10 09:21:24 +03:00

230 lines
9.3 KiB
Python

"""Node-owned X4 bootstrap; the same fixed job serves local and paired Core UI.
This exists before the optional model package is installed. All package bytes
and hashes come from the Node release, never from a device or a client request.
"""
import fcntl
import hashlib
import json
import os
import platform
import re
import subprocess
import sys
import tempfile
import time
import uuid
from pathlib import Path
SHARE = Path("/usr/share/mission-core-node/profiles/insta360-x4")
STATE = Path("/var/lib/mission-core-node-profiles/insta360-x4")
PLUGIN_STATE = Path("/var/lib/mission-core-insta360")
UNIT = "mission-core-node-insta360-x4-prepare.service"
PACKAGE = "mission-core-insta360-x4"
def trusted(path, directory=False):
info = path.lstat()
if path.is_symlink() or info.st_uid != 0 or info.st_mode & 0o022:
raise RuntimeError("Небезопасный файл профиля камеры.")
if path.is_dir() != directory:
raise RuntimeError("Недопустимый файл профиля камеры.")
return path
def publish(value):
fd, name = tempfile.mkstemp(prefix=".preparation-", dir=STATE)
try:
with os.fdopen(fd, "w") as stream:
os.fchmod(stream.fileno(), 0o644)
json.dump(value, stream, ensure_ascii=False)
stream.flush()
os.fsync(stream.fileno())
target = STATE / "preparation.json"
if target.exists() or target.is_symlink():
trusted(target)
os.replace(name, target)
descriptor = os.open(STATE, os.O_RDONLY)
try:
os.fsync(descriptor)
finally:
os.close(descriptor)
finally:
if os.path.exists(name):
os.unlink(name)
def payload():
for path in (SHARE.parent, SHARE):
trusted(path, True)
value = json.loads(trusted(SHARE / "profile.json").read_text())
if value.get("schema") != "missioncore.node.bundled-model/v1":
raise RuntimeError("Неизвестный профиль камеры.")
version = value["version"]
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:-[1-9][0-9]*)?", version):
raise RuntimeError("Некорректная версия профиля камеры.")
if not re.fullmatch(r"[0-9a-f]{24}", value["revision"]):
raise RuntimeError("Некорректная версия драйвера.")
path = trusted(SHARE / (PACKAGE + "_" + version + "_amd64.deb"))
data = path.read_bytes()
if len(data) != value["bytes"] or hashlib.sha256(data).hexdigest() != value["sha256"]:
raise RuntimeError("Встроенный пакет камеры повреждён. Переустановите Mission Core Node.")
return value, path
def prepare():
for path in (STATE.parent, STATE):
path.mkdir(mode=0o755, exist_ok=True)
trusted(path, True)
fd = os.open(STATE / "prepare.lock", os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
with os.fdopen(fd, "r+b") as lock:
trusted(STATE / "prepare.lock")
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
return prepare_locked()
def prepare_locked():
report = {
"schema": "missioncore.node.device-preparation/v1",
"model_id": "insta360.x4",
"run_id": uuid.uuid4().hex,
"started_at": time.time(),
"monotonic_started": time.monotonic(),
"state": "running",
"steps": [
{"id": key, "label": label, "state": "pending"}
for key, label in (
("platform", "Проверка совместимости системы"),
("payload", "Проверка встроенного пакета камеры"),
("package", "Установка драйвера камеры"),
("prepare", "Подготовка камеры"),
)
],
}
env = {
"PATH": "/usr/sbin:/usr/bin:/sbin:/bin",
"LANG": "C.UTF-8",
"DEBIAN_FRONTEND": "noninteractive",
}
evidence = STATE / report["run_id"]
evidence.mkdir(mode=0o700)
trusted(evidence, True)
sequence = 0
def run(command, timeout=60):
nonlocal sequence
sequence += 1
result = subprocess.run(command, capture_output=True, env=env, timeout=timeout)
for suffix, data in (("stdout", result.stdout), ("stderr", result.stderr)):
path = evidence / (str(sequence) + "." + suffix)
with path.open("xb") as stream:
os.fchmod(stream.fileno(), 0o600)
stream.write(data)
if result.returncode:
raise RuntimeError(
"Этап установки камеры не завершён. Повторите подготовку устройства."
)
return result.stdout.decode().strip()
publish(report)
try:
for step in report["steps"]:
step["state"] = "running"
publish(report)
if step["id"] == "platform":
release = platform.freedesktop_os_release()
if (release.get("ID"), release.get("VERSION_ID"), platform.machine()) != (
"ubuntu",
"24.04",
"x86_64",
):
raise RuntimeError("Этот профиль поддерживает Ubuntu 24.04 amd64.")
elif step["id"] == "payload":
bundle, path = payload()
report.update(revision=bundle["revision"], package_sha256=bundle["sha256"])
elif step["id"] == "package":
result = subprocess.run(
["/usr/bin/dpkg-query", "-W", "-f", "${Version}\t${Status}", PACKAGE],
env=env,
capture_output=True,
text=True,
timeout=10,
)
previous = result.stdout.strip().split("\t") if result.returncode == 0 else []
if (
previous
and subprocess.run(
[
"/usr/bin/dpkg",
"--compare-versions",
previous[0],
"gt",
bundle["version"],
],
env=env,
capture_output=True,
timeout=10,
).returncode
== 0
):
raise RuntimeError(
"Установлен более новый драйвер X4. Обновите Mission Core Node."
)
if previous != [bundle["version"], "install ok installed"]:
# Node already declares every OS dependency. Install only
# the hash-verified local archive: APT --no-download drops
# its local-file acquisition path on Ubuntu 24.04. dpkg
# retains dependency and package-lock checks, without a
# network acquisition or changes to unrelated packages.
# Model preinst/prerm retain recording/preview safety.
run(
[
"/usr/bin/dpkg",
"--install",
str(path),
],
timeout=None,
)
elif step["id"] == "prepare":
current = PLUGIN_STATE / "preparation.json"
# Package postinst prepares an existing installation during an
# upgrade. Avoid a second preparation while its SDK connects.
value = json.loads(trusted(current).read_text()) if current.exists() else {}
same_upgrade = (
previous != [bundle["version"], "install ok installed"]
and value.get("started_at", 0) >= report["started_at"]
and value.get("state") == "complete"
and value.get("revision") == bundle["revision"]
)
if not same_upgrade:
run(["/usr/bin/systemctl", "start", UNIT], timeout=200)
value = json.loads(trusted(current).read_text())
if value.get("state") != "complete" or value.get("revision") != bundle["revision"]:
raise RuntimeError("Подготовка драйвера камеры не подтверждена.")
step["state"] = "complete"
publish(report)
report["state"] = "complete"
except (OSError, ValueError, KeyError, RuntimeError, subprocess.SubprocessError) as error:
report["state"] = "error"
report["message"] = (
str(error)[:300]
if isinstance(error, RuntimeError)
else "Не удалось подготовить камеру."
)
for step in report["steps"]:
if step["state"] == "running":
step.update(state="error", message=report["message"])
elif step["state"] == "pending":
step["state"] = "blocked"
report["duration_seconds"] = time.monotonic() - report["monotonic_started"]
publish(report)
return report["state"] == "complete"
if __name__ == "__main__":
if os.geteuid() or sys.argv[1:]:
sys.exit(1)
os.umask(0o022)
sys.exit(0 if prepare() else 1)