From 66f3078bbc895dce5c573c14ccbb044332649b01 Mon Sep 17 00:00:00 2001 From: Charlie Tonneslan Date: Sat, 16 May 2026 13:30:20 -0400 Subject: [PATCH] serve: don't drop the server start error on the way out The run loop selects between two channels: lch (the server's exit error from s.Start) and done (signal or close). When s.Start fails early (e.g. bind: permission denied on :22 under an unprivileged user, see #645), the goroutine writes to lch and then doneOnce() closes done. Both are then ready at the same time, and Go picks one at random. About half the time the select takes done first, falls through to the shutdown path, and the bind error never gets surfaced to the user, who just sees soft "start" successfully and then no listener on the configured port. When the done case fires with a nil sig (i.e. done was closed by doneOnce, not by a real signal), drain lch and return whatever error the server reported. Real signals (SIGINT/TERM with a non-nil sig) still take the graceful path. Closes #645 Signed-off-by: Charlie Tonneslan --- cmd/soft/serve/serve.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cmd/soft/serve/serve.go b/cmd/soft/serve/serve.go index 7472f3d0b..85ec95e43 100644 --- a/cmd/soft/serve/serve.go +++ b/cmd/soft/serve/serve.go @@ -121,6 +121,20 @@ var ( } continue } + // A nil sig means doneOnce() closed `done` because the server + // goroutine returned, not because we got a real signal. The + // server's error sits in lch and the outer select could have + // dropped it (#645). Surface it before shutting down so that + // e.g. a permission-denied on :22 doesn't disappear silently. + if sig == nil { + select { + case err := <-lch: + if err != nil { + return fmt.Errorf("server error: %w", err) + } + default: + } + } } break