lesavka/server/src/bin/lesavka_uvc/coverage_startup.rs

357 lines
11 KiB
Rust

fn main() -> Result<()> {
let (dev, _cfg) = parse_args()?;
let _ = load_interfaces();
let _ = UvcConfig::from_env();
let _ = open_with_retry(&dev)?;
anyhow::bail!("coverage harness: control loop disabled");
}
#[cfg(coverage)]
fn parse_args() -> Result<(String, UvcConfig)> {
let args: Vec<String> = env::args().skip(1).collect();
let dev = parse_device_arg(&args)
.or_else(|| env::var("LESAVKA_UVC_DEV").ok())
.context("missing --device (or LESAVKA_UVC_DEV)")?;
Ok((dev, UvcConfig::from_env()))
}
#[cfg(coverage)]
fn parse_device_arg(args: &[String]) -> Option<String> {
args
.windows(2)
.find_map(|pair| (pair[0] == "--device" || pair[0] == "-d").then(|| pair[1].clone()))
.or_else(|| {
args.iter()
.rev()
.find(|arg| {
arg.as_str() != "--device" && arg.as_str() != "-d" && arg.starts_with('/')
})
.cloned()
})
}
#[cfg(coverage)]
impl UvcConfig {
fn from_env() -> Self {
let width = env_u32("LESAVKA_UVC_WIDTH", 1280);
let height = env_u32("LESAVKA_UVC_HEIGHT", 720);
let fps = env_u32("LESAVKA_UVC_FPS", 30).max(1);
let frame_size = uvc_frame_size_for_active_mode(width, height, fps);
let interval = env_u32("LESAVKA_UVC_INTERVAL", 0);
let bulk = uvc_bulk_transfer_enabled();
let mut max_packet = env_u32("LESAVKA_UVC_MAXPACKET", 1024);
if let Some(limit) = compute_payload_cap(bulk).map(|cap| cap.limit) {
max_packet = max_packet.min(limit);
}
max_packet = if bulk {
max_packet.min(512)
} else {
max_packet.min(1024)
};
let interval = if interval == 0 {
10_000_000 / fps
} else {
interval
};
Self {
width,
height,
fps,
interval,
max_packet,
frame_size,
}
}
}
#[cfg(coverage)]
impl UvcState {
fn new(cfg: UvcConfig) -> Self {
let ctrl_len = stream_ctrl_len();
let default = build_streaming_control(&cfg, ctrl_len);
Self {
cfg,
ctrl_len,
default,
probe: default,
commit: default,
cfg_snapshot: None,
}
}
}
#[cfg(coverage)]
fn load_interfaces() -> UvcInterfaces {
let control = env_u8("LESAVKA_UVC_CTRL_INTF").unwrap_or(UVC_STRING_CONTROL_IDX);
let streaming = env_u8("LESAVKA_UVC_STREAM_INTF").unwrap_or(UVC_STRING_STREAMING_IDX);
UvcInterfaces { control, streaming }
}
#[cfg(coverage)]
fn read_interface(path: &str) -> Option<u8> {
std::fs::read_to_string(path)
.ok()
.and_then(|v| v.trim().parse::<u8>().ok())
}
#[cfg(coverage)]
fn open_with_retry(path: &str) -> Result<std::fs::File> {
let read_only = uvc_control_read_only();
let mut opts = OpenOptions::new();
opts.read(true);
if !read_only {
opts.write(true);
}
if env::var("LESAVKA_UVC_BLOCKING").is_err() {
opts.custom_flags(libc::O_NONBLOCK);
}
opts.open(path).with_context(|| format!("open {path}"))
}
#[cfg(coverage)]
/// Keep coverage-mode UVC control opens read-only unless a test opts into writes.
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)
}
#[cfg(coverage)]
/// Returns the bounded UVC request-buffer count used by the helper.
///
/// Inputs: `LESAVKA_UVC_BUFFER_COUNT`. Outputs: a value clamped to the safe
/// range accepted by the helper. Why: coverage contracts need the same backlog
/// bound as the real UVC binary without opening a gadget device.
fn uvc_buffer_count() -> u32 {
env_u32("LESAVKA_UVC_BUFFER_COUNT", DEFAULT_UVC_BUFFER_COUNT).clamp(1, 8)
}
#[cfg(coverage)]
/// Returns the idle frame-pump sleep interval used when no fresh frame is ready.
///
/// Inputs: `LESAVKA_UVC_IDLE_PUMP_MS`. Outputs: a duration in milliseconds.
/// Why: this value controls how quickly the UVC helper retries the freshness
/// spool when browsers are already streaming.
fn uvc_idle_pump_sleep() -> std::time::Duration {
std::time::Duration::from_millis(env_u64(
"LESAVKA_UVC_IDLE_PUMP_MS",
DEFAULT_UVC_IDLE_PUMP_MS,
))
}
#[cfg(coverage)]
/// Returns the frame queue pacing period for the negotiated UVC frame rate.
///
/// Inputs: `LESAVKA_UVC_QUEUE_PACING` plus the active frame rate. Output:
/// `None` when pacing is explicitly disabled. Why: the RCT-facing host must
/// not be overfed faster than the advertised descriptor cadence.
fn uvc_queue_period(fps: u32) -> Option<std::time::Duration> {
if !env_flag_enabled("LESAVKA_UVC_QUEUE_PACING", true) {
return None;
}
let fps = fps.max(1);
Some(std::time::Duration::from_nanos(1_000_000_000 / u64::from(fps)))
}
#[cfg(coverage)]
/// Bound MJPEG frames before they enter the USB UVC helper.
///
/// Inputs: UVC mode plus optional `LESAVKA_UVC_FRAME_MAX_BYTES` or
/// `LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC`. Output: maximum accepted JPEG
/// byte length. Why: coverage tests should lock the artifact-prevention budget
/// that turns oversized UVC frames into freezes instead of grey half-frames.
fn uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
if !uvc_frame_size_guard_enabled() {
return MAX_MJPEG_FRAME_BYTES;
}
if let Some(limit) = env_u32_opt("LESAVKA_UVC_FRAME_MAX_BYTES") {
return if limit == 0 {
derived_uvc_frame_max_bytes(cfg)
} else {
(limit as usize).min(MAX_MJPEG_FRAME_BYTES)
};
}
derived_uvc_frame_max_bytes(cfg)
}
#[cfg(coverage)]
fn derived_uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
derived_uvc_frame_max_bytes_for_fps(cfg.fps)
}
#[cfg(coverage)]
fn derived_uvc_frame_max_bytes_for_fps(fps: u32) -> usize {
let fps = fps.max(1);
let budget_per_sec = env_u32(
"LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC",
DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC,
)
.max(1);
let per_frame = (budget_per_sec / fps).max(64 * 1024);
per_frame.min(MAX_MJPEG_FRAME_BYTES as u32) as usize
}
#[cfg(coverage)]
fn env_flag_enabled(name: &str, default: bool) -> bool {
env::var(name)
.ok()
.map(|value| {
let trimmed = value.trim();
if trimmed.eq_ignore_ascii_case("0")
|| trimmed.eq_ignore_ascii_case("false")
|| trimmed.eq_ignore_ascii_case("no")
|| trimmed.eq_ignore_ascii_case("off")
{
false
} else if trimmed.eq_ignore_ascii_case("1")
|| trimmed.eq_ignore_ascii_case("true")
|| trimmed.eq_ignore_ascii_case("yes")
|| trimmed.eq_ignore_ascii_case("on")
{
true
} else {
default
}
})
.unwrap_or(default)
}
#[cfg(coverage)]
fn uvc_frame_size_guard_enabled() -> bool {
env_flag_enabled("LESAVKA_UVC_FRAME_SIZE_GUARD", true)
}
#[cfg(coverage)]
fn uvc_bulk_transfer_enabled() -> bool {
if !env_flag_enabled("LESAVKA_UVC_BULK", true) {
return false;
}
let base = std::path::Path::new(CONFIGFS_UVC_BASE);
if base.exists() && !base.join("streaming_bulk").exists() {
return false;
}
true
}
#[cfg(coverage)]
fn uvc_frame_size_for_active_mode(width: u32, height: u32, fps: u32) -> u32 {
env_u32_opt("LESAVKA_UVC_FRAME_SIZE")
.unwrap_or_else(|| derived_uvc_frame_max_bytes_for_fps(fps).min(u32::MAX as usize) as u32)
.max((width.saturating_mul(height) / 32).max(64 * 1024))
}
#[cfg(coverage)]
fn uvc_stats_path() -> Option<std::path::PathBuf> {
match std::env::var("LESAVKA_UVC_STATS_PATH") {
Ok(value) => {
let trimmed = value.trim();
if trimmed.is_empty() || trimmed == "0" {
None
} else {
Some(std::path::PathBuf::from(trimmed))
}
}
Err(_) => Some(std::path::PathBuf::from(DEFAULT_UVC_STATS_PATH)),
}
}
#[cfg(coverage)]
fn write_atomic_text(path: &std::path::Path, text: &str) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let tmp = path.with_extension(format!("tmp.{}", std::process::id()));
std::fs::write(&tmp, text)?;
std::fs::rename(&tmp, path)?;
Ok(())
}
#[cfg(coverage)]
fn uvc_stats_snapshot_json(stats: &UvcVideoStats, frame_cap: usize) -> String {
format!(
"{{\"queued\":{},\"reloaded\":{},\"stale_replay\":{},\"rejected_oversize\":{},\"rejected_invalid\":{},\"fallback_idle\":{},\"latest_bytes\":{},\"frame_cap\":{},\"paced_sleeps\":{},\"paced_sleep_ms\":{}}}\n",
stats.queued,
stats.reloaded,
stats.replayed_stale,
stats.rejected_oversize,
stats.rejected_invalid,
stats.fallback_idle,
stats.latest_bytes,
frame_cap,
stats.paced_sleeps,
stats.paced_sleep_ms
)
}
#[cfg(coverage)]
fn uvc_stats_interval() -> Option<std::time::Duration> {
match env_u64("LESAVKA_UVC_STATS_INTERVAL_MS", DEFAULT_UVC_STATS_INTERVAL_MS) {
0 => None,
value => Some(std::time::Duration::from_millis(value)),
}
}
#[cfg(coverage)]
/// Returns the optional maximum age for a spooled MJPEG frame.
///
/// Inputs: `LESAVKA_UVC_FRAME_MAX_AGE_MS`. Outputs: `None` when the TTL is
/// disabled. Why: stale-frame replay must be explicit because it trades
/// smoothness against freshness during browser stream recovery.
fn frame_spool_max_age() -> Option<std::time::Duration> {
match env_u64(
"LESAVKA_UVC_FRAME_MAX_AGE_MS",
DEFAULT_UVC_FRAME_MAX_AGE_MS,
) {
0 => None,
value => Some(std::time::Duration::from_millis(value)),
}
}
#[cfg(coverage)]
/// Determines whether a spooled MJPEG frame is too old to replay.
///
/// Inputs: frame path and optional age limit. Outputs: true when the frame is
/// missing or older than the limit. Why: the coverage harness should guard the
/// same freshness cut-off that prevents seconds-old video from re-entering UVC.
fn frame_spool_is_stale(path: &std::path::Path, max_age: Option<std::time::Duration>) -> bool {
let Some(max_age) = max_age else {
return false;
};
let Ok(metadata) = std::fs::metadata(path) else {
return true;
};
let Ok(modified) = metadata.modified() else {
return false;
};
std::time::SystemTime::now()
.duration_since(modified)
.map(|age| age > max_age)
.unwrap_or(false)
}
#[cfg(coverage)]
/// Parses an unsigned 64-bit environment value with a fallback.
///
/// Inputs: variable name and default value. Outputs: parsed value or default.
/// Why: UVC freshness settings use millisecond durations that should not be
/// truncated to 32-bit just because the coverage harness is lightweight.
fn env_u64(name: &str, default: u64) -> u64 {
env::var(name)
.ok()
.and_then(|value| value.parse::<u64>().ok())
.unwrap_or(default)
}