install: restart idle server without cycling gadget

This commit is contained in:
Brad Stein 2026-05-12 12:32:34 -03:00
parent 38ef8327d3
commit cfbd3f979a
8 changed files with 193 additions and 55 deletions

6
Cargo.lock generated
View File

@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "lesavka_client"
version = "0.22.13"
version = "0.22.14"
dependencies = [
"anyhow",
"async-stream",
@ -1686,7 +1686,7 @@ dependencies = [
[[package]]
name = "lesavka_common"
version = "0.22.13"
version = "0.22.14"
dependencies = [
"anyhow",
"base64",
@ -1698,7 +1698,7 @@ dependencies = [
[[package]]
name = "lesavka_server"
version = "0.22.13"
version = "0.22.14"
dependencies = [
"anyhow",
"base64",

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package]
name = "lesavka_client"
version = "0.22.13"
version = "0.22.14"
edition = "2024"
[dependencies]

View File

@ -1,6 +1,6 @@
[package]
name = "lesavka_common"
version = "0.22.13"
version = "0.22.14"
edition = "2024"
build = "build.rs"

View File

@ -605,6 +605,46 @@ list_server_bound_inactive_lines() {
sudo ss -H -B -tn "sport = :$port" 2>/dev/null | sed '/^$/d' || true
}
list_server_established_client_lines_proc() {
local port=$1 hex_port
hex_port=$(printf '%04X' "$port")
sudo awk -v target="$hex_port" '
FNR == 1 { next }
$4 == "01" {
split($2, local, ":")
if (toupper(local[2]) == target) print $0
}
' /proc/net/tcp /proc/net/tcp6 2>/dev/null | sed '/^$/d'
}
list_server_established_client_lines() {
local port=$1
if command -v ss >/dev/null 2>&1; then
sudo ss -Htn state established "sport = :$port" 2>/dev/null | sed '/^$/d' || true
fi
list_server_established_client_lines_proc "$port"
}
lesavka_server_has_active_clients() {
local port
port=$(server_bind_port) || return 1
list_server_established_client_lines "$port" | grep -q .
}
server_active_client_detail() {
local port detail
port=$(server_bind_port) || {
printf 'unable to parse server bind port'
return 0
}
detail=$(list_server_established_client_lines "$port" | head -n 5 | paste -sd ';' -)
if [[ -z $detail ]]; then
printf 'no established clients on :%s' "$port"
return 0
fi
printf 'established clients on :%s => %s' "$port" "$detail"
}
list_server_listener_inodes_proc() {
local port=$1 hex_port
hex_port=$(printf '%04X' "$port")
@ -809,6 +849,51 @@ validate_server_ready() {
return 1
}
install_lesavka_server_unit_file() {
cat <<'UNIT' | sudo tee /etc/systemd/system/lesavka-server.service >/dev/null
[Unit]
Description=lesavka gRPC relay
After=network.target lesavka-core.service lesavka-uvc.service
StartLimitIntervalSec=30
StartLimitBurst=10
[Service]
ExecStart=/usr/local/bin/lesavka-server
TimeoutStopSec=10
KillSignal=SIGTERM
KillMode=control-group
Restart=always
Environment=RUST_LOG=lesavka_server=info,lesavka_server::audio=info,lesavka_server::video=info,lesavka_server::gadget=info
Environment=RUST_BACKTRACE=1
Environment=GST_DEBUG="*:2,alsasink:6,alsasrc:6"
Environment=LESAVKA_UVC_EXTERNAL=1
Environment=LESAVKA_EYE_ADAPTIVE=1
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_SERVER_LOG_PATH=/var/log/lesavka/server.log
EnvironmentFile=-/etc/lesavka/uvc.env
EnvironmentFile=-/etc/lesavka/server.env
Restart=always
RestartSec=5
StandardError=append:/var/log/lesavka/server.stderr
User=root
[Install]
WantedBy=multi-user.target
UNIT
}
restart_lesavka_server_only() {
sudo truncate -s 0 /var/log/lesavka/server.stderr
sudo systemctl stop lesavka-server >/dev/null 2>&1 || true
clear_stale_server_listener
sudo systemctl reset-failed lesavka-server >/dev/null 2>&1 || true
sudo systemctl restart lesavka-server
validate_server_ready
}
normalize_hdmi_connector() {
local name="$1"
if [[ $name =~ (HDMI-A-[0-9]+)$ ]]; then
@ -1481,17 +1566,44 @@ if [[ "$HOST_GADGET_PROTECTED" == "1" ]] \
fi
if [[ "$ATTACHED_UVC_CHANGE_DEFERRED" == "1" ]]; then
echo "⚠️ UVC runtime settings or live descriptors differ while the host is attached." >&2
echo " Refusing to rewrite UVC/server runtime env or restart Lesavka services because that can wedge the Pi USB controller." >&2
echo " Leaving /etc/lesavka/server.env and /etc/lesavka/uvc.env unchanged; binaries were installed but running services were left untouched." >&2
echo " The installer will not restart lesavka-core or lesavka-uvc because that can wedge the Pi USB controller." >&2
echo " To apply descriptor-changing UVC settings, use a maintenance window with LESAVKA_ALLOW_GADGET_RESET=1 LESAVKA_FORCE_GADGET_REBUILD=1." >&2
if lesavka_server_has_active_clients && [[ "${LESAVKA_INSTALL_RESTART_WITH_CLIENTS:-0}" != "1" ]]; then
echo "⚠️ Active gRPC clients are connected; leaving the running server untouched for this install." >&2
echo " $(server_active_client_detail)" >&2
echo " Leaving /etc/lesavka/server.env and /etc/lesavka/uvc.env unchanged; binaries were installed but running services were left untouched." >&2
echo " Rerun the installer after clients disconnect, or set LESAVKA_INSTALL_RESTART_WITH_CLIENTS=1 to force a server-only restart." >&2
rm -f "$SERVER_ENV_TMP" "$UVC_ENV_TMP"
INSTALLED_VERSION=$(manifest_package_version "$SRC_DIR/server/Cargo.toml" 2>/dev/null || true)
INSTALLED_SHA=$(git -C "$SCRIPT_REPO_ROOT" rev-parse --short HEAD 2>/dev/null || true)
sudo systemctl start lesavka-recovery-ladder.timer >/dev/null 2>&1 || true
echo "✅ lesavka-server binaries installed; live service restart deferred because clients are connected."
echo "➡️ Installed binaries: lesavka-server ${INSTALLED_VERSION:-unknown}${INSTALLED_SHA:+ ($INSTALLED_SHA)}"
echo "➡️ Recovery ladder: installed, enabled, and started in server-only mode"
echo "➡️ Deferred UVC codec request: ${INSTALL_UVC_CODEC}"
exit 0
fi
echo "✅ No active gRPC clients detected; applying runtime env and restarting lesavka-server only." >&2
echo " Preserving lesavka-core and lesavka-uvc so the attached USB gadget is not cycled." >&2
sudo install -m 0644 "$SERVER_ENV_TMP" /etc/lesavka/server.env
sudo install -m 0644 "$UVC_ENV_TMP" /etc/lesavka/uvc.env
rm -f "$SERVER_ENV_TMP" "$UVC_ENV_TMP"
sudo install -d -m 0755 /var/log/lesavka
sudo truncate -s 0 /var/log/lesavka/server.log
install_lesavka_server_unit_file
sudo systemctl daemon-reload
sudo systemctl enable lesavka-server >/dev/null 2>&1 || true
restart_lesavka_server_only
sudo /usr/local/bin/lesavka-recovery-ladder snapshot || true
INSTALLED_VERSION=$(manifest_package_version "$SRC_DIR/server/Cargo.toml" 2>/dev/null || true)
INSTALLED_SHA=$(git -C "$SCRIPT_REPO_ROOT" rev-parse --short HEAD 2>/dev/null || true)
sudo systemctl start lesavka-recovery-ladder.timer >/dev/null 2>&1 || true
echo "✅ lesavka-server binaries installed; live service restart deferred for attached-gadget safety."
echo "➡️ Installed binaries: lesavka-server ${INSTALLED_VERSION:-unknown}${INSTALLED_SHA:+ ($INSTALLED_SHA)}"
echo "✅ lesavka-server binaries, runtime env, and server unit installed; server-only restart completed."
echo "➡️ Installed and running: lesavka-server ${INSTALLED_VERSION:-unknown}${INSTALLED_SHA:+ ($INSTALLED_SHA)}"
echo "➡️ Recovery ladder: installed, enabled, and started in server-only mode"
echo "➡️ Deferred UVC codec request: ${INSTALL_UVC_CODEC}"
echo "➡️ UVC codec request persisted for the next safe gadget rebuild: ${INSTALL_UVC_CODEC}"
echo "➡️ Live UVC descriptors may still differ until a maintenance-window gadget rebuild is allowed."
exit 0
fi
sudo install -m 0644 "$SERVER_ENV_TMP" /etc/lesavka/server.env
@ -1533,39 +1645,7 @@ WantedBy=multi-user.target
UNIT
echo "==> 6b. Systemd units - lesavka-server"
cat <<'UNIT' | sudo tee /etc/systemd/system/lesavka-server.service >/dev/null
[Unit]
Description=lesavka gRPC relay
After=network.target lesavka-core.service lesavka-uvc.service
StartLimitIntervalSec=30
StartLimitBurst=10
[Service]
ExecStart=/usr/local/bin/lesavka-server
TimeoutStopSec=10
KillSignal=SIGTERM
KillMode=control-group
Restart=always
Environment=RUST_LOG=lesavka_server=info,lesavka_server::audio=info,lesavka_server::video=info,lesavka_server::gadget=info
Environment=RUST_BACKTRACE=1
Environment=GST_DEBUG="*:2,alsasink:6,alsasrc:6"
Environment=LESAVKA_UVC_EXTERNAL=1
Environment=LESAVKA_EYE_ADAPTIVE=1
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_SERVER_LOG_PATH=/var/log/lesavka/server.log
EnvironmentFile=-/etc/lesavka/uvc.env
EnvironmentFile=-/etc/lesavka/server.env
Restart=always
RestartSec=5
StandardError=append:/var/log/lesavka/server.stderr
User=root
[Install]
WantedBy=multi-user.target
UNIT
install_lesavka_server_unit_file
echo "==> 6c. Systemd units - initialization"
sudo install -d -m 0755 /var/log/lesavka
@ -1687,12 +1767,7 @@ fi
validate_uvc_gadget_ready
sudo truncate -s 0 /var/log/lesavka/server.stderr
sudo systemctl stop lesavka-server >/dev/null 2>&1 || true
clear_stale_server_listener
sudo systemctl reset-failed lesavka-server >/dev/null 2>&1 || true
sudo systemctl restart lesavka-server
validate_server_ready
restart_lesavka_server_only
sudo /usr/local/bin/lesavka-recovery-ladder snapshot || true
sudo systemctl start lesavka-recovery-ladder.timer
INSTALLED_VERSION=$(manifest_package_version "$SRC_DIR/server/Cargo.toml" 2>/dev/null || true)

View File

@ -10,7 +10,7 @@ bench = false
[package]
name = "lesavka_server"
version = "0.22.13"
version = "0.22.14"
edition = "2024"
autobins = false

View File

@ -1,7 +1,22 @@
/*──────────────── main ───────────────────────*/
fn print_version_and_exit_requested() -> bool {
if std::env::args()
.skip(1)
.any(|arg| arg == "--version" || arg == "-V")
{
println!("{} {}", PKG_NAME, lesavka_server::VERSION);
return true;
}
false
}
#[cfg(not(coverage))]
#[tokio::main(worker_threads = 4)]
async fn main() -> anyhow::Result<()> {
if print_version_and_exit_requested() {
return Ok(());
}
let _guard = init_tracing()?;
info!("🚀 {} v{} starting up", PKG_NAME, lesavka_server::VERSION);
@ -43,6 +58,10 @@ async fn main() -> anyhow::Result<()> {
#[cfg(coverage)]
#[tokio::main(worker_threads = 2)]
async fn main() -> anyhow::Result<()> {
if print_version_and_exit_requested() {
return Ok(());
}
let gadget = UsbGadget::new("lesavka");
let _handler = Handler::new(gadget).await?;
Err(anyhow::anyhow!("coverage mode skips live gRPC serve loop"))

View File

@ -110,6 +110,30 @@ fn wait_for_log(path: &PathBuf, needle: &str, deadline: Instant) -> String {
}
}
#[test]
#[serial]
fn server_binary_reports_version_without_starting_relay() {
let Some(bin) = find_binary("lesavka-server") else {
return;
};
let output = Command::new(bin)
.arg("--version")
.output()
.expect("run lesavka-server --version");
assert!(
output.status.success(),
"version probe should exit successfully; stderr was:\n{}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
if let Some(version) = server_package_version() {
assert!(
stdout.contains(&format!("lesavka_server {version}")),
"version probe should print the package version; stdout was:\n{stdout}"
);
}
}
#[test]
#[serial]
fn server_binary_stays_up_with_missing_hid_nodes_and_current_version() {

View File

@ -256,13 +256,33 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
"server installs should stop any stale recovery timer before a long build/install cycle"
);
assert!(
SERVER_INSTALL
.contains("Leaving /etc/lesavka/server.env and /etc/lesavka/uvc.env unchanged"),
"deferred attached-gadget installs should preserve the last known live runtime env"
SERVER_INSTALL.contains("lesavka_server_has_active_clients")
&& SERVER_INSTALL.contains("list_server_established_client_lines")
&& SERVER_INSTALL.contains("LESAVKA_INSTALL_RESTART_WITH_CLIENTS"),
"attached-gadget installs should distinguish idle safe updates from active client sessions"
);
assert!(
SERVER_INSTALL.contains("binaries were installed but running services were left untouched"),
"deferred attached-gadget installs must not restart live services"
SERVER_INSTALL
.contains("Active gRPC clients are connected; leaving the running server untouched"),
"active clients should keep the old server process alive instead of forcing a disruptive update"
);
assert!(
SERVER_INSTALL
.contains("Leaving /etc/lesavka/server.env and /etc/lesavka/uvc.env unchanged"),
"active-client deferral should preserve the last known live runtime env"
);
assert!(
SERVER_INSTALL
.contains("No active gRPC clients detected; applying runtime env and restarting lesavka-server only")
&& SERVER_INSTALL
.contains("Preserving lesavka-core and lesavka-uvc so the attached USB gadget is not cycled")
&& SERVER_INSTALL.contains("restart_lesavka_server_only"),
"idle attached-gadget installs should leave the newest server running without cycling USB gadget services"
);
assert!(
SERVER_INSTALL.contains("server-only restart completed")
&& SERVER_INSTALL.contains("Live UVC descriptors may still differ"),
"idle attached-gadget installs should be explicit about server readiness and deferred descriptor rebuilds"
);
assert!(
SERVER_INSTALL