fix(sync): stop uvc helper from guessing /dev/video0

This commit is contained in:
Brad Stein 2026-04-28 05:06:19 -03:00
parent b65bdfb259
commit 1462f736f4
8 changed files with 132 additions and 19 deletions

6
Cargo.lock generated
View File

@ -1642,7 +1642,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "lesavka_client" name = "lesavka_client"
version = "0.14.39" version = "0.14.40"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
@ -1676,7 +1676,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_common" name = "lesavka_common"
version = "0.14.39" version = "0.14.40"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@ -1688,7 +1688,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_server" name = "lesavka_server"
version = "0.14.39" version = "0.14.40"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package] [package]
name = "lesavka_client" name = "lesavka_client"
version = "0.14.39" version = "0.14.40"
edition = "2024" edition = "2024"
[dependencies] [dependencies]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "lesavka_common" name = "lesavka_common"
version = "0.14.39" version = "0.14.40"
edition = "2024" edition = "2024"
build = "build.rs" build = "build.rs"

View File

@ -8,9 +8,42 @@ if [[ -r /etc/lesavka/uvc.env ]]; then
source /etc/lesavka/uvc.env source /etc/lesavka/uvc.env
fi fi
DEV=${LESAVKA_UVC_DEV:-/dev/v4l/by-path/platform-1000480000.usb-video-index0} resolve_default_uvc_dev() {
if [[ ! -e "$DEV" ]]; then local ctrl=""
DEV=/dev/video0 ctrl=$(ls /sys/class/udc 2>/dev/null | head -n1 || true)
if [[ -n $ctrl ]]; then
printf '/dev/v4l/by-path/platform-%s-video-index0\n' "$ctrl"
return 0
fi
local candidate=""
candidate=$(ls /dev/v4l/by-path/platform-*-video-index0 2>/dev/null | head -n1 || true)
[[ -n $candidate ]] || return 1
printf '%s\n' "$candidate"
}
wait_for_uvc_dev() {
local dev="$1"
for _ in {1..50}; do
[[ -e $dev ]] && return 0
sleep 0.1
done
return 1
}
if [[ -n ${LESAVKA_UVC_DEV:-} ]]; then
DEV=${LESAVKA_UVC_DEV}
else
DEV=$(resolve_default_uvc_dev || true)
fi
if [[ -z ${DEV:-} ]]; then
echo "[lesavka-uvc] no gadget video_output node discovered; waiting for /dev/v4l/by-path/platform-*-video-index0 or set LESAVKA_UVC_DEV" >&2
exit 1
fi
if ! wait_for_uvc_dev "$DEV"; then
echo "[lesavka-uvc] gadget video_output node is still absent: $DEV" >&2
exit 1
fi fi
exec /usr/local/bin/lesavka-uvc --device "$DEV" exec /usr/local/bin/lesavka-uvc --device "$DEV"

View File

@ -10,7 +10,7 @@ bench = false
[package] [package]
name = "lesavka_server" name = "lesavka_server"
version = "0.14.39" version = "0.14.40"
edition = "2024" edition = "2024"
autobins = false autobins = false

View File

@ -13,6 +13,20 @@ fn uvc_by_path_root() -> String {
std::env::var("LESAVKA_UVC_BY_PATH_ROOT").unwrap_or_else(|_| "/dev/v4l/by-path".to_string()) std::env::var("LESAVKA_UVC_BY_PATH_ROOT").unwrap_or_else(|_| "/dev/v4l/by-path".to_string())
} }
fn any_platform_uvc_by_path() -> Option<String> {
let root = uvc_by_path_root();
let root = Path::new(&root);
let entries = std::fs::read_dir(root).ok()?;
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if name.starts_with("platform-") && name.ends_with("-video-index0") {
return Some(entry.path().to_string_lossy().into_owned());
}
}
None
}
/// Pick the UVC gadget video node. /// Pick the UVC gadget video node.
/// ///
/// Inputs: none; the function inspects environment overrides and udev state. /// Inputs: none; the function inspects environment overrides and udev state.
@ -51,7 +65,10 @@ pub fn pick_uvc_device() -> anyhow::Result<String> {
} }
} }
let mut fallback: Option<String> = None; if let Some(by_path) = any_platform_uvc_by_path() {
return Ok(by_path);
}
if std::env::var("LESAVKA_UVC_SKIP_UDEV").is_err() if std::env::var("LESAVKA_UVC_SKIP_UDEV").is_err()
&& let Ok(mut enumerator) = udev::Enumerator::new() && let Ok(mut enumerator) = udev::Enumerator::new()
{ {
@ -82,19 +99,12 @@ pub fn pick_uvc_device() -> anyhow::Result<String> {
{ {
return Ok(node); return Ok(node);
} }
if fallback.is_none() {
fallback = Some(node);
}
} }
} }
} }
if let Some(node) = fallback {
return Ok(node);
}
Err(anyhow::anyhow!( Err(anyhow::anyhow!(
"no video_output v4l2 node found; set LESAVKA_UVC_DEV" "no Lesavka video_output v4l2 node found; wait for /dev/v4l/by-path/platform-<udc>-video-index0 or set LESAVKA_UVC_DEV"
)) ))
} }

