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]]
|
[[package]]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.14.42"
|
version = "0.14.43"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1676,7 +1676,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.14.42"
|
version = "0.14.43"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@ -1688,7 +1688,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.14.42"
|
version = "0.14.43"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.14.42"
|
version = "0.14.43"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.14.42"
|
version = "0.14.43"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -315,11 +315,23 @@ wait_for_unit_running() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validate_server_ready() {
|
validate_server_ready() {
|
||||||
local bind_addr
|
local bind_addr port
|
||||||
bind_addr=$(server_bind_addr)
|
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
|
if wait_for_unit_running lesavka-server; then
|
||||||
echo "✅ lesavka-server is active and running on ${bind_addr}."
|
for _ in {1..50}; do
|
||||||
return 0
|
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
|
fi
|
||||||
|
|
||||||
echo "❌ lesavka-server failed to reach active/running state on ${bind_addr}." >&2
|
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_EYE_FPS=20
|
||||||
Environment=LESAVKA_MIC_INIT_ATTEMPTS=5
|
Environment=LESAVKA_MIC_INIT_ATTEMPTS=5
|
||||||
Environment=LESAVKA_MIC_INIT_DELAY_MS=250
|
Environment=LESAVKA_MIC_INIT_DELAY_MS=250
|
||||||
Environment=LESAVKA_ALLOW_GADGET_CYCLE=1
|
|
||||||
Environment=LESAVKA_SERVER_LOG_PATH=/var/log/lesavka/server.log
|
Environment=LESAVKA_SERVER_LOG_PATH=/var/log/lesavka/server.log
|
||||||
EnvironmentFile=-/etc/lesavka/uvc.env
|
EnvironmentFile=-/etc/lesavka/uvc.env
|
||||||
EnvironmentFile=-/etc/lesavka/server.env
|
EnvironmentFile=-/etc/lesavka/server.env
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.14.42"
|
version = "0.14.43"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -22,9 +22,15 @@ impl Handler {
|
|||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
{
|
{
|
||||||
if !runtime_support::allow_gadget_cycle() {
|
if !runtime_support::allow_gadget_cycle() {
|
||||||
info!(
|
if runtime_support::external_uvc_helper_owns_gadget() {
|
||||||
"🔒 gadget cycle disabled at startup (set LESAVKA_ALLOW_GADGET_CYCLE=1 to enable)"
|
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 …");
|
info!("🛠️ opening HID endpoints …");
|
||||||
}
|
}
|
||||||
@ -127,5 +133,4 @@ impl Handler {
|
|||||||
.max(self.active_eye_source_count().await);
|
.max(self.active_eye_source_count().await);
|
||||||
state
|
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.
|
/// Check whether gadget auto-recovery is enabled.
|
||||||
///
|
///
|
||||||
/// Inputs: none.
|
/// Inputs: none.
|
||||||
/// Outputs: `true` only when the explicit recovery opt-in env var is present.
|
/// 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
|
/// Why: cycling the whole USB gadget can be disruptive, and it is especially
|
||||||
/// choose that behavior deliberately on each deployment.
|
/// unsafe while the external UVC helper owns the gadget video node.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn allow_gadget_cycle() -> bool {
|
pub fn allow_gadget_cycle() -> bool {
|
||||||
std::env::var("LESAVKA_ALLOW_GADGET_CYCLE").is_ok()
|
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.
|
/// Return whether a HID write error should trigger recovery.
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
use super::{
|
use super::{
|
||||||
allow_gadget_cycle, detect_uac_card_candidates, init_tracing, next_stream_id,
|
allow_gadget_cycle, detect_uac_card_candidates, external_uvc_helper_owns_gadget, init_tracing,
|
||||||
open_ear_with_retry, open_hid_if_ready, open_with_retry, parse_uac_named_card_candidates,
|
next_stream_id, open_ear_with_retry, open_hid_if_ready, open_with_retry,
|
||||||
parse_uac_numeric_card_ids, parse_uac_pcm_candidates, preferred_uac_device_candidates,
|
parse_uac_named_card_candidates, parse_uac_numeric_card_ids, parse_uac_pcm_candidates,
|
||||||
push_audio_candidate, push_audio_candidate_family, should_recover_hid_error, write_hid_report,
|
preferred_uac_device_candidates, push_audio_candidate, push_audio_candidate_family,
|
||||||
|
should_recover_hid_error, write_hid_report,
|
||||||
};
|
};
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
use std::collections::BTreeSet;
|
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]
|
#[test]
|
||||||
fn should_recover_hid_error_matches_transport_failures() {
|
fn should_recover_hid_error_matches_transport_failures() {
|
||||||
assert!(should_recover_hid_error(Some(libc::ENOTCONN)));
|
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_INSTALL.contains("Wants=lesavka-uvc.service"),
|
||||||
"server unit should pull in the external UVC helper on UVC installs"
|
"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!(
|
assert!(
|
||||||
SERVER_INSTALL.contains("/var/log/lesavka/server.log"),
|
SERVER_INSTALL.contains("/var/log/lesavka/server.log"),
|
||||||
"install script should keep server logs out of sticky /tmp"
|
"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"),
|
SERVER_INSTALL.contains("validate_server_ready"),
|
||||||
"install script should verify that lesavka-server reaches a running state"
|
"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!(
|
assert!(
|
||||||
SERVER_INSTALL.contains("failed to reach active/running state"),
|
SERVER_INSTALL.contains("failed to reach active/running state"),
|
||||||
"install script should explain server startup failures instead of claiming success"
|
"install script should explain server startup failures instead of claiming success"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user