diff --git a/Cargo.lock b/Cargo.lock index d63c657..6ab5821 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.16.14" +version = "0.16.15" dependencies = [ "anyhow", "async-stream", @@ -1686,7 +1686,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.16.14" +version = "0.16.15" dependencies = [ "anyhow", "base64", @@ -1698,7 +1698,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.16.14" +version = "0.16.15" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index 622c9af..bad1d6e 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.16.14" +version = "0.16.15" edition = "2024" [dependencies] diff --git a/client/src/input/camera/capture_pipeline.rs b/client/src/input/camera/capture_pipeline.rs index 3e8d268..b9da408 100644 --- a/client/src/input/camera/capture_pipeline.rs +++ b/client/src/input/camera/capture_pipeline.rs @@ -22,7 +22,8 @@ impl CameraCapture { true, ), Some(fragment) => { - let dev = Self::find_device(fragment).unwrap_or_else(|| "/dev/video0".into()); + let dev = Self::find_device(fragment) + .with_context(|| format!("requested camera '{fragment}' was not found"))?; (format!("v4l2src device={dev} do-timestamp=true"), dev, true) } None => { diff --git a/client/src/input/camera/device_selection.rs b/client/src/input/camera/device_selection.rs index 1c95c05..9d2dfea 100644 --- a/client/src/input/camera/device_selection.rs +++ b/client/src/input/camera/device_selection.rs @@ -2,13 +2,13 @@ impl CameraCapture { /// Fuzzy‑match devices under `/dev/v4l/by-id`, preferring capture nodes #[cfg(not(coverage))] fn find_device(substr: &str) -> Option { - let wanted = substr.to_ascii_lowercase(); + let wanted = normalize_device_fragment(substr); let mut matches: Vec<_> = std::fs::read_dir("/dev/v4l/by-id") .ok()? .flatten() .filter_map(|e| { let p = e.path(); - let name = p.file_name()?.to_string_lossy().to_ascii_lowercase(); + let name = normalize_device_fragment(&p.file_name()?.to_string_lossy()); if name.contains(&wanted) { Some(p) } else { @@ -33,7 +33,7 @@ impl CameraCapture { #[cfg(coverage)] fn find_device(substr: &str) -> Option { - let wanted = substr.to_ascii_lowercase(); + let wanted = normalize_device_fragment(substr); let by_id_dir = std::env::var("LESAVKA_CAM_BY_ID_DIR").unwrap_or_else(|_| "/dev/v4l/by-id".to_string()); let dev_root = std::env::var("LESAVKA_CAM_DEV_ROOT").unwrap_or_else(|_| "/dev".to_string()); @@ -42,7 +42,7 @@ impl CameraCapture { .flatten() .filter_map(|e| { let p = e.path(); - let name = p.file_name()?.to_string_lossy().to_ascii_lowercase(); + let name = normalize_device_fragment(&p.file_name()?.to_string_lossy()); if name.contains(&wanted) { Some(p) } else { @@ -100,3 +100,11 @@ impl CameraCapture { } } + +fn normalize_device_fragment(value: &str) -> String { + value + .chars() + .filter(|ch| ch.is_ascii_alphanumeric()) + .flat_map(char::to_lowercase) + .collect() +} diff --git a/common/Cargo.toml b/common/Cargo.toml index d83459e..75d8f34 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.16.14" +version = "0.16.15" edition = "2024" build = "build.rs" diff --git a/scripts/manual/browser_consumer_probe.py b/scripts/manual/browser_consumer_probe.py index f42e539..c45ec8d 100755 --- a/scripts/manual/browser_consumer_probe.py +++ b/scripts/manual/browser_consumer_probe.py @@ -293,8 +293,9 @@ class ProbeHandler(http.server.BaseHTTPRequestHandler): pass -class ReusableTcpServer(socketserver.TCPServer): +class ReusableTcpServer(socketserver.ThreadingMixIn, socketserver.TCPServer): allow_reuse_address = True + daemon_threads = True def main() -> None: diff --git a/scripts/manual/local_av_stimulus.py b/scripts/manual/local_av_stimulus.py index dfe2a3f..266a3b6 100755 --- a/scripts/manual/local_av_stimulus.py +++ b/scripts/manual/local_av_stimulus.py @@ -241,8 +241,9 @@ class StimulusHandler(http.server.BaseHTTPRequestHandler): pass -class ReusableTcpServer(socketserver.TCPServer): +class ReusableTcpServer(socketserver.ThreadingMixIn, socketserver.TCPServer): allow_reuse_address = True + daemon_threads = True def main() -> None: diff --git a/scripts/manual/run_upstream_browser_av_sync.sh b/scripts/manual/run_upstream_browser_av_sync.sh index a8b7e0d..c6c4cae 100755 --- a/scripts/manual/run_upstream_browser_av_sync.sh +++ b/scripts/manual/run_upstream_browser_av_sync.sh @@ -128,7 +128,12 @@ while true; do done echo "==> triggering browser recording" -ssh ${SSH_OPTS} "${TETHYS_HOST}" "curl -fsS -X POST http://127.0.0.1:${BROWSER_PORT}/start >/dev/null" +if ! ssh ${SSH_OPTS} "${TETHYS_HOST}" "curl --max-time 10 -fsS -X POST http://127.0.0.1:${BROWSER_PORT}/start >/dev/null"; then + status_json=$(ssh ${SSH_OPTS} "${TETHYS_HOST}" "test -f '${REMOTE_STATUS}' && cat '${REMOTE_STATUS}'" || true) + echo "browser consumer start request failed or timed out" >&2 + [[ -n "${status_json:-}" ]] && echo "last status: ${status_json}" >&2 + exit 1 +fi sleep 1 diff --git a/server/Cargo.toml b/server/Cargo.toml index 6aff42b..3fb79f4 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.16.14" +version = "0.16.15" edition = "2024" autobins = false diff --git a/testing/tests/client_camera_include_contract.rs b/testing/tests/client_camera_include_contract.rs index ea54f77..4b3395b 100644 --- a/testing/tests/client_camera_include_contract.rs +++ b/testing/tests/client_camera_include_contract.rs @@ -128,6 +128,31 @@ mod camera_include_contract { )); } + #[test] + #[cfg(coverage)] + #[serial] + fn find_device_normalizes_spaces_underscores_and_case() { + let dir = tempdir().expect("tempdir"); + let by_id = dir.path().join("by-id"); + std::fs::create_dir_all(&by_id).expect("create by-id"); + symlink( + "../video42", + by_id.join("usb-046d_Logitech_BRIO-video-index0"), + ) + .expect("create camera symlink"); + + with_var( + "LESAVKA_CAM_BY_ID_DIR", + Some(by_id.to_string_lossy().to_string()), + || { + with_var("LESAVKA_CAM_DEV_ROOT", Some("/dev".to_string()), || { + let found = CameraCapture::find_device("Logitech BRIO"); + assert_eq!(found.as_deref(), Some("/dev/video42")); + }); + }, + ); + } + #[test] #[serial] fn find_device_honors_override_roots_and_handles_non_capture_targets() {