Repository navigation
Fix intermittent CLI socket-not-found errors for tagged/debug runs #832
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -451,9 +451,159 @@ private enum SocketPasswordResolver { | |||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| private enum CLISocketPathSource { | ||||||||||||||||||||||||||
| case explicitFlag | ||||||||||||||||||||||||||
| case environment | ||||||||||||||||||||||||||
| case implicitDefault | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| private enum CLISocketPathResolver { | ||||||||||||||||||||||||||
| static let defaultSocketPath = "/tmp/cmux.sock" | ||||||||||||||||||||||||||
| private static let fallbackSocketPath = "/tmp/cmux-debug.sock" | ||||||||||||||||||||||||||
| private static let stagingSocketPath = "/tmp/cmux-staging.sock" | ||||||||||||||||||||||||||
| private static let lastSocketPathFile = "/tmp/cmux-last-socket-path" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| static func resolve( | ||||||||||||||||||||||||||
| requestedPath: String, | ||||||||||||||||||||||||||
| source: CLISocketPathSource, | ||||||||||||||||||||||||||
| environment: [String: String] = ProcessInfo.processInfo.environment | ||||||||||||||||||||||||||
| ) -> String { | ||||||||||||||||||||||||||
| guard source == .implicitDefault else { | ||||||||||||||||||||||||||
| return requestedPath | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let candidates = dedupe(candidatePaths(requestedPath: requestedPath, environment: environment)) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Prefer sockets that are currently accepting connections. | ||||||||||||||||||||||||||
| for path in candidates where canConnect(to: path) { | ||||||||||||||||||||||||||
| return path | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // If the listener is still starting, prefer existing socket files. | ||||||||||||||||||||||||||
| for path in candidates where isSocketFile(path) { | ||||||||||||||||||||||||||
| return path | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return requestedPath | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| private static func candidatePaths(requestedPath: String, environment: [String: String]) -> [String] { | ||||||||||||||||||||||||||
| var candidates: [String] = [] | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if let tag = normalized(environment["CMUX_TAG"]) { | ||||||||||||||||||||||||||
| let slug = sanitizeTagSlug(tag) | ||||||||||||||||||||||||||
| candidates.append("/tmp/cmux-debug-\(slug).sock") | ||||||||||||||||||||||||||
| candidates.append("/tmp/cmux-\(slug).sock") | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| candidates.append(requestedPath) | ||||||||||||||||||||||||||
| candidates.append(fallbackSocketPath) | ||||||||||||||||||||||||||
| candidates.append(stagingSocketPath) | ||||||||||||||||||||||||||
| candidates.append(contentsOf: discoverTaggedSockets(limit: 12)) | ||||||||||||||||||||||||||
| if let last = readLastSocketPath() { | ||||||||||||||||||||||||||
| candidates.append(last) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| return candidates | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| private static func readLastSocketPath() -> String? { | ||||||||||||||||||||||||||
| guard let data = try? String(contentsOfFile: lastSocketPathFile, encoding: .utf8) else { | ||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| return normalized(data) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| private static func discoverTaggedSockets(limit: Int) -> [String] { | ||||||||||||||||||||||||||
| guard let entries = try? FileManager.default.contentsOfDirectory(atPath: "/tmp") else { | ||||||||||||||||||||||||||
| return [] | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| var discovered: [(path: String, mtime: TimeInterval)] = [] | ||||||||||||||||||||||||||
| discovered.reserveCapacity(min(limit, entries.count)) | ||||||||||||||||||||||||||
| for name in entries where name.hasPrefix("cmux") && name.hasSuffix(".sock") { | ||||||||||||||||||||||||||
| let path = "/tmp/\(name)" | ||||||||||||||||||||||||||
| var st = stat() | ||||||||||||||||||||||||||
| guard lstat(path, &st) == 0 else { continue } | ||||||||||||||||||||||||||
| guard (st.st_mode & mode_t(S_IFMT)) == mode_t(S_IFSOCK) else { continue } | ||||||||||||||||||||||||||
| if path == defaultSocketPath || path == fallbackSocketPath || path == stagingSocketPath { | ||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| let modified = TimeInterval(st.st_mtimespec.tv_sec) + TimeInterval(st.st_mtimespec.tv_nsec) / 1_000_000_000 | ||||||||||||||||||||||||||
| discovered.append((path: path, mtime: modified)) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| discovered.sort { $0.mtime > $1.mtime } | ||||||||||||||||||||||||||
| return discovered.prefix(limit).map(\.path) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| private static func isSocketFile(_ path: String) -> Bool { | ||||||||||||||||||||||||||
| var st = stat() | ||||||||||||||||||||||||||
| return lstat(path, &st) == 0 && (st.st_mode & mode_t(S_IFMT)) == mode_t(S_IFSOCK) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+539
to
+542
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Socket autodiscovery can pick sockets not owned by the current user, which then fails immediately in Prompt for AI agents
Suggested change
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| private static func canConnect(to path: String) -> Bool { | ||||||||||||||||||||||||||
| guard isSocketFile(path) else { return false } | ||||||||||||||||||||||||||
| let fd = socket(AF_UNIX, SOCK_STREAM, 0) | ||||||||||||||||||||||||||
| guard fd >= 0 else { return false } | ||||||||||||||||||||||||||
| defer { Darwin.close(fd) } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| var addr = sockaddr_un() | ||||||||||||||||||||||||||
| addr.sun_family = sa_family_t(AF_UNIX) | ||||||||||||||||||||||||||
| let maxLength = MemoryLayout.size(ofValue: addr.sun_path) | ||||||||||||||||||||||||||
| path.withCString { ptr in | ||||||||||||||||||||||||||
| withUnsafeMutablePointer(to: &addr.sun_path) { pathPtr in | ||||||||||||||||||||||||||
| let buf = UnsafeMutableRawPointer(pathPtr).assumingMemoryBound(to: CChar.self) | ||||||||||||||||||||||||||
| strncpy(buf, ptr, maxLength - 1) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+553
to
+557
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: head -c 50000 CLI/cmux.swift | tail -c +20000 | wc -lRepository: manaflow-ai/cmux Length of output: 63 🏁 Script executed: wc -l CLI/cmux.swiftRepository: manaflow-ai/cmux Length of output: 79 🏁 Script executed: sed -n '540,570p' CLI/cmux.swiftRepository: manaflow-ai/cmux Length of output: 1317 🏁 Script executed: sed -n '640,670p' CLI/cmux.swiftRepository: manaflow-ai/cmux Length of output: 1389 🏁 Script executed: rg -n -C3 'sockaddr_un|sun_path' CLI/cmux.swiftRepository: manaflow-ai/cmux Length of output: 1820 🏁 Script executed: rg -n 'lengthOfBytes|ENAMETOOLONG|Socket path is too long' CLI/cmux.swiftRepository: manaflow-ai/cmux Length of output: 42 Validate Path bytes are copied with Add a length check before each Proposed fixesLine 553 (canConnect method): let maxLength = MemoryLayout.size(ofValue: addr.sun_path)
+ guard path.lengthOfBytes(using: .utf8) < maxLength else { return false }
path.withCString { ptr inLine 655 (socket connection method): let maxLength = MemoryLayout.size(ofValue: addr.sun_path)
+ guard path.lengthOfBytes(using: .utf8) < maxLength else {
+ throw CLIError(message: "Socket path is too long: \(path)")
+ }
path.withCString { ptr in📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let result = withUnsafePointer(to: &addr) { ptr in | ||||||||||||||||||||||||||
| ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPtr in | ||||||||||||||||||||||||||
| Darwin.connect(fd, sockaddrPtr, socklen_t(MemoryLayout<sockaddr_un>.size)) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| return result == 0 | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| private static func sanitizeTagSlug(_ raw: String) -> String { | ||||||||||||||||||||||||||
| let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() | ||||||||||||||||||||||||||
| let slug = trimmed | ||||||||||||||||||||||||||
| .replacingOccurrences(of: "[^a-z0-9]+", with: "-", options: .regularExpression) | ||||||||||||||||||||||||||
| .replacingOccurrences(of: "-+", with: "-", options: .regularExpression) | ||||||||||||||||||||||||||
| .trimmingCharacters(in: CharacterSet(charactersIn: "-")) | ||||||||||||||||||||||||||
| return slug.isEmpty ? "agent" : slug | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| private static func normalized(_ value: String?) -> String? { | ||||||||||||||||||||||||||
| guard let value else { return nil } | ||||||||||||||||||||||||||
| let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) | ||||||||||||||||||||||||||
| return trimmed.isEmpty ? nil : trimmed | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| private static func dedupe(_ paths: [String]) -> [String] { | ||||||||||||||||||||||||||
| var seen: Set<String> = [] | ||||||||||||||||||||||||||
| var ordered: [String] = [] | ||||||||||||||||||||||||||
| ordered.reserveCapacity(paths.count) | ||||||||||||||||||||||||||
| for path in paths where !path.isEmpty { | ||||||||||||||||||||||||||
| if seen.insert(path).inserted { | ||||||||||||||||||||||||||
| ordered.append(path) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| return ordered | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| final class SocketClient { | ||||||||||||||||||||||||||
| private let path: String | ||||||||||||||||||||||||||
| private var socketFD: Int32 = -1 | ||||||||||||||||||||||||||
| private static let connectRetryWindowSeconds: TimeInterval = 2.0 | ||||||||||||||||||||||||||
| private static let connectRetryIntervalSeconds: TimeInterval = 0.1 | ||||||||||||||||||||||||||
| private static let retriableConnectErrnos: Set<Int32> = [ | ||||||||||||||||||||||||||
| ENOENT, | ||||||||||||||||||||||||||
| ECONNREFUSED, | ||||||||||||||||||||||||||
| EAGAIN, | ||||||||||||||||||||||||||
| EINTR | ||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||
| private static let defaultResponseTimeoutSeconds: TimeInterval = 15.0 | ||||||||||||||||||||||||||
| private static let responseTimeoutSeconds: TimeInterval = { | ||||||||||||||||||||||||||
| let env = ProcessInfo.processInfo.environment | ||||||||||||||||||||||||||
|
|
@@ -472,40 +622,66 @@ final class SocketClient { | |||||||||||||||||||||||||
| func connect() throws { | ||||||||||||||||||||||||||
| if socketFD >= 0 { return } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Verify socket is owned by the current user to prevent fake-socket attacks | ||||||||||||||||||||||||||
| var st = stat() | ||||||||||||||||||||||||||
| guard stat(path, &st) == 0 else { | ||||||||||||||||||||||||||
| throw CLIError(message: "Socket not found at \(path)") | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| guard st.st_uid == getuid() else { | ||||||||||||||||||||||||||
| throw CLIError(message: "Socket at \(path) is not owned by the current user — refusing to connect") | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| let deadline = Date().addingTimeInterval(Self.connectRetryWindowSeconds) | ||||||||||||||||||||||||||
| var lastError: CLIError? | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| socketFD = socket(AF_UNIX, SOCK_STREAM, 0) | ||||||||||||||||||||||||||
| if socketFD < 0 { | ||||||||||||||||||||||||||
| throw CLIError(message: "Failed to create socket") | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| while true { | ||||||||||||||||||||||||||
| // Verify socket is owned by the current user to prevent fake-socket attacks. | ||||||||||||||||||||||||||
| var st = stat() | ||||||||||||||||||||||||||
| guard stat(path, &st) == 0 else { | ||||||||||||||||||||||||||
| let error = CLIError(message: "Socket not found at \(path)") | ||||||||||||||||||||||||||
| lastError = error | ||||||||||||||||||||||||||
| if errno == ENOENT, Date() < deadline { | ||||||||||||||||||||||||||
| Thread.sleep(forTimeInterval: Self.connectRetryIntervalSeconds) | ||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| throw error | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| guard (st.st_mode & mode_t(S_IFMT)) == mode_t(S_IFSOCK) else { | ||||||||||||||||||||||||||
| throw CLIError(message: "Path exists at \(path) but is not a Unix socket") | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| guard st.st_uid == getuid() else { | ||||||||||||||||||||||||||
| throw CLIError(message: "Socket at \(path) is not owned by the current user — refusing to connect") | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| var addr = sockaddr_un() | ||||||||||||||||||||||||||
| addr.sun_family = sa_family_t(AF_UNIX) | ||||||||||||||||||||||||||
| let maxLength = MemoryLayout.size(ofValue: addr.sun_path) | ||||||||||||||||||||||||||
| path.withCString { ptr in | ||||||||||||||||||||||||||
| withUnsafeMutablePointer(to: &addr.sun_path) { pathPtr in | ||||||||||||||||||||||||||
| let buf = UnsafeMutableRawPointer(pathPtr).assumingMemoryBound(to: CChar.self) | ||||||||||||||||||||||||||
| strncpy(buf, ptr, maxLength - 1) | ||||||||||||||||||||||||||
| socketFD = socket(AF_UNIX, SOCK_STREAM, 0) | ||||||||||||||||||||||||||
| if socketFD < 0 { | ||||||||||||||||||||||||||
| throw CLIError(message: "Failed to create socket") | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let result = withUnsafePointer(to: &addr) { ptr in | ||||||||||||||||||||||||||
| ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPtr in | ||||||||||||||||||||||||||
| Darwin.connect(socketFD, sockaddrPtr, socklen_t(MemoryLayout<sockaddr_un>.size)) | ||||||||||||||||||||||||||
| var addr = sockaddr_un() | ||||||||||||||||||||||||||
| addr.sun_family = sa_family_t(AF_UNIX) | ||||||||||||||||||||||||||
| let maxLength = MemoryLayout.size(ofValue: addr.sun_path) | ||||||||||||||||||||||||||
| path.withCString { ptr in | ||||||||||||||||||||||||||
| withUnsafeMutablePointer(to: &addr.sun_path) { pathPtr in | ||||||||||||||||||||||||||
| let buf = UnsafeMutableRawPointer(pathPtr).assumingMemoryBound(to: CChar.self) | ||||||||||||||||||||||||||
| strncpy(buf, ptr, maxLength - 1) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| if result != 0 { | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let result = withUnsafePointer(to: &addr) { ptr in | ||||||||||||||||||||||||||
| ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPtr in | ||||||||||||||||||||||||||
| Darwin.connect(socketFD, sockaddrPtr, socklen_t(MemoryLayout<sockaddr_un>.size)) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| if result == 0 { | ||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let connectErrno = errno | ||||||||||||||||||||||||||
| Darwin.close(socketFD) | ||||||||||||||||||||||||||
| socketFD = -1 | ||||||||||||||||||||||||||
| throw CLIError(message: "Failed to connect to socket at \(path)") | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let error = CLIError(message: "Failed to connect to socket at \(path)") | ||||||||||||||||||||||||||
| lastError = error | ||||||||||||||||||||||||||
| if Self.retriableConnectErrnos.contains(connectErrno), Date() < deadline { | ||||||||||||||||||||||||||
| Thread.sleep(forTimeInterval: Self.connectRetryIntervalSeconds) | ||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| throw error | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| throw lastError ?? CLIError(message: "Failed to connect to socket at \(path)") | ||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unreachable code - the |
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| func close() { | ||||||||||||||||||||||||||
|
|
@@ -614,7 +790,19 @@ struct CMUXCLI { | |||||||||||||||||||||||||
| let args: [String] | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| func run() throws { | ||||||||||||||||||||||||||
| var socketPath = ProcessInfo.processInfo.environment["CMUX_SOCKET_PATH"] ?? "/tmp/cmux.sock" | ||||||||||||||||||||||||||
| let processEnv = ProcessInfo.processInfo.environment | ||||||||||||||||||||||||||
| let envSocketPath: String? = { | ||||||||||||||||||||||||||
| guard let raw = processEnv["CMUX_SOCKET_PATH"] else { return nil } | ||||||||||||||||||||||||||
| let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) | ||||||||||||||||||||||||||
| return trimmed.isEmpty ? nil : trimmed | ||||||||||||||||||||||||||
| }() | ||||||||||||||||||||||||||
| var socketPath = envSocketPath ?? CLISocketPathResolver.defaultSocketPath | ||||||||||||||||||||||||||
| var socketPathSource: CLISocketPathSource | ||||||||||||||||||||||||||
| if let envSocketPath { | ||||||||||||||||||||||||||
| socketPathSource = envSocketPath == CLISocketPathResolver.defaultSocketPath ? .implicitDefault : .environment | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| socketPathSource = .implicitDefault | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| var jsonOutput = false | ||||||||||||||||||||||||||
| var idFormatArg: String? = nil | ||||||||||||||||||||||||||
| var windowId: String? = nil | ||||||||||||||||||||||||||
|
|
@@ -628,6 +816,7 @@ struct CMUXCLI { | |||||||||||||||||||||||||
| throw CLIError(message: "--socket requires a path") | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| socketPath = args[index + 1] | ||||||||||||||||||||||||||
| socketPathSource = .explicitFlag | ||||||||||||||||||||||||||
| index += 2 | ||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
@@ -682,7 +871,12 @@ struct CMUXCLI { | |||||||||||||||||||||||||
| command: command, | ||||||||||||||||||||||||||
| commandArgs: commandArgs, | ||||||||||||||||||||||||||
| socketPath: socketPath, | ||||||||||||||||||||||||||
| processEnv: ProcessInfo.processInfo.environment | ||||||||||||||||||||||||||
| processEnv: processEnv | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| let resolvedSocketPath = CLISocketPathResolver.resolve( | ||||||||||||||||||||||||||
| requestedPath: socketPath, | ||||||||||||||||||||||||||
| source: socketPathSource, | ||||||||||||||||||||||||||
| environment: processEnv | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if command == "version" { | ||||||||||||||||||||||||||
|
|
@@ -692,7 +886,7 @@ struct CMUXCLI { | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // If the argument looks like a path (not a known command), open a workspace there. | ||||||||||||||||||||||||||
| if looksLikePath(command) { | ||||||||||||||||||||||||||
| try openPath(command, socketPath: socketPath) | ||||||||||||||||||||||||||
| try openPath(command, socketPath: resolvedSocketPath) | ||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -706,16 +900,28 @@ struct CMUXCLI { | |||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let client = SocketClient(path: socketPath) | ||||||||||||||||||||||||||
| let client = SocketClient(path: resolvedSocketPath) | ||||||||||||||||||||||||||
| if resolvedSocketPath != socketPath { | ||||||||||||||||||||||||||
| cliTelemetry.breadcrumb( | ||||||||||||||||||||||||||
| "socket.path.autodiscovered", | ||||||||||||||||||||||||||
| data: [ | ||||||||||||||||||||||||||
| "requested_path": socketPath, | ||||||||||||||||||||||||||
| "resolved_path": resolvedSocketPath | ||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| cliTelemetry.breadcrumb( | ||||||||||||||||||||||||||
| "socket.connect.attempt", | ||||||||||||||||||||||||||
| data: ["command": command] | ||||||||||||||||||||||||||
| data: [ | ||||||||||||||||||||||||||
| "command": command, | ||||||||||||||||||||||||||
| "path": resolvedSocketPath | ||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| do { | ||||||||||||||||||||||||||
| try client.connect() | ||||||||||||||||||||||||||
| cliTelemetry.breadcrumb("socket.connect.success") | ||||||||||||||||||||||||||
| cliTelemetry.breadcrumb("socket.connect.success", data: ["path": resolvedSocketPath]) | ||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||
| cliTelemetry.breadcrumb("socket.connect.failure") | ||||||||||||||||||||||||||
| cliTelemetry.breadcrumb("socket.connect.failure", data: ["path": resolvedSocketPath]) | ||||||||||||||||||||||||||
| cliTelemetry.captureError(stage: "socket_connect", error: error) | ||||||||||||||||||||||||||
| throw error | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
@@ -6470,7 +6676,8 @@ struct CMUXCLI { | |||||||||||||||||||||||||
| ALL commands (send, list-panels, new-split, notify, etc.). | ||||||||||||||||||||||||||
| CMUX_TAB_ID Optional alias used by `tab-action`/`rename-tab` as default --tab. | ||||||||||||||||||||||||||
| CMUX_SURFACE_ID Auto-set in cmux terminals. Used as default --surface. | ||||||||||||||||||||||||||
| CMUX_SOCKET_PATH Override the default Unix socket path (/tmp/cmux.sock). | ||||||||||||||||||||||||||
| CMUX_SOCKET_PATH Override the Unix socket path. Without this, the CLI defaults | ||||||||||||||||||||||||||
| to /tmp/cmux.sock and auto-discovers tagged/debug sockets. | ||||||||||||||||||||||||||
| CMUX_CLI_SENTRY_DISABLED | ||||||||||||||||||||||||||
| Set to 1 to disable CLI Sentry socket diagnostics. | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CMUX_TAGcan still route commands to the wrong instance during startup race.With
CMUX_TAGpresent (Lines 493-497), Line 478 still returns the first connectable socket across all candidates. If the tagged socket is not yet accepting connections but/tmp/cmux.sockis, the CLI can connect to the wrong instance.🔧 Proposed fix
static func resolve( requestedPath: String, source: CLISocketPathSource, environment: [String: String] = ProcessInfo.processInfo.environment ) -> String { guard source == .implicitDefault else { return requestedPath } - let candidates = dedupe(candidatePaths(requestedPath: requestedPath, environment: environment)) + let tagged = dedupe(taggedCandidatePaths(environment: environment)) + let candidates = dedupe(candidatePaths(requestedPath: requestedPath, environment: environment)) + // When CMUX_TAG is set, prefer tagged sockets first (including startup race fallback). + if !tagged.isEmpty { + for path in tagged where canConnect(to: path) { return path } + for path in tagged where isSocketFile(path) { return path } + } + // Prefer sockets that are currently accepting connections. for path in candidates where canConnect(to: path) { return path } @@ } + + private static func taggedCandidatePaths(environment: [String: String]) -> [String] { + guard let tag = normalized(environment["CMUX_TAG"]) else { return [] } + let slug = sanitizeTagSlug(tag) + return [ + "/tmp/cmux-debug-\(slug).sock", + "/tmp/cmux-\(slug).sock" + ] + }Also applies to: 493-497
🤖 Prompt for AI Agents