View File

@ -149,6 +149,37 @@ fn pick_uvc_device_prefers_controller_by_path_override_root() {
); );
} }
#[test]
#[serial]
fn pick_uvc_device_uses_platform_by_path_when_controller_is_unknown() {
let dir = tempdir().expect("tempdir");
let sys_root = dir.path().join("sys");
let by_path = dir.path().join("v4l/by-path");
fs::create_dir_all(&sys_root).expect("create fake sys root");
fs::create_dir_all(&by_path).expect("create fake by-path root");
let expected = by_path.join("platform-fallback.usb-video-index0");
fs::write(&expected, "").expect("touch by-path node");
temp_env::with_var(
"LESAVKA_GADGET_SYSFS_ROOT",
Some(sys_root.to_string_lossy().to_string()),
|| {
temp_env::with_var(
"LESAVKA_UVC_BY_PATH_ROOT",
Some(by_path.to_string_lossy().to_string()),
|| {
temp_env::with_var("LESAVKA_UVC_SKIP_UDEV", Some("1"), || {
temp_env::with_var("LESAVKA_UVC_DEV", None::<&str>, || {
let picked = pick_uvc_device().expect("pick platform by-path device");
assert_eq!(picked, expected.to_string_lossy());
});
});
},
);
},
);
}
#[test] #[test]
#[serial] #[serial]
fn pick_uvc_device_errors_when_overrides_disable_all_discovery_paths() { fn pick_uvc_device_errors_when_overrides_disable_all_discovery_paths() {
@ -169,7 +200,14 @@ fn pick_uvc_device_errors_when_overrides_disable_all_discovery_paths() {
temp_env::with_var("LESAVKA_UVC_SKIP_UDEV", Some("1"), || { temp_env::with_var("LESAVKA_UVC_SKIP_UDEV", Some("1"), || {
temp_env::with_var("LESAVKA_UVC_DEV", None::<&str>, || { temp_env::with_var("LESAVKA_UVC_DEV", None::<&str>, || {
let err = pick_uvc_device().expect_err("missing paths should error"); let err = pick_uvc_device().expect_err("missing paths should error");
assert!(err.to_string().contains("LESAVKA_UVC_DEV")); assert!(
err.to_string().contains("LESAVKA_UVC_DEV"),
"error should mention explicit override escape hatch: {err:#}"
);
assert!(
err.to_string().contains("platform-<udc>-video-index0"),
"error should point operators at the gadget by-path node: {err:#}"
);
}); });
}); });
}, },

View File

@ -0,0 +1,32 @@
//! Contract tests for the standalone UVC launcher shell script.
//!
//! Scope: statically guard `scripts/daemon/lesavka-uvc.sh`.
//! Targets: gadget-node discovery and wrong-device avoidance.
//! Why: falling back to an unrelated `/dev/video0` can wedge the helper in
//! kernel space and poison later gadget recovery loops.
const UVC_SCRIPT: &str = include_str!("../../scripts/daemon/lesavka-uvc.sh");
#[test]
fn uvc_script_waits_for_gadget_by_path_node() {
for expected in [
"resolve_default_uvc_dev()",
"wait_for_uvc_dev()",
"platform-%s-video-index0",
"platform-*-video-index0",
"gadget video_output node is still absent",
] {
assert!(
UVC_SCRIPT.contains(expected),
"lesavka-uvc launcher guard missing: {expected}"
);
}
}
#[test]
fn uvc_script_refuses_unrelated_video0_fallback() {
assert!(
!UVC_SCRIPT.contains("DEV=/dev/video0"),
"lesavka-uvc launcher must not fall back to an unrelated /dev/video0"
);
}