on main help sign in
fix(server): spawn after stale shutdown + handle socket errors

Two related gaps in the client startup path. After a version mismatch we were shutting the old server down and then never starting a new one, leaving the next caller to time out against a missing socket. And any unexpected error from inspecting the socket path (something other than "not found") was silently ignored, so we'd fall straight into the same timeout. Both paths now converge on "needs (re)start" and we try to clean up any stale file in the way before spawning. Co-Authored-By: Charm Crush <crush@charm.land>

commitd17d61ce2dc90fad83a99952f43a2e1d8f40a8ae authorChristian Rocha <christian@rocha.is> dateMon 11 May 2026, 16:01 (5 months ago) parent8021114
1 file changed+25−11
internal/cmd/root.go+25−11
internal/cmd/root.go+25−11
@@ -382,13 +382,23 @@ func ensureServer(cmd *cobra.Command, hostURL *url.URL) error {
382382 switch hostURL.Scheme {
383383 case "unix", "npipe":
384384 needsStart := false
385− if _, err := os.Stat(hostURL.Host); err != nil && errors.Is(err, fs.ErrNotExist) {
385+ _, statErr := os.Stat(hostURL.Host)
386+ switch {
387+ case statErr == nil:
388+ restarted, err := restartIfStale(cmd, hostURL)
389+ if err != nil {
390+ slog.Warn("Failed to check server version", "error", err)
391+ }
392+ needsStart = restarted || err != nil
393+ case errors.Is(statErr, fs.ErrNotExist):
386394 needsStart = true
387− } else if err == nil {
388− if err := restartIfStale(cmd, hostURL); err != nil {
389− slog.Warn("Failed to check server version, restarting", "error", err)
390− needsStart = true
395+ default:
396+ slog.Warn("Unexpected error stat'ing server socket, attempting cleanup",
397+ "path", hostURL.Host, "error", statErr)
398+ if err := os.Remove(hostURL.Host); err != nil && !errors.Is(err, fs.ErrNotExist) {
399+ return fmt.Errorf("failed to remove stale server socket %q: %v", hostURL.Host, err)
391400 }
401+ needsStart = true
392402 }
393403
394404 if needsStart {
@@ -517,17 +527,21 @@ func probeHealth(ctx context.Context, h *http.Client, reqURL string, hostURL *ur
517527// restartIfStale checks whether the running server matches the current
518528// client version. When they differ, it sends a shutdown command and
519529// removes the stale socket so the caller can start a fresh server.
520−func restartIfStale(cmd *cobra.Command, hostURL *url.URL) error {
530+//
531+// It returns restarted=true when it has shut down a stale server and the
532+// caller must spawn a new one. When the server matches the client version
533+// (or the check itself fails), restarted is false.
534+func restartIfStale(cmd *cobra.Command, hostURL *url.URL) (restarted bool, err error) {
521535 c, err := client.NewClient("", hostURL.Scheme, hostURL.Host)
522536 if err != nil {
523− return err
537+ return false, err
524538 }
525539 vi, err := c.VersionInfo(cmd.Context())
526540 if err != nil {
527− return err
541+ return false, err
528542 }
529543 if vi.Version == version.Version {
530− return nil
544+ return false, nil
531545 }
532546 slog.Info("Server version mismatch, restarting",
533547 "server", vi.Version,
@@ -541,13 +555,13 @@ func restartIfStale(cmd *cobra.Command, hostURL *url.URL) error {
541555 }
542556 select {
543557 case <-cmd.Context().Done():
544− return cmd.Context().Err()
558+ return true, cmd.Context().Err()
545559 case <-time.After(100 * time.Millisecond):
546560 }
547561 }
548562 // Force-remove if the socket is still lingering.
549563 _ = os.Remove(hostURL.Host)
550− return nil
564+ return true, nil
551565}
552566
553567var safeNameRegexp = regexp.MustCompile(`[^a-zA-Z0-9._-]`)