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]]
|
[[package]]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.27.10"
|
version = "0.27.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1692,7 +1692,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.27.10"
|
version = "0.27.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@ -1704,7 +1704,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.27.10"
|
version = "0.27.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.27.10"
|
version = "0.27.11"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.27.10"
|
version = "0.27.11"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
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
|
timestamps. This is a hardware-testable correction; marked Tethys capture is
|
||||||
still required before declaring the tear/smear class fixed.
|
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
|
## 7. The Resolved Downstream Video Failure
|
||||||
|
|
||||||
The current blank downstream feeds fail before transport or decoding.
|
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
|
6. Run `scripts/install/server.sh` as the trusted deployment path. Preserve the
|
||||||
already-attached USB gadget unless a controlled rebuild is explicitly
|
already-attached USB gadget unless a controlled rebuild is explicitly
|
||||||
required.
|
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.
|
direct MJPEG normalizer timeout `0`, and a coherent UVC contract.
|
||||||
8. Open both downstream RPCs and prove that each emits changing, decodable H.264
|
8. Open both downstream RPCs and prove that each emits changing, decodable H.264
|
||||||
frames.
|
frames.
|
||||||
@ -420,7 +425,7 @@ hardware contract is repeatable. The remaining work falls into five groups.
|
|||||||
|
|
||||||
### A. Install And Version Parity
|
### 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.
|
- Confirm client/server version and revision in every hardware probe artifact.
|
||||||
- Eliminate the current state where a fixed client talks to an unfixed server.
|
- 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
|
9. disconnect/reconnect and device changes recover without stale media; and
|
||||||
10. diagnostics identify the failed physical stage when any item breaks.
|
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
|
current release should be described as a validated code correction awaiting
|
||||||
hardware deployment and end-to-end acceptance, not as a completed product fix.
|
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
|
analysis_elapsed = 0.0
|
||||||
raw_capture_bytes = 0
|
raw_capture_bytes = 0
|
||||||
ffmpeg_rc: int | None = None
|
ffmpeg_rc: int | None = None
|
||||||
|
capture_timed_out = False
|
||||||
previous: bytes | None = None
|
previous: bytes | None = None
|
||||||
frame_index = 0
|
frame_index = 0
|
||||||
suspicious_count = 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")
|
(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)
|
print(f"capturing raw RCT frames before analysis: {raw_path}", file=sys.stderr)
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
proc = subprocess.run(capture_command, stdout=subprocess.DEVNULL, stderr=err, check=False)
|
try:
|
||||||
capture_elapsed = time.monotonic() - started
|
proc = subprocess.run(
|
||||||
|
capture_command,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=err,
|
||||||
|
check=False,
|
||||||
|
timeout=max(5.0, args.duration + 10.0),
|
||||||
|
)
|
||||||
ffmpeg_rc = proc.returncode
|
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
|
raw_capture_bytes = raw_path.stat().st_size if raw_path.exists() else 0
|
||||||
print(
|
print(
|
||||||
f"analyzing captured raw RCT frames bytes={raw_capture_bytes} capture_s={capture_elapsed:.3f}",
|
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),
|
"duration_observed_s": round(elapsed, 3),
|
||||||
"analysis_duration_s": round(analysis_elapsed, 3),
|
"analysis_duration_s": round(analysis_elapsed, 3),
|
||||||
"ffmpeg_rc": ffmpeg_rc,
|
"ffmpeg_rc": ffmpeg_rc,
|
||||||
|
"capture_timed_out": capture_timed_out,
|
||||||
"raw_capture_bytes": raw_capture_bytes,
|
"raw_capture_bytes": raw_capture_bytes,
|
||||||
"frames": frame_index,
|
"frames": frame_index,
|
||||||
"fps_observed": round(frame_index / elapsed, 3),
|
"fps_observed": round(frame_index / elapsed, 3),
|
||||||
@ -633,6 +650,7 @@ def format_summary(summary: dict[str, Any]) -> str:
|
|||||||
f"device: {summary['device']}",
|
f"device: {summary['device']}",
|
||||||
f"mode: {summary['width']}x{summary['height']}@{summary['fps_requested']}",
|
f"mode: {summary['width']}x{summary['height']}@{summary['fps_requested']}",
|
||||||
f"frames: {summary['frames']} ({summary['fps_observed']} fps observed)",
|
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"suspicious: {summary['suspicious_frames']} ({summary['suspicious_pct']}%)",
|
||||||
f"static: {summary.get('static_frames', 0)} ({summary.get('static_pct', 0.0)}%)",
|
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)}",
|
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]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.27.10"
|
version = "0.27.11"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
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]
|
#[test]
|
||||||
fn rct_uvc_artifact_probe_self_test_flags_synthetic_lower_half_slab() {
|
fn rct_uvc_artifact_probe_self_test_flags_synthetic_lower_half_slab() {
|
||||||
let dir = tempfile::tempdir().expect("tempdir");
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user