fix(uvc): keep control helper off stream queue

This commit is contained in:
Brad Stein 2026-04-28 21:00:56 -03:00
parent 5eb984ce08
commit ce8f855db2
9 changed files with 82 additions and 16 deletions

6
Cargo.lock generated
View File

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

View File

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

View File

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

View File

@ -36,6 +36,7 @@ LESAVKA_UVC_WIDTH=${LESAVKA_UVC_WIDTH:-640}
LESAVKA_UVC_HEIGHT=${LESAVKA_UVC_HEIGHT:-480}
LESAVKA_UVC_CODEC=${INSTALL_UVC_CODEC}
LESAVKA_UVC_BLOCKING=${LESAVKA_UVC_BLOCKING:-1}
LESAVKA_UVC_CONTROL_READ_ONLY=${LESAVKA_UVC_CONTROL_READ_ONLY:-1}
LESAVKA_UVC_MAXBURST=${LESAVKA_UVC_MAXBURST:-0}
EOF
}

View File

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

View File

@ -2,7 +2,7 @@
use anyhow::{Context, Result};
use std::env;
use std::fs::OpenOptions;
use std::fs::{File, OpenOptions};
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
use std::thread;
@ -135,6 +135,7 @@ struct ConfigfsSnapshot {
fn main() -> Result<()> {
let (dev, cfg) = parse_args()?;
let _singleton = acquire_singleton_lock()?;
let interfaces = load_interfaces();
eprintln!("[lesavka-uvc] starting (dev={dev})");
eprintln!(
@ -425,15 +426,20 @@ fn read_interface(path: &str) -> Option<u8> {
fn open_with_retry(path: &str) -> Result<std::fs::File> {
let nonblock = env::var("LESAVKA_UVC_BLOCKING").is_err();
let read_only = uvc_control_read_only();
for attempt in 1..=200 {
let mut opts = OpenOptions::new();
opts.read(true).write(true);
opts.read(true);
if !read_only {
opts.write(true);
}
if nonblock {
opts.custom_flags(libc::O_NONBLOCK);
}
match opts.open(path) {
Ok(f) => {
eprintln!("[lesavka-uvc] opened {path} (attempt {attempt})");
let mode = if read_only { "ro" } else { "rw" };
eprintln!("[lesavka-uvc] opened {path} mode={mode} (attempt {attempt})");
return Ok(f);
}
Err(err) if err.raw_os_error() == Some(libc::ENOENT) => {
@ -445,6 +451,42 @@ fn open_with_retry(path: &str) -> Result<std::fs::File> {
Err(anyhow::anyhow!("timeout opening {path}"))
}
fn uvc_control_read_only() -> bool {
env::var("LESAVKA_UVC_CONTROL_READ_ONLY")
.ok()
.map(|value| {
let trimmed = value.trim();
!(trimmed.eq_ignore_ascii_case("0")
|| trimmed.eq_ignore_ascii_case("false")
|| trimmed.eq_ignore_ascii_case("no")
|| trimmed.eq_ignore_ascii_case("off"))
})
.unwrap_or(true)
}
fn acquire_singleton_lock() -> Result<File> {
let path =
env::var("LESAVKA_UVC_LOCK_PATH").unwrap_or_else(|_| "/run/lesavka-uvc.lock".to_string());
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&path)
.with_context(|| format!("open singleton lock {path}"))?;
let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
if rc < 0 {
let err = std::io::Error::last_os_error();
match err.raw_os_error() {
Some(code) if code == libc::EWOULDBLOCK || code == libc::EAGAIN => {
eprintln!("[lesavka-uvc] another helper already owns {path}; exiting");
std::process::exit(0);
}
_ => return Err(err).with_context(|| format!("lock singleton {path}")),
}
}
Ok(file)
}
fn subscribe_event(fd: i32, req: libc::c_ulong, event: u32) -> Result<()> {
let mut sub = V4l2EventSubscription {
type_: event,
@ -951,14 +993,15 @@ fn compute_payload_cap(bulk: bool) -> Option<PayloadCap> {
let mut non_periodic =
read_fifo_min("/sys/module/dwc2/parameters/g_np_tx_fifo_size").map(|v| (v, "dwc2.params"));
if (periodic.is_none() || non_periodic.is_none())
&& let Some((p, np)) = read_debugfs_fifos() {
if periodic.is_none() {
periodic = p.map(|v| (v, "debugfs.params"));
}
if non_periodic.is_none() {
non_periodic = np.map(|v| (v, "debugfs.params"));
}
&& let Some((p, np)) = read_debugfs_fifos()
{
if periodic.is_none() {
periodic = p.map(|v| (v, "debugfs.params"));
}
if non_periodic.is_none() {
non_periodic = np.map(|v| (v, "debugfs.params"));
}
}
let periodic_dw = periodic.map(|(v, _)| v);
let non_periodic_dw = non_periodic.map(|(v, _)| v);

View File

@ -30,6 +30,7 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
"LESAVKA_UVC_WIDTH=",
"LESAVKA_UVC_HEIGHT=",
"LESAVKA_UVC_CODEC=",
"LESAVKA_UVC_CONTROL_READ_ONLY=",
] {
assert!(
SERVER_INSTALL.contains(expected),
@ -52,6 +53,7 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_INTERVAL:-500000}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_WIDTH:-640}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_HEIGHT:-480}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_CONTROL_READ_ONLY:-1}"));
assert!(
!SERVER_INSTALL.contains("LESAVKA_UVC_CODEC=${LESAVKA_UVC_CODEC:-mjpeg}"),
"install script should not let ambient LESAVKA_UVC_CODEC leak into persisted defaults"

View File

@ -311,6 +311,22 @@ mod uvc_binary {
});
}
#[test]
#[serial]
fn uvc_control_open_mode_defaults_read_only_with_escape_hatch() {
with_var("LESAVKA_UVC_CONTROL_READ_ONLY", None::<&str>, || {
assert!(uvc_control_read_only());
});
for disabled in ["0", "false", "no", "off"] {
with_var("LESAVKA_UVC_CONTROL_READ_ONLY", Some(disabled), || {
assert!(!uvc_control_read_only());
});
}
with_var("LESAVKA_UVC_CONTROL_READ_ONLY", Some("1"), || {
assert!(uvc_control_read_only());
});
}
#[test]
fn interface_helpers_and_configfs_snapshot_are_stable_without_sysfs() {
let tmp = NamedTempFile::new().expect("tmp");

View File

@ -73,9 +73,11 @@ fn uvc_binary_applies_env_config_and_fails_fast_on_non_v4l2_node() {
};
let fake_device = NamedTempFile::new().expect("temp device");
let lock = NamedTempFile::new().expect("temp lock");
let child = Command::new(Path::new(&bin))
.arg("--device")
.arg(fake_device.path())
.env("LESAVKA_UVC_LOCK_PATH", lock.path())
.env("LESAVKA_UVC_MAXPAYLOAD_LIMIT", "256")
.env("LESAVKA_UVC_MAXPACKET", "4096")
.env("LESAVKA_UVC_BULK", "1")
@ -98,8 +100,10 @@ fn uvc_binary_accepts_positional_device_argument() {
};
let fake_device = NamedTempFile::new().expect("temp device");
let lock = NamedTempFile::new().expect("temp lock");
let child = Command::new(Path::new(&bin))
.arg(fake_device.path())
.env("LESAVKA_UVC_LOCK_PATH", lock.path())
.env_remove("LESAVKA_UVC_BULK")
.env("LESAVKA_UVC_MAXPAYLOAD_LIMIT", "2048")
.env("LESAVKA_UVC_MAXPACKET", "1024")