fix(daemon): complete socket path shortening for long workspace paths (GH#1001) (#1008)
fix(daemon): socket path shortening for long workspace paths Fixes GH#1001 where long workspace paths (e.g., pytest temp directories) caused socket path mismatches. The daemon now uses rpc.ShortSocketPath() consistently with the client. Changes: - daemon.go: Use rpc.ShortSocketPath() + EnsureSocketDir() for daemon socket - daemon_config.go: Update getSocketPathForPID() to use rpc.ShortSocketPath() - Added tests for long path handling and client-daemon socket path agreement Co-Authored-By: Eugene Sukhodolin <sukhodolin@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
89b5f8e203
commit
a731f5a48f
@@ -478,7 +478,12 @@ func runDaemonLoop(interval time.Duration, autoCommit, autoPush, autoPull, local
|
||||
// Get workspace path (.beads directory) - beadsDir already defined above
|
||||
// Get actual workspace root (parent of .beads)
|
||||
workspacePath := filepath.Dir(beadsDir)
|
||||
socketPath := filepath.Join(beadsDir, "bd.sock")
|
||||
// Use short socket path to avoid Unix socket path length limits (macOS: 104 chars)
|
||||
socketPath, err := rpc.EnsureSocketDir(rpc.ShortSocketPath(workspacePath))
|
||||
if err != nil {
|
||||
log.Error("failed to create socket directory", "error", err)
|
||||
return
|
||||
}
|
||||
serverCtx, serverCancel := context.WithCancel(ctx)
|
||||
defer serverCancel()
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/steveyegge/beads/internal/beads"
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
)
|
||||
|
||||
// ensureBeadsDir ensures the local beads directory exists (.beads in the current workspace)
|
||||
@@ -60,13 +61,16 @@ func getEnvBool(key string, defaultValue bool) bool {
|
||||
|
||||
// getSocketPathForPID determines the socket path for a given PID file.
|
||||
// If BD_SOCKET env var is set, uses that value instead.
|
||||
// Uses rpc.ShortSocketPath to avoid Unix socket path length limits (macOS: 104 chars).
|
||||
func getSocketPathForPID(pidFile string) string {
|
||||
// Check environment variable first (enables test isolation)
|
||||
if socketPath := os.Getenv("BD_SOCKET"); socketPath != "" {
|
||||
return socketPath
|
||||
}
|
||||
// Socket is in same directory as PID file
|
||||
return filepath.Join(filepath.Dir(pidFile), "bd.sock")
|
||||
// PID file is in .beads/, so workspace is parent of that
|
||||
beadsDir := filepath.Dir(pidFile)
|
||||
workspacePath := filepath.Dir(beadsDir)
|
||||
return rpc.ShortSocketPath(workspacePath)
|
||||
}
|
||||
|
||||
// getPIDFilePath returns the path to the daemon PID file
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
)
|
||||
|
||||
// TestSocketPathEnvOverride verifies that BD_SOCKET env var overrides default socket path.
|
||||
@@ -53,6 +55,77 @@ func TestSocketPathDefaultBehavior(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSocketPathForPIDLongPath verifies that long workspace paths use shortened socket paths.
|
||||
// This fixes GH#1001 where pytest temp directories exceeded macOS's 104-byte socket path limit.
|
||||
func TestSocketPathForPIDLongPath(t *testing.T) {
|
||||
t.Setenv("BD_SOCKET", "")
|
||||
|
||||
// Create a path that would exceed the 103-byte limit when .beads/bd.sock is appended
|
||||
// /long/path/.beads/daemon.pid -> workspace is /long/path
|
||||
// socket would be /long/path/.beads/bd.sock
|
||||
longWorkspace := "/" + strings.Repeat("a", 90) // 91 bytes
|
||||
pidFile := filepath.Join(longWorkspace, ".beads", "daemon.pid")
|
||||
|
||||
got := getSocketPathForPID(pidFile)
|
||||
|
||||
// Should NOT be the natural path (which would be too long)
|
||||
naturalPath := filepath.Join(longWorkspace, ".beads", "bd.sock")
|
||||
if got == naturalPath {
|
||||
t.Errorf("getSocketPathForPID should use short path for long workspaces, got natural path %q (%d bytes)",
|
||||
got, len(got))
|
||||
}
|
||||
|
||||
// Should be in /tmp/beads-{hash}/
|
||||
if !strings.HasPrefix(got, "/tmp/beads-") {
|
||||
t.Errorf("getSocketPathForPID(%q) = %q, want path starting with /tmp/beads-", pidFile, got)
|
||||
}
|
||||
|
||||
// Should end with bd.sock
|
||||
if !strings.HasSuffix(got, "/bd.sock") {
|
||||
t.Errorf("getSocketPathForPID(%q) = %q, want path ending with /bd.sock", pidFile, got)
|
||||
}
|
||||
|
||||
// Should be under the limit
|
||||
if len(got) > 103 {
|
||||
t.Errorf("getSocketPathForPID returned path of %d bytes, want <= 103", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
// TestSocketPathForPIDClientDaemonAgreement verifies that getSocketPathForPID
|
||||
// returns the same path as rpc.ShortSocketPath for the same workspace.
|
||||
// This is critical - if they disagree, the daemon listens on one path while
|
||||
// the client tries to connect to another, causing connection failures.
|
||||
// This test caught the GH#1001 bug where daemon.go used filepath.Join directly
|
||||
// instead of rpc.ShortSocketPath.
|
||||
func TestSocketPathForPIDClientDaemonAgreement(t *testing.T) {
|
||||
t.Setenv("BD_SOCKET", "")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
workspacePath string
|
||||
}{
|
||||
{"short_path", "/home/user/project"},
|
||||
{"medium_path", "/Users/testuser/Documents/projects/myapp"},
|
||||
{"long_path", "/" + strings.Repeat("a", 90)}, // Forces short socket path
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// What getSocketPathForPID returns (used by daemon operations)
|
||||
pidFile := filepath.Join(tt.workspacePath, ".beads", "daemon.pid")
|
||||
fromPID := getSocketPathForPID(pidFile)
|
||||
|
||||
// What rpc.ShortSocketPath returns (used by client via getSocketPath)
|
||||
fromRPC := rpc.ShortSocketPath(tt.workspacePath)
|
||||
|
||||
if fromPID != fromRPC {
|
||||
t.Errorf("socket path mismatch for workspace %q:\n getSocketPathForPID: %q\n rpc.ShortSocketPath: %q",
|
||||
tt.workspacePath, fromPID, fromRPC)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDaemonSocketIsolation demonstrates that two test instances can use different sockets.
|
||||
// This is the key pattern for parallel test isolation.
|
||||
func TestDaemonSocketIsolation(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user