fix(server): avoid auto gadget cycles with external uvc
This commit is contained in:
parent
ee7550dfe5
commit
5eb984ce08
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1642,7 +1642,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_client"
|
||||
version = "0.14.42"
|
||||
version = "0.14.43"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@ -1676,7 +1676,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_common"
|
||||
version = "0.14.42"
|
||||
version = "0.14.43"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@ -1688,7 +1688,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_server"
|
||||
version = "0.14.42"
|
||||
version = "0.14.43"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.14.42"
|
||||
version = "0.14.43"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.14.42"
|
||||
version = "0.14.43"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -315,11 +315,23 @@ wait_for_unit_running() {
|
||||
}
|
||||
|
||||
validate_server_ready() {
|
||||
local bind_addr
|
||||
local bind_addr port
|
||||
bind_addr=$(server_bind_addr)
|
||||
port=$(server_bind_port) || {
|
||||
echo "❌ could not parse LESAVKA_SERVER_BIND_ADDR='${bind_addr}' while validating server readiness." >&2
|
||||
return 1
|
||||
}
|
||||
if wait_for_unit_running lesavka-server; then
|
||||
echo "✅ lesavka-server is active and running on ${bind_addr}."
|
||||
return 0
|
||||
for _ in {1..50}; do
|
||||
if list_server_listener_inodes_proc "$port" | grep -q .; then
|
||||
echo "✅ lesavka-server is active and listening on ${bind_addr}."
|
||||
return 0
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
echo "❌ lesavka-server reached active/running state but did not open TCP :${port}." >&2
|
||||
sudo journalctl -b -u lesavka-server -n 80 --no-pager >&2 || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "❌ lesavka-server failed to reach active/running state on ${bind_addr}." >&2
|
||||
@ -850,7 +862,6 @@ Environment=LESAVKA_EYE_MIN_FPS=12
|
||||
Environment=LESAVKA_EYE_FPS=20
|
||||
Environment=LESAVKA_MIC_INIT_ATTEMPTS=5
|
||||
Environment=LESAVKA_MIC_INIT_DELAY_MS=250
|
||||
Environment=LESAVKA_ALLOW_GADGET_CYCLE=1
|
||||
Environment=LESAVKA_SERVER_LOG_PATH=/var/log/lesavka/server.log
|
||||
EnvironmentFile=-/etc/lesavka/uvc.env
|
||||
EnvironmentFile=-/etc/lesavka/server.env
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.14.42"
|
||||
version = "0.14.43"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -22,9 +22,15 @@ impl Handler {
|
||||
#[cfg(not(coverage))]
|
||||
{
|
||||
if !runtime_support::allow_gadget_cycle() {
|
||||
info!(
|
||||
"🔒 gadget cycle disabled at startup (set LESAVKA_ALLOW_GADGET_CYCLE=1 to enable)"
|
||||
);
|
||||
if runtime_support::external_uvc_helper_owns_gadget() {
|
||||
info!(
|
||||
"🔒 gadget cycle disabled at startup because external UVC helper owns the gadget"
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
"🔒 gadget cycle disabled at startup (set LESAVKA_ALLOW_GADGET_CYCLE=1 to enable)"
|
||||
);
|
||||
}
|
||||
}
|
||||
info!("🛠️ opening HID endpoints …");
|
||||
}
|
||||
@ -127,5 +133,4 @@ impl Handler {
|
||||
.max(self.active_eye_source_count().await);
|
||||
state
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -155,15 +155,29 @@ pub fn hid_endpoint_open_is_temporarily_unavailable(code: Option<i32>) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// Check whether the standalone UVC helper owns the gadget device.
|
||||
///
|
||||
/// Inputs: process environment.
|
||||
/// Outputs: `true` when UVC is enabled and supervised by systemd instead of
|
||||
/// the server process.
|
||||
/// Why: automatic whole-gadget resets can wedge configfs if they race the UVC
|
||||
/// helper's open video-output node.
|
||||
#[must_use]
|
||||
pub fn external_uvc_helper_owns_gadget() -> bool {
|
||||
std::env::var("LESAVKA_UVC_EXTERNAL").is_ok() && std::env::var("LESAVKA_DISABLE_UVC").is_err()
|
||||
}
|
||||
|
||||
/// Check whether gadget auto-recovery is enabled.
|
||||
///
|
||||
/// Inputs: none.
|
||||
/// Outputs: `true` only when the explicit recovery opt-in env var is present.
|
||||
/// Why: cycling the whole USB gadget can be disruptive, so operators must
|
||||
/// choose that behavior deliberately on each deployment.
|
||||
/// Why: cycling the whole USB gadget can be disruptive, and it is especially
|
||||
/// unsafe while the external UVC helper owns the gadget video node.
|
||||
#[must_use]
|
||||
pub fn allow_gadget_cycle() -> bool {
|
||||
std::env::var("LESAVKA_ALLOW_GADGET_CYCLE").is_ok()
|
||||
&& (!external_uvc_helper_owns_gadget()
|
||||
|| std::env::var("LESAVKA_ALLOW_EXTERNAL_UVC_GADGET_CYCLE").is_ok())
|
||||
}
|
||||
|
||||
/// Return whether a HID write error should trigger recovery.
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
use super::{
|
||||
allow_gadget_cycle, detect_uac_card_candidates, init_tracing, next_stream_id,
|
||||
open_ear_with_retry, open_hid_if_ready, open_with_retry, parse_uac_named_card_candidates,
|
||||
parse_uac_numeric_card_ids, parse_uac_pcm_candidates, preferred_uac_device_candidates,
|
||||
push_audio_candidate, push_audio_candidate_family, should_recover_hid_error, write_hid_report,
|
||||
allow_gadget_cycle, detect_uac_card_candidates, external_uvc_helper_owns_gadget, init_tracing,
|
||||
next_stream_id, open_ear_with_retry, open_hid_if_ready, open_with_retry,
|
||||
parse_uac_named_card_candidates, parse_uac_numeric_card_ids, parse_uac_pcm_candidates,
|
||||
preferred_uac_device_candidates, push_audio_candidate, push_audio_candidate_family,
|
||||
should_recover_hid_error, write_hid_report,
|
||||
};
|
||||
use serial_test::serial;
|
||||
use std::collections::BTreeSet;
|
||||
@ -24,6 +25,40 @@ fn allow_gadget_cycle_tracks_env_presence() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn allow_gadget_cycle_defers_to_external_uvc_owner() {
|
||||
with_var("LESAVKA_ALLOW_GADGET_CYCLE", Some("1"), || {
|
||||
with_var("LESAVKA_UVC_EXTERNAL", Some("1"), || {
|
||||
with_var("LESAVKA_DISABLE_UVC", None::<&str>, || {
|
||||
with_var(
|
||||
"LESAVKA_ALLOW_EXTERNAL_UVC_GADGET_CYCLE",
|
||||
None::<&str>,
|
||||
|| {
|
||||
assert!(external_uvc_helper_owns_gadget());
|
||||
assert!(
|
||||
!allow_gadget_cycle(),
|
||||
"server must not reset the gadget while external UVC owns it"
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn allow_gadget_cycle_can_be_forced_with_external_uvc_owner() {
|
||||
with_var("LESAVKA_ALLOW_GADGET_CYCLE", Some("1"), || {
|
||||
with_var("LESAVKA_UVC_EXTERNAL", Some("1"), || {
|
||||
with_var("LESAVKA_ALLOW_EXTERNAL_UVC_GADGET_CYCLE", Some("1"), || {
|
||||
assert!(allow_gadget_cycle());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_recover_hid_error_matches_transport_failures() {
|
||||
assert!(should_recover_hid_error(Some(libc::ENOTCONN)));
|
||||
|
||||
@ -131,6 +131,10 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
|
||||
SERVER_INSTALL.contains("Wants=lesavka-uvc.service"),
|
||||
"server unit should pull in the external UVC helper on UVC installs"
|
||||
);
|
||||
assert!(
|
||||
!SERVER_INSTALL.contains("Environment=LESAVKA_ALLOW_GADGET_CYCLE=1"),
|
||||
"server unit should not auto-cycle the gadget while the external UVC helper owns it"
|
||||
);
|
||||
assert!(
|
||||
SERVER_INSTALL.contains("/var/log/lesavka/server.log"),
|
||||
"install script should keep server logs out of sticky /tmp"
|
||||
@ -187,6 +191,14 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
|
||||
SERVER_INSTALL.contains("validate_server_ready"),
|
||||
"install script should verify that lesavka-server reaches a running state"
|
||||
);
|
||||
assert!(
|
||||
SERVER_INSTALL.contains("active and listening"),
|
||||
"install script should require the TCP listener before declaring server readiness"
|
||||
);
|
||||
assert!(
|
||||
SERVER_INSTALL.contains("did not open TCP"),
|
||||
"install script should explain active-but-not-listening server failures"
|
||||
);
|
||||
assert!(
|
||||
SERVER_INSTALL.contains("failed to reach active/running state"),
|
||||
"install script should explain server startup failures instead of claiming success"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user