fix(probe): bound long UVC capture

This commit is contained in:
Brad Stein 2026-08-13 03:07:34 -03:00
parent 2a5f059a0b
commit 9b1bf46cb9
7 changed files with 51 additions and 11 deletions

6
Cargo.lock generated
View File

@ -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",

View File

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

View File

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

View File

@ -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.

View File

@ -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)
capture_elapsed = time.monotonic() - started
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
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)}",

View File

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

View File

@ -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");