on main help sign in
fix(server): clear leftover sockets so server can always start

When the server starts and finds a socket file with no process behind it, it now removes the dead socket before binding instead of failing with an address-already-in-use error. A live server is never disturbed, and the cleanup is recorded in the log. Co-Authored-By: Charm Crush <crush@charm.land>

commit443d0ab339d92bf9181f1054a2c760e178f964e0 authorChristian Rocha <christian@rocha.is> dateMon 8 Jun 2026, 09:26 (4 months ago) parent62baa95
3 files changed+74−7
internal/server/net_other.go+54−3 internal/server/net_windows.go+16−3 internal/server/server.go+4−1
internal/server/net_other.go+54−3
@@ -2,9 +2,60 @@
22
33package server
44
5−import "net"
5+import (
6+ "errors"
7+ "io/fs"
8+ "net"
9+ "os"
10+ "time"
11+)
612
7−func listen(network, address string) (net.Listener, error) {
13+// staleSocketDialTimeout bounds the probe used to detect whether a Unix
14+// socket file on disk is backed by a live listener.
15+const staleSocketDialTimeout = 200 * time.Millisecond
16+
17+// listen binds a net.Listener on the given network and address.
18+//
19+// For unix sockets it self-heals from stale socket files: if the path
20+// already exists on disk, it first probes with a short net.DialTimeout.
21+// A successful dial means a live server owns the socket, so we proceed
22+// to net.Listen (which surfaces the usual "address already in use"
23+// error). A failed dial that isStaleSocketErr classifies as stale
24+// triggers an os.Remove of the path (ignoring fs.ErrNotExist) before
25+// the bind.
26+//
27+// The returned removedStale bool reports whether a stale socket file
28+// was removed prior to binding so callers can log it. The operation
29+// is idempotent: removing an absent file is a no-op, and a live
30+// socket is never removed.
31+func listen(network, address string) (net.Listener, bool, error) {
32+ var removedStale bool
33+ if network == "unix" && address != "" {
34+ if _, err := os.Stat(address); err == nil {
35+ conn, dialErr := net.DialTimeout(network, address, staleSocketDialTimeout)
36+ if dialErr == nil {
37+ // A live server owns the socket. Fall through to
38+ // net.Listen so the caller sees the standard
39+ // "address already in use" error.
40+ conn.Close()
41+ } else if isStaleSocketErr(dialErr) {
42+ rmErr := os.Remove(address)
43+ switch {
44+ case rmErr == nil:
45+ removedStale = true
46+ case errors.Is(rmErr, fs.ErrNotExist):
47+ // Another process removed it between our
48+ // stat and remove; treat as a no-op.
49+ default:
50+ return nil, false, rmErr
51+ }
52+ }
53+ }
54+ }
855 //nolint:noctx
9− return net.Listen(network, address)
56+ ln, err := net.Listen(network, address)
57+ if err != nil {
58+ return nil, removedStale, err
59+ }
60+ return ln, removedStale, nil
1061}
internal/server/net_windows.go+16−3
@@ -9,7 +9,12 @@ import (
99 "github.com/Microsoft/go-winio"
1010)
1111
12−func listen(network, address string) (net.Listener, error) {
12+// listen binds a net.Listener on the given network and address.
13+//
14+// On Windows there is no Unix-socket stale-file recovery to perform,
15+// so removedStale is always false. The signature matches the
16+// non-Windows implementation so callers can use a single code path.
17+func listen(network, address string) (net.Listener, bool, error) {
1318 switch network {
1419 case "npipe":
1520 cfg := &winio.PipeConfig{
@@ -17,8 +22,16 @@ func listen(network, address string) (net.Listener, error) {
1722 InputBufferSize: 65536,
1823 OutputBufferSize: 65536,
1924 }
20− return winio.ListenPipe(address, cfg)
25+ ln, err := winio.ListenPipe(address, cfg)
26+ if err != nil {
27+ return nil, false, err
28+ }
29+ return ln, false, nil
2130 default:
22− return net.Listen(network, address) //nolint:noctx
31+ ln, err := net.Listen(network, address) //nolint:noctx
32+ if err != nil {
33+ return nil, false, err
34+ }
35+ return ln, false, nil
2336 }
2437}
internal/server/server.go+4−1
@@ -234,10 +234,13 @@ func (s *Server) ListenAndServe() error {
234234 if s.ln != nil {
235235 return fmt.Errorf("server already started")
236236 }
237− ln, err := listen(s.network, s.Addr)
237+ ln, removedStale, err := listen(s.network, s.Addr)
238238 if err != nil {
239239 return fmt.Errorf("failed to listen on %s: %w", s.Addr, err)
240240 }
241+ if removedStale && s.logger != nil {
242+ s.logger.Warn("Removed stale socket before binding", "address", s.Addr)
243+ }
241244 return s.Serve(ln)
242245}
243246