From 904c659d9a0c0441a8d9448ce6e69909b7aa1f39 Mon Sep 17 00:00:00 2001 From: Fran Zekan Date: Tue, 28 Oct 2025 17:42:04 +0100 Subject: [PATCH] Fix tmux socket name length exceeding filesystem limits Long project or branch names could push the tmux socket path past the unix socket sun_path limit (104 bytes on macOS), causing 'File name too long' errors on start. The session portion of the instance ID is now truncated to fit a budget derived from the actual tmux socket directory (mirroring tmux's $TMUX_TMPDIR / /tmp + tmux- logic, symlinks resolved), so it adapts to any uid or TMUX_TMPDIR instead of a fixed cap. Also clean up the command-center socket on a fatal exit: utils.Fatal calls os.Exit, which skips deferred cleanup and orphaned .overmind.sock when startup failed. Fatal now runs registered cleanup functions first. Co-Authored-By: Claude Opus 4.8 (1M context) --- start/command.go | 42 ++++++++++++++++++++++++++++++++++++++++- start/command_center.go | 5 +++++ utils/utils.go | 14 ++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/start/command.go b/start/command.go index 9ba2af2..90d3ab7 100644 --- a/start/command.go +++ b/start/command.go @@ -67,7 +67,7 @@ func newCommand(h *Handler) (*command, error) { return nil, err } - instanceID := fmt.Sprintf("overmind-%s-%s", session, nanoid) + instanceID := buildInstanceID(session, nanoid) c.output = newMultiOutput(pf.MaxNameLength(), h.ShowTimestamps) c.tmux = newTmuxClient(session, instanceID, root, h.TmuxConfigPath, c.output.Offset()) @@ -265,3 +265,43 @@ func (c *command) waitForTimeoutOrStop() { func sanitizeProcName(name string) string { return disallowedProcNameCharacters.ReplaceAllString(name, "_") } + +// maxSocketPathLen is a conservative upper bound for a unix socket path. macOS +// caps sockaddr_un.sun_path at 104 bytes including the NUL terminator (Linux +// allows 108); using the smaller limit keeps overmind portable. +const maxSocketPathLen = 103 + +// buildInstanceID assembles the tmux instance ID (used as both the tmux socket +// name and the script directory name), truncating the session so the resulting +// socket path stays within maxSocketPathLen regardless of how long the project +// or branch name is. +func buildInstanceID(session, nanoid string) string { + prefix := "overmind-" + suffix := "-" + nanoid + + // +1 accounts for the path separator between the socket dir and the name. + budget := maxSocketPathLen - len(tmuxSocketDir()) - 1 - len(prefix) - len(suffix) + if budget < 0 { + budget = 0 + } + if len(session) > budget { + session = session[:budget] + } + + return prefix + session + suffix +} + +// tmuxSocketDir returns the directory tmux uses for its sockets, mirroring +// tmux's own logic ($TMUX_TMPDIR or /tmp, plus a tmux- subdirectory). +// Symlinks are resolved so the length matches the path tmux actually binds +// (e.g. /tmp is a symlink to /private/tmp on macOS). +func tmuxSocketDir() string { + base := os.Getenv("TMUX_TMPDIR") + if base == "" { + base = "/tmp" + } + if resolved, err := filepath.EvalSymlinks(base); err == nil { + base = resolved + } + return fmt.Sprintf("%s/tmux-%d", base, os.Getuid()) +} diff --git a/start/command_center.go b/start/command_center.go index b0ff77f..93a1612 100644 --- a/start/command_center.go +++ b/start/command_center.go @@ -41,6 +41,11 @@ func (c *commandCenter) Start() (err error) { return } + // A graceful shutdown removes the socket via the deferred Stop, but + // os.Exit (e.g. when tmux fails to start) skips deferred cleanup, so + // register Stop to also run on a fatal exit and avoid orphaning the socket. + utils.AddCleanup(c.Stop) + go func(c *commandCenter) { for { if conn, err := c.listener.Accept(); err == nil { diff --git a/utils/utils.go b/utils/utils.go index cf63ca4..df4e5e7 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -15,6 +15,16 @@ import ( var badTitleCharsRe = regexp.MustCompile(`[^a-zA-Z0-9]`) var dashesRe = regexp.MustCompile(`-{2,}`) +// cleanupFns are run, in registration order, right before Fatal exits the +// process. They exist because os.Exit skips deferred cleanup, which would +// otherwise orphan resources such as the command-center socket file. +var cleanupFns []func() + +// AddCleanup registers fn to run before the process exits via Fatal. +func AddCleanup(fn func()) { + cleanupFns = append(cleanupFns, fn) +} + // FatalOnErr prints error and exits if errir is not nil func FatalOnErr(err error) { if err != nil { @@ -24,6 +34,10 @@ func FatalOnErr(err error) { // Fatal prints error and exits if errir func Fatal(i ...interface{}) { + for _, fn := range cleanupFns { + fn() + } + fmt.Fprint(os.Stderr, "overmind: ") fmt.Fprintln(os.Stderr, i...) os.Exit(1)