From f6ead775ed3520f416e44f8e3b8891b0678e2594 Mon Sep 17 00:00:00 2001 From: Eric Curtin Date: Fri, 2 Oct 2026 22:11:05 +0100 Subject: [PATCH] fix(install): stop waiting when the gateway service fails Fixes #4040 Signed-off-by: Eric Curtin --- install.sh | 24 ++++++++- tasks/scripts/test-install-sh.sh | 91 ++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 83b7c479be..7cc8637f6e 100755 --- a/install.sh +++ b/install.sh @@ -935,10 +935,21 @@ start_user_gateway() { info "registering local gateway as ${TARGET_USER}..." register_local_gateway - wait_for_local_gateway_listener + wait_for_local_gateway_listener user_gateway_service_failed wait_for_local_gateway_status } +# Succeeds when the gateway user service has failed or is waiting to restart +# after a failure. A unit that is starting or running does not match, even if +# it failed before it was restarted. +user_gateway_service_failed() { + _unit_state="$(as_target_user systemctl --user show openshell-gateway -p ActiveState -p SubState 2>/dev/null)" || return 1 + case "$_unit_state" in + *ActiveState=failed* | *SubState=auto-restart*) return 0 ;; + esac + return 1 +} + dump_local_gateway_diagnostics() { _lines="${OPENSHELL_INSTALL_LOG_LINES:-80}" case "$_lines" in @@ -1015,10 +1026,14 @@ dump_user_service_gateway_diagnostics() { fi } +# An optional command name stops the wait early when it succeeds, so a service +# that already failed does not run out the full timeout. wait_for_local_gateway_listener() { + _failed_check="${1:-}" _timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}" _elapsed=0 _last_output="" + _service_failed=0 _probe_url="$(local_gateway_endpoint)/" _mtls_dir="${TARGET_HOME}/.config/openshell/gateways/openshell/mtls" @@ -1030,12 +1045,19 @@ wait_for_local_gateway_listener() { info "local gateway listener is reachable" return 0 fi + if [ -n "$_failed_check" ] && "$_failed_check"; then + _service_failed=1 + break + fi sleep 1 _elapsed=$((_elapsed + 1)) done [ -z "$_last_output" ] || printf '%s\n' "$_last_output" >&2 dump_local_gateway_diagnostics + if [ "$_service_failed" -eq 1 ]; then + error "the openshell-gateway service failed to start; fix the cause shown above, then run: systemctl --user restart openshell-gateway" + fi error "local gateway listener did not become reachable at ${_probe_url} within ${_timeout}s" } diff --git a/tasks/scripts/test-install-sh.sh b/tasks/scripts/test-install-sh.sh index 581251891c..c0a42b6700 100755 --- a/tasks/scripts/test-install-sh.sh +++ b/tasks/scripts/test-install-sh.sh @@ -539,6 +539,97 @@ if [[ -z $(find "$snap_user_tls" -maxdepth 0 -perm 700) ]]; then exit 1 fi +assert_user_gateway_service_failed() { + local name=$1 + local unit_state=$2 + local expected=$3 + local actual=0 + + ( + as_target_user() { printf '%s\n' "$unit_state"; } + user_gateway_service_failed + ) >/dev/null || actual=$? + if [ "$actual" != "$expected" ]; then + echo "FAIL: ${name}: expected status ${expected}, got ${actual}" >&2 + exit 1 + fi +} + +assert_user_gateway_service_failed "failed unit" $'ActiveState=failed\nSubState=failed' 0 +assert_user_gateway_service_failed "unit restarting after a failure" $'ActiveState=activating\nSubState=auto-restart' 0 +assert_user_gateway_service_failed "unit starting after a failure" $'ActiveState=activating\nSubState=start' 1 +assert_user_gateway_service_failed "running unit" $'ActiveState=active\nSubState=running' 1 +if ( + as_target_user() { return 1; } + user_gateway_service_failed +); then + echo "FAIL: unreachable user systemd must not count as a failed unit" >&2 + exit 1 +fi + +# Runs the listener wait against an unreachable gateway and prints the number +# of one second waits. The wait is expected to fail. +run_listener_wait() { + local unit_state=$1 + local sleeps_file="${tmpdir}/listener-sleeps" + shift + + : >"$sleeps_file" + ( + as_target_user() { + case "$1" in + systemctl) printf '%s\n' "$unit_state" ;; + *) return 7 ;; + esac + } + sleep() { printf '.' >>"$sleeps_file"; } + info() { :; } + dump_local_gateway_diagnostics() { echo "gateway diagnostics" >&2; } + TARGET_HOME="${tmpdir}/listener-home" + PLATFORM=linux + OPENSHELL_INSTALL_GATEWAY_TIMEOUT=5 wait_for_local_gateway_listener "$@" + ) >"$out" 2>"$err" && return 1 + wc -c <"$sleeps_file" | tr -d ' ' +} + +listener_mtls_dir="${tmpdir}/listener-home/.config/openshell/gateways/openshell/mtls" +mkdir -p "$listener_mtls_dir" +: >"${listener_mtls_dir}/ca.crt" +: >"${listener_mtls_dir}/tls.crt" +: >"${listener_mtls_dir}/tls.key" + +restarting_unit=$'ActiveState=activating\nSubState=auto-restart' +running_unit=$'ActiveState=active\nSubState=running' + +if [ "$(run_listener_wait "$restarting_unit" user_gateway_service_failed)" != "0" ]; then + echo "FAIL: a failed gateway service must stop the listener wait immediately" >&2 + exit 1 +fi +if [ "$(tail -n 1 "$err")" != "openshell: error: the openshell-gateway service failed to start; fix the cause shown above, then run: systemctl --user restart openshell-gateway" ]; then + echo "FAIL: a failed gateway service must end with the service error" >&2 + cat "$err" >&2 + exit 1 +fi +if ! grep -Fq "gateway diagnostics" "$err"; then + echo "FAIL: a failed gateway service must dump diagnostics" >&2 + exit 1 +fi + +if [ "$(run_listener_wait "$running_unit" user_gateway_service_failed)" != "5" ]; then + echo "FAIL: a running gateway service must not stop the listener wait" >&2 + exit 1 +fi +if ! grep -Fq "did not become reachable" "$err"; then + echo "FAIL: a running gateway service must end with the listener timeout" >&2 + cat "$err" >&2 + exit 1 +fi + +if [ "$(run_listener_wait "$restarting_unit")" != "5" ]; then + echo "FAIL: the listener wait must ignore the service state without a check" >&2 + exit 1 +fi + if [ "$(PLATFORM=darwin local_gateway_endpoint)" != "https://localhost:17670" ]; then echo "FAIL: macOS local gateway endpoint must use a TLS-compatible loopback hostname" >&2 exit 1