fix(probe): bound long UVC capture
This commit is contained in:
parent
2a5f059a0b
commit
9b1bf46cb9
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.27.10"
|
||||
version = "0.27.11"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.27.10"
|
||||
version = "0.27.11"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)}",
|
||||
|
||||
@ -16,7 +16,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.27.10"
|
||||
version = "0.27.11"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user