//go:build !windows package rpc import ( "crypto/sha256" "encoding/hex" "os" "path/filepath" "strings" "github.com/steveyegge/beads/internal/utils" ) // MaxUnixSocketPath is the maximum length for Unix socket paths. // macOS has a 104-byte limit (including null terminator), Linux has 108. // We use 103 to be safe across platforms. const MaxUnixSocketPath = 103 // ShortSocketPath returns a short socket path suitable for Unix sockets. // On Unix systems with socket path length limits (macOS: 104 chars, Linux: 108), // this function returns a path in /tmp/beads-{hash}/ to avoid exceeding limits. // // The hash is derived from the canonicalized workspace path, ensuring: // - Different workspaces get different socket directories // - The same workspace always gets the same hash (deterministic) // - Symlinks and case differences resolve to the same hash // // If the computed .beads/bd.sock path is short enough, it returns that directly. // This preserves backwards compatibility for workspaces with short paths. func ShortSocketPath(workspacePath string) string { // Canonicalize path for consistent hashing across symlinks and case canonical := utils.NormalizePathForComparison(workspacePath) if canonical == "" { canonical = workspacePath } // Compute the "natural" socket path in .beads/ naturalPath := filepath.Join(workspacePath, ".beads", "bd.sock") // If natural path is short enough, use it (backwards compatible) if len(naturalPath) <= MaxUnixSocketPath { return naturalPath } // Path too long - use /tmp with hash return shortSocketDir(canonical) } // shortSocketDir returns a socket path in /tmp/beads-{hash}/. // The hash is 8 hex characters derived from SHA256 of the workspace path. func shortSocketDir(canonicalPath string) string { hash := sha256.Sum256([]byte(canonicalPath)) hashStr := hex.EncodeToString(hash[:4]) // 8 hex chars from 4 bytes dir := filepath.Join(tmpDir, "beads-"+hashStr) return filepath.Join(dir, "bd.sock") } // tmpDir returns the temp directory for sockets. // We always use /tmp because: // - On macOS, $TMPDIR is very long (/var/folders/xx/xxxxxxxxxxxx/T/) // - On Linux, /tmp is standard // - We need short paths due to Unix socket length limits const tmpDir = "/tmp" // EnsureSocketDir creates the socket directory if it doesn't exist. // Returns the socket path (unchanged) and any error. // This should be called by the daemon before listening. func EnsureSocketDir(socketPath string) (string, error) { dir := filepath.Dir(socketPath) // Only create if it's a /tmp/beads-* directory // Don't create .beads directories - those should exist if strings.HasPrefix(dir, filepath.Join(tmpDir, "beads-")) { if err := os.MkdirAll(dir, 0700); err != nil { return "", err } } return socketPath, nil } // CleanupSocketDir removes the socket directory if it's in /tmp/beads-*. // This should be called when the daemon shuts down. func CleanupSocketDir(socketPath string) error { dir := filepath.Dir(socketPath) // Only remove if it's a /tmp/beads-* directory we created if strings.HasPrefix(dir, filepath.Join(tmpDir, "beads-")) { // Remove socket file first _ = os.Remove(socketPath) // Remove directory (will fail if not empty, which is fine) return os.Remove(dir) } // For .beads/ directories, just remove the socket file return os.Remove(socketPath) } // NeedsShortPath returns true if the workspace path would result in a socket // path exceeding Unix limits. func NeedsShortPath(workspacePath string) bool { naturalPath := filepath.Join(workspacePath, ".beads", "bd.sock") return len(naturalPath) > MaxUnixSocketPath }