Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion start/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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-<uid> 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())
}
5 changes: 5 additions & 0 deletions start/command_center.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 14 additions & 0 deletions utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down