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]]
|
[[package]]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.16.14"
|
version = "0.16.15"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1686,7 +1686,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.16.14"
|
version = "0.16.15"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@ -1698,7 +1698,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.16.14"
|
version = "0.16.15"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.16.14"
|
version = "0.16.15"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -22,7 +22,8 @@ impl CameraCapture {
|
|||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
Some(fragment) => {
|
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)
|
(format!("v4l2src device={dev} do-timestamp=true"), dev, true)
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
|||||||
@ -2,13 +2,13 @@ impl CameraCapture {
|
|||||||
/// Fuzzy‑match devices under `/dev/v4l/by-id`, preferring capture nodes
|
/// Fuzzy‑match devices under `/dev/v4l/by-id`, preferring capture nodes
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
fn find_device(substr: &str) -> Option<String> {
|
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")
|
let mut matches: Vec<_> = std::fs::read_dir("/dev/v4l/by-id")
|
||||||
.ok()?
|
.ok()?
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter_map(|e| {
|
.filter_map(|e| {
|
||||||
let p = e.path();
|
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) {
|
if name.contains(&wanted) {
|
||||||
Some(p)
|
Some(p)
|
||||||
} else {
|
} else {
|
||||||
@ -33,7 +33,7 @@ impl CameraCapture {
|
|||||||
|
|
||||||
#[cfg(coverage)]
|
#[cfg(coverage)]
|
||||||
fn find_device(substr: &str) -> Option<String> {
|
fn find_device(substr: &str) -> Option<String> {
|
||||||
let wanted = substr.to_ascii_lowercase();
|
let wanted = normalize_device_fragment(substr);
|
||||||
let by_id_dir =
|
let by_id_dir =
|
||||||
std::env::var("LESAVKA_CAM_BY_ID_DIR").unwrap_or_else(|_| "/dev/v4l/by-id".to_string());
|
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());
|
let dev_root = std::env::var("LESAVKA_CAM_DEV_ROOT").unwrap_or_else(|_| "/dev".to_string());
|
||||||
@ -42,7 +42,7 @@ impl CameraCapture {
|
|||||||
.flatten()
|
.flatten()
|
||||||
.filter_map(|e| {
|
.filter_map(|e| {
|
||||||
let p = e.path();
|
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) {
|
if name.contains(&wanted) {
|
||||||
Some(p)
|
Some(p)
|
||||||
} else {
|
} 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]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.16.14"
|
version = "0.16.15"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -293,8 +293,9 @@ class ProbeHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ReusableTcpServer(socketserver.TCPServer):
|
class ReusableTcpServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||||
allow_reuse_address = True
|
allow_reuse_address = True
|
||||||
|
daemon_threads = True
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
|||||||
@ -241,8 +241,9 @@ class StimulusHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ReusableTcpServer(socketserver.TCPServer):
|
class ReusableTcpServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||||
allow_reuse_address = True
|
allow_reuse_address = True
|
||||||
|
daemon_threads = True
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
|||||||
@ -128,7 +128,12 @@ while true; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
echo "==> triggering browser recording"
|
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
|
sleep 1
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.16.14"
|
version = "0.16.15"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
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]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn find_device_honors_override_roots_and_handles_non_capture_targets() {
|
fn find_device_honors_override_roots_and_handles_non_capture_targets() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user