media: harden uvc mjpeg transport
This commit is contained in:
parent
22dd45aa39
commit
eb3b029071
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_client"
|
||||
version = "0.22.30"
|
||||
version = "0.22.31"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@ -1686,7 +1686,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_common"
|
||||
version = "0.22.30"
|
||||
version = "0.22.31"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@ -1698,7 +1698,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_server"
|
||||
version = "0.22.30"
|
||||
version = "0.22.31"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.22.30"
|
||||
version = "0.22.31"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -55,6 +55,7 @@ fn send_clipboard_via_hid(server_addr: &str, text: &str) -> Result<()> {
|
||||
let delay = clipboard_hid_delay();
|
||||
let report_count = reports.len();
|
||||
let timeout = clipboard_transport_timeout();
|
||||
let ack_timeout = clipboard_hid_ack_timeout(report_count, delay);
|
||||
let rt = RuntimeBuilder::new_current_thread().enable_all().build()?;
|
||||
rt.block_on(async {
|
||||
let channel = tokio::time::timeout(timeout, relay_transport::connect(server_addr))
|
||||
@ -73,7 +74,7 @@ fn send_clipboard_via_hid(server_addr: &str, text: &str) -> Result<()> {
|
||||
.await
|
||||
.map_err(|_| anyhow!("timed out opening keyboard fallback stream after {:?}", timeout))??;
|
||||
let mut echoed = 0usize;
|
||||
let deadline = tokio::time::Instant::now() + timeout;
|
||||
let deadline = tokio::time::Instant::now() + ack_timeout;
|
||||
while echoed < report_count {
|
||||
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
|
||||
if remaining.is_zero() {
|
||||
@ -132,6 +133,16 @@ fn clipboard_hid_delay() -> Duration {
|
||||
Duration::from_millis(delay_ms)
|
||||
}
|
||||
|
||||
fn clipboard_hid_ack_timeout(report_count: usize, delay: Duration) -> Duration {
|
||||
let base = clipboard_transport_timeout();
|
||||
let per_report_ms = delay.as_millis().saturating_mul(report_count as u128);
|
||||
let total_ms = base
|
||||
.as_millis()
|
||||
.saturating_add(per_report_ms)
|
||||
.saturating_add(1_000);
|
||||
Duration::from_millis(total_ms.min(u64::MAX as u128) as u64)
|
||||
}
|
||||
|
||||
fn clipboard_transport_timeout() -> Duration {
|
||||
let timeout_ms = std::env::var("LESAVKA_CLIPBOARD_TIMEOUT_MS")
|
||||
.ok()
|
||||
@ -142,7 +153,10 @@ fn clipboard_transport_timeout() -> Duration {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{build_hid_paste_reports, clipboard_hid_delay, clipboard_transport_timeout};
|
||||
use super::{
|
||||
build_hid_paste_reports, clipboard_hid_ack_timeout, clipboard_hid_delay,
|
||||
clipboard_transport_timeout,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
@ -170,4 +184,12 @@ mod tests {
|
||||
fn clipboard_transport_timeout_has_stable_default() {
|
||||
assert_eq!(clipboard_transport_timeout(), Duration::from_millis(6_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipboard_hid_ack_timeout_scales_with_report_count() {
|
||||
assert_eq!(
|
||||
clipboard_hid_ack_timeout(1_962, Duration::from_millis(18)),
|
||||
Duration::from_millis(42_316)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.22.30"
|
||||
version = "0.22.31"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -212,7 +212,7 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta
|
||||
| `LESAVKA_OUTPUT_FRESHNESS_MAX_DRIFT_MS` | manual direct UVC/UAC probe freshness gate; maximum allowed freshness drift across paired probe events, defaults to `100` |
|
||||
| `LESAVKA_PASTE_DELAY_MS` | input routing/clipboard override |
|
||||
| `LESAVKA_PASTE_KEY` | input routing/clipboard override |
|
||||
| `LESAVKA_PASTE_KEY_FILE` | input routing/clipboard override |
|
||||
| `LESAVKA_PASTE_KEY_FILE` | encrypted paste shared-key file; server installer defaults to `/etc/lesavka/paste-key` and includes `paste-key` in the client enrollment bundle, while the client installer defaults to `~/.config/lesavka/paste-key` |
|
||||
| `LESAVKA_PASTE_MAX` | input routing/clipboard override |
|
||||
| `LESAVKA_PASTE_RPC` | input routing/clipboard override |
|
||||
| `LESAVKA_PERFORMANCE_GATE_PUSHGATEWAY_JOB` | CI metrics destination override for latency/performance checks |
|
||||
@ -313,7 +313,7 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta
|
||||
| `LESAVKA_UVC_APP_MAX_BYTES` | server UVC appsrc memory guard; defaults to `4194304` queued bytes |
|
||||
| `LESAVKA_UVC_APP_MAX_TIME_NS` | server UVC appsrc memory guard; defaults to `200000000` ns of queued media |
|
||||
| `LESAVKA_UVC_BLOCKING` | server hardware/device override |
|
||||
| `LESAVKA_UVC_BULK` | server hardware/device override |
|
||||
| `LESAVKA_UVC_BULK` | UVC transfer-mode override; defaults to `1` so patched kernels prefer reliable bulk transfer over lossy isochronous MJPEG. Set `0` to force classic isochronous descriptors |
|
||||
| `LESAVKA_UVC_BUFFER_COUNT` | UVC helper freshness override; number of queued gadget output buffers, defaults to `2` for live-call freshness |
|
||||
| `LESAVKA_UVC_BY_PATH_ROOT` | server hardware/device override |
|
||||
| `LESAVKA_UVC_CODEC` | server hardware/device override |
|
||||
@ -331,7 +331,7 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta
|
||||
| `LESAVKA_UVC_FRAME_META_PATH` | UVC helper diagnostic override; explicit path for the optional MJPEG spool metadata sidecar |
|
||||
| `LESAVKA_UVC_FRAME_MAX_AGE_MS` | UVC helper freshness override; stale spooled MJPEG frames older than this are not replayed, defaults to `1000`; `0` disables TTL |
|
||||
| `LESAVKA_UVC_FRAME_MAX_BYTES` | UVC helper MJPEG frame-size guard; explicit maximum accepted frame bytes. Unset or `0` uses the live-call byte budget so oversized frames freeze instead of tearing on the host |
|
||||
| `LESAVKA_UVC_FRAME_SIZE` | server hardware/device override |
|
||||
| `LESAVKA_UVC_FRAME_SIZE` | UVC advertised maximum MJPEG frame bytes; defaults to the live-call byte budget instead of raw uncompressed frame size so the gadget/host do not schedule oversized phantom frames |
|
||||
| `LESAVKA_UVC_FRAME_SIZE_GUARD` | UVC helper MJPEG frame-size guard toggle; defaults to `1`; set `0` only for diagnostics when oversized MJPEG frames must be allowed through |
|
||||
| `LESAVKA_UVC_HEIGHT` | server hardware/device override |
|
||||
| `LESAVKA_UVC_HEVC_SPOOL_PULL_TIMEOUT_MS` | server HEVC decode-to-MJPEG freshness override; appsink pull wait for decoded MJPEG handoff before publishing newest frame to the UVC helper, defaults to `20` and is capped at `50` |
|
||||
@ -346,6 +346,7 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta
|
||||
| `LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC` | UVC helper MJPEG budget guard; derives a per-frame byte cap from target FPS when `LESAVKA_UVC_FRAME_MAX_BYTES` is unset |
|
||||
| `LESAVKA_UVC_SKIP_UDEV` | server hardware/device override |
|
||||
| `LESAVKA_UVC_STATS_INTERVAL_MS` | UVC helper telemetry interval for queued/reloaded/rejected MJPEG frame counters; defaults to `5000`, `0` disables |
|
||||
| `LESAVKA_UVC_STATS_PATH` | UVC helper JSON stats snapshot path for queued/reloaded/rejected MJPEG frame counters; defaults to `/run/lesavka-uvc-video-stats.json`, set `0` or empty to disable file snapshots |
|
||||
| `LESAVKA_UVC_STREAMING_INTERVAL` | server hardware/device override |
|
||||
| `LESAVKA_UVC_STREAM_INTF` | server hardware/device override |
|
||||
| `LESAVKA_UVC_WIDTH` | server hardware/device override |
|
||||
|
||||
@ -173,7 +173,18 @@ UVC_WIDTH=${LESAVKA_UVC_WIDTH:-1280}
|
||||
UVC_HEIGHT=${LESAVKA_UVC_HEIGHT:-720}
|
||||
UVC_FPS=${LESAVKA_UVC_FPS:-30}
|
||||
UVC_DISABLE_IRQ=${LESAVKA_UVC_DISABLE_IRQ:-}
|
||||
UVC_BULK=${LESAVKA_UVC_BULK:-}
|
||||
flag_enabled() {
|
||||
case "${1,,}" in
|
||||
1|true|yes|on) return 0 ;;
|
||||
0|false|no|off|"") return 1 ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
}
|
||||
if flag_enabled "${LESAVKA_UVC_BULK:-1}"; then
|
||||
UVC_BULK=1
|
||||
else
|
||||
UVC_BULK=
|
||||
fi
|
||||
UVC_CODEC=${LESAVKA_UVC_CODEC:-mjpeg}
|
||||
case "${UVC_CODEC,,}" in
|
||||
mjpeg|mjpg|jpeg)
|
||||
@ -306,6 +317,34 @@ fi
|
||||
UVC_FRAME_SIZE=${LESAVKA_UVC_FRAME_SIZE:-$((UVC_WIDTH * UVC_HEIGHT * 2))}
|
||||
UVC_INTERVAL_30=${LESAVKA_UVC_INTERVAL_30:-333333}
|
||||
UVC_INTERVAL_20=${LESAVKA_UVC_INTERVAL_20:-500000}
|
||||
UVC_MJPEG_BUDGET_BYTES_PER_SEC=${LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC:-10000000}
|
||||
|
||||
uvc_mjpeg_frame_size_for_fps() {
|
||||
local fps=$1
|
||||
if ((fps < 1)); then
|
||||
fps=1
|
||||
fi
|
||||
local per_frame=$((UVC_MJPEG_BUDGET_BYTES_PER_SEC / fps))
|
||||
if ((per_frame < 65536)); then
|
||||
per_frame=65536
|
||||
elif ((per_frame > 8388608)); then
|
||||
per_frame=8388608
|
||||
fi
|
||||
echo "$per_frame"
|
||||
}
|
||||
|
||||
uvc_fps_for_interval() {
|
||||
local interval=$1
|
||||
if ((interval <= 0)); then
|
||||
echo "$UVC_FPS"
|
||||
else
|
||||
echo $((10000000 / interval))
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ -z ${LESAVKA_UVC_FRAME_SIZE:-} ]]; then
|
||||
UVC_FRAME_SIZE="$(uvc_mjpeg_frame_size_for_fps "$UVC_FPS")"
|
||||
fi
|
||||
|
||||
uvc_selected_frame_index() {
|
||||
case "${UVC_WIDTH}x${UVC_HEIGHT}" in
|
||||
@ -320,7 +359,7 @@ uvc_frame_size_for() {
|
||||
if [[ $width == "$UVC_WIDTH" && $height == "$UVC_HEIGHT" && -n ${LESAVKA_UVC_FRAME_SIZE:-} ]]; then
|
||||
echo "$LESAVKA_UVC_FRAME_SIZE"
|
||||
else
|
||||
echo $((width * height * 2))
|
||||
uvc_mjpeg_frame_size_for_fps "$(uvc_fps_for_interval "$(uvc_default_interval_for "$width" "$height")")"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ INSTALL_SOURCE=${LESAVKA_INSTALL_SOURCE:-auto}
|
||||
export TMPDIR=${TMPDIR:-/var/tmp}
|
||||
USER_HOME=$(getent passwd "$ORIG_USER" | cut -d: -f6)
|
||||
CLIENT_PKI_DIR=${LESAVKA_CLIENT_PKI_DIR:-$USER_HOME/.config/lesavka/pki}
|
||||
CLIENT_PASTE_KEY_FILE=${LESAVKA_PASTE_KEY_FILE:-$USER_HOME/.config/lesavka/paste-key}
|
||||
CLIENT_PKI_AUTO_FETCH=${LESAVKA_CLIENT_PKI_AUTO_FETCH:-1}
|
||||
CLIENT_PKI_SSH_SOURCE=${LESAVKA_CLIENT_PKI_SSH_SOURCE:-theia:/etc/lesavka/lesavka-client-pki.tar.gz}
|
||||
CLIENT_CAPTURE_DIR=${LESAVKA_CLIENT_CAPTURE_DIR:-$USER_HOME/Pictures/lesavka}
|
||||
@ -424,8 +425,9 @@ install_client_pki_bundle() {
|
||||
local bundle=${LESAVKA_CLIENT_PKI_BUNDLE:-}
|
||||
local fetched_bundle=0
|
||||
if [[ -z $bundle ]]; then
|
||||
if [[ -s "$CLIENT_PKI_DIR/ca.crt" && -s "$CLIENT_PKI_DIR/client.crt" && -s "$CLIENT_PKI_DIR/client.key" ]]; then
|
||||
if [[ -s "$CLIENT_PKI_DIR/ca.crt" && -s "$CLIENT_PKI_DIR/client.crt" && -s "$CLIENT_PKI_DIR/client.key" && -s "$CLIENT_PASTE_KEY_FILE" ]]; then
|
||||
echo " ↪ TLS client identity already present: $CLIENT_PKI_DIR"
|
||||
echo " ↪ paste shared key already present: $CLIENT_PASTE_KEY_FILE"
|
||||
return 0
|
||||
fi
|
||||
|
||||
@ -433,6 +435,11 @@ install_client_pki_bundle() {
|
||||
fetched_bundle=1
|
||||
echo " ↪ fetched TLS client enrollment bundle from $CLIENT_PKI_SSH_SOURCE"
|
||||
else
|
||||
if [[ -s "$CLIENT_PKI_DIR/ca.crt" && -s "$CLIENT_PKI_DIR/client.crt" && -s "$CLIENT_PKI_DIR/client.key" ]]; then
|
||||
echo "⚠️ TLS client identity is present but encrypted paste key is missing: $CLIENT_PASTE_KEY_FILE"
|
||||
echo " Rerun after the server installer refreshes $CLIENT_PKI_SSH_SOURCE, or provide LESAVKA_CLIENT_PKI_BUNDLE."
|
||||
return 0
|
||||
fi
|
||||
echo "⚠️ no TLS client identity installed."
|
||||
echo " Rerun with LESAVKA_CLIENT_PKI_BUNDLE=/path/to/lesavka-client-pki.tar.gz,"
|
||||
echo " or make $CLIENT_PKI_SSH_SOURCE readable over SSH and rerun the installer."
|
||||
@ -456,11 +463,20 @@ install_client_pki_bundle() {
|
||||
sudo install -m 0644 -o "$ORIG_USER" -g "$ORIG_USER" "$tmp/ca.crt" "$CLIENT_PKI_DIR/ca.crt"
|
||||
sudo install -m 0644 -o "$ORIG_USER" -g "$ORIG_USER" "$tmp/client.crt" "$CLIENT_PKI_DIR/client.crt"
|
||||
sudo install -m 0600 -o "$ORIG_USER" -g "$ORIG_USER" "$tmp/client.key" "$CLIENT_PKI_DIR/client.key"
|
||||
if [[ -s "$tmp/paste-key" ]]; then
|
||||
sudo install -d -m 0700 -o "$ORIG_USER" -g "$ORIG_USER" "$(dirname "$CLIENT_PASTE_KEY_FILE")"
|
||||
sudo install -m 0600 -o "$ORIG_USER" -g "$ORIG_USER" "$tmp/paste-key" "$CLIENT_PASTE_KEY_FILE"
|
||||
else
|
||||
echo "⚠️ TLS client bundle $bundle does not include paste-key; encrypted paste RPC will use HID fallback."
|
||||
fi
|
||||
sudo rm -rf "$tmp"
|
||||
if [[ $fetched_bundle == 1 ]]; then
|
||||
rm -f "$bundle"
|
||||
fi
|
||||
echo " ↪ installed TLS client identity: $CLIENT_PKI_DIR"
|
||||
if [[ -s "$CLIENT_PASTE_KEY_FILE" ]]; then
|
||||
echo " ↪ installed encrypted paste key: $CLIENT_PASTE_KEY_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
mkdir -p "$TMPDIR"
|
||||
@ -593,6 +609,7 @@ echo " User PATH alias: $USER_HOME/.local/bin/lesavka-client"
|
||||
echo " Desktop entry: /usr/share/applications/lesavka.desktop"
|
||||
echo " Build source: $SRC/target/release/lesavka-client"
|
||||
echo " TLS identity: $CLIENT_PKI_DIR"
|
||||
echo " Paste key: $CLIENT_PASTE_KEY_FILE"
|
||||
echo " Captures: $CLIENT_CAPTURE_DIR"
|
||||
warn_running_stale_client_processes
|
||||
echo "✅ Installed version: lesavka-client ${INSTALLED_VERSION:-unknown}${INSTALLED_SHA:+ ($INSTALLED_SHA)}"
|
||||
|
||||
@ -84,6 +84,7 @@ INSTALL_UVC_FRAME_META_LOG_PATH=${LESAVKA_INSTALL_UVC_FRAME_META_LOG_PATH:-${LES
|
||||
INSTALL_SERVER_BIND_ADDR=${LESAVKA_INSTALL_SERVER_BIND_ADDR:-0.0.0.0:50051}
|
||||
LESAVKA_TLS_DIR=${LESAVKA_TLS_DIR:-/etc/lesavka/pki}
|
||||
LESAVKA_CLIENT_BUNDLE=${LESAVKA_CLIENT_BUNDLE:-/etc/lesavka/lesavka-client-pki.tar.gz}
|
||||
LESAVKA_PASTE_KEY_FILE=${LESAVKA_PASTE_KEY_FILE:-/etc/lesavka/paste-key}
|
||||
DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=0
|
||||
DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=135090
|
||||
DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US=1280x720@20=0,1280x720@30=0,1920x1080@20=0,1920x1080@30=0
|
||||
@ -355,9 +356,11 @@ LESAVKA_UVC_CODEC=${INSTALL_UVC_CODEC}
|
||||
LESAVKA_UVC_BLOCKING=$(uvc_env_value LESAVKA_UVC_BLOCKING 1)
|
||||
LESAVKA_UVC_CONTROL_READ_ONLY=$(uvc_env_value LESAVKA_UVC_CONTROL_READ_ONLY 0)
|
||||
LESAVKA_UVC_MAXBURST=$(uvc_env_value LESAVKA_UVC_MAXBURST 0)
|
||||
LESAVKA_UVC_BULK=$(uvc_env_value LESAVKA_UVC_BULK 1)
|
||||
LESAVKA_UVC_FRAME_SIZE_GUARD=$(uvc_env_value LESAVKA_UVC_FRAME_SIZE_GUARD 1)
|
||||
LESAVKA_UVC_FRAME_MAX_BYTES=$(uvc_env_value LESAVKA_UVC_FRAME_MAX_BYTES 0)
|
||||
LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC=$(uvc_env_value LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC 10000000)
|
||||
LESAVKA_UVC_STATS_PATH=$(uvc_env_value LESAVKA_UVC_STATS_PATH /run/lesavka-uvc-video-stats.json)
|
||||
EOF
|
||||
}
|
||||
|
||||
@ -479,12 +482,25 @@ ensure_server_tls_pki() {
|
||||
sudo chmod 0644 "$LESAVKA_TLS_DIR/"*.crt
|
||||
rm -f "$ext_file" "$client_ext_file"
|
||||
|
||||
local paste_key_dir paste_tmp
|
||||
paste_key_dir=$(dirname "$LESAVKA_PASTE_KEY_FILE")
|
||||
sudo install -d -m 0750 "$paste_key_dir"
|
||||
if ! sudo test -s "$LESAVKA_PASTE_KEY_FILE"; then
|
||||
echo " ↪ generating encrypted paste shared key"
|
||||
paste_tmp=$(mktemp)
|
||||
openssl rand -hex 32 >"$paste_tmp"
|
||||
sudo install -m 0600 -o root -g root "$paste_tmp" "$LESAVKA_PASTE_KEY_FILE"
|
||||
rm -f "$paste_tmp"
|
||||
fi
|
||||
sudo chmod 0600 "$LESAVKA_PASTE_KEY_FILE"
|
||||
|
||||
local bundle_tmp
|
||||
bundle_tmp=$(mktemp -d)
|
||||
sudo cp "$LESAVKA_TLS_DIR/ca.crt" "$bundle_tmp/ca.crt"
|
||||
sudo cp "$LESAVKA_TLS_DIR/client.crt" "$bundle_tmp/client.crt"
|
||||
sudo cp "$LESAVKA_TLS_DIR/client.key" "$bundle_tmp/client.key"
|
||||
sudo tar -C "$bundle_tmp" -czf "$LESAVKA_CLIENT_BUNDLE" ca.crt client.crt client.key
|
||||
sudo cp "$LESAVKA_PASTE_KEY_FILE" "$bundle_tmp/paste-key"
|
||||
sudo tar -C "$bundle_tmp" -czf "$LESAVKA_CLIENT_BUNDLE" ca.crt client.crt client.key paste-key
|
||||
sudo chown "$ORIG_USER":"$ORIG_USER" "$LESAVKA_CLIENT_BUNDLE"
|
||||
sudo chmod 0600 "$LESAVKA_CLIENT_BUNDLE"
|
||||
sudo rm -rf "$bundle_tmp"
|
||||
@ -1596,6 +1612,7 @@ SERVER_ENV_TMP=$(mktemp)
|
||||
printf 'LESAVKA_TLS_CERT=%s\n' "${LESAVKA_TLS_CERT:-$LESAVKA_TLS_DIR/server.crt}"
|
||||
printf 'LESAVKA_TLS_KEY=%s\n' "${LESAVKA_TLS_KEY:-$LESAVKA_TLS_DIR/server.key}"
|
||||
printf 'LESAVKA_TLS_CLIENT_CA=%s\n' "${LESAVKA_TLS_CLIENT_CA:-$LESAVKA_TLS_DIR/ca.crt}"
|
||||
printf 'LESAVKA_PASTE_KEY_FILE=%s\n' "$LESAVKA_PASTE_KEY_FILE"
|
||||
} >"$SERVER_ENV_TMP"
|
||||
|
||||
UVC_ENV_TMP=$(mktemp)
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.22.30"
|
||||
version = "0.22.31"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -53,6 +53,7 @@ const DEFAULT_UVC_IDLE_PUMP_MS: u64 = 2;
|
||||
const DEFAULT_UVC_FRAME_MAX_AGE_MS: u64 = 1_000;
|
||||
const DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC: u32 = 10_000_000;
|
||||
const DEFAULT_UVC_STATS_INTERVAL_MS: u64 = 5_000;
|
||||
const DEFAULT_UVC_STATS_PATH: &str = "/run/lesavka-uvc-video-stats.json";
|
||||
|
||||
#[repr(C)]
|
||||
struct V4l2EventSubscription {
|
||||
@ -521,6 +522,12 @@ impl UvcVideoStream {
|
||||
self.stats.latest_bytes,
|
||||
self.frame_payload_limit()
|
||||
);
|
||||
if let Some(path) = uvc_stats_path() {
|
||||
let _ = write_atomic_text(
|
||||
&path,
|
||||
&uvc_stats_snapshot_json(&self.stats, self.frame_payload_limit()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -565,6 +572,20 @@ fn frame_spool_path() -> std::path::PathBuf {
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from("/run/lesavka-uvc-frame.mjpg"))
|
||||
}
|
||||
|
||||
fn uvc_stats_path() -> Option<std::path::PathBuf> {
|
||||
match 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)),
|
||||
}
|
||||
}
|
||||
|
||||
fn looks_like_mjpeg_frame(frame: &[u8]) -> bool {
|
||||
frame.len() > MINIMAL_MJPEG_FRAME.len()
|
||||
&& frame.starts_with(&[0xff, 0xd8])
|
||||
@ -605,7 +626,11 @@ fn uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
|
||||
}
|
||||
|
||||
fn derived_uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
|
||||
let fps = cfg.fps.max(1);
|
||||
derived_uvc_frame_max_bytes_for_fps(cfg.fps)
|
||||
}
|
||||
|
||||
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,
|
||||
@ -615,17 +640,66 @@ fn derived_uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
|
||||
per_frame.min(MAX_MJPEG_FRAME_BYTES as u32) as usize
|
||||
}
|
||||
|
||||
fn uvc_frame_size_guard_enabled() -> bool {
|
||||
env::var("LESAVKA_UVC_FRAME_SIZE_GUARD")
|
||||
fn env_flag_enabled(name: &str, default: bool) -> bool {
|
||||
env::var(name)
|
||||
.ok()
|
||||
.map(|value| {
|
||||
let trimmed = value.trim();
|
||||
!(trimmed.eq_ignore_ascii_case("0")
|
||||
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"))
|
||||
|| 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(true)
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn uvc_frame_size_guard_enabled() -> bool {
|
||||
env_flag_enabled("LESAVKA_UVC_FRAME_SIZE_GUARD", true)
|
||||
}
|
||||
|
||||
fn uvc_bulk_transfer_enabled() -> bool {
|
||||
env_flag_enabled("LESAVKA_UVC_BULK", true)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
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\":{}}}\n",
|
||||
stats.queued,
|
||||
stats.reloaded,
|
||||
stats.replayed_stale,
|
||||
stats.rejected_oversize,
|
||||
stats.rejected_invalid,
|
||||
stats.fallback_idle,
|
||||
stats.latest_bytes,
|
||||
frame_cap
|
||||
)
|
||||
}
|
||||
|
||||
fn uvc_stats_interval() -> Option<Duration> {
|
||||
@ -906,8 +980,8 @@ impl UvcConfig {
|
||||
}
|
||||
}
|
||||
let mut max_packet = env_u32("LESAVKA_UVC_MAXPACKET", 1024);
|
||||
let frame_size = env_u32("LESAVKA_UVC_FRAME_SIZE", width * height * 2);
|
||||
let bulk = env::var("LESAVKA_UVC_BULK").is_ok();
|
||||
let frame_size = uvc_frame_size_for_active_mode(width, height, fps);
|
||||
let bulk = uvc_bulk_transfer_enabled();
|
||||
if let Some(cap) = compute_payload_cap(bulk) {
|
||||
if max_packet > cap.limit {
|
||||
eprintln!(
|
||||
@ -952,7 +1026,7 @@ impl UvcConfig {
|
||||
);
|
||||
}
|
||||
}
|
||||
if env::var("LESAVKA_UVC_BULK").is_ok() {
|
||||
if bulk {
|
||||
max_packet = max_packet.min(512);
|
||||
} else {
|
||||
max_packet = max_packet.min(1024);
|
||||
@ -1441,8 +1515,7 @@ fn uvc_frame_index_for_request(requested: u8, cfg: &UvcConfig) -> u8 {
|
||||
|
||||
fn uvc_frame_size_for_index(frame_index: u8, fallback: u32) -> u32 {
|
||||
match frame_index {
|
||||
1 => 1920 * 1080 * 2,
|
||||
2 => 1280 * 720 * 2,
|
||||
1 | 2 => fallback,
|
||||
_ => fallback,
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,8 +52,7 @@ fn uvc_frame_index_for_request(requested: u8, cfg: &UvcConfig) -> u8 {
|
||||
|
||||
fn uvc_frame_size_for_index(frame_index: u8, fallback: u32) -> u32 {
|
||||
match frame_index {
|
||||
1 => 1920 * 1080 * 2,
|
||||
2 => 1280 * 720 * 2,
|
||||
1 | 2 => fallback,
|
||||
_ => fallback,
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,6 +58,8 @@ const DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC: u32 = 10_000_000;
|
||||
#[cfg(coverage)]
|
||||
const DEFAULT_UVC_STATS_INTERVAL_MS: u64 = 5_000;
|
||||
#[cfg(coverage)]
|
||||
const DEFAULT_UVC_STATS_PATH: &str = "/run/lesavka-uvc-video-stats.json";
|
||||
#[cfg(coverage)]
|
||||
const MAX_MJPEG_FRAME_BYTES: usize = 8 * 1024 * 1024;
|
||||
#[cfg(coverage)]
|
||||
const MINIMAL_MJPEG_FRAME: &[u8] = &[0xff, 0xd8, 0xff, 0xd9];
|
||||
@ -160,6 +162,18 @@ struct UvcVideoStream {
|
||||
frame_max_bytes: usize,
|
||||
}
|
||||
|
||||
#[cfg(coverage)]
|
||||
#[derive(Default)]
|
||||
struct UvcVideoStats {
|
||||
queued: u64,
|
||||
reloaded: u64,
|
||||
replayed_stale: u64,
|
||||
rejected_oversize: u64,
|
||||
rejected_invalid: u64,
|
||||
fallback_idle: u64,
|
||||
latest_bytes: usize,
|
||||
}
|
||||
|
||||
#[cfg(coverage)]
|
||||
impl UvcVideoStream {
|
||||
fn new(_fd: i32) -> Self {
|
||||
|
||||
@ -38,9 +38,9 @@ impl UvcConfig {
|
||||
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 = env_u32("LESAVKA_UVC_FRAME_SIZE", width * height * 2);
|
||||
let frame_size = uvc_frame_size_for_active_mode(width, height, fps);
|
||||
let interval = env_u32("LESAVKA_UVC_INTERVAL", 0);
|
||||
let bulk = env::var("LESAVKA_UVC_BULK").is_ok();
|
||||
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) {
|
||||
@ -175,7 +175,12 @@ fn uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
|
||||
|
||||
#[cfg(coverage)]
|
||||
fn derived_uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
|
||||
let fps = cfg.fps.max(1);
|
||||
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,
|
||||
@ -186,17 +191,86 @@ fn derived_uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
|
||||
}
|
||||
|
||||
#[cfg(coverage)]
|
||||
fn uvc_frame_size_guard_enabled() -> bool {
|
||||
env::var("LESAVKA_UVC_FRAME_SIZE_GUARD")
|
||||
fn env_flag_enabled(name: &str, default: bool) -> bool {
|
||||
env::var(name)
|
||||
.ok()
|
||||
.map(|value| {
|
||||
let trimmed = value.trim();
|
||||
!(trimmed.eq_ignore_ascii_case("0")
|
||||
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"))
|
||||
|| 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(true)
|
||||
.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 {
|
||||
env_flag_enabled("LESAVKA_UVC_BULK", 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\":{}}}\n",
|
||||
stats.queued,
|
||||
stats.reloaded,
|
||||
stats.replayed_stale,
|
||||
stats.rejected_oversize,
|
||||
stats.rejected_invalid,
|
||||
stats.fallback_idle,
|
||||
stats.latest_bytes,
|
||||
frame_cap
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(coverage)]
|
||||
|
||||
@ -96,8 +96,13 @@ fn core_script_keeps_uvc_output_on_supported_mjpeg_descriptor() {
|
||||
"UVC_CODEC=${LESAVKA_UVC_CODEC:-mjpeg}",
|
||||
"UVC codec '$UVC_CODEC' is not supported by the MJPEG UVC helper; using mjpeg",
|
||||
"UVC_CODEC=mjpeg",
|
||||
"flag_enabled \"${LESAVKA_UVC_BULK:-1}\"",
|
||||
"uvc_mjpeg_frame_size_for_fps()",
|
||||
"UVC_MJPEG_BUDGET_BYTES_PER_SEC=${LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC:-10000000}",
|
||||
"UVC_FRAME_SIZE=\"$(uvc_mjpeg_frame_size_for_fps \"$UVC_FPS\")\"",
|
||||
"write_mjpeg_frame_descriptor 1080p 1920 1080",
|
||||
"write_mjpeg_frame_descriptor 720p 1280 720",
|
||||
"echo \"$(uvc_frame_size_for \"$width\" \"$height\")\" >\"$frame/dwMaxVideoFrameBufferSize\"",
|
||||
"UVC_INTERVAL_30=${LESAVKA_UVC_INTERVAL_30:-333333}",
|
||||
"UVC_INTERVAL_20=${LESAVKA_UVC_INTERVAL_20:-500000}",
|
||||
] {
|
||||
|
||||
@ -18,7 +18,7 @@ mod uvc_binary {
|
||||
|
||||
use serial_test::serial;
|
||||
use std::fs;
|
||||
use temp_env::with_var;
|
||||
use temp_env::{with_var, with_vars};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
fn sample_cfg() -> UvcConfig {
|
||||
@ -71,7 +71,7 @@ mod uvc_binary {
|
||||
with_var("LESAVKA_UVC_INTERVAL", Some("200000"), || {
|
||||
with_var("LESAVKA_UVC_MAXPAYLOAD_LIMIT", Some("1500"), || {
|
||||
with_var("LESAVKA_UVC_MAXPACKET", Some("1200"), || {
|
||||
with_var("LESAVKA_UVC_BULK", None::<&str>, || {
|
||||
with_var("LESAVKA_UVC_BULK", Some("0"), || {
|
||||
let cfg = UvcConfig::from_env();
|
||||
assert_eq!(cfg.interval, 200_000);
|
||||
assert_eq!(cfg.max_packet, 1024);
|
||||
@ -134,7 +134,7 @@ mod uvc_binary {
|
||||
assert_eq!(out[2], 1);
|
||||
assert_eq!(out[3], 1);
|
||||
assert_eq!(read_le32(&out, 4), 333_333);
|
||||
assert_eq!(read_le32(&out, 18), 1920 * 1080 * 2);
|
||||
assert_eq!(read_le32(&out, 18), state.cfg.frame_size);
|
||||
assert_eq!(read_le32(&out, 22), state.cfg.max_packet);
|
||||
}
|
||||
|
||||
@ -312,6 +312,69 @@ mod uvc_binary {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn uvc_defaults_to_reliable_bulk_and_budgeted_mjpeg_frame_size() {
|
||||
with_vars(
|
||||
[
|
||||
("LESAVKA_UVC_WIDTH", Some("1280")),
|
||||
("LESAVKA_UVC_HEIGHT", Some("720")),
|
||||
("LESAVKA_UVC_FPS", Some("30")),
|
||||
("LESAVKA_UVC_INTERVAL", None::<&str>),
|
||||
("LESAVKA_UVC_FRAME_SIZE", None::<&str>),
|
||||
("LESAVKA_UVC_BULK", None::<&str>),
|
||||
("LESAVKA_UVC_MAXPACKET", Some("1024")),
|
||||
("LESAVKA_UVC_MAXPAYLOAD_LIMIT", None::<&str>),
|
||||
("LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC", Some("9000000")),
|
||||
],
|
||||
|| {
|
||||
let cfg = UvcConfig::from_env();
|
||||
assert_eq!(cfg.interval, 10_000_000 / 30);
|
||||
assert_eq!(cfg.frame_size, 300_000);
|
||||
assert_eq!(cfg.max_packet, 512);
|
||||
assert!(uvc_bulk_transfer_enabled());
|
||||
},
|
||||
);
|
||||
|
||||
with_var("LESAVKA_UVC_BULK", Some("0"), || {
|
||||
assert!(!uvc_bulk_transfer_enabled());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn uvc_stats_snapshot_can_be_disabled_or_written() {
|
||||
let stats = UvcVideoStats {
|
||||
queued: 7,
|
||||
reloaded: 6,
|
||||
replayed_stale: 2,
|
||||
rejected_oversize: 1,
|
||||
rejected_invalid: 3,
|
||||
fallback_idle: 4,
|
||||
latest_bytes: 77_036,
|
||||
last_report: None,
|
||||
};
|
||||
let json = uvc_stats_snapshot_json(&stats, 333_333);
|
||||
assert!(json.contains("\"queued\":7"));
|
||||
assert!(json.contains("\"latest_bytes\":77036"));
|
||||
assert!(json.contains("\"frame_cap\":333333"));
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let path = dir.path().join("uvc").join("stats.json");
|
||||
write_atomic_text(&path, &json).expect("write stats");
|
||||
assert_eq!(fs::read_to_string(&path).expect("read stats"), json);
|
||||
|
||||
with_var("LESAVKA_UVC_STATS_PATH", Some("0"), || {
|
||||
assert!(uvc_stats_path().is_none());
|
||||
});
|
||||
with_var("LESAVKA_UVC_STATS_PATH", None::<&str>, || {
|
||||
assert_eq!(
|
||||
uvc_stats_path().expect("default stats path"),
|
||||
std::path::PathBuf::from(DEFAULT_UVC_STATS_PATH)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn uvc_control_open_mode_defaults_read_only_with_escape_hatch() {
|
||||
|
||||
@ -18,14 +18,18 @@ fn client_install_accepts_server_generated_tls_bundle() {
|
||||
"ca.crt",
|
||||
"client.crt",
|
||||
"client.key",
|
||||
"paste-key",
|
||||
"install_client_pki_bundle",
|
||||
"fetch_client_pki_bundle",
|
||||
"LESAVKA_CLIENT_PKI_SSH_SOURCE",
|
||||
"CLIENT_PASTE_KEY_FILE",
|
||||
"LESAVKA_CLIENT_CAPTURE_DIR",
|
||||
"theia:/etc/lesavka/lesavka-client-pki.tar.gz",
|
||||
"Pictures/lesavka",
|
||||
"HTTPS/mTLS relay connections will not work until this bundle is installed",
|
||||
"encrypted paste key:",
|
||||
"TLS identity:",
|
||||
"Paste key:",
|
||||
"Captures:",
|
||||
"$USER_HOME/.local/bin/lesavka-client",
|
||||
"User PATH alias:",
|
||||
@ -39,6 +43,10 @@ fn client_install_accepts_server_generated_tls_bundle() {
|
||||
CLIENT_INSTALL.contains(".config/lesavka/pki"),
|
||||
"client cert bundle should land in the same path the desktop app auto-loads"
|
||||
);
|
||||
assert!(
|
||||
CLIENT_INSTALL.contains(".config/lesavka/paste-key"),
|
||||
"client installer should land the server-issued paste key where runtime paste auto-loads it"
|
||||
);
|
||||
assert!(
|
||||
CLIENT_INSTALL.contains("0600"),
|
||||
"client private key should be installed with private permissions"
|
||||
|
||||
@ -50,6 +50,7 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
|
||||
"LESAVKA_TLS_CERT=%s",
|
||||
"LESAVKA_TLS_KEY=%s",
|
||||
"LESAVKA_TLS_CLIENT_CA=%s",
|
||||
"LESAVKA_PASTE_KEY_FILE=%s",
|
||||
] {
|
||||
assert!(
|
||||
SERVER_INSTALL.contains(expected),
|
||||
@ -181,13 +182,25 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
|
||||
"installer should persist HEVC-specific calibration maps"
|
||||
);
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_INSTALL_SERVER_BIND_ADDR:-0.0.0.0:50051}"));
|
||||
assert!(
|
||||
SERVER_INSTALL
|
||||
.contains("LESAVKA_PASTE_KEY_FILE=${LESAVKA_PASTE_KEY_FILE:-/etc/lesavka/paste-key}")
|
||||
);
|
||||
assert!(SERVER_INSTALL.contains("openssl rand -hex 32"));
|
||||
assert!(SERVER_INSTALL.contains("paste-key"));
|
||||
assert!(SERVER_INSTALL.contains("ca.crt client.crt client.key paste-key"));
|
||||
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_MAXPACKET 1024"));
|
||||
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_INTERVAL 333333"));
|
||||
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_WIDTH 1280"));
|
||||
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_HEIGHT 720"));
|
||||
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_CONTROL_READ_ONLY 0"));
|
||||
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_BULK 1"));
|
||||
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_FRAME_SIZE_GUARD 1"));
|
||||
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_FRAME_MAX_BYTES 0"));
|
||||
assert!(
|
||||
SERVER_INSTALL
|
||||
.contains("uvc_env_value LESAVKA_UVC_STATS_PATH /run/lesavka-uvc-video-stats.json")
|
||||
);
|
||||
assert!(
|
||||
!SERVER_INSTALL.contains("LESAVKA_UVC_CODEC=${LESAVKA_UVC_CODEC:-mjpeg}"),
|
||||
"install script should not let ambient LESAVKA_UVC_CODEC leak into persisted defaults"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user