Add packaged per-camera X4 wake and preview recovery
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
"""Fixed Bluetooth privilege boundary. Never accepts serials, paths or SDK calls."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import pwd
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from layout import CODE, trusted, write # noqa: E402
|
||||
from runtime.http import Server # noqa: E402
|
||||
from runtime.identity import read_binding # noqa: E402
|
||||
from runtime.lifecycle import acquisition # noqa: E402
|
||||
|
||||
ROOT = Path("/var/lib/mission-core-x4-recovery")
|
||||
SOCKET = Path("/run/mission-core-x4-recovery/driver.sock")
|
||||
IDENTIFIER = re.compile(r"instax4_[0-9a-f]{32}")
|
||||
|
||||
|
||||
def connected():
|
||||
values = []
|
||||
for path in Path("/sys/bus/usb/devices").iterdir():
|
||||
with suppress(OSError, ValueError):
|
||||
values.append(read_binding(path.name))
|
||||
return values
|
||||
|
||||
|
||||
class WakeService:
|
||||
def __init__(self, root=ROOT):
|
||||
self.root = root
|
||||
self.lock = threading.RLock()
|
||||
self.advertiser = threading.Lock()
|
||||
self.running = {}
|
||||
|
||||
def load(self, ident):
|
||||
path = self.root / (ident + ".json")
|
||||
if not path.exists():
|
||||
return {"enabled": False, "attempts": []}
|
||||
return json.loads(trusted(path).read_text())
|
||||
|
||||
def save(self, ident, value):
|
||||
write(self.root / (ident + ".json"), json.dumps(value).encode(), 0o600)
|
||||
|
||||
def control(self, ident, enabled):
|
||||
with self.lock:
|
||||
value = self.load(ident)
|
||||
if not enabled and "serial" not in value:
|
||||
return {"enabled": False}
|
||||
if enabled:
|
||||
matches = [x for x in connected() if x.device_id == ident]
|
||||
if len(matches) != 1 or not re.fullmatch(r"[A-Za-z0-9]{7,64}", matches[0].serial):
|
||||
raise ValueError("Connect the exact X4 before enabling recovery")
|
||||
if len(list(self.root.glob("instax4_*.json"))) >= 500 and "serial" not in value:
|
||||
raise ValueError("Recovery inventory is full")
|
||||
value["serial"] = matches[0].serial
|
||||
value["enabled"] = enabled
|
||||
self.save(ident, value)
|
||||
if not enabled and ident in self.running:
|
||||
self.running[ident].terminate()
|
||||
return {"enabled": enabled}
|
||||
|
||||
def wake(self, ident, attempt):
|
||||
if not self.advertiser.acquire(blocking=False):
|
||||
return {"state": "busy"}
|
||||
try:
|
||||
# Installation cannot replace code/state while a wake is in flight.
|
||||
with acquisition():
|
||||
return self.wake_locked(ident, attempt)
|
||||
finally:
|
||||
self.advertiser.release()
|
||||
|
||||
def wake_locked(self, ident, attempt):
|
||||
with self.lock:
|
||||
value = self.load(ident)
|
||||
if not value["enabled"] or "serial" not in value:
|
||||
return {"state": "disabled"}
|
||||
serial = value["serial"]
|
||||
if any(x.device_id == ident for x in connected()):
|
||||
return {"state": "connected"}
|
||||
# A radio suffix is weaker than full USB identity. Refuse known collisions.
|
||||
for path in self.root.glob("instax4_*.json"):
|
||||
other = json.loads(trusted(path).read_text()).get("serial", "")
|
||||
if other and other != serial and other[-6:] == serial[-6:]:
|
||||
return {"state": "identity_conflict"}
|
||||
now = time.time()
|
||||
attempts = [x for x in value["attempts"] if now - x["at"] < 600]
|
||||
if any(x["id"] == attempt for x in attempts):
|
||||
return {"state": "already_attempted"}
|
||||
if len(attempts) >= 3:
|
||||
return {"state": "exhausted"}
|
||||
value["attempts"] = attempts + [{"id": attempt, "at": now}]
|
||||
# Persist before advertising; a crash cannot silently replay the attempt.
|
||||
self.save(ident, value)
|
||||
target = {
|
||||
"device_id": ident,
|
||||
"serial": serial,
|
||||
"bluetooth_address": None,
|
||||
"wakeup_enabled": True,
|
||||
}
|
||||
with tempfile.TemporaryFile(dir=self.root) as output:
|
||||
process = subprocess.Popen(
|
||||
["/usr/bin/python3", "-I", str(CODE / "ble_wake.py")],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=output,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
self.running[ident] = process
|
||||
process.stdin.write(json.dumps(target).encode())
|
||||
process.stdin.close()
|
||||
# Release state lock during hardware wait so disable can cancel.
|
||||
self.lock.release()
|
||||
try:
|
||||
try:
|
||||
process.wait(timeout=45)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait(timeout=5)
|
||||
output.seek(0)
|
||||
raw = output.read(1024 * 1024 + 1)
|
||||
finally:
|
||||
self.lock.acquire()
|
||||
self.running.pop(ident, None)
|
||||
state = "unavailable"
|
||||
try:
|
||||
if len(raw) <= 1024 * 1024:
|
||||
report = json.loads(raw)
|
||||
write(self.root / (ident + ".last-wake"), raw, 0o600)
|
||||
if process.returncode == 0 and report.get("sdk_usb_returned"):
|
||||
state = "connected"
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
return {"state": state if self.load(ident)["enabled"] else "disabled"}
|
||||
|
||||
def dispatch(self, method, route, value, _headers):
|
||||
if method == "GET" and route == "/health":
|
||||
return {"ready": True}
|
||||
if method != "POST" or not isinstance(value, dict):
|
||||
raise ValueError("Unsupported recovery request")
|
||||
ident = value.get("device_id")
|
||||
if not isinstance(ident, str) or not IDENTIFIER.fullmatch(ident):
|
||||
raise ValueError("Invalid recovery identity")
|
||||
if route == "/control" and set(value) == {"device_id", "enabled"}:
|
||||
if type(value["enabled"]) is not bool:
|
||||
raise ValueError("Recovery requires an explicit boolean")
|
||||
return self.control(ident, value["enabled"])
|
||||
if route == "/wake" and set(value) == {"device_id", "attempt"}:
|
||||
if not isinstance(value["attempt"], str) or not re.fullmatch(
|
||||
r"[0-9a-f]{32}", value["attempt"]
|
||||
):
|
||||
raise ValueError("Invalid wake attempt")
|
||||
return self.wake(ident, value["attempt"])
|
||||
raise ValueError("Unsupported recovery request")
|
||||
|
||||
|
||||
def main():
|
||||
if os.geteuid() or sys.argv[1:]:
|
||||
raise RuntimeError("Use the installed fixed wake service")
|
||||
trusted(ROOT, True)
|
||||
if SOCKET.exists():
|
||||
SOCKET.unlink()
|
||||
allowed = {0, pwd.getpwnam("mission-core-insta360").pw_uid}
|
||||
with Server(SOCKET, WakeService().dispatch, allowed) as server:
|
||||
SOCKET.chmod(0o660)
|
||||
server.serve_forever(poll_interval=0.5)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user