diff --git a/go.mod b/go.mod index 4b2ee96..a9f84de 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/Use-Tusk/tusk-drift-cli go 1.25.0 require ( - github.com/Use-Tusk/fence v0.1.35 + github.com/Use-Tusk/fence v0.1.36 github.com/Use-Tusk/tusk-drift-schemas v0.1.30 github.com/agnivade/levenshtein v1.0.3 github.com/aymanbagabas/go-osc52/v2 v2.0.1 diff --git a/go.sum b/go.sum index 080376a..7c7ca77 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/Use-Tusk/fence v0.1.35 h1:gnicM/foE/gumn8D5e5qdQCtCsHUaUY5pWkVGX2ICMw= -github.com/Use-Tusk/fence v0.1.35/go.mod h1:YkowBDzXioVKJE16vg9z3gSVC6vhzkIZZw2dFf7MW/o= +github.com/Use-Tusk/fence v0.1.36 h1:8S15y8cp3X+xXukx6AN0Ky/aX9/dZyW3fLw5XOQ8YtE= +github.com/Use-Tusk/fence v0.1.36/go.mod h1:YkowBDzXioVKJE16vg9z3gSVC6vhzkIZZw2dFf7MW/o= github.com/Use-Tusk/tusk-drift-schemas v0.1.30 h1:A45pJ/Za6BLIfTLF53BhuzKHHSJ9L7dXEisnuKT5dTc= github.com/Use-Tusk/tusk-drift-schemas v0.1.30/go.mod h1:pa3EvTj9kKxl9f904RVFkj9YK1zB75QogboKi70zalM= github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0= diff --git a/internal/runner/server.go b/internal/runner/server.go index 0123e26..cfc37e8 100644 --- a/internal/runner/server.go +++ b/internal/runner/server.go @@ -33,8 +33,11 @@ import ( type CommunicationType string const ( - CommunicationUnix CommunicationType = "unix" - CommunicationTCP CommunicationType = "tcp" + CommunicationUnix CommunicationType = "unix" + CommunicationTCP CommunicationType = "tcp" + unixSocketDirName string = ".tusk" + unixSocketName string = ".s" + fallbackSocketName string = ".t.sock" ) // Server handles Unix socket communication with the SDK @@ -197,16 +200,35 @@ func (ms *Server) GetAnalyticsClient() *analytics.Client { } func (ms *Server) startUnix() error { - ms.socketPath = filepath.Join(os.TempDir(), "tusk-connect.sock") - _ = os.Remove(ms.socketPath) - - listener, err := net.Listen("unix", ms.socketPath) + cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("failed to create Unix socket listener: %w", err) + return fmt.Errorf("failed to determine working directory for Unix socket: %w", err) } + candidates := unixSocketCandidates(cwd) - ms.listener = listener - log.Debug("Mock server started with Unix socket", "socket", ms.socketPath) + var listenErrs []string + for _, candidate := range candidates { + if err := os.MkdirAll(filepath.Dir(candidate), 0o750); err != nil { + listenErrs = append(listenErrs, fmt.Sprintf("%s: create parent dir: %v", candidate, err)) + continue + } + _ = os.Remove(candidate) + + listener, err := net.Listen("unix", candidate) + if err != nil { + listenErrs = append(listenErrs, fmt.Sprintf("%s: %v", candidate, err)) + continue + } + + ms.socketPath = candidate + ms.listener = listener + log.Debug("Mock server started with Unix socket", "socket", ms.socketPath) + break + } + + if ms.listener == nil { + return fmt.Errorf("failed to create Unix socket listener: %s", strings.Join(listenErrs, "; ")) + } // Verify the socket file exists and is accessible if _, err := os.Stat(ms.socketPath); err != nil { @@ -225,6 +247,42 @@ func (ms *Server) startUnix() error { return nil } +func unixSocketCandidates(cwd string) []string { + candidates := []string{ + filepath.Join(cwd, unixSocketDirName, unixSocketName), + filepath.Join(cwd, fallbackSocketName), + } + + shortFallbackName := unixSocketShortFallbackName(cwd) + for dir := filepath.Dir(cwd); ; dir = filepath.Dir(dir) { + candidates = append(candidates, filepath.Join(dir, shortFallbackName)) + parent := filepath.Dir(dir) + if parent == dir { + break + } + } + + seen := make(map[string]struct{}, len(candidates)) + deduped := make([]string, 0, len(candidates)) + for _, candidate := range candidates { + if _, ok := seen[candidate]; ok { + continue + } + seen[candidate] = struct{}{} + deduped = append(deduped, candidate) + } + + return deduped +} + +func unixSocketShortFallbackName(cwd string) string { + hash := utils.GenerateDeterministicHash(cwd) + if len(hash) > 12 { + hash = hash[:12] + } + return ".t-" + hash +} + func (ms *Server) startTCP() error { addr := fmt.Sprintf("0.0.0.0:%d", ms.tcpPort) listener, err := net.Listen("tcp", addr) diff --git a/internal/runner/server_test.go b/internal/runner/server_test.go index cfb4990..93e27e7 100644 --- a/internal/runner/server_test.go +++ b/internal/runner/server_test.go @@ -1,6 +1,9 @@ package runner import ( + "os" + "path/filepath" + "strings" "testing" "time" @@ -127,7 +130,58 @@ func TestServerUnixMode(t *testing.T) { socketPath, tcpPort := server.GetConnectionInfo() assert.NotEmpty(t, socketPath, "Unix mode should have socket path") assert.Equal(t, 0, tcpPort, "Unix mode should have zero TCP port") - assert.Contains(t, socketPath, "tusk-connect.sock") + assert.Contains(t, socketPath, filepath.Join(unixSocketDirName, unixSocketName)) +} + +func TestServerUnixMode_FallsBackForLongPaths(t *testing.T) { + config.Invalidate() + + testServiceConfig := &config.ServiceConfig{ + ID: "test-unix-service-long-path", + Port: 3000, + Start: config.StartConfig{ + Command: "npm run dev", + }, + Communication: config.CommunicationConfig{ + Type: "unix", + TCPPort: 9001, + }, + } + + tempHome := t.TempDir() + t.Setenv("HOME", tempHome) + + baseDir := t.TempDir() + deepDir := baseDir + for len(filepath.Join(deepDir, unixSocketDirName, unixSocketName)) <= 140 { + deepDir = filepath.Join(deepDir, "nested-segment-1234567890") + } + require.NoError(t, os.MkdirAll(deepDir, 0o750)) + + originalWD, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(deepDir)) + t.Cleanup(func() { + require.NoError(t, os.Chdir(originalWD)) + }) + workingDir, err := os.Getwd() + require.NoError(t, err) + + server, err := NewServer("test-unix-service-long-path", testServiceConfig) + require.NoError(t, err) + t.Cleanup(func() { _ = server.Stop() }) + + err = server.Start() + require.NoError(t, err) + + socketPath, tcpPort := server.GetConnectionInfo() + assert.NotEmpty(t, socketPath, "Unix mode should have socket path") + assert.Equal(t, 0, tcpPort, "Unix mode should have zero TCP port") + assert.NotEqual(t, filepath.Join(workingDir, unixSocketDirName, unixSocketName), socketPath) + assert.NotEqual(t, filepath.Join(workingDir, fallbackSocketName), socketPath) + assert.Contains(t, unixSocketCandidates(workingDir), socketPath) + assert.True(t, strings.HasPrefix(filepath.Base(socketPath), ".t-"), "expected fallback to use the short ancestor socket name: %s", socketPath) + assert.Less(t, len(socketPath), len(filepath.Join(workingDir, fallbackSocketName)), "expected fallback to shorten the socket path: %s", socketPath) } func TestDetermineCommunicationType(t *testing.T) {