diff --git a/install.sh b/install.sh index 35384fecd..9bbbfcf27 100755 --- a/install.sh +++ b/install.sh @@ -4038,6 +4038,61 @@ EOF safe_systemctl daemon-reload } +# Tracks whether Pulse was running before an update-time stop, so start_pulse can +# guarantee it comes back up afterward instead of silently leaving it stopped +# (#1323: on unprivileged LXC the installer's restart can silently fail). +PULSE_WAS_ACTIVE="false" + +# Poll until the service is active, up to timeout_seconds (default 20). +wait_for_service_active() { + local service_name=$1 + local timeout_seconds="${2:-20}" + local elapsed=0 + while (( elapsed < timeout_seconds )); do + if timeout 5 systemctl is-active --quiet "$service_name" 2>/dev/null; then + return 0 + fi + sleep 1 + ((elapsed += 1)) + done + return 1 +} + +# Stop Pulse for an update, remembering whether it was running so start_pulse can +# guarantee it comes back up afterward (#1323). +stop_pulse_for_update() { + PULSE_WAS_ACTIVE="false" + if timeout 5 systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then + PULSE_WAS_ACTIVE="true" + fi + systemctl stop "$SERVICE_NAME" 2>/dev/null || true +} + +# After an update, if Pulse was running beforehand, make sure it is running again: +# retry one explicit start, and surface a clear error if it still will not come up +# — rather than printing a success completion over a stopped service (#1323). +ensure_pulse_running_after_update() { + [[ "$PULSE_WAS_ACTIVE" == "true" ]] || return 0 + PULSE_WAS_ACTIVE="false" + + if wait_for_service_active "$SERVICE_NAME" 20; then + return 0 + fi + + print_warn "Pulse did not come back up after the update; retrying start..." + safe_systemctl start "$SERVICE_NAME" || true + if wait_for_service_active "$SERVICE_NAME" 20; then + print_success "Pulse is running again." + return 0 + fi + + print_error "Pulse was running before the update but did not come back up (#1323)." + print_info "The new binary is installed; investigate why the service will not start:" + print_info " systemctl status $SERVICE_NAME" + print_info " journalctl -u $SERVICE_NAME -n 50" + return 1 +} + start_pulse() { print_info "Starting Pulse..." @@ -4048,8 +4103,12 @@ start_pulse() { if ! safe_systemctl start $SERVICE_NAME; then print_info "Note: systemctl start failed (common in unprivileged containers)" - print_info "The service will start automatically when the container starts" - return 0 + if [[ "$PULSE_WAS_ACTIVE" != "true" ]]; then + print_info "The service will start automatically when the container starts" + return 0 + fi + # An update stopped a running Pulse; do not accept a silent start failure + # here — fall through to the active-state verification below (#1323). fi # Wait for service to start @@ -4063,6 +4122,8 @@ start_pulse() { # Don't exit, just warn print_info "Service may not be running. You might need to start it manually." fi + + ensure_pulse_running_after_update } create_marker_file() { @@ -4284,7 +4345,7 @@ main() { fi backup_existing - systemctl stop $SERVICE_NAME || true + stop_pulse_for_update create_user download_pulse setup_update_command @@ -4484,7 +4545,7 @@ main() { fi backup_existing - systemctl stop $SERVICE_NAME || true + stop_pulse_for_update create_user download_pulse setup_update_command @@ -4539,7 +4600,7 @@ main() { fi backup_existing - systemctl stop $SERVICE_NAME || true + stop_pulse_for_update create_user download_pulse setup_directories @@ -4766,6 +4827,10 @@ reset_pulse() { exit 0 } +# When sourced (e.g. by tests) rather than executed, define the functions above +# but do not run the installer. +[[ "${BASH_SOURCE[0]}" == "$0" ]] || return 0 + # Parse command line arguments while [[ $# -gt 0 ]]; do case $1 in diff --git a/scripts/tests/test-install-update-resilience.sh b/scripts/tests/test-install-update-resilience.sh new file mode 100755 index 000000000..17af0e208 --- /dev/null +++ b/scripts/tests/test-install-update-resilience.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# +# Tests the interactive-update service-resilience helpers in install.sh (#1323): +# after an update that stopped a running Pulse, the installer must verify the +# service came back up, retry one explicit start, and surface a clear error +# instead of silently leaving Pulse stopped (common on unprivileged LXC where +# the installer's restart silently fails). +set -uo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +INSTALL_SCRIPT="${ROOT_DIR}/install.sh" + +# Sourcing relies on the BASH_SOURCE guard so main() does not run on import. +source "${INSTALL_SCRIPT}" +set +e # this test drives helpers that intentionally return non-zero + +SERVICE_NAME="pulse" + +# --- Deterministic stubs: no real sleeps, no real systemctl/timeout --------- +sleep() { :; } +timeout() { shift; "$@"; } # drop the duration; run the wrapped (stubbed) command + +SYSTEMCTL_ACTIVE="no" +START_ATTEMPTS=0 +systemctl() { + case "$1" in + is-active) [[ "$SYSTEMCTL_ACTIVE" == "yes" ]] && return 0 || return 1 ;; + stop) SYSTEMCTL_ACTIVE="no"; return 0 ;; + start) START_ATTEMPTS=$((START_ATTEMPTS + 1)); return 0 ;; + *) return 0 ;; + esac +} +safe_systemctl() { systemctl "$@"; } + +PRINT_BUF="" +print_info() { PRINT_BUF+="INFO:$*"$'\n'; } +print_warn() { PRINT_BUF+="WARN:$*"$'\n'; } +print_error() { PRINT_BUF+="ERROR:$*"$'\n'; } +print_success() { PRINT_BUF+="OK:$*"$'\n'; } + +fail() { echo "FAIL: $*" >&2; exit 1; } + +# --- Case 1: stop_pulse_for_update records whether Pulse was running -------- +SYSTEMCTL_ACTIVE="yes"; PULSE_WAS_ACTIVE="false" +stop_pulse_for_update +[[ "$PULSE_WAS_ACTIVE" == "true" ]] || fail "should record was-active=true when running" +[[ "$SYSTEMCTL_ACTIVE" == "no" ]] || fail "should stop the service" + +SYSTEMCTL_ACTIVE="no"; PULSE_WAS_ACTIVE="true" +stop_pulse_for_update +[[ "$PULSE_WAS_ACTIVE" == "false" ]] || fail "should record was-active=false when not running" + +# --- Case 2: no-op when Pulse was not running before the update ------------ +PULSE_WAS_ACTIVE="false"; PRINT_BUF="" +ensure_pulse_running_after_update || fail "should no-op (succeed) when was-active=false" +[[ -z "$PRINT_BUF" ]] || fail "should be silent when was-active=false, got: $PRINT_BUF" + +# --- Case 3: service comes back up -> success, flag consumed ---------------- +PULSE_WAS_ACTIVE="true"; SYSTEMCTL_ACTIVE="yes"; PRINT_BUF="" +ensure_pulse_running_after_update || fail "should succeed when the service is active" +[[ "$PULSE_WAS_ACTIVE" == "false" ]] || fail "should consume the was-active flag" + +# --- Case 4: service stays down -> retries once, then a clear error --------- +PULSE_WAS_ACTIVE="true"; SYSTEMCTL_ACTIVE="no"; START_ATTEMPTS=0; PRINT_BUF="" +if ensure_pulse_running_after_update; then + fail "should return non-zero when the service will not come up" +fi +[[ "$START_ATTEMPTS" -ge 1 ]] || fail "should attempt an explicit restart, got $START_ATTEMPTS" +[[ "$PRINT_BUF" == *"did not come back up"* ]] || fail "should surface a clear error, got: $PRINT_BUF" + +echo "PASS: install.sh update-resilience helpers (#1323)"