"""Owner-facing, versioned installer; first SDK Open belongs to fixed prepare. Invoke through the release's local Ubuntu terminal. The OS password is never accepted by this script or sent to Mission Core. No arbitrary commands/URLs. """ import hashlib import http.client import json import os import re import socket import subprocess import sys import time import uuid from contextlib import suppress from datetime import UTC, datetime, timedelta from pathlib import Path STAGING = Path("/var/tmp/mission-core-x4-installs") SERVICES = ( "mission-core-node.service", "mission-core-k1.service", "mission-core-realsense.service", ) def private(path): path.mkdir(mode=0o700, exist_ok=True) info = path.lstat() if path.is_symlink() or not path.is_dir() or info.st_uid != 0 or info.st_mode & 0o077: raise RuntimeError("Installer staging is not root-owned and private") def request(path, operation=None): timeout = 55 if operation else 3 client = http.client.HTTPConnection("driver", timeout=timeout) client.sock = socket.socket(socket.AF_UNIX) client.sock.settimeout(timeout) try: client.sock.connect(str(path)) if operation is None: client.request("GET", "/snapshot") else: client.request( "POST", "/operation", json.dumps(operation), {"Content-Type": "application/json"} ) response = client.getresponse() content = response.read(65537) if response.status != 200 or len(content) > 65536: raise RuntimeError("Camera status is unavailable") return json.loads(content) finally: client.close() def main(): if os.geteuid() or sys.argv[1:]: raise RuntimeError("Запустите установщик в локальном окне Ubuntu через sudo.") os.umask(0o077) source = Path(__file__).resolve().parent manifest = json.loads((source / "release.json").read_text()) if manifest["schema"] != "missioncore.insta360.owner-release/v1": raise ValueError("Unsupported release") version = manifest["version"] revision = manifest["runtime_revision"] if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:-[1-9][0-9]*)?", version) or not re.fullmatch( r"[0-9a-f]{24}", revision ): raise ValueError("Invalid release version") if manifest["qualification_profile"] != "sdk-status-and-image-verification": raise ValueError("Unsupported qualification profile") package = "mission-core-insta360-x4_" + version + "_amd64.deb" admitted = manifest["files"] if set(admitted) != {package, "install_release.py", "install"}: raise ValueError("Unexpected installer contents") for name, expected in admitted.items(): data = (source / name).read_bytes() if len(data) != expected["bytes"] or hashlib.sha256(data).hexdigest() != expected["sha256"]: raise ValueError("Установщик повреждён. Контрольная сумма не совпала.") private(STAGING) identifier = uuid.uuid4().hex folder = STAGING / identifier private(folder) # Snapshot the exact admitted bytes to root-only staging before APT, so # it never consumes a mutable package from the operator's Downloads path. data = (source / package).read_bytes() if hashlib.sha256(data).hexdigest() != admitted[package]["sha256"]: raise ValueError("Package changed before staging") (folder / package).write_bytes(data) report = { "schema": "missioncore.insta360.install-run/v1", "session_id": identifier, "started_at": datetime.now(UTC).isoformat(), "monotonic_started": time.monotonic(), "release_sha256": hashlib.sha256((source / "release.json").read_bytes()).hexdigest(), "package_sha256": admitted[package]["sha256"], "state": "running", "scope": "install-profile-SDK-status-and-bounded-image-verification", "steps": [], } env = { "PATH": "/usr/sbin:/usr/bin:/sbin:/bin", "LANG": "C.UTF-8", "DEBIAN_FRONTEND": "noninteractive", } def publish(): (folder / "report.json").write_text(json.dumps(report, indent=2) + "\n") def run(step, command, timeout=180): report["steps"].append( {"id": step, "state": "running", "started_at": datetime.now(UTC).isoformat()} ) publish() result = subprocess.run(command, env=env, capture_output=True, timeout=timeout) (folder / (step + ".stdout")).write_bytes(result.stdout) (folder / (step + ".stderr")).write_bytes(result.stderr) report["steps"][-1].update( state="complete" if result.returncode == 0 else "error", exit_code=result.returncode ) publish() if result.returncode: raise RuntimeError("Не завершён этап установки: " + step) return result.stdout publish() try: # These remain private to the authenticated owner; the terminal wrapper # saves stdout with umask 077. No camera identifiers enter public logs. previous = sorted(STAGING.glob("*/report.json"), key=lambda p: p.stat().st_mtime) for path in previous[-6:]: if path.parent != folder: print("MISSION_CORE_X4_PREVIOUS " + path.read_text().replace("\n", ""), flush=True) baseline = run( "baseline", [ "/usr/bin/systemctl", "show", *SERVICES, "-p", "Id", "-p", "ActiveState", "-p", "NRestarts", ], ) report["existing_services"] = baseline.decode().splitlines() run( "apt-plan", ["/usr/bin/apt-get", "--simulate", "--no-remove", "install", str(folder / package)], ) run( "package", ["/usr/bin/apt-get", "install", "-y", "--no-remove", str(folder / package)], timeout=None, ) # This is the identical installed model job started by Node's local or # paired remote prepare command. No SDK demo, root SDK or manual grant. preparation = Path("/var/lib/mission-core-insta360/preparation.json") prepared = json.loads(preparation.read_text()) if preparation.exists() else {} active = Path("/var/lib/mission-core-insta360/active.path") # An upgrade's postinst already executes this same fixed preparation. # Do not race its asynchronously connecting workers with a second run. if not ( prepared.get("state") == "complete" and prepared.get("revision") == revision and active.exists() and active.read_text().strip() == revision ): run( "prepare", ["/usr/bin/systemctl", "start", "mission-core-node-insta360-x4-prepare.service"], ) deadline = time.monotonic() + 50 while True: values = [] for path in Path("/run/mission-core-x4-instances").glob("instax4_*/driver.sock"): if not re.fullmatch(r"instax4_[0-9a-f]{32}", path.parent.name): continue with suppress(OSError, ValueError, RuntimeError, http.client.HTTPException): values.append(request(path)) if values and all(item.get("prepared") and item.get("online") for item in values): break if time.monotonic() >= deadline: report["camera_status"] = values raise RuntimeError( "Пакет установлен, но подключение X4 не подтверждено. " "Проверьте питание и USB-режим камеры." ) time.sleep(1) report["camera_status"] = values if len(values) != 1: raise RuntimeError("Для этой аппаратной проверки требуется ровно одна X4.") camera = values[0] if not re.fullmatch(r"instax4_[0-9a-f]{32}", camera["id"]): raise RuntimeError("Camera identity is invalid") now = datetime.now(UTC) operation = "op_" + uuid.uuid4().hex command = { "api_version": "missioncore.nodedc/plugin-sdk/v0alpha2", "kind": "OperationRequest", "operation_id": operation, "idempotency_key": operation, "session": {"device_id": camera["id"], "session_id": camera["session_id"]}, "action_id": "verify", "requested_at": now.isoformat(), "deadline_at": (now + timedelta(seconds=60)).isoformat(), "parameters": {}, } report["image_verification"] = {"request": command, "state": "running"} publish() result = request( Path("/run/mission-core-x4-instances") / camera["id"] / "driver.sock", command ) report["image_verification"].update(state=result.get("state"), result=result) publish() if ( result.get("state") != "complete" or result.get("result", {}).get("verified") is not True ): raise RuntimeError("SDK подключён, но проверка изображения не завершилась успешно.") after = run( "existing-services-after", [ "/usr/bin/systemctl", "show", *SERVICES, "-p", "Id", "-p", "ActiveState", "-p", "NRestarts", ], ) report["existing_services_unchanged"] = after == baseline report["state"] = "complete" print("Пакет X4 установлен. SDK подключился; получение изображения подтверждено.") print("WebRTC и команды записи остаются отдельными проверками.") except Exception as error: report["state"] = "error" report["error"] = str(error)[:300] raise finally: report["finished_at"] = datetime.now(UTC).isoformat() report["duration_seconds"] = time.monotonic() - report["monotonic_started"] publish() # Retain bounded private evidence and remove only the installer-owned # temporary package copy. Runtime/journals remain owned by the .deb. (folder / package).unlink() print("MISSION_CORE_X4_RESULT " + json.dumps(report, ensure_ascii=False), flush=True) print("Отчёт установки:", folder / "report.json") if __name__ == "__main__": try: main() except Exception as error: print(str(error), file=sys.stderr) sys.exit(1)