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.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"""Actual plugin-SDK and Core inventory validation, with synthetic identities."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[3]
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
from missioncore_plugin_sdk.v0alpha2.session import DeviceSessionSnapshot # noqa: E402
|
||||
from runtime import broker, identity, worker # noqa: E402
|
||||
|
||||
from k1link.fleet.sensors import validate_inventory # noqa: E402
|
||||
|
||||
NODE = "node_" + "a" * 64
|
||||
|
||||
|
||||
class InventoryTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temporary = tempfile.TemporaryDirectory(dir=REPO / "plugins/insta360-x4/build")
|
||||
self.addCleanup(self.temporary.cleanup)
|
||||
root = Path(self.temporary.name)
|
||||
binding = identity.Binding(identity.device_id("SYNTHETIC"), "1-2", 1, 2, "SYNTHETIC")
|
||||
self.worker = worker.Worker(binding, root, root)
|
||||
self.broker = broker.Broker()
|
||||
|
||||
def test_idle_active_unknown_and_disconnected_match_real_session_contract(self):
|
||||
revisions = []
|
||||
for ready, preview, recording in (
|
||||
(False, None, None),
|
||||
(True, 0, 0),
|
||||
(True, 1, 0),
|
||||
(True, -1, -1),
|
||||
(False, None, None),
|
||||
):
|
||||
self.worker.ready = ready
|
||||
self.worker.status = {"connected": ready, "preview": preview, "recording": recording}
|
||||
self.worker.observed = time.monotonic() if ready else 0
|
||||
value = self.broker.item(self.worker.snapshot(), NODE)
|
||||
snapshot = DeviceSessionSnapshot.model_validate(value["snapshot"])
|
||||
revisions.append(snapshot.revision)
|
||||
validate_inventory({"devices": [value]}, NODE)
|
||||
self.assertEqual(revisions, sorted(revisions))
|
||||
self.assertGreater(revisions[-1], revisions[0])
|
||||
|
||||
def test_500_distinct_cameras_fit_bounded_carrier_and_501_are_rejected(self):
|
||||
self.worker.ready = True
|
||||
self.worker.observed = time.monotonic()
|
||||
self.worker.status = {"connected": True, "preview": 0, "recording": 0}
|
||||
snapshot = self.worker.snapshot()
|
||||
values = []
|
||||
for index in range(500):
|
||||
value = dict(snapshot, id=identity.device_id("SYNTHETIC-" + str(index)))
|
||||
values.append(self.broker.item(value, NODE))
|
||||
state = {"items": values, "operations": []}
|
||||
payload = {"devices": values, "sensor_state": state}
|
||||
self.assertIs(validate_inventory(payload, NODE), state)
|
||||
self.assertLess(len(json.dumps({"items": values}).encode()), 2 * 1024 * 1024)
|
||||
self.assertLess(len(json.dumps(payload).encode()), 8 * 1024 * 1024)
|
||||
payload["devices"] = values + [values[0]]
|
||||
with self.assertRaises(ValueError):
|
||||
validate_inventory(payload, NODE)
|
||||
|
||||
def test_cross_node_or_duplicate_identity_is_rejected(self):
|
||||
value = self.broker.item(self.worker.snapshot(), NODE)
|
||||
with self.assertRaises(ValueError):
|
||||
validate_inventory({"devices": [value]}, "node_" + "b" * 64)
|
||||
with self.assertRaises(ValueError):
|
||||
validate_inventory({"devices": [value, value]}, NODE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -0,0 +1,413 @@
|
||||
"""Real codecs/WebRTC over loopback; synthetic frames, no CameraSDK or device."""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
from fractions import Fraction
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from aiortc import RTCConfiguration, RTCPeerConnection, RTCSessionDescription
|
||||
from check_runtime import ROOT, VideoCamera, synthetic_video
|
||||
from runtime import broker, frames, http, media
|
||||
from runtime.verification import Control
|
||||
|
||||
|
||||
def packet(number, data=b"synthetic", generation=1):
|
||||
return {
|
||||
"stream_index": 0,
|
||||
"codec": 0,
|
||||
"generation": generation,
|
||||
"sequence": number,
|
||||
"timestamp": number,
|
||||
}, data
|
||||
|
||||
|
||||
class FrameTests(unittest.TestCase):
|
||||
def test_repeated_preview_commands_preserve_generation_and_readers(self):
|
||||
feed = frames.Feed(None)
|
||||
camera = VideoCamera([], preview=1)
|
||||
control = Control(camera, feed)
|
||||
feed.append(packet(1))
|
||||
before = feed.read(0, 0)
|
||||
self.assertEqual(control.call("preview.start", {})["state"], "complete")
|
||||
self.assertEqual(feed.read(0, 0), before)
|
||||
control.call("preview.stop", {})
|
||||
self.assertIsNone(feed.read(0, 0))
|
||||
generation = feed.generation
|
||||
control.call("preview.stop", {})
|
||||
self.assertEqual(feed.generation, generation)
|
||||
control.call("preview.start", {})
|
||||
self.assertGreater(feed.generation, generation)
|
||||
|
||||
def test_uncertain_redundant_start_does_not_release_existing_decoder(self):
|
||||
value, events = broker.Broker(), []
|
||||
identifier = "instax4_" + "a" * 32
|
||||
session = {"device_id": identifier, "session_id": "synthetic"}
|
||||
|
||||
class Engine:
|
||||
def capture(self, key, path, start):
|
||||
events.append(start)
|
||||
|
||||
value.media = Engine()
|
||||
|
||||
def request(path, route, command=None, **kwargs):
|
||||
if route == "/snapshot":
|
||||
return {
|
||||
"id": identifier,
|
||||
"session_id": "synthetic",
|
||||
"online": True,
|
||||
"status": {"preview": 1},
|
||||
}
|
||||
return {"state": "unknown"}
|
||||
|
||||
with patch.object(broker, "request", request):
|
||||
result = value.capture_operation(
|
||||
identifier, {"session": session, "action_id": "preview.start"}
|
||||
)
|
||||
self.assertEqual(result["state"], "unknown")
|
||||
self.assertEqual(events, [True])
|
||||
|
||||
def test_broker_primes_before_sdk_start_and_releases_after_sdk_stop(self):
|
||||
value = broker.Broker()
|
||||
events = []
|
||||
identifier = "instax4_" + "a" * 32
|
||||
session = {"device_id": identifier, "session_id": "synthetic"}
|
||||
|
||||
class Engine:
|
||||
def capture(self, key, path, start):
|
||||
events.append("prime" if start else "release")
|
||||
|
||||
value.media = Engine()
|
||||
|
||||
def request(path, route, command=None, **kwargs):
|
||||
if route == "/snapshot":
|
||||
return {"id": identifier, "session_id": "synthetic", "online": True}
|
||||
events.append(command["action_id"])
|
||||
return {"state": "complete"}
|
||||
|
||||
with patch.object(broker, "request", request):
|
||||
for action in ("preview.start", "preview.stop"):
|
||||
value.capture_operation(identifier, {"session": session, "action_id": action})
|
||||
self.assertEqual(events, ["prime", "preview.start", "preview.stop", "release"])
|
||||
|
||||
def test_late_decoder_gets_parameters_after_initial_packets_leave_queue(self):
|
||||
import av
|
||||
|
||||
encoder = av.CodecContext.create("libx264", "w")
|
||||
encoder.width, encoder.height, encoder.pix_fmt = 32, 16, "yuv420p"
|
||||
encoder.time_base = Fraction(1, 30)
|
||||
encoder.options = {
|
||||
"preset": "ultrafast",
|
||||
"tune": "zerolatency",
|
||||
"x264-params": "keyint=6:min-keyint=6:scenecut=0",
|
||||
}
|
||||
feed = frames.Feed(None)
|
||||
for number in range(96):
|
||||
frame = av.VideoFrame(32, 16, "yuv420p")
|
||||
frame.pts = number
|
||||
for plane in frame.planes:
|
||||
plane.update(bytes([64]) * plane.buffer_size)
|
||||
for encoded in encoder.encode(frame):
|
||||
data = bytes(encoded)
|
||||
if number:
|
||||
# Model a camera which sends SPS/PPS only at preview start.
|
||||
units = re.split(b"\x00\x00(?:\x00)?\x01", data)
|
||||
data = b"".join(
|
||||
b"\x00\x00\x00\x01" + unit
|
||||
for unit in units
|
||||
if unit and unit[0] & 31 not in (7, 8)
|
||||
)
|
||||
feed.append(packet(number, data))
|
||||
self.assertEqual(len(feed.queue), frames.MAX_ENTRIES)
|
||||
self.assertGreater(feed.queue[0][0]["sequence"], 0)
|
||||
decoder, decoded, cursor = av.CodecContext.create("h264", "r"), [], 0
|
||||
while (value := feed.read(cursor, 0)) is not None:
|
||||
header, data = value
|
||||
cursor = header["cursor"]
|
||||
try:
|
||||
for encoded in decoder.parse(data):
|
||||
decoded.extend(decoder.decode(encoded))
|
||||
except av.error.FFmpegError:
|
||||
pass
|
||||
self.assertGreaterEqual(len(decoded), 2)
|
||||
self.assertEqual((decoded[-1].width, decoded[-1].height), (32, 16))
|
||||
|
||||
def test_parameter_cache_cannot_cross_stream_device_or_preview_generation(self):
|
||||
config = b"\x00\x00\x01\x67sps\x00\x00\x01\x68pps"
|
||||
keyframe = b"\x00\x00\x01\x65frame"
|
||||
first, second = frames.Feed(None), frames.Feed(None)
|
||||
first.append(packet(1, config))
|
||||
first.append(packet(2, keyframe))
|
||||
self.assertEqual(first.queue[-1][1], config + keyframe)
|
||||
second.append(packet(1, keyframe))
|
||||
self.assertEqual(second.queue[-1][1], keyframe)
|
||||
header, data = packet(3, keyframe)
|
||||
header["stream_index"] = 1
|
||||
first.append((header, data))
|
||||
self.assertEqual(first.queue[-1][1], keyframe)
|
||||
first.append(packet(4, keyframe, generation=2))
|
||||
self.assertEqual(first.queue[-1][1], keyframe)
|
||||
first.append(packet(5, config, generation=2))
|
||||
first.clear()
|
||||
first.append(packet(6, keyframe, generation=2))
|
||||
self.assertEqual(first.queue[-1][1], keyframe)
|
||||
|
||||
def test_parameter_cache_is_bounded_and_hevc_parameters_are_distinct(self):
|
||||
feed = frames.Feed(None)
|
||||
huge = b"\x00\x00\x01\x67" + b"x" * frames.MAX_PARAMETERS
|
||||
feed.append(packet(1, huge))
|
||||
self.assertFalse(any(feed.parameters.streams.values()))
|
||||
config = b"".join(b"\x00\x00\x01" + bytes([kind << 1, 1, 2]) for kind in (32, 33, 34))
|
||||
keyframe = b"\x00\x00\x01\x26\x01frame"
|
||||
header, _ = packet(2)
|
||||
header["codec"] = 1
|
||||
feed.append((header, config))
|
||||
feed.append((header, keyframe))
|
||||
self.assertEqual(feed.queue[-1][1], config + keyframe)
|
||||
|
||||
def test_independent_readers_and_devices_preserve_packet_identity(self):
|
||||
first, second = frames.Feed(None), frames.Feed(None)
|
||||
a, b = first.reader(), first.reader()
|
||||
first.append(packet(1, b"first"))
|
||||
second.append(packet(1, b"second"))
|
||||
self.assertEqual(a()[1], b"first")
|
||||
self.assertEqual(b()[1], b"first")
|
||||
self.assertEqual(second.read(0, 0)[1], b"second")
|
||||
self.assertIsNone(a())
|
||||
|
||||
def test_overrun_and_preview_boundary_are_visible_to_decoder(self):
|
||||
feed = frames.Feed(None)
|
||||
for n in range(100):
|
||||
feed.append(packet(n))
|
||||
value = feed.read(1, 0)
|
||||
self.assertTrue(value[0]["gap"])
|
||||
self.assertLessEqual(len(feed.queue), frames.MAX_ENTRIES)
|
||||
old = value[0]["generation"]
|
||||
feed.change(lambda: None)
|
||||
feed.append(packet(101))
|
||||
self.assertGreater(feed.read(0, 0)[0]["generation"], old)
|
||||
|
||||
def test_wire_bounds_and_types_reject_corrupted_or_unknown_packets(self):
|
||||
feed = frames.Feed(None)
|
||||
feed.append(packet(1))
|
||||
value = feed.read(0, 0)
|
||||
self.assertEqual(frames.decode(frames.encode(value)), value)
|
||||
for data in (b"x", b"\xff" * 8, b"\0\0\0\1x", frames.encode(value)[:5]):
|
||||
with self.assertRaises((ValueError, UnicodeError)):
|
||||
frames.decode(data)
|
||||
with self.assertRaises(ValueError):
|
||||
feed.read(True, 0)
|
||||
|
||||
def test_public_ice_candidates_are_rejected_before_network_work(self):
|
||||
with self.assertRaises(ValueError):
|
||||
media.admit_sdp("v=0\r\na=candidate:1 1 udp 1 203.0.113.6 1234 typ host\r\n")
|
||||
media.admit_sdp("v=0\r\na=candidate:1 1 udp 1 127.0.0.1 1234 typ host\r\n")
|
||||
|
||||
|
||||
class MediaTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_primed_decoder_survives_late_viewer_and_peer_close(self):
|
||||
with tempfile.TemporaryDirectory(dir=ROOT / "build") as temporary:
|
||||
descriptor = os.open(temporary, os.O_RDONLY)
|
||||
path = Path(f"/proc/self/fd/{descriptor}/video.sock")
|
||||
feed, stop = frames.Feed(None), threading.Event()
|
||||
packets = synthetic_video(220) # One initial keyframe; >64-packet queue.
|
||||
|
||||
def publish():
|
||||
for number, data in enumerate(packets):
|
||||
if stop.is_set():
|
||||
break
|
||||
feed.append(packet(number, data))
|
||||
stop.wait(1 / 30)
|
||||
|
||||
def dispatch(method, route, value, headers):
|
||||
if route == "/snapshot":
|
||||
return {"session_id": "synthetic", "online": True, "status": {"preview": 1}}
|
||||
if method != "POST" or route != "/video" or value["session_id"] != "synthetic":
|
||||
raise ValueError("Unexpected source command")
|
||||
return http.Binary(frames.encode(feed.read(value["cursor"])))
|
||||
|
||||
server = http.Server(path, dispatch, {os.geteuid()})
|
||||
serving = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
producer = threading.Thread(target=publish, daemon=True)
|
||||
serving.start()
|
||||
peers, key = media.Peers(), ("synthetic-camera", "synthetic")
|
||||
receiver = RTCPeerConnection(RTCConfiguration(iceServers=[]))
|
||||
received = asyncio.get_running_loop().create_future()
|
||||
tasks = []
|
||||
|
||||
@receiver.on("track")
|
||||
def track(value):
|
||||
async def observe():
|
||||
first, second = await value.recv(), await value.recv()
|
||||
if not received.done():
|
||||
received.set_result((first, second))
|
||||
|
||||
tasks.append(asyncio.create_task(observe()))
|
||||
|
||||
try:
|
||||
await peers.prime(key, path)
|
||||
source = peers.sources[key]
|
||||
producer.start()
|
||||
await asyncio.sleep(2.4)
|
||||
self.assertGreater(feed.queue[0][0]["sequence"], 0)
|
||||
self.assertGreater(source.current()[0], 2)
|
||||
with patch.object(media, "_host_addresses", return_value=["127.0.0.1"]):
|
||||
receiver.addTransceiver("video", direction="recvonly")
|
||||
receiver.createDataChannel("sensor")
|
||||
await receiver.setLocalDescription(await receiver.createOffer())
|
||||
answer = await peers.offer(
|
||||
key, path, {"sdp": receiver.localDescription.sdp, "layer": "preview"}
|
||||
)
|
||||
self.assertNotIn("VP8/", answer["sdp"])
|
||||
await receiver.setRemoteDescription(
|
||||
RTCSessionDescription(sdp=answer["sdp"], type="answer")
|
||||
)
|
||||
first, second = await asyncio.wait_for(received, 4)
|
||||
self.assertEqual((first.width, first.height), (32, 16))
|
||||
self.assertGreater(second.pts, first.pts)
|
||||
# A second UI can send START from its stale idle snapshot.
|
||||
# No new camera keyframe is produced for this idempotent call.
|
||||
control = Control(VideoCamera([], preview=1), feed)
|
||||
control.call("preview.start", {})
|
||||
verified = await peers.verify(key, timeout=1)
|
||||
self.assertEqual(verified["state"], "complete")
|
||||
self.assertGreaterEqual(verified["result"]["streams"][0]["frames"], 2)
|
||||
await peers.close(key, answer["peer_id"])
|
||||
self.assertIs(peers.sources[key], source)
|
||||
self.assertFalse(source.closed.is_set())
|
||||
finally:
|
||||
await receiver.close()
|
||||
await peers.release(key)
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
stop.set()
|
||||
if producer.is_alive():
|
||||
producer.join(2)
|
||||
feed.close()
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
serving.join(2)
|
||||
os.close(descriptor)
|
||||
self.assertFalse(peers.items)
|
||||
self.assertFalse(peers.sources)
|
||||
self.assertFalse(peers.primed)
|
||||
|
||||
async def test_verification_never_accepts_retained_frames_or_another_camera(self):
|
||||
peers = media.Peers()
|
||||
key = ("synthetic", "one")
|
||||
|
||||
class Retained:
|
||||
closed = threading.Event()
|
||||
|
||||
def current(self):
|
||||
return 50, object(), media.time.monotonic()
|
||||
|
||||
retained = Retained()
|
||||
peers.sources[key] = retained
|
||||
self.assertEqual((await peers.verify(key, timeout=0.05))["state"], "error")
|
||||
self.assertEqual((await peers.verify(("another", "one")))["state"], "error")
|
||||
retained.closed.set()
|
||||
self.assertEqual((await peers.verify(key))["state"], "error")
|
||||
|
||||
async def test_synthetic_frames_cross_private_ipc_and_real_webrtc_with_cleanup(self):
|
||||
self.loop_slow_callback_duration = 1
|
||||
with tempfile.TemporaryDirectory(dir=ROOT / "build") as temporary:
|
||||
directory = Path(temporary)
|
||||
descriptor = os.open(directory, os.O_RDONLY)
|
||||
path = Path(f"/proc/self/fd/{descriptor}/video.sock")
|
||||
feed = frames.Feed(None)
|
||||
stop = threading.Event()
|
||||
packets = synthetic_video()
|
||||
|
||||
def publish():
|
||||
number = 0
|
||||
while not stop.is_set():
|
||||
feed.append(
|
||||
packet(
|
||||
number,
|
||||
packets[number % len(packets)],
|
||||
generation=number // len(packets) + 1,
|
||||
)
|
||||
)
|
||||
number += 1
|
||||
stop.wait(1 / 30)
|
||||
|
||||
def dispatch(method, route, value, headers):
|
||||
if method != "POST" or route != "/video" or value["session_id"] != "synthetic":
|
||||
raise ValueError("Wrong camera/session")
|
||||
return http.Binary(frames.encode(feed.read(value["cursor"])))
|
||||
|
||||
server = http.Server(path, dispatch, {os.geteuid()})
|
||||
server_thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
producer = threading.Thread(target=publish, daemon=True)
|
||||
server_thread.start()
|
||||
producer.start()
|
||||
peers = media.Peers()
|
||||
receiver = RTCPeerConnection(RTCConfiguration(iceServers=[]))
|
||||
received = asyncio.get_running_loop().create_future()
|
||||
tasks = []
|
||||
identifier = None
|
||||
key = ("synthetic-camera", "synthetic")
|
||||
|
||||
@receiver.on("track")
|
||||
def track(value):
|
||||
async def observe():
|
||||
first = await value.recv()
|
||||
second = await value.recv()
|
||||
if not received.done():
|
||||
received.set_result((first, second))
|
||||
|
||||
tasks.append(asyncio.create_task(observe()))
|
||||
|
||||
try:
|
||||
# Both peers use only 127.0.0.1. No STUN, LAN candidates or
|
||||
# camera packet is admitted to this synthetic qualification.
|
||||
with patch.object(media, "_host_addresses", return_value=["127.0.0.1"]):
|
||||
receiver.addTransceiver("video", direction="recvonly")
|
||||
channel = receiver.createDataChannel("sensor")
|
||||
|
||||
@channel.on("open")
|
||||
def opened():
|
||||
channel.send("keepalive")
|
||||
|
||||
await receiver.setLocalDescription(await receiver.createOffer())
|
||||
answer = await peers.offer(
|
||||
key, path, {"sdp": receiver.localDescription.sdp, "layer": "preview"}
|
||||
)
|
||||
identifier = answer["peer_id"]
|
||||
await receiver.setRemoteDescription(
|
||||
RTCSessionDescription(sdp=answer["sdp"], type="answer")
|
||||
)
|
||||
first, second = await asyncio.wait_for(received, 12)
|
||||
self.assertEqual((first.width, first.height), (32, 16))
|
||||
self.assertGreater(second.pts, first.pts)
|
||||
with self.assertRaises(ValueError):
|
||||
await peers.close(("another-camera", "synthetic"), identifier)
|
||||
self.assertIn(identifier, peers.items)
|
||||
finally:
|
||||
await receiver.close()
|
||||
if identifier:
|
||||
await peers.close(key, identifier)
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
stop.set()
|
||||
feed.close()
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
server_thread.join(2)
|
||||
producer.join(2)
|
||||
os.close(descriptor)
|
||||
self.assertFalse(peers.items)
|
||||
self.assertFalse(peers.sources)
|
||||
self.assertFalse(server_thread.is_alive())
|
||||
self.assertFalse(producer.is_alive())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -0,0 +1,164 @@
|
||||
"""Synthetic cold-host bootstrap transactions; no actual APT/systemd/USB."""
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
REPO = Path(__file__).resolve().parents[3]
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"x4_node_profile", REPO / "apps/node-agent/packaging/insta360_profile.py"
|
||||
)
|
||||
profile = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(profile)
|
||||
|
||||
|
||||
class ProfileTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory(dir=REPO / "plugins/insta360-x4/build")
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
root = Path(self.temp.name)
|
||||
self.share, self.state, self.plugin = (root / name for name in ("share", "state", "plugin"))
|
||||
for directory in (self.share, self.state, self.plugin):
|
||||
directory.mkdir()
|
||||
self.bundle = {
|
||||
"schema": "missioncore.node.bundled-model/v1",
|
||||
"version": "0.1.1",
|
||||
"revision": "a" * 24,
|
||||
"bytes": 3,
|
||||
"sha256": hashlib.sha256(b"deb").hexdigest(),
|
||||
}
|
||||
(self.share / "profile.json").write_text(json.dumps(self.bundle))
|
||||
(self.share / "mission-core-insta360-x4_0.1.1_amd64.deb").write_bytes(b"deb")
|
||||
self.calls = []
|
||||
self.previous = None
|
||||
self.failure = False
|
||||
for name, value in (
|
||||
("SHARE", self.share),
|
||||
("STATE", self.state),
|
||||
("PLUGIN_STATE", self.plugin),
|
||||
):
|
||||
context = patch.object(profile, name, value)
|
||||
context.start()
|
||||
self.addCleanup(context.stop)
|
||||
|
||||
# Production checks uid 0. Tests remain unprivileged and only admit the
|
||||
# three private fixture directories, never paths on the installed OS.
|
||||
def trusted(path, directory=False):
|
||||
self.assertTrue(path == root or root in path.parents)
|
||||
self.assertFalse(path.is_symlink())
|
||||
self.assertEqual(path.is_dir(), directory)
|
||||
return path
|
||||
|
||||
for context in (
|
||||
patch.object(profile, "trusted", trusted),
|
||||
patch.object(profile.platform, "machine", return_value="x86_64"),
|
||||
patch.object(
|
||||
profile.platform,
|
||||
"freedesktop_os_release",
|
||||
return_value={"ID": "ubuntu", "VERSION_ID": "24.04"},
|
||||
),
|
||||
patch.object(profile.subprocess, "run", side_effect=self.run_command),
|
||||
):
|
||||
context.start()
|
||||
self.addCleanup(context.stop)
|
||||
|
||||
def prepared(self):
|
||||
(self.plugin / "preparation.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"state": "complete",
|
||||
"revision": self.bundle["revision"],
|
||||
"started_at": time.time(),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def run_command(self, command, **kwargs):
|
||||
self.calls.append(command)
|
||||
name = Path(command[0]).name
|
||||
if name == "dpkg-query":
|
||||
return subprocess.CompletedProcess(
|
||||
command,
|
||||
1 if self.previous is None else 0,
|
||||
"" if self.previous is None else self.previous + "\tinstall ok installed",
|
||||
"",
|
||||
)
|
||||
if name == "dpkg" and command[1] == "--compare-versions":
|
||||
return subprocess.CompletedProcess(command, 0 if self.previous == "0.2.0" else 1)
|
||||
if name == "dpkg" and command[1] == "--install":
|
||||
if self.failure:
|
||||
return subprocess.CompletedProcess(command, 100, b"", b"synthetic package refusal")
|
||||
if self.previous:
|
||||
self.prepared() # Upgrade postinst owns preparation.
|
||||
elif name == "systemctl":
|
||||
self.assertEqual(command, ["/usr/bin/systemctl", "start", profile.UNIT])
|
||||
self.prepared()
|
||||
else:
|
||||
self.fail("Unexpected OS command")
|
||||
return subprocess.CompletedProcess(command, 0, b"", b"")
|
||||
|
||||
def names(self):
|
||||
return [Path(command[0]).name for command in self.calls]
|
||||
|
||||
def test_cold_host_installs_only_bundled_package_then_fixed_prepare(self):
|
||||
self.assertTrue(profile.prepare())
|
||||
self.assertEqual(self.names(), ["dpkg-query", "dpkg", "systemctl"])
|
||||
self.assertEqual(
|
||||
self.calls[1],
|
||||
[
|
||||
"/usr/bin/dpkg",
|
||||
"--install",
|
||||
str(self.share / "mission-core-insta360-x4_0.1.1_amd64.deb"),
|
||||
],
|
||||
)
|
||||
|
||||
def test_existing_version_reuses_package_and_revalidates_runtime(self):
|
||||
self.previous = "0.1.1"
|
||||
self.assertTrue(profile.prepare())
|
||||
self.assertFalse(any("--install" in command for command in self.calls))
|
||||
self.assertIn("systemctl", self.names())
|
||||
|
||||
def test_debian_revision_remains_a_fixed_local_package(self):
|
||||
old = self.share / "mission-core-insta360-x4_0.1.1_amd64.deb"
|
||||
old.rename(self.share / "mission-core-insta360-x4_0.1.1-1_amd64.deb")
|
||||
self.bundle["version"] = "0.1.1-1"
|
||||
(self.share / "profile.json").write_text(json.dumps(self.bundle))
|
||||
self.assertTrue(profile.prepare())
|
||||
self.assertEqual(Path(self.calls[1][-1]).name, "mission-core-insta360-x4_0.1.1-1_amd64.deb")
|
||||
|
||||
def test_upgrade_does_not_race_postinst_with_second_prepare(self):
|
||||
self.previous = "0.1.0"
|
||||
self.assertTrue(profile.prepare())
|
||||
self.assertTrue(any("--install" in command for command in self.calls))
|
||||
self.assertNotIn("systemctl", self.names())
|
||||
|
||||
def test_corruption_never_reaches_package_manager_or_driver(self):
|
||||
(self.share / "mission-core-insta360-x4_0.1.1_amd64.deb").write_bytes(b"bad")
|
||||
self.assertFalse(profile.prepare())
|
||||
self.assertEqual(self.calls, [])
|
||||
|
||||
def test_newer_driver_is_preserved(self):
|
||||
self.previous = "0.2.0"
|
||||
self.assertFalse(profile.prepare())
|
||||
self.assertFalse(any("--install" in command for command in self.calls))
|
||||
self.assertNotIn("systemctl", self.names())
|
||||
|
||||
def test_failed_package_never_opens_camera_and_keeps_error_evidence(self):
|
||||
self.failure = True
|
||||
self.assertFalse(profile.prepare())
|
||||
self.assertNotIn("systemctl", self.names())
|
||||
report = json.loads((self.state / "preparation.json").read_text())
|
||||
self.assertEqual(report["steps"][-1]["state"], "blocked")
|
||||
self.assertEqual(
|
||||
(self.state / report["run_id"] / "1.stderr").read_bytes(), b"synthetic package refusal"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,349 @@
|
||||
"""Qualification on Ubuntu using a freshly extracted pinned Python runtime.
|
||||
|
||||
Only fake camera calls are executed. No vendor SDK import, USB or system change.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
import uuid
|
||||
import zipfile
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from fractions import Fraction
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path[:0] = [str(ROOT), str(ROOT / "packaging")]
|
||||
import prepare # noqa: E402
|
||||
from runtime import broker, http, identity, operations, worker # noqa: E402
|
||||
from runtime.verification import Control # noqa: E402
|
||||
|
||||
|
||||
class VideoCamera:
|
||||
def __init__(self, packets, preview=0, recording=0, stop_unknown=False):
|
||||
self.packets = list(packets)
|
||||
self.preview, self.recording = preview, recording
|
||||
self.stop_unknown = stop_unknown
|
||||
self.calls = []
|
||||
self.sequence = 0
|
||||
|
||||
def call(self, action, values):
|
||||
self.calls.append(action)
|
||||
if action == "details":
|
||||
return {
|
||||
"state": "complete",
|
||||
"result": {"connected": True, "preview": self.preview, "recording": self.recording},
|
||||
}
|
||||
if action == "preview.start":
|
||||
self.preview = 1
|
||||
if action == "preview.stop":
|
||||
self.preview = 0
|
||||
if self.stop_unknown:
|
||||
return {"state": "unknown"}
|
||||
return {"state": "complete", "result": {"ok": True}}
|
||||
|
||||
def video(self):
|
||||
if not self.packets:
|
||||
return None
|
||||
self.sequence += 1
|
||||
return {
|
||||
"stream_index": 0,
|
||||
"codec": 0,
|
||||
"generation": 1,
|
||||
"sequence": self.sequence,
|
||||
}, self.packets.pop(0)
|
||||
|
||||
|
||||
def synthetic_video(count=4):
|
||||
import av
|
||||
|
||||
encoder = av.CodecContext.create("libx264", "w")
|
||||
encoder.width, encoder.height = 32, 16
|
||||
encoder.pix_fmt = "yuv420p"
|
||||
encoder.time_base = Fraction(1, 30)
|
||||
encoder.options = {"preset": "ultrafast", "tune": "zerolatency"}
|
||||
output = []
|
||||
for number in range(count):
|
||||
frame = av.VideoFrame(32, 16, "yuv420p")
|
||||
frame.pts = number
|
||||
for index, plane in enumerate(frame.planes):
|
||||
plane.update(bytes([16 if index == 0 else 128]) * plane.buffer_size)
|
||||
output.extend(bytes(packet) for packet in encoder.encode(frame))
|
||||
output.extend(bytes(packet) for packet in encoder.encode(None))
|
||||
return output
|
||||
|
||||
|
||||
class Camera:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def call(self, action, values):
|
||||
self.calls.append((action, values))
|
||||
return {"state": "complete", "result": {"ok": True}}
|
||||
|
||||
|
||||
def command(ident, session, action="record.start"):
|
||||
now = datetime.now(UTC)
|
||||
operation = "op_" + uuid.uuid4().hex
|
||||
return {
|
||||
"api_version": "missioncore.nodedc/plugin-sdk/v0alpha2",
|
||||
"kind": "OperationRequest",
|
||||
"operation_id": operation,
|
||||
"idempotency_key": operation,
|
||||
"session": {"device_id": ident, "session_id": session},
|
||||
"action_id": action,
|
||||
"requested_at": now.isoformat(),
|
||||
"deadline_at": (now + timedelta(seconds=60)).isoformat(),
|
||||
"parameters": {},
|
||||
}
|
||||
|
||||
|
||||
class RuntimeTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temporary = tempfile.TemporaryDirectory(dir=ROOT / "build")
|
||||
self.root = Path(self.temporary.name)
|
||||
|
||||
def tearDown(self):
|
||||
self.temporary.cleanup()
|
||||
|
||||
def test_verification_uses_live_source_and_deduplicates_across_preview_states(self):
|
||||
value = broker.Broker()
|
||||
ident, session = identity.device_id("SYNTHETIC-A"), "session_one"
|
||||
current = {
|
||||
"id": ident,
|
||||
"session_id": session,
|
||||
"online": True,
|
||||
"status": {"preview": 1, "recording": 0},
|
||||
}
|
||||
calls = []
|
||||
|
||||
class Engine:
|
||||
def verify(self, key):
|
||||
calls.append(key)
|
||||
return {"state": "complete", "result": {"verified": True}}
|
||||
|
||||
value.media = Engine()
|
||||
sdk = []
|
||||
|
||||
def request(path, route, command=None, **kwargs):
|
||||
if route == "/snapshot":
|
||||
return current
|
||||
sdk.append(command)
|
||||
return {"state": "complete", "result": {"verified": True}}
|
||||
|
||||
first = command(ident, session, "verify")
|
||||
with patch.object(broker, "STATE", self.root), patch.object(broker, "request", request):
|
||||
self.assertEqual(value.verify_operation(ident, first)["state"], "complete")
|
||||
current["status"]["preview"] = 0
|
||||
self.assertEqual(value.verify_operation(ident, first)["state"], "complete")
|
||||
self.assertEqual(len(calls), 1)
|
||||
self.assertFalse(sdk)
|
||||
second = command(ident, session, "verify")
|
||||
self.assertEqual(value.verify_operation(ident, second)["state"], "complete")
|
||||
self.assertEqual(sdk, [second])
|
||||
current["status"]["recording"] = 1
|
||||
refused = value.verify_operation(ident, command(ident, session, "verify"))
|
||||
self.assertIn("остановите запись", refused["error"])
|
||||
self.assertEqual(len(sdk), 1)
|
||||
|
||||
def test_error_translation_preserves_verification_reason_and_redacts_vendor_text(self):
|
||||
class Failed:
|
||||
error = "preview_no_decodable_image"
|
||||
|
||||
def call(self, action, params):
|
||||
return {"state": "error", "error": self.error}
|
||||
|
||||
camera = Failed()
|
||||
ident = identity.device_id("SYNTHETIC-A")
|
||||
runtime = operations.Operations(ident, "session_one", self.root, camera)
|
||||
result = runtime.execute(command(ident, "session_one", "verify"))
|
||||
self.assertEqual(result["error"], "Камера не передала декодируемое изображение.")
|
||||
camera.error = "PRIVATE VENDOR DEVICE IDENTIFIER"
|
||||
result = runtime.execute(command(ident, "session_one", "verify"))
|
||||
self.assertEqual(result["error"], "Не удалось выполнить команду камеры.")
|
||||
camera.error = "unsupported_camera_parameter"
|
||||
result = runtime.execute(command(ident, "session_one", "verify"))
|
||||
self.assertEqual(result, operations.INVALID)
|
||||
|
||||
def test_receipt_deduplicates_start_after_worker_recreation(self):
|
||||
camera = Camera()
|
||||
ident = identity.device_id("SYNTHETIC-A")
|
||||
original = operations.Operations(ident, "session_one", self.root, camera)
|
||||
value = command(ident, "session_one")
|
||||
self.assertEqual(original.execute(value)["state"], "complete")
|
||||
resumed = operations.Operations(ident, "session_two", self.root, camera)
|
||||
self.assertEqual(resumed.execute(value)["state"], "complete")
|
||||
self.assertEqual(len(camera.calls), 1)
|
||||
|
||||
def test_two_instances_never_share_commands_or_receipts(self):
|
||||
first, second = Camera(), Camera()
|
||||
a, b = identity.device_id("SYNTHETIC-A"), identity.device_id("SYNTHETIC-B")
|
||||
left = operations.Operations(a, "session_a", self.root / "a", first)
|
||||
right = operations.Operations(b, "session_b", self.root / "b", second)
|
||||
value = command(a, "session_a")
|
||||
left.execute(value)
|
||||
with self.assertRaises(ValueError):
|
||||
right.execute(value)
|
||||
self.assertEqual(len(first.calls), 1)
|
||||
self.assertEqual(second.calls, [])
|
||||
|
||||
def test_expired_or_stale_session_has_no_sdk_effect(self):
|
||||
camera = Camera()
|
||||
ident = identity.device_id("SYNTHETIC-A")
|
||||
runtime = operations.Operations(ident, "session_two", self.root, camera)
|
||||
for value in [command(ident, "session_one"), command(ident, "session_two")]:
|
||||
if value["session"]["session_id"] == "session_two":
|
||||
value["requested_at"] = (datetime.now(UTC) - timedelta(seconds=90)).isoformat()
|
||||
value["deadline_at"] = (datetime.now(UTC) - timedelta(seconds=10)).isoformat()
|
||||
with self.assertRaises(ValueError):
|
||||
runtime.execute(value)
|
||||
self.assertEqual(camera.calls, [])
|
||||
|
||||
def test_sd_recording_and_unknown_status_block_profile_replacement(self):
|
||||
binding = identity.Binding(identity.device_id("SYNTHETIC"), "1-2", 1, 2, "SYNTHETIC")
|
||||
instance = worker.Worker(binding, self.root, self.root)
|
||||
instance.ready = True
|
||||
for recording, preview, expected in [
|
||||
(0, 0, True),
|
||||
(1, 0, False),
|
||||
(-1, 0, False),
|
||||
(0, 1, False),
|
||||
]:
|
||||
instance.status = {"connected": True, "recording": recording, "preview": preview}
|
||||
instance.observed = time.monotonic()
|
||||
self.assertEqual(instance.snapshot()["preparation_safe"], expected)
|
||||
instance.observed = 0
|
||||
self.assertFalse(instance.snapshot()["preparation_safe"])
|
||||
|
||||
def test_installer_fails_closed_when_worker_exists_but_broker_is_missing(self):
|
||||
instances = self.root / "instances"
|
||||
instances.mkdir()
|
||||
(instances / identity.device_id("SYNTHETIC")).mkdir()
|
||||
with (
|
||||
patch.object(prepare, "SOCKET", self.root / "absent.sock"),
|
||||
patch.object(prepare, "INSTANCES", instances),
|
||||
self.assertRaises(RuntimeError),
|
||||
):
|
||||
prepare.assert_safe()
|
||||
|
||||
def test_installer_refuses_active_recording_even_if_preview_idle(self):
|
||||
socket = self.root / "driver.sock"
|
||||
socket.touch()
|
||||
with (
|
||||
patch.object(prepare, "SOCKET", socket),
|
||||
patch.object(prepare, "request", return_value={"safe": False}),
|
||||
self.assertRaises(RuntimeError),
|
||||
):
|
||||
prepare.assert_safe()
|
||||
|
||||
def test_runtime_integrity_rechecks_existing_install(self):
|
||||
files = {"lib/synthetic.so": b"SYNTHETIC-NOT-AN-ELF", "python/example.py": b"value = 1\n"}
|
||||
data = io.BytesIO()
|
||||
with zipfile.ZipFile(data, "w") as archive:
|
||||
for name, content in files.items():
|
||||
archive.writestr(name, content)
|
||||
payload = data.getvalue()
|
||||
share, state = self.root / "share", self.root / "state"
|
||||
share.mkdir()
|
||||
state.mkdir()
|
||||
(share / "payload.zip").write_bytes(payload)
|
||||
bundle = {
|
||||
"revision": "a" * 24,
|
||||
"payload_sha256": hashlib.sha256(payload).hexdigest(),
|
||||
"files": {
|
||||
name: {"bytes": len(content), "sha256": hashlib.sha256(content).hexdigest()}
|
||||
for name, content in files.items()
|
||||
},
|
||||
}
|
||||
|
||||
def directory(path):
|
||||
path.mkdir(exist_ok=True)
|
||||
|
||||
with (
|
||||
patch.object(prepare, "STATE", state),
|
||||
patch.object(prepare, "SHARE", share),
|
||||
patch.object(prepare, "trusted", side_effect=lambda path, directory=False: path),
|
||||
patch.object(prepare, "directory", side_effect=directory),
|
||||
):
|
||||
ident = prepare.install_runtime(bundle)
|
||||
self.assertEqual(prepare.install_runtime(bundle), ident)
|
||||
(state / "runtime" / ident / "python/example.py").write_bytes(b"modified")
|
||||
with self.assertRaises(RuntimeError):
|
||||
prepare.install_runtime(bundle)
|
||||
|
||||
def test_payload_rejects_traversal_before_extraction(self):
|
||||
data = io.BytesIO()
|
||||
with zipfile.ZipFile(data, "w") as archive:
|
||||
archive.writestr("../escape", b"synthetic")
|
||||
info = {"bytes": 9, "sha256": hashlib.sha256(b"synthetic").hexdigest()}
|
||||
with (
|
||||
zipfile.ZipFile(io.BytesIO(data.getvalue())) as archive,
|
||||
self.assertRaises(RuntimeError),
|
||||
):
|
||||
list(prepare.entries(archive, {"files": {"../escape": info}}))
|
||||
self.assertFalse((self.root.parent / "escape").exists())
|
||||
|
||||
def test_private_socket_checks_kernel_peer_credentials(self):
|
||||
# Keep the socket in owned staging through a short directory-FD alias;
|
||||
# Linux sockaddr_un cannot hold the long build-source pathname.
|
||||
descriptor = os.open(self.root, os.O_RDONLY)
|
||||
self.addCleanup(os.close, descriptor)
|
||||
for allowed, success in [({os.geteuid()}, True), ({os.geteuid() + 10000}, False)]:
|
||||
path = Path(f"/proc/self/fd/{descriptor}") / ("yes.sock" if success else "no.sock")
|
||||
server = http.Server(path, lambda *_: {"ok": True}, allowed)
|
||||
thread = threading.Thread(target=server.serve_forever, kwargs={"poll_interval": 0.01})
|
||||
thread.start()
|
||||
try:
|
||||
if success:
|
||||
self.assertTrue(http.request(path, "/snapshot")["ok"])
|
||||
else:
|
||||
with self.assertRaises((OSError, http.http.client.HTTPException)):
|
||||
http.request(path, "/snapshot")
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(2)
|
||||
self.assertFalse(thread.is_alive())
|
||||
|
||||
def test_worker_routes_never_accept_arbitrary_device_paths(self):
|
||||
for value in ("../another.sock", "/run/driver.sock", "instax4_bad", None):
|
||||
with self.assertRaises(ValueError):
|
||||
broker.worker_socket(value)
|
||||
|
||||
def test_verification_requires_decoded_images_and_only_closes_own_preview(self):
|
||||
camera = VideoCamera(synthetic_video())
|
||||
control = Control(camera)
|
||||
result = control.verify(timeout=1)
|
||||
self.assertEqual(result["state"], "complete")
|
||||
self.assertTrue(control.verified)
|
||||
self.assertGreaterEqual(result["result"]["streams"][0]["frames"], 2)
|
||||
self.assertEqual(result["result"]["streams"][0]["width"], 32)
|
||||
self.assertEqual(camera.calls, ["details", "preview.start", "preview.stop"])
|
||||
existing = VideoCamera(synthetic_video(), preview=1)
|
||||
self.assertEqual(Control(existing).verify(timeout=1)["state"], "complete")
|
||||
self.assertEqual(existing.calls, ["details"])
|
||||
|
||||
def test_packets_and_unconfirmed_preview_cleanup_are_not_verified(self):
|
||||
broken = VideoCamera([b"SYNTHETIC-NOT-VIDEO"])
|
||||
control = Control(broken)
|
||||
self.assertEqual(control.verify(timeout=0.03)["state"], "error")
|
||||
self.assertFalse(control.verified)
|
||||
uncertain = VideoCamera(synthetic_video(), stop_unknown=True)
|
||||
control = Control(uncertain)
|
||||
self.assertEqual(control.verify(timeout=1)["state"], "unknown")
|
||||
self.assertFalse(control.verified)
|
||||
|
||||
def test_image_verification_does_not_interrupt_sd_recording(self):
|
||||
camera = VideoCamera([], recording=1)
|
||||
self.assertEqual(Control(camera).verify(timeout=0.03)["state"], "error")
|
||||
self.assertEqual(camera.calls, ["details"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user