205 lines
6.8 KiB
Rust
205 lines
6.8 KiB
Rust
#![forbid(unsafe_code)]
|
|
|
|
use anyhow::Context as _;
|
|
use std::path::Path;
|
|
use std::time::Duration;
|
|
use tokio::process::Command;
|
|
use tracing::{info, warn};
|
|
|
|
use crate::gadget::UsbGadget;
|
|
|
|
/// Pick the UVC gadget video node.
|
|
///
|
|
/// Inputs: none; the function inspects environment overrides and udev state.
|
|
/// Outputs: the best-matching V4L2 output node for the active USB gadget.
|
|
/// Why: the relay must target the gadget output itself, not an unrelated
|
|
/// capture card that happens to exist on the same machine.
|
|
pub fn pick_uvc_device() -> anyhow::Result<String> {
|
|
if let Ok(path) = std::env::var("LESAVKA_UVC_DEV") {
|
|
return Ok(path);
|
|
}
|
|
|
|
let ctrl = UsbGadget::find_controller().ok();
|
|
if let Some(ctrl) = ctrl.as_deref() {
|
|
let by_path = format!("/dev/v4l/by-path/platform-{ctrl}-video-index0");
|
|
if Path::new(&by_path).exists() {
|
|
return Ok(by_path);
|
|
}
|
|
}
|
|
|
|
let mut fallback: Option<String> = None;
|
|
if let Ok(mut enumerator) = udev::Enumerator::new() {
|
|
let _ = enumerator.match_subsystem("video4linux");
|
|
if let Ok(devices) = enumerator.scan_devices() {
|
|
for device in devices {
|
|
let caps = device
|
|
.property_value("ID_V4L_CAPABILITIES")
|
|
.and_then(|value| value.to_str())
|
|
.unwrap_or_default();
|
|
if !caps.contains(":video_output:") {
|
|
continue;
|
|
}
|
|
let Some(node) = device.devnode() else {
|
|
continue;
|
|
};
|
|
let node = node.to_string_lossy().into_owned();
|
|
let product = device
|
|
.property_value("ID_V4L_PRODUCT")
|
|
.and_then(|value| value.to_str())
|
|
.unwrap_or_default();
|
|
let path = device
|
|
.property_value("ID_PATH")
|
|
.and_then(|value| value.to_str())
|
|
.unwrap_or_default();
|
|
if let Some(ctrl) = ctrl.as_deref() {
|
|
if product == ctrl || path.contains(ctrl) {
|
|
return Ok(node);
|
|
}
|
|
}
|
|
if fallback.is_none() {
|
|
fallback = Some(node);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(node) = fallback {
|
|
return Ok(node);
|
|
}
|
|
|
|
Err(anyhow::anyhow!(
|
|
"no video_output v4l2 node found; set LESAVKA_UVC_DEV"
|
|
))
|
|
}
|
|
|
|
/// Resolve the UVC control helper binary path.
|
|
///
|
|
/// Inputs: none.
|
|
/// Outputs: the configured executable path.
|
|
/// Why: production installs ship the helper as a separate binary, but CI and
|
|
/// local development sometimes need to override that location.
|
|
#[must_use]
|
|
pub fn uvc_ctrl_bin() -> String {
|
|
std::env::var("LESAVKA_UVC_CTRL_BIN")
|
|
.unwrap_or_else(|_| "/usr/local/bin/lesavka-uvc".to_string())
|
|
}
|
|
|
|
/// Spawn the external UVC control helper.
|
|
///
|
|
/// Inputs: the helper binary path plus the selected UVC device node.
|
|
/// Outputs: a running child process handle.
|
|
/// Why: the helper owns low-level configfs and V4L2 control handling that we
|
|
/// keep out of the main gRPC server process.
|
|
pub fn spawn_uvc_control(bin: &str, uvc_dev: &str) -> anyhow::Result<tokio::process::Child> {
|
|
Command::new(bin)
|
|
.arg("--device")
|
|
.arg(uvc_dev)
|
|
.spawn()
|
|
.context("spawning lesavka-uvc")
|
|
}
|
|
|
|
/// Supervise the external UVC control helper forever.
|
|
///
|
|
/// Inputs: the helper binary path.
|
|
/// Outputs: none; the task loops until the process exits.
|
|
/// Why: UVC device nodes can appear after boot, so the supervisor waits for a
|
|
/// usable device and restarts the helper whenever it exits.
|
|
pub async fn supervise_uvc_control(bin: String) {
|
|
let mut waiting_logged = false;
|
|
loop {
|
|
let uvc_dev = match pick_uvc_device() {
|
|
Ok(device) => {
|
|
if waiting_logged {
|
|
info!(%device, "📷 UVC device discovered");
|
|
waiting_logged = false;
|
|
}
|
|
device
|
|
}
|
|
Err(error) => {
|
|
if !waiting_logged {
|
|
warn!("⚠️ UVC device not ready: {error:#}");
|
|
waiting_logged = true;
|
|
}
|
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
continue;
|
|
}
|
|
};
|
|
|
|
match spawn_uvc_control(&bin, &uvc_dev) {
|
|
Ok(mut child) => {
|
|
info!(%uvc_dev, "📷 UVC control helper started");
|
|
match child.wait().await {
|
|
Ok(status) => {
|
|
warn!(%uvc_dev, "⚠️ lesavka-uvc exited: {status}");
|
|
}
|
|
Err(error) => {
|
|
warn!(%uvc_dev, "⚠️ lesavka-uvc wait failed: {error:#}");
|
|
}
|
|
}
|
|
}
|
|
Err(error) => {
|
|
warn!(%uvc_dev, "⚠️ failed to start lesavka-uvc: {error:#}");
|
|
}
|
|
}
|
|
|
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{pick_uvc_device, spawn_uvc_control, uvc_ctrl_bin};
|
|
use serial_test::serial;
|
|
use std::fs;
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use temp_env::with_var;
|
|
use tempfile::tempdir;
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn uvc_ctrl_bin_prefers_env_override() {
|
|
with_var("LESAVKA_UVC_CTRL_BIN", None::<&str>, || {
|
|
assert_eq!(uvc_ctrl_bin(), "/usr/local/bin/lesavka-uvc");
|
|
});
|
|
with_var("LESAVKA_UVC_CTRL_BIN", Some("/tmp/uvc-helper"), || {
|
|
assert_eq!(uvc_ctrl_bin(), "/tmp/uvc-helper");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn pick_uvc_device_prefers_env_override() {
|
|
with_var("LESAVKA_UVC_DEV", Some("/dev/video-test"), || {
|
|
assert_eq!(pick_uvc_device().unwrap(), "/dev/video-test");
|
|
});
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn spawn_uvc_control_runs_the_helper_script() {
|
|
let dir = tempdir().expect("tempdir");
|
|
let output = dir.path().join("args.txt");
|
|
let script = dir.path().join("helper.sh");
|
|
fs::write(
|
|
&script,
|
|
format!(
|
|
"#!/usr/bin/env bash\nset -euo pipefail\nprintf '%s %s' \"$1\" \"$2\" > '{}'\n",
|
|
output.display()
|
|
),
|
|
)
|
|
.expect("write helper");
|
|
let mut perms = fs::metadata(&script).expect("metadata").permissions();
|
|
perms.set_mode(0o755);
|
|
fs::set_permissions(&script, perms).expect("chmod helper");
|
|
|
|
let mut child = spawn_uvc_control(script.to_str().unwrap(), "/dev/video42")
|
|
.expect("helper should spawn");
|
|
let status = child.wait().await.expect("wait helper");
|
|
assert!(status.success());
|
|
assert_eq!(
|
|
fs::read_to_string(output).expect("read output"),
|
|
"--device /dev/video42"
|
|
);
|
|
}
|
|
}
|