install: restart idle server without cycling gadget
This commit is contained in:
parent
38ef8327d3
commit
cfbd3f979a
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.22.13"
|
||||
version = "0.22.14"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.22.13"
|
||||
version = "0.22.14"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.22.13"
|
||||
version = "0.22.14"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user