feat(server): store runtime socket in per-user runtime directory

Christian Rocha and Charm Crush created

Moves the Crush socket out of the shared temp folder and into the
per-user runtime directory on Linux and macOS, falling back to the
temp folder when needed. Guards against socket paths that are too
long to bind.

Co-Authored-By: Charm Crush <crush@charm.land>

Change summary

internal/server/server.go | 33 ++++++++++++++++++++++++++++++++-
1 file changed, 32 insertions(+), 1 deletion(-)

Detailed changes

internal/server/server.go 🔗

@@ -7,7 +7,9 @@ import (
 	"net"
 	"net/http"
 	"net/url"
+	"os"
 	"os/user"
+	"path/filepath"
 	"runtime"
 	"strings"
 
@@ -17,6 +19,23 @@ import (
 	httpswagger "github.com/swaggo/http-swagger/v2"
 )
 
+// maxUnixSocketPathLen is the maximum length of a Unix domain socket
+// path. The macOS sun_path field is 104 bytes; Linux allows 108. We
+// use 104 so the resulting path is portable across both platforms.
+const maxUnixSocketPathLen = 104
+
+// socketDir returns the directory used for the Crush Unix socket.
+// It prefers $XDG_RUNTIME_DIR when set (systemd's per-user runtime
+// directory on Linux), and otherwise falls back to [os.TempDir],
+// which resolves to the per-user private $TMPDIR on macOS and to
+// /tmp on Linux.
+func socketDir() string {
+	if dir := os.Getenv("XDG_RUNTIME_DIR"); dir != "" {
+		return dir
+	}
+	return os.TempDir()
+}
+
 // ErrServerClosed is returned when the server is closed.
 var ErrServerClosed = http.ErrServerClosed
 
@@ -44,6 +63,14 @@ func ParseHostURL(host string) (*url.URL, error) {
 }
 
 // DefaultHost returns the default server host.
+//
+// On Windows the address is a named pipe under \\.\pipe\. On Unix
+// platforms the socket lives in the per-user runtime directory
+// returned by [socketDir] and is named crush-<uid>.sock, falling
+// back to crush.sock when the current uid cannot be determined. If
+// the composed path would exceed [maxUnixSocketPathLen] bytes (the
+// macOS sun_path limit), we fall back to /tmp/crush-<uid>.sock so
+// the socket remains bindable.
 func DefaultHost() string {
 	sock := "crush.sock"
 	usr, err := user.Current()
@@ -53,7 +80,11 @@ func DefaultHost() string {
 	if runtime.GOOS == "windows" {
 		return fmt.Sprintf("npipe:////./pipe/%s", sock)
 	}
-	return fmt.Sprintf("unix:///tmp/%s", sock)
+	path := filepath.Join(socketDir(), sock)
+	if len(path) > maxUnixSocketPathLen {
+		path = filepath.Join("/tmp", sock)
+	}
+	return "unix://" + path
 }
 
 // Server represents a Crush server bound to a specific address.