probe: pause injector upstream during synthetic rct runs

This commit is contained in:
Brad Stein 2026-05-18 04:34:06 -03:00
parent 6d9e152a9f
commit b8906ba8ed
6 changed files with 185 additions and 8 deletions

6
Cargo.lock generated
View File

@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "lesavka_client" name = "lesavka_client"
version = "0.22.57" version = "0.22.58"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
@ -1686,7 +1686,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_common" name = "lesavka_common"
version = "0.22.57" version = "0.22.58"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@ -1698,7 +1698,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_server" name = "lesavka_server"
version = "0.22.57" version = "0.22.58"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package] [package]
name = "lesavka_client" name = "lesavka_client"
version = "0.22.57" version = "0.22.58"
edition = "2024" edition = "2024"
[dependencies] [dependencies]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "lesavka_common" name = "lesavka_common"
version = "0.22.57" version = "0.22.58"
edition = "2024" edition = "2024"
build = "build.rs" build = "build.rs"

View File

@ -27,6 +27,123 @@ MARKER_COLUMNS = 16
CADENCE_REASONS = {"frame_repeat", "frame_gap", "frame_backwards"} CADENCE_REASONS = {"frame_repeat", "frame_gap", "frame_backwards"}
NON_VISUAL_REASONS = CADENCE_REASONS | {"sequence_marker_mismatch"} NON_VISUAL_REASONS = CADENCE_REASONS | {"sequence_marker_mismatch"}
REMOTE_MEDIA_CONTROL_PAUSE = r"""
import base64
import json
import pathlib
import sys
import time
DEFAULT_MEDIA_CONTROL_PATH = "/tmp/lesavka-media.control"
def media_control_with_camera(raw, enabled):
tokens = raw.split() if raw else []
rendered = []
saw_camera = False
saw_microphone = False
saw_audio = False
for token in tokens:
key, sep, _value = token.partition("=")
if sep and key == "camera":
rendered.append(f"camera={1 if enabled else 0}")
saw_camera = True
else:
rendered.append(token)
saw_microphone = saw_microphone or (sep and key in {"microphone", "mic"})
saw_audio = saw_audio or (sep and key in {"audio", "speaker"})
if not saw_camera:
rendered.insert(0, f"camera={1 if enabled else 0}")
if not saw_microphone:
rendered.append("microphone=1")
if not saw_audio:
rendered.append("audio=1")
return " ".join(rendered) + "\n"
def discover_media_control_paths():
candidates = set()
proc = pathlib.Path("/proc")
if not proc.exists():
return []
for entry in proc.iterdir():
if not entry.name.isdigit():
continue
try:
environ = (entry / "environ").read_bytes()
cmdline = (entry / "cmdline").read_bytes().replace(b"\0", b" ")
except (FileNotFoundError, PermissionError, ProcessLookupError, OSError):
continue
if b"lesavka" not in cmdline and b"LESAVKA_MEDIA_CONTROL=" not in environ:
continue
for token in environ.split(b"\0"):
if token.startswith(b"LESAVKA_MEDIA_CONTROL="):
raw_path = token.split(b"=", 1)[1].decode(errors="replace")
if raw_path:
candidates.add(pathlib.Path(raw_path))
return sorted(
candidates,
key=lambda path: (
not path.exists(),
-path.stat().st_mtime if path.exists() else 0,
str(path),
),
)
request = json.loads(sys.argv[1])
state_path = pathlib.Path(request["state_path"])
explicit_path = request.get("media_control_path") or ""
discovered = [] if explicit_path else discover_media_control_paths()
path = (
pathlib.Path(explicit_path)
if explicit_path
else (discovered[0] if discovered else pathlib.Path(DEFAULT_MEDIA_CONTROL_PATH))
)
original = path.read_bytes() if path.exists() else None
original_text = original.decode(errors="replace") if original is not None else None
path.write_text(media_control_with_camera(original_text, False))
state_path.write_text(
json.dumps(
{
"path": str(path),
"had_original": original is not None,
"original_b64": base64.b64encode(original or b"").decode(),
},
sort_keys=True,
)
+ "\n"
)
time.sleep(0.5)
print(
json.dumps(
{
"path": str(path),
"state_path": str(state_path),
"discovered": [str(path) for path in discovered],
}
)
)
"""
REMOTE_MEDIA_CONTROL_RESTORE = r"""
import base64
import json
import pathlib
import sys
request = json.loads(sys.argv[1])
state_path = pathlib.Path(request["state_path"])
state = json.loads(state_path.read_text())
path = pathlib.Path(state["path"])
if state.get("had_original"):
path.write_bytes(base64.b64decode(state.get("original_b64") or ""))
else:
path.unlink(missing_ok=True)
state_path.unlink(missing_ok=True)
print(json.dumps({"path": str(path), "state_path": str(state_path)}))
"""
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
@ -57,7 +174,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument( parser.add_argument(
"--pause-local-live-upstream", "--pause-local-live-upstream",
action="store_true", action="store_true",
help="temporarily write camera=0 to the local Lesavka media control file so a live client does not preempt the synthetic injector", help="temporarily write camera=0 to the injector host's Lesavka media control file so a live client does not preempt the synthetic injector",
) )
parser.add_argument( parser.add_argument(
"--media-control-path", "--media-control-path",
@ -262,6 +379,44 @@ def restore_local_live_upstream(path: pathlib.Path, original: bytes | None) -> N
print(f"restored local live media control at {path}", file=sys.stderr) print(f"restored local live media control at {path}", file=sys.stderr)
def run_remote_python(host: str, script: str, payload: dict[str, Any]) -> dict[str, Any]:
output = subprocess.check_output(
["ssh", host, f"python3 - {shlex.quote(json.dumps(payload, sort_keys=True))}"],
input=script,
text=True,
)
return json.loads(output.strip().splitlines()[-1])
def pause_remote_live_upstream(host: str, args: argparse.Namespace) -> dict[str, Any]:
state_path = f"/tmp/lesavka-synthetic-rct-media-control-{os.getpid()}.json"
state = run_remote_python(
host,
REMOTE_MEDIA_CONTROL_PAUSE,
{
"media_control_path": args.media_control_path,
"state_path": state_path,
},
)
print(
f"paused injector-host live camera upstream on {host} via {state['path']}",
file=sys.stderr,
)
return state
def restore_remote_live_upstream(host: str, state: dict[str, Any]) -> None:
restored = run_remote_python(
host,
REMOTE_MEDIA_CONTROL_RESTORE,
{"state_path": state["state_path"]},
)
print(
f"restored injector-host live media control on {host} at {restored['path']}",
file=sys.stderr,
)
def run_remote_orchestrated(args: argparse.Namespace) -> int: def run_remote_orchestrated(args: argparse.Namespace) -> int:
if (not args.inject_host and not args.local_inject) or not args.rct_host: if (not args.inject_host and not args.local_inject) or not args.rct_host:
raise SystemExit( raise SystemExit(
@ -442,9 +597,14 @@ def run_remote_orchestrated(args: argparse.Namespace) -> int:
capture: subprocess.Popen[Any] | None = None capture: subprocess.Popen[Any] | None = None
diagnosis: list[str] = [] diagnosis: list[str] = []
paused_control: tuple[pathlib.Path, bytes | None] | None = None paused_control: tuple[pathlib.Path, bytes | None] | None = None
paused_remote_control: tuple[str, dict[str, Any]] | None = None
try: try:
if args.pause_local_live_upstream: if args.pause_local_live_upstream:
paused_control = pause_local_live_upstream(args) if args.local_inject:
paused_control = pause_local_live_upstream(args)
else:
remote_state = pause_remote_live_upstream(args.inject_host, args)
paused_remote_control = (args.inject_host, remote_state)
if args.capture_before_inject: if args.capture_before_inject:
capture = start_capture() capture = start_capture()
time.sleep(1.0) time.sleep(1.0)
@ -464,6 +624,8 @@ def run_remote_orchestrated(args: argparse.Namespace) -> int:
capture = start_capture() capture = start_capture()
capture_rc, inject_rc = wait_capture_or_inject_exit(capture, inject) capture_rc, inject_rc = wait_capture_or_inject_exit(capture, inject)
finally: finally:
if paused_remote_control is not None:
restore_remote_live_upstream(*paused_remote_control)
if paused_control is not None: if paused_control is not None:
restore_local_live_upstream(*paused_control) restore_local_live_upstream(*paused_control)
local_capture = artifact_dir / "capture" local_capture = artifact_dir / "capture"
@ -494,7 +656,15 @@ def run_remote_orchestrated(args: argparse.Namespace) -> int:
) )
frames = int(capture_data.get("frames") or 0) frames = int(capture_data.get("frames") or 0)
reason_counts = capture_data.get("reason_counts") or {} reason_counts = capture_data.get("reason_counts") or {}
visual_reasons = capture_data.get("visual_reason_counts") or {}
visual_frames = int(capture_data.get("visual_suspicious_frames") or 0)
suspicious_frames = int(capture_data.get("suspicious_frames") or 0)
repeats = int(reason_counts.get("frame_repeat") or 0) repeats = int(reason_counts.get("frame_repeat") or 0)
cadence_only = suspicious_frames > 0 and visual_frames == 0 and not visual_reasons
if cadence_only:
diagnosis.append(
"RCT capture had cadence-only repeat/gap events; no visual tear/mixed-frame corruption was detected in aligned synthetic frames"
)
if frames > 0 and repeats >= max(3, int(frames * 0.9)): if frames > 0 and repeats >= max(3, int(frames * 0.9)):
diagnosis.append( diagnosis.append(
"RCT capture repeated nearly every decoded synthetic marker; the received UVC stream was stale/frozen instead of advancing" "RCT capture repeated nearly every decoded synthetic marker; the received UVC stream was stale/frozen instead of advancing"

View File

@ -16,7 +16,7 @@ bench = false
[package] [package]
name = "lesavka_server" name = "lesavka_server"
version = "0.22.57" version = "0.22.58"
edition = "2024" edition = "2024"
autobins = false autobins = false

View File

@ -97,10 +97,17 @@ fn synthetic_probe_keeps_bundled_network_ingress_and_rct_comparison_markers() {
"synthetic uplink exited before capture warmup completed", "synthetic uplink exited before capture warmup completed",
"paused local live camera upstream", "paused local live camera upstream",
"restored local live media control", "restored local live media control",
"paused injector-host live camera upstream",
"restored injector-host live media control",
"media_control_with_camera", "media_control_with_camera",
"discover_media_control_paths", "discover_media_control_paths",
"resolve_media_control_path", "resolve_media_control_path",
"pause_remote_live_upstream",
"restore_remote_live_upstream",
"run_remote_python",
"discovered live Lesavka media control path", "discovered live Lesavka media control path",
"cadence-only repeat/gap",
"no visual tear/mixed-frame corruption was detected",
"max_lower_mae", "max_lower_mae",
"ffmpeg", "ffmpeg",
"v4l2", "v4l2",