probe: harden mirrored camera selection
This commit is contained in:
parent
08d424b023
commit
0d9121f921
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.16.14"
|
||||
version = "0.16.15"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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<String> {
|
||||
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<String> {
|
||||
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()
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.16.14"
|
||||
version = "0.16.15"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.16.14"
|
||||
version = "0.16.15"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user