From 9b1bf46cb90def073605cc322f75e692ed7eaf18 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Thu, 13 Aug 2026 03:07:34 -0300 Subject: [PATCH] fix(probe): bound long UVC capture --- Cargo.lock | 6 ++--- client/Cargo.toml | 2 +- common/Cargo.toml | 2 +- docs/architecture-and-media-readiness.md | 11 +++++++--- scripts/manual/run_rct_uvc_artifact_probe.py | 22 +++++++++++++++++-- server/Cargo.toml | 2 +- .../rct_uvc_artifact_probe_manual_contract.rs | 17 ++++++++++++++ 7 files changed, 51 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 665e919..fc30882 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1658,7 +1658,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.27.10" +version = "0.27.11" dependencies = [ "anyhow", "async-stream", @@ -1692,7 +1692,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.27.10" +version = "0.27.11" dependencies = [ "anyhow", "base64", @@ -1704,7 +1704,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.27.10" +version = "0.27.11" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index c06a01e..5ba45ea 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.27.10" +version = "0.27.11" edition = "2024" [dependencies] diff --git a/common/Cargo.toml b/common/Cargo.toml index 1f55ed7..da1d66c 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.27.10" +version = "0.27.11" edition = "2024" build = "build.rs" diff --git a/docs/architecture-and-media-readiness.md b/docs/architecture-and-media-readiness.md index ed76268..136dede 100644 --- a/docs/architecture-and-media-readiness.md +++ b/docs/architecture-and-media-readiness.md @@ -326,6 +326,11 @@ probe terminate with evidence even when malformed packets produce no decoded timestamps. This is a hardware-testable correction; marked Tethys capture is still required before declaring the tear/smear class fixed. +Release 0.27.11 extends the same wall-clock guarantee to the long-running real +webcam artifact detector. A malformed stream now yields a bounded report with +`capture_timed_out`, FFmpeg diagnostics, and extracted reference/suspicious +frames instead of blocking the soak indefinitely. + ## 7. The Resolved Downstream Video Failure The current blank downstream feeds fail before transport or decoding. @@ -402,7 +407,7 @@ The safe completion sequence for this incident is: 6. Run `scripts/install/server.sh` as the trusted deployment path. Preserve the already-attached USB gadget unless a controlled rebuild is explicitly required. -7. Confirm Theia reports server version `0.27.10`, the pushed release revision, +7. Confirm Theia reports server version `0.27.11`, the pushed release revision, direct MJPEG normalizer timeout `0`, and a coherent UVC contract. 8. Open both downstream RPCs and prove that each emits changing, decodable H.264 frames. @@ -420,7 +425,7 @@ hardware contract is repeatable. The remaining work falls into five groups. ### A. Install And Version Parity -- Push and deploy `0.27.10` through the client/server install scripts. +- Push and deploy `0.27.11` through the client/server install scripts. - Confirm client/server version and revision in every hardware probe artifact. - Eliminate the current state where a fixed client talks to an unfixed server. @@ -486,6 +491,6 @@ host repair: 9. disconnect/reconnect and device changes recover without stale media; and 10. diagnostics identify the failed physical stage when any item breaks. -Until that sequence passes on the installed `0.27.10` client/server pair, the +Until that sequence passes on the installed `0.27.11` client/server pair, the current release should be described as a validated code correction awaiting hardware deployment and end-to-end acceptance, not as a completed product fix. diff --git a/scripts/manual/run_rct_uvc_artifact_probe.py b/scripts/manual/run_rct_uvc_artifact_probe.py index 30d6e2d..db688b1 100755 --- a/scripts/manual/run_rct_uvc_artifact_probe.py +++ b/scripts/manual/run_rct_uvc_artifact_probe.py @@ -476,6 +476,7 @@ def run_capture(args: argparse.Namespace) -> int: analysis_elapsed = 0.0 raw_capture_bytes = 0 ffmpeg_rc: int | None = None + capture_timed_out = False previous: bytes | None = None frame_index = 0 suspicious_count = 0 @@ -569,9 +570,24 @@ def run_capture(args: argparse.Namespace) -> int: (artifact_dir / "command.txt").write_text(" ".join(shlex.quote(part) for part in capture_command) + "\n") print(f"capturing raw RCT frames before analysis: {raw_path}", file=sys.stderr) started = time.monotonic() - proc = subprocess.run(capture_command, stdout=subprocess.DEVNULL, stderr=err, check=False) + try: + proc = subprocess.run( + capture_command, + stdout=subprocess.DEVNULL, + stderr=err, + check=False, + timeout=max(5.0, args.duration + 10.0), + ) + ffmpeg_rc = proc.returncode + except subprocess.TimeoutExpired: + capture_timed_out = True + ffmpeg_rc = 124 + err.write( + f"capture exceeded wall timeout after {args.duration + 10.0:.1f}s\n".encode() + ) + err.flush() capture_elapsed = time.monotonic() - started - ffmpeg_rc = proc.returncode + raw_path.touch(exist_ok=True) raw_capture_bytes = raw_path.stat().st_size if raw_path.exists() else 0 print( f"analyzing captured raw RCT frames bytes={raw_capture_bytes} capture_s={capture_elapsed:.3f}", @@ -601,6 +617,7 @@ def run_capture(args: argparse.Namespace) -> int: "duration_observed_s": round(elapsed, 3), "analysis_duration_s": round(analysis_elapsed, 3), "ffmpeg_rc": ffmpeg_rc, + "capture_timed_out": capture_timed_out, "raw_capture_bytes": raw_capture_bytes, "frames": frame_index, "fps_observed": round(frame_index / elapsed, 3), @@ -633,6 +650,7 @@ def format_summary(summary: dict[str, Any]) -> str: f"device: {summary['device']}", f"mode: {summary['width']}x{summary['height']}@{summary['fps_requested']}", f"frames: {summary['frames']} ({summary['fps_observed']} fps observed)", + f"capture timed out: {summary.get('capture_timed_out', False)}", f"suspicious: {summary['suspicious_frames']} ({summary['suspicious_pct']}%)", f"static: {summary.get('static_frames', 0)} ({summary.get('static_pct', 0.0)}%)", f"max deltas: upper={summary.get('max_upper_delta', 0.0)} lower={summary.get('max_lower_delta', 0.0)}", diff --git a/server/Cargo.toml b/server/Cargo.toml index bc4edff..638c6d0 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -16,7 +16,7 @@ bench = false [package] name = "lesavka_server" -version = "0.27.10" +version = "0.27.11" edition = "2024" autobins = false diff --git a/tests/manual/server/rct/rct_uvc_artifact_probe_manual_contract.rs b/tests/manual/server/rct/rct_uvc_artifact_probe_manual_contract.rs index 314c072..d14f509 100644 --- a/tests/manual/server/rct/rct_uvc_artifact_probe_manual_contract.rs +++ b/tests/manual/server/rct/rct_uvc_artifact_probe_manual_contract.rs @@ -52,6 +52,23 @@ fn rct_uvc_artifact_probe_documents_late_path_lower_half_detection() { } } +#[test] +fn rct_uvc_artifact_probe_terminates_when_malformed_video_never_decodes() { + for expected in [ + "timeout=max(5.0, args.duration + 10.0)", + "except subprocess.TimeoutExpired:", + "capture_timed_out = True", + "ffmpeg_rc = 124", + "raw_path.touch(exist_ok=True)", + "\"capture_timed_out\": capture_timed_out", + ] { + assert!( + PROBE_SRC.contains(expected), + "long receiver probe must report instead of hanging when corrupt UVC packets produce no decoded frames: {expected}" + ); + } +} + #[test] fn rct_uvc_artifact_probe_self_test_flags_synthetic_lower_half_slab() { let dir = tempfile::tempdir().expect("tempdir");