Fallback stable socket listener to a user-scoped path (#1351)
* Fallback stable socket listener to user socket path * Move stable socket path out of /tmp * Keep socket health checks active on fallback paths --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
This commit is contained in:
parent
126c6c6e56
commit
8a0934b801
13 changed files with 479 additions and 83 deletions
|
|
@ -116,12 +116,12 @@ private final class CLISocketSentryTelemetry {
|
|||
context["socket_errno_description"] = String(cString: strerror(code))
|
||||
}
|
||||
|
||||
let tmpSockets = Self.discoverTmpCmuxSockets(limit: 10)
|
||||
let tmpSockets = Self.discoverSockets(in: "/tmp", limit: 10)
|
||||
if !tmpSockets.isEmpty {
|
||||
context["tmp_cmux_sockets"] = tmpSockets
|
||||
}
|
||||
let taggedSockets = tmpSockets.filter { $0 != "/tmp/cmux.sock" }
|
||||
if socketPath == "/tmp/cmux.sock",
|
||||
let taggedSockets = tmpSockets.filter { $0 != CLISocketPathResolver.legacyDefaultSocketPath }
|
||||
if CLISocketPathResolver.isImplicitDefaultPath(socketPath),
|
||||
(envSocketPath?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true),
|
||||
!taggedSockets.isEmpty {
|
||||
context["possible_root_cause"] = "CMUX_SOCKET_PATH/CMUX_SOCKET missing while tagged sockets exist"
|
||||
|
|
@ -145,14 +145,16 @@ private final class CLISocketSentryTelemetry {
|
|||
}
|
||||
}
|
||||
|
||||
private static func discoverTmpCmuxSockets(limit: Int) -> [String] {
|
||||
guard let entries = try? FileManager.default.contentsOfDirectory(atPath: "/tmp") else {
|
||||
private static func discoverSockets(in directory: String, limit: Int) -> [String] {
|
||||
guard let entries = try? FileManager.default.contentsOfDirectory(atPath: directory) else {
|
||||
return []
|
||||
}
|
||||
var sockets: [String] = []
|
||||
for name in entries.sorted() {
|
||||
guard name.hasPrefix("cmux"), name.hasSuffix(".sock") else { continue }
|
||||
let fullPath = "/tmp/\(name)"
|
||||
let fullPath = URL(fileURLWithPath: directory)
|
||||
.appendingPathComponent(name, isDirectory: false)
|
||||
.path
|
||||
var st = stat()
|
||||
guard lstat(fullPath, &st) == 0 else { continue }
|
||||
guard (st.st_mode & mode_t(S_IFMT)) == mode_t(S_IFSOCK) else { continue }
|
||||
|
|
@ -458,10 +460,24 @@ private enum CLISocketPathSource {
|
|||
}
|
||||
|
||||
private enum CLISocketPathResolver {
|
||||
static let defaultSocketPath = "/tmp/cmux.sock"
|
||||
private static let appSupportDirectoryName = "cmux"
|
||||
private static let stableSocketFileName = "cmux.sock"
|
||||
private static let lastSocketPathFileName = "last-socket-path"
|
||||
static let legacyDefaultSocketPath = "/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"
|
||||
private static let legacyLastSocketPathFile = "/tmp/cmux-last-socket-path"
|
||||
|
||||
static var defaultSocketPath: String {
|
||||
let stablePath: String? = stableSocketDirectoryURL()?
|
||||
.appendingPathComponent(stableSocketFileName, isDirectory: false)
|
||||
.path
|
||||
return stablePath ?? legacyDefaultSocketPath
|
||||
}
|
||||
|
||||
static func isImplicitDefaultPath(_ path: String) -> Bool {
|
||||
path == defaultSocketPath || path == legacyDefaultSocketPath
|
||||
}
|
||||
|
||||
static func resolve(
|
||||
requestedPath: String,
|
||||
|
|
@ -497,6 +513,8 @@ private enum CLISocketPathResolver {
|
|||
}
|
||||
|
||||
candidates.append(requestedPath)
|
||||
candidates.append(defaultSocketPath)
|
||||
candidates.append(legacyDefaultSocketPath)
|
||||
candidates.append(fallbackSocketPath)
|
||||
candidates.append(stagingSocketPath)
|
||||
candidates.append(contentsOf: discoverTaggedSockets(limit: 12))
|
||||
|
|
@ -507,33 +525,46 @@ private enum CLISocketPathResolver {
|
|||
}
|
||||
|
||||
private static func readLastSocketPath() -> String? {
|
||||
guard let data = try? String(contentsOfFile: lastSocketPathFile, encoding: .utf8) else {
|
||||
return nil
|
||||
let primaryCandidate: String? = stableSocketDirectoryURL()?
|
||||
.appendingPathComponent(lastSocketPathFileName, isDirectory: false)
|
||||
.path
|
||||
let candidates = [primaryCandidate, legacyLastSocketPathFile].compactMap { $0 }
|
||||
|
||||
for candidate in candidates {
|
||||
guard let data = try? String(contentsOfFile: candidate, encoding: .utf8) else {
|
||||
continue
|
||||
}
|
||||
return normalized(data)
|
||||
if let value = normalized(data) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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 directory in socketDiscoveryDirectories() {
|
||||
guard let entries = try? FileManager.default.contentsOfDirectory(atPath: directory) else {
|
||||
continue
|
||||
}
|
||||
discovered.reserveCapacity(min(limit, discovered.count + entries.count))
|
||||
for name in entries where name.hasPrefix("cmux") && name.hasSuffix(".sock") {
|
||||
let path = "/tmp/\(name)"
|
||||
let path = URL(fileURLWithPath: directory)
|
||||
.appendingPathComponent(name, isDirectory: false)
|
||||
.path
|
||||
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 {
|
||||
if path == defaultSocketPath || path == legacyDefaultSocketPath || 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)
|
||||
return dedupe(discovered.prefix(limit).map(\.path))
|
||||
}
|
||||
|
||||
private static func isSocketFile(_ path: String) -> Bool {
|
||||
|
|
@ -580,6 +611,21 @@ private enum CLISocketPathResolver {
|
|||
return trimmed.isEmpty ? nil : trimmed
|
||||
}
|
||||
|
||||
private static func stableSocketDirectoryURL() -> URL? {
|
||||
guard let appSupportDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
|
||||
return nil
|
||||
}
|
||||
return appSupportDirectory.appendingPathComponent(appSupportDirectoryName, isDirectory: true)
|
||||
}
|
||||
|
||||
private static func socketDiscoveryDirectories() -> [String] {
|
||||
let appSupportSocketDirectory: String = stableSocketDirectoryURL()?.path ?? ""
|
||||
return dedupe([
|
||||
"/tmp",
|
||||
appSupportSocketDirectory,
|
||||
])
|
||||
}
|
||||
|
||||
private static func dedupe(_ paths: [String]) -> [String] {
|
||||
var seen: Set<String> = []
|
||||
var ordered: [String] = []
|
||||
|
|
@ -806,7 +852,7 @@ struct CMUXCLI {
|
|||
var socketPath = envSocketPath ?? CLISocketPathResolver.defaultSocketPath
|
||||
var socketPathSource: CLISocketPathSource
|
||||
if let envSocketPath {
|
||||
socketPathSource = envSocketPath == CLISocketPathResolver.defaultSocketPath ? .implicitDefault : .environment
|
||||
socketPathSource = CLISocketPathResolver.isImplicitDefaultPath(envSocketPath) ? .implicitDefault : .environment
|
||||
} else {
|
||||
socketPathSource = .implicitDefault
|
||||
}
|
||||
|
|
@ -7312,7 +7358,7 @@ struct CMUXCLI {
|
|||
let requestedSocketPath = envSocketPath ?? CLISocketPathResolver.defaultSocketPath
|
||||
let source: CLISocketPathSource
|
||||
if let envSocketPath {
|
||||
source = envSocketPath == CLISocketPathResolver.defaultSocketPath ? .implicitDefault : .environment
|
||||
source = CLISocketPathResolver.isImplicitDefaultPath(envSocketPath) ? .implicitDefault : .environment
|
||||
} else {
|
||||
source = .implicitDefault
|
||||
}
|
||||
|
|
@ -9277,7 +9323,7 @@ struct CMUXCLI {
|
|||
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 Unix socket path. Without this, the CLI defaults
|
||||
to /tmp/cmux.sock and auto-discovers tagged/debug sockets.
|
||||
to ~/Library/Application Support/cmux/cmux.sock and auto-discovers tagged/debug sockets.
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2946,13 +2946,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
private func restartSocketListenerIfEnabled(source: String) {
|
||||
guard let tabManager,
|
||||
let config = socketListenerConfigurationIfEnabled() else { return }
|
||||
let restartPath = TerminalController.shared.activeSocketPath(preferredPath: config.path)
|
||||
sentryBreadcrumb("socket.listener.restart", category: "socket", data: [
|
||||
"mode": config.mode.rawValue,
|
||||
"path": config.path,
|
||||
"path": restartPath,
|
||||
"source": source
|
||||
])
|
||||
TerminalController.shared.stop()
|
||||
TerminalController.shared.start(tabManager: tabManager, socketPath: config.path, accessMode: config.mode)
|
||||
TerminalController.shared.start(tabManager: tabManager, socketPath: restartPath, accessMode: config.mode)
|
||||
}
|
||||
|
||||
private func startSocketListenerHealthMonitorIfNeeded() {
|
||||
|
|
@ -2980,8 +2981,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
private func restartSocketListenerIfNeededForHealthCheck(source: String) {
|
||||
guard !socketListenerHealthCheckInFlight,
|
||||
let config = socketListenerConfigurationIfEnabled() else { return }
|
||||
let expectedSocketPath = config.path
|
||||
let terminalController = TerminalController.shared
|
||||
let expectedSocketPath = terminalController.activeSocketPath(preferredPath: config.path)
|
||||
socketListenerHealthCheckInFlight = true
|
||||
Thread.detachNewThread { [weak self, expectedSocketPath, source, terminalController] in
|
||||
let health = terminalController.socketListenerHealth(expectedSocketPath: expectedSocketPath)
|
||||
|
|
@ -3002,8 +3003,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
source: String,
|
||||
expectedSocketPath: String
|
||||
) {
|
||||
guard let config = socketListenerConfigurationIfEnabled(),
|
||||
config.path == expectedSocketPath else { return }
|
||||
guard let config = socketListenerConfigurationIfEnabled() else { return }
|
||||
let currentExpectedSocketPath = TerminalController.shared.activeSocketPath(preferredPath: config.path)
|
||||
guard currentExpectedSocketPath == expectedSocketPath else { return }
|
||||
guard !health.isHealthy else {
|
||||
lastSocketListenerUnhealthyCaptureAt = .distantPast
|
||||
return
|
||||
|
|
@ -3011,7 +3013,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
let failureSignals = health.failureSignals
|
||||
var data: [String: Any] = [
|
||||
"source": source,
|
||||
"path": config.path,
|
||||
"path": currentExpectedSocketPath,
|
||||
"isRunning": health.isRunning ? 1 : 0,
|
||||
"acceptLoopAlive": health.acceptLoopAlive ? 1 : 0,
|
||||
"socketPathMatches": health.socketPathMatches ? 1 : 0,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import Darwin
|
||||
import Foundation
|
||||
#if canImport(Security)
|
||||
import Security
|
||||
|
|
@ -292,6 +293,26 @@ struct SocketControlSettings {
|
|||
static let socketPasswordEnvKey = "CMUX_SOCKET_PASSWORD"
|
||||
static let launchTagEnvKey = "CMUX_TAG"
|
||||
static let baseDebugBundleIdentifier = "com.cmuxterm.app.debug"
|
||||
private static let socketDirectoryName = "cmux"
|
||||
private static let stableSocketFileName = "cmux.sock"
|
||||
private static let lastSocketPathFileName = "last-socket-path"
|
||||
static let legacyStableDefaultSocketPath = "/tmp/cmux.sock"
|
||||
static let legacyLastSocketPathFile = "/tmp/cmux-last-socket-path"
|
||||
|
||||
static var stableDefaultSocketPath: String {
|
||||
stableSocketFileURL()?.path ?? legacyStableDefaultSocketPath
|
||||
}
|
||||
|
||||
static var lastSocketPathFile: String {
|
||||
lastSocketPathFileURL()?.path ?? legacyLastSocketPathFile
|
||||
}
|
||||
|
||||
enum StableDefaultSocketPathEntry: Equatable {
|
||||
case missing
|
||||
case socket(ownerUserID: uid_t)
|
||||
case other(ownerUserID: uid_t)
|
||||
case inaccessible(errnoCode: Int32)
|
||||
}
|
||||
|
||||
private static func normalizeMode(_ raw: String) -> String {
|
||||
raw
|
||||
|
|
@ -402,9 +423,16 @@ struct SocketControlSettings {
|
|||
static func socketPath(
|
||||
environment: [String: String] = ProcessInfo.processInfo.environment,
|
||||
bundleIdentifier: String? = Bundle.main.bundleIdentifier,
|
||||
isDebugBuild: Bool = SocketControlSettings.isDebugBuild
|
||||
isDebugBuild: Bool = SocketControlSettings.isDebugBuild,
|
||||
currentUserID: uid_t = getuid(),
|
||||
probeStableDefaultPathEntry: (String) -> StableDefaultSocketPathEntry = inspectStableDefaultSocketPathEntry
|
||||
) -> String {
|
||||
let fallback = defaultSocketPath(bundleIdentifier: bundleIdentifier, isDebugBuild: isDebugBuild)
|
||||
let fallback = defaultSocketPath(
|
||||
bundleIdentifier: bundleIdentifier,
|
||||
isDebugBuild: isDebugBuild,
|
||||
currentUserID: currentUserID,
|
||||
probeStableDefaultPathEntry: probeStableDefaultPathEntry
|
||||
)
|
||||
|
||||
guard let override = environment["CMUX_SOCKET_PATH"], !override.isEmpty else {
|
||||
return fallback
|
||||
|
|
@ -421,7 +449,12 @@ struct SocketControlSettings {
|
|||
return fallback
|
||||
}
|
||||
|
||||
static func defaultSocketPath(bundleIdentifier: String?, isDebugBuild: Bool) -> String {
|
||||
static func defaultSocketPath(
|
||||
bundleIdentifier: String?,
|
||||
isDebugBuild: Bool,
|
||||
currentUserID: uid_t = getuid(),
|
||||
probeStableDefaultPathEntry: (String) -> StableDefaultSocketPathEntry = inspectStableDefaultSocketPathEntry
|
||||
) -> String {
|
||||
if bundleIdentifier == "com.cmuxterm.app.nightly" {
|
||||
return "/tmp/cmux-nightly.sock"
|
||||
}
|
||||
|
|
@ -431,7 +464,38 @@ struct SocketControlSettings {
|
|||
if isStagingBundleIdentifier(bundleIdentifier) {
|
||||
return "/tmp/cmux-staging.sock"
|
||||
}
|
||||
return "/tmp/cmux.sock"
|
||||
return resolvedStableDefaultSocketPath(
|
||||
currentUserID: currentUserID,
|
||||
probeStableDefaultPathEntry: probeStableDefaultPathEntry
|
||||
)
|
||||
}
|
||||
|
||||
static func userScopedStableSocketPath(currentUserID: uid_t = getuid()) -> String {
|
||||
stableSocketDirectoryURL()?
|
||||
.appendingPathComponent("cmux-\(currentUserID).sock", isDirectory: false)
|
||||
.path ?? "/tmp/cmux-\(currentUserID).sock"
|
||||
}
|
||||
|
||||
static func resolvedStableDefaultSocketPath(
|
||||
currentUserID: uid_t = getuid(),
|
||||
probeStableDefaultPathEntry: (String) -> StableDefaultSocketPathEntry = inspectStableDefaultSocketPathEntry
|
||||
) -> String {
|
||||
switch probeStableDefaultPathEntry(stableDefaultSocketPath) {
|
||||
case .missing:
|
||||
return stableDefaultSocketPath
|
||||
case .socket(let ownerUserID) where ownerUserID == currentUserID:
|
||||
return stableDefaultSocketPath
|
||||
case .socket, .other, .inaccessible:
|
||||
return userScopedStableSocketPath(currentUserID: currentUserID)
|
||||
}
|
||||
}
|
||||
|
||||
static func recordLastSocketPath(_ path: String, filePath: String = lastSocketPathFile) {
|
||||
let payload = Data((path + "\n").utf8)
|
||||
writeSocketPathMarker(payload, to: filePath)
|
||||
if filePath != legacyLastSocketPathFile {
|
||||
writeSocketPathMarker(payload, to: legacyLastSocketPathFile)
|
||||
}
|
||||
}
|
||||
|
||||
static func shouldHonorSocketPathOverride(
|
||||
|
|
@ -460,6 +524,51 @@ struct SocketControlSettings {
|
|||
|| bundleIdentifier.hasPrefix("com.cmuxterm.app.staging.")
|
||||
}
|
||||
|
||||
static func stableSocketDirectoryURL(fileManager: FileManager = .default) -> URL? {
|
||||
guard let appSupportDirectory = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
|
||||
return nil
|
||||
}
|
||||
return appSupportDirectory.appendingPathComponent(socketDirectoryName, isDirectory: true)
|
||||
}
|
||||
|
||||
static func stableSocketFileURL(fileManager: FileManager = .default) -> URL? {
|
||||
stableSocketDirectoryURL(fileManager: fileManager)?
|
||||
.appendingPathComponent(stableSocketFileName, isDirectory: false)
|
||||
}
|
||||
|
||||
static func lastSocketPathFileURL(fileManager: FileManager = .default) -> URL? {
|
||||
stableSocketDirectoryURL(fileManager: fileManager)?
|
||||
.appendingPathComponent(lastSocketPathFileName, isDirectory: false)
|
||||
}
|
||||
|
||||
private static func writeSocketPathMarker(_ payload: Data, to filePath: String) {
|
||||
let fileURL = URL(fileURLWithPath: filePath)
|
||||
let parentURL = fileURL.deletingLastPathComponent()
|
||||
try? FileManager.default.createDirectory(
|
||||
at: parentURL,
|
||||
withIntermediateDirectories: true,
|
||||
attributes: [.posixPermissions: 0o700]
|
||||
)
|
||||
try? payload.write(to: fileURL, options: .atomic)
|
||||
}
|
||||
|
||||
private static func inspectStableDefaultSocketPathEntry(_ path: String) -> StableDefaultSocketPathEntry {
|
||||
var st = stat()
|
||||
guard lstat(path, &st) == 0 else {
|
||||
let errnoCode = errno
|
||||
if errnoCode == ENOENT {
|
||||
return .missing
|
||||
}
|
||||
return .inaccessible(errnoCode: errnoCode)
|
||||
}
|
||||
|
||||
let fileType = st.st_mode & mode_t(S_IFMT)
|
||||
if fileType == mode_t(S_IFSOCK) {
|
||||
return .socket(ownerUserID: st.st_uid)
|
||||
}
|
||||
return .other(ownerUserID: st.st_uid)
|
||||
}
|
||||
|
||||
static func isTruthy(_ raw: String?) -> Bool {
|
||||
guard let raw else { return false }
|
||||
switch raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class TerminalController {
|
|||
|
||||
static let shared = TerminalController()
|
||||
|
||||
private nonisolated(unsafe) var socketPath = "/tmp/cmux.sock"
|
||||
private nonisolated(unsafe) var socketPath = SocketControlSettings.stableDefaultSocketPath
|
||||
private nonisolated(unsafe) var serverSocket: Int32 = -1
|
||||
private nonisolated(unsafe) var isRunning = false
|
||||
private nonisolated(unsafe) var acceptLoopAlive = false
|
||||
|
|
@ -73,6 +73,13 @@ class TerminalController {
|
|||
let acceptLoopAlive: Bool
|
||||
let activeGeneration: UInt64
|
||||
let pendingRearmGeneration: UInt64?
|
||||
let listenerStartInProgress: Bool
|
||||
}
|
||||
|
||||
private enum SocketBindAttemptResult {
|
||||
case success(path: String)
|
||||
case pathTooLong(path: String)
|
||||
case failure(path: String, stage: String, errnoCode: Int32)
|
||||
}
|
||||
|
||||
private static let focusIntentV1Commands: Set<String> = [
|
||||
|
|
@ -174,11 +181,20 @@ class TerminalController {
|
|||
isRunning: isRunning,
|
||||
acceptLoopAlive: acceptLoopAlive,
|
||||
activeGeneration: activeAcceptLoopGeneration,
|
||||
pendingRearmGeneration: pendingAcceptLoopRearmGeneration
|
||||
pendingRearmGeneration: pendingAcceptLoopRearmGeneration,
|
||||
listenerStartInProgress: listenerStartInProgress
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func activeSocketPath(preferredPath: String) -> String {
|
||||
let snapshot = listenerStateSnapshot()
|
||||
if snapshot.isRunning || snapshot.acceptLoopAlive || snapshot.listenerStartInProgress || snapshot.serverSocket >= 0 {
|
||||
return snapshot.socketPath
|
||||
}
|
||||
return preferredPath
|
||||
}
|
||||
|
||||
private nonisolated func shouldContinueAcceptLoop(generation: UInt64) -> Bool {
|
||||
withListenerState {
|
||||
isRunning && generation == activeAcceptLoopGeneration
|
||||
|
|
@ -650,6 +666,60 @@ class TerminalController {
|
|||
return (false, connectErrno)
|
||||
}
|
||||
|
||||
private nonisolated static func bindListenerSocket(_ socket: Int32, path: String) -> SocketBindAttemptResult {
|
||||
if let errnoCode = ensureSocketParentDirectoryExists(path: path) {
|
||||
return .failure(path: path, stage: "create_directory", errnoCode: errnoCode)
|
||||
}
|
||||
if unlink(path) != 0, errno != ENOENT {
|
||||
return .failure(path: path, stage: "unlink", errnoCode: errno)
|
||||
}
|
||||
|
||||
guard let bindResult = bindUnixSocket(socket, path: path) else {
|
||||
return .pathTooLong(path: path)
|
||||
}
|
||||
guard bindResult >= 0 else {
|
||||
return .failure(path: path, stage: "bind", errnoCode: errno)
|
||||
}
|
||||
return .success(path: path)
|
||||
}
|
||||
|
||||
private nonisolated static func ensureSocketParentDirectoryExists(path: String) -> Int32? {
|
||||
let parentURL = URL(fileURLWithPath: path).deletingLastPathComponent()
|
||||
do {
|
||||
try FileManager.default.createDirectory(
|
||||
at: parentURL,
|
||||
withIntermediateDirectories: true,
|
||||
attributes: [.posixPermissions: 0o700]
|
||||
)
|
||||
return nil
|
||||
} catch let error as NSError {
|
||||
if error.domain == NSPOSIXErrorDomain {
|
||||
return Int32(error.code)
|
||||
}
|
||||
return EIO
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated static func fallbackSocketPathAfterBindFailure(
|
||||
requestedPath: String,
|
||||
stage: String,
|
||||
errnoCode: Int32,
|
||||
currentUserID: uid_t = getuid()
|
||||
) -> String? {
|
||||
guard requestedPath == SocketControlSettings.stableDefaultSocketPath else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch stage {
|
||||
case "unlink" where errnoCode == EACCES || errnoCode == EPERM:
|
||||
return SocketControlSettings.userScopedStableSocketPath(currentUserID: currentUserID)
|
||||
case "bind" where errnoCode == EACCES || errnoCode == EPERM || errnoCode == EADDRINUSE:
|
||||
return SocketControlSettings.userScopedStableSocketPath(currentUserID: currentUserID)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func start(tabManager: TabManager, socketPath: String, accessMode: SocketControlMode) {
|
||||
self.tabManager = tabManager
|
||||
self.accessMode = accessMode
|
||||
|
|
@ -668,8 +738,9 @@ class TerminalController {
|
|||
stop()
|
||||
}
|
||||
|
||||
var activeSocketPath = socketPath
|
||||
withListenerState {
|
||||
self.socketPath = socketPath
|
||||
self.socketPath = activeSocketPath
|
||||
listenerStartInProgress = true
|
||||
}
|
||||
var listenerActivated = false
|
||||
|
|
@ -681,9 +752,6 @@ class TerminalController {
|
|||
}
|
||||
}
|
||||
|
||||
// Remove existing socket file
|
||||
unlink(socketPath)
|
||||
|
||||
// Create socket
|
||||
let newServerSocket = socket(AF_UNIX, SOCK_STREAM, 0)
|
||||
guard newServerSocket >= 0 else {
|
||||
|
|
@ -697,29 +765,58 @@ class TerminalController {
|
|||
return
|
||||
}
|
||||
|
||||
// Bind to path
|
||||
guard let bindResult = Self.bindUnixSocket(newServerSocket, path: socketPath) else {
|
||||
var bindAttempt = Self.bindListenerSocket(newServerSocket, path: activeSocketPath)
|
||||
if case .failure(let failedPath, let failedStage, let failedErrnoCode) = bindAttempt,
|
||||
let fallbackPath = Self.fallbackSocketPathAfterBindFailure(
|
||||
requestedPath: failedPath,
|
||||
stage: failedStage,
|
||||
errnoCode: failedErrnoCode
|
||||
),
|
||||
fallbackPath != failedPath {
|
||||
sentryBreadcrumb(
|
||||
"socket.listener.path.fallback",
|
||||
category: "socket",
|
||||
data: [
|
||||
"requestedPath": failedPath,
|
||||
"fallbackPath": fallbackPath,
|
||||
"stage": failedStage,
|
||||
"errno": Int(failedErrnoCode)
|
||||
]
|
||||
)
|
||||
activeSocketPath = fallbackPath
|
||||
withListenerState {
|
||||
self.socketPath = activeSocketPath
|
||||
}
|
||||
bindAttempt = Self.bindListenerSocket(newServerSocket, path: activeSocketPath)
|
||||
}
|
||||
|
||||
switch bindAttempt {
|
||||
case .success(let boundPath):
|
||||
activeSocketPath = boundPath
|
||||
withListenerState {
|
||||
self.socketPath = activeSocketPath
|
||||
}
|
||||
case .pathTooLong(let failedPath):
|
||||
close(newServerSocket)
|
||||
reportSocketListenerFailure(
|
||||
message: "socket.listener.start.failed",
|
||||
stage: "bind_path_too_long",
|
||||
errnoCode: ENAMETOOLONG,
|
||||
extra: [
|
||||
"pathLength": socketPath.utf8.count,
|
||||
"path": failedPath,
|
||||
"pathLength": failedPath.utf8.count,
|
||||
"maxPathLength": Self.unixSocketPathMaxLength
|
||||
]
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
guard bindResult >= 0 else {
|
||||
let errnoCode = errno
|
||||
case .failure(let failedPath, let failedStage, let failedErrnoCode):
|
||||
print("TerminalController: Failed to bind socket")
|
||||
close(newServerSocket)
|
||||
reportSocketListenerFailure(
|
||||
message: "socket.listener.start.failed",
|
||||
stage: "bind",
|
||||
errnoCode: errnoCode
|
||||
stage: failedStage,
|
||||
errnoCode: failedErrnoCode,
|
||||
extra: ["path": failedPath]
|
||||
)
|
||||
return
|
||||
}
|
||||
|
|
@ -739,6 +836,8 @@ class TerminalController {
|
|||
return
|
||||
}
|
||||
|
||||
SocketControlSettings.recordLastSocketPath(activeSocketPath)
|
||||
|
||||
let generation = withListenerState {
|
||||
isRunning = true
|
||||
pendingAcceptLoopRearmGeneration = nil
|
||||
|
|
@ -751,12 +850,12 @@ class TerminalController {
|
|||
}
|
||||
listenerActivated = true
|
||||
let listenerSocket = newServerSocket
|
||||
print("TerminalController: Listening on \(socketPath)")
|
||||
print("TerminalController: Listening on \(activeSocketPath)")
|
||||
sentryBreadcrumb(
|
||||
"socket.listener.listening",
|
||||
category: "socket",
|
||||
data: [
|
||||
"path": socketPath,
|
||||
"path": activeSocketPath,
|
||||
"mode": accessMode.rawValue,
|
||||
"generation": generation,
|
||||
"backlog": Self.socketListenBacklog
|
||||
|
|
|
|||
|
|
@ -14609,6 +14609,29 @@ final class GhosttyTerminalViewVisibilityPolicyTests: XCTestCase {
|
|||
}
|
||||
|
||||
final class TerminalControllerSocketListenerHealthTests: XCTestCase {
|
||||
func testStableSocketBindPermissionFailureFallsBackToUserScopedSocket() {
|
||||
XCTAssertEqual(
|
||||
TerminalController.fallbackSocketPathAfterBindFailure(
|
||||
requestedPath: SocketControlSettings.stableDefaultSocketPath,
|
||||
stage: "bind",
|
||||
errnoCode: EACCES,
|
||||
currentUserID: 501
|
||||
),
|
||||
SocketControlSettings.userScopedStableSocketPath(currentUserID: 501)
|
||||
)
|
||||
}
|
||||
|
||||
func testNonStableSocketBindFailureDoesNotFallback() {
|
||||
XCTAssertNil(
|
||||
TerminalController.fallbackSocketPathAfterBindFailure(
|
||||
requestedPath: "/tmp/cmux-debug.sock",
|
||||
stage: "bind",
|
||||
errnoCode: EACCES,
|
||||
currentUserID: 501
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private func makeTempSocketPath() -> String {
|
||||
"/tmp/cmux-socket-health-\(UUID().uuidString).sock"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1033,10 +1033,11 @@ final class SocketControlSettingsTests: XCTestCase {
|
|||
"CMUX_SOCKET_PATH": "/tmp/cmux-debug-issue-153-tmux-compat.sock",
|
||||
],
|
||||
bundleIdentifier: "com.cmuxterm.app",
|
||||
isDebugBuild: false
|
||||
isDebugBuild: false,
|
||||
probeStableDefaultPathEntry: { _ in .missing }
|
||||
)
|
||||
|
||||
XCTAssertEqual(path, "/tmp/cmux.sock")
|
||||
XCTAssertEqual(path, SocketControlSettings.stableDefaultSocketPath)
|
||||
}
|
||||
|
||||
func testNightlyReleaseUsesDedicatedDefaultAndIgnoresAmbientSocketOverride() {
|
||||
|
|
@ -1045,7 +1046,8 @@ final class SocketControlSettingsTests: XCTestCase {
|
|||
"CMUX_SOCKET_PATH": "/tmp/cmux-debug-issue-153-tmux-compat.sock",
|
||||
],
|
||||
bundleIdentifier: "com.cmuxterm.app.nightly",
|
||||
isDebugBuild: false
|
||||
isDebugBuild: false,
|
||||
probeStableDefaultPathEntry: { _ in .missing }
|
||||
)
|
||||
|
||||
XCTAssertEqual(path, "/tmp/cmux-nightly.sock")
|
||||
|
|
@ -1082,7 +1084,8 @@ final class SocketControlSettingsTests: XCTestCase {
|
|||
"CMUX_ALLOW_SOCKET_OVERRIDE": "1",
|
||||
],
|
||||
bundleIdentifier: "com.cmuxterm.app",
|
||||
isDebugBuild: false
|
||||
isDebugBuild: false,
|
||||
probeStableDefaultPathEntry: { _ in .missing }
|
||||
)
|
||||
|
||||
XCTAssertEqual(path, "/tmp/cmux-debug-forced.sock")
|
||||
|
|
@ -1090,23 +1093,61 @@ final class SocketControlSettingsTests: XCTestCase {
|
|||
|
||||
func testDefaultSocketPathByChannel() {
|
||||
XCTAssertEqual(
|
||||
SocketControlSettings.defaultSocketPath(bundleIdentifier: "com.cmuxterm.app", isDebugBuild: false),
|
||||
"/tmp/cmux.sock"
|
||||
SocketControlSettings.defaultSocketPath(
|
||||
bundleIdentifier: "com.cmuxterm.app",
|
||||
isDebugBuild: false,
|
||||
probeStableDefaultPathEntry: { _ in .missing }
|
||||
),
|
||||
SocketControlSettings.stableDefaultSocketPath
|
||||
)
|
||||
XCTAssertEqual(
|
||||
SocketControlSettings.defaultSocketPath(bundleIdentifier: "com.cmuxterm.app.nightly", isDebugBuild: false),
|
||||
SocketControlSettings.defaultSocketPath(
|
||||
bundleIdentifier: "com.cmuxterm.app.nightly",
|
||||
isDebugBuild: false,
|
||||
probeStableDefaultPathEntry: { _ in .missing }
|
||||
),
|
||||
"/tmp/cmux-nightly.sock"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
SocketControlSettings.defaultSocketPath(bundleIdentifier: "com.cmuxterm.app.debug.tag", isDebugBuild: false),
|
||||
SocketControlSettings.defaultSocketPath(
|
||||
bundleIdentifier: "com.cmuxterm.app.debug.tag",
|
||||
isDebugBuild: false,
|
||||
probeStableDefaultPathEntry: { _ in .missing }
|
||||
),
|
||||
"/tmp/cmux-debug.sock"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
SocketControlSettings.defaultSocketPath(bundleIdentifier: "com.cmuxterm.app.staging.tag", isDebugBuild: false),
|
||||
SocketControlSettings.defaultSocketPath(
|
||||
bundleIdentifier: "com.cmuxterm.app.staging.tag",
|
||||
isDebugBuild: false,
|
||||
probeStableDefaultPathEntry: { _ in .missing }
|
||||
),
|
||||
"/tmp/cmux-staging.sock"
|
||||
)
|
||||
}
|
||||
|
||||
func testStableReleaseFallsBackToUserScopedSocketWhenStablePathOwnedByDifferentUser() {
|
||||
let path = SocketControlSettings.defaultSocketPath(
|
||||
bundleIdentifier: "com.cmuxterm.app",
|
||||
isDebugBuild: false,
|
||||
currentUserID: 501,
|
||||
probeStableDefaultPathEntry: { _ in .socket(ownerUserID: 0) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(path, SocketControlSettings.userScopedStableSocketPath(currentUserID: 501))
|
||||
}
|
||||
|
||||
func testStableReleaseFallsBackToUserScopedSocketWhenStablePathIsBlockedByNonSocketEntry() {
|
||||
let path = SocketControlSettings.defaultSocketPath(
|
||||
bundleIdentifier: "com.cmuxterm.app",
|
||||
isDebugBuild: false,
|
||||
currentUserID: 501,
|
||||
probeStableDefaultPathEntry: { _ in .other(ownerUserID: 501) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(path, SocketControlSettings.userScopedStableSocketPath(currentUserID: 501))
|
||||
}
|
||||
|
||||
func testUntaggedDebugBundleBlockedWithoutLaunchTag() {
|
||||
XCTAssertTrue(
|
||||
SocketControlSettings.shouldBlockUntaggedDebugLaunch(
|
||||
|
|
|
|||
|
|
@ -982,6 +982,7 @@ final class MultiWindowNotificationsUITests: XCTestCase {
|
|||
if includeGlobalFallback {
|
||||
candidates.append(contentsOf: discoverTmpSocketCandidates(limit: 12))
|
||||
candidates.append("/tmp/cmux-debug.sock")
|
||||
candidates.append(stableSocketPath())
|
||||
candidates.append("/tmp/cmux.sock")
|
||||
}
|
||||
|
||||
|
|
@ -995,6 +996,13 @@ final class MultiWindowNotificationsUITests: XCTestCase {
|
|||
return unique
|
||||
}
|
||||
|
||||
private func stableSocketPath() -> String {
|
||||
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?
|
||||
.appendingPathComponent("cmux", isDirectory: true)
|
||||
.appendingPathComponent("cmux.sock", isDirectory: false)
|
||||
.path ?? "/tmp/cmux.sock"
|
||||
}
|
||||
|
||||
private func socketMatchesRequiredWorkspace(_ candidatePath: String, workspaceId: String?) -> Bool {
|
||||
guard let workspaceId, !workspaceId.isEmpty else { return true }
|
||||
let originalPath = socketPath
|
||||
|
|
|
|||
|
|
@ -10,6 +10,15 @@ BUNDLE_SET=0
|
|||
DERIVED_SET=0
|
||||
TAG=""
|
||||
CMUX_DEBUG_LOG=""
|
||||
LAST_SOCKET_PATH_DIR="$HOME/Library/Application Support/cmux"
|
||||
LAST_SOCKET_PATH_FILE="${LAST_SOCKET_PATH_DIR}/last-socket-path"
|
||||
|
||||
write_last_socket_path() {
|
||||
local socket_path="$1"
|
||||
mkdir -p "$LAST_SOCKET_PATH_DIR"
|
||||
echo "$socket_path" > "$LAST_SOCKET_PATH_FILE" || true
|
||||
echo "$socket_path" > /tmp/cmux-last-socket-path || true
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
|
|
@ -270,7 +279,7 @@ if [[ -n "$TAG" && "$APP_NAME" != "$SEARCH_APP_NAME" ]]; then
|
|||
CMUXD_SOCKET="${APP_SUPPORT_DIR}/cmuxd-dev-${TAG_SLUG}.sock"
|
||||
CMUX_SOCKET="/tmp/cmux-debug-${TAG_SLUG}.sock"
|
||||
CMUX_DEBUG_LOG="/tmp/cmux-debug-${TAG_SLUG}.log"
|
||||
echo "$CMUX_SOCKET" > /tmp/cmux-last-socket-path || true
|
||||
write_last_socket_path "$CMUX_SOCKET"
|
||||
echo "$CMUX_DEBUG_LOG" > /tmp/cmux-last-debug-log-path || true
|
||||
/usr/libexec/PlistBuddy -c "Add :LSEnvironment dict" "$INFO_PLIST" 2>/dev/null || true
|
||||
/usr/libexec/PlistBuddy -c "Set :LSEnvironment:CMUXD_UNIX_PATH \"${CMUXD_SOCKET}\"" "$INFO_PLIST" 2>/dev/null \
|
||||
|
|
|
|||
|
|
@ -9,6 +9,15 @@ NAME_SET=0
|
|||
BUNDLE_SET=0
|
||||
DERIVED_SET=0
|
||||
TAG=""
|
||||
LAST_SOCKET_PATH_DIR="$HOME/Library/Application Support/cmux"
|
||||
LAST_SOCKET_PATH_FILE="${LAST_SOCKET_PATH_DIR}/last-socket-path"
|
||||
|
||||
write_last_socket_path() {
|
||||
local socket_path="$1"
|
||||
mkdir -p "$LAST_SOCKET_PATH_DIR"
|
||||
echo "$socket_path" > "$LAST_SOCKET_PATH_FILE" || true
|
||||
echo "$socket_path" > /tmp/cmux-last-socket-path || true
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
|
|
@ -186,12 +195,12 @@ if [[ -f "$INFO_PLIST" ]]; then
|
|||
|| /usr/libexec/PlistBuddy -c "Add :CFBundleIdentifier string $BUNDLE_ID" "$INFO_PLIST"
|
||||
|
||||
# Inject staging socket paths via LSEnvironment so the Release binary
|
||||
# (which defaults to /tmp/cmux.sock) uses isolated sockets instead.
|
||||
# (which defaults to the per-user stable socket) uses isolated sockets instead.
|
||||
STAGING_SLUG="${TAG_SLUG:-staging}"
|
||||
APP_SUPPORT_DIR="$HOME/Library/Application Support/cmux"
|
||||
CMUXD_SOCKET="${APP_SUPPORT_DIR}/cmuxd-${STAGING_SLUG}.sock"
|
||||
CMUX_SOCKET="/tmp/cmux-${STAGING_SLUG}.sock"
|
||||
echo "$CMUX_SOCKET" > /tmp/cmux-last-socket-path || true
|
||||
write_last_socket_path "$CMUX_SOCKET"
|
||||
/usr/libexec/PlistBuddy -c "Add :LSEnvironment dict" "$INFO_PLIST" 2>/dev/null || true
|
||||
/usr/libexec/PlistBuddy -c "Set :LSEnvironment:CMUXD_UNIX_PATH \"${CMUXD_SOCKET}\"" "$INFO_PLIST" 2>/dev/null \
|
||||
|| /usr/libexec/PlistBuddy -c "Add :LSEnvironment:CMUXD_UNIX_PATH string \"${CMUXD_SOCKET}\"" "$INFO_PLIST"
|
||||
|
|
|
|||
|
|
@ -45,7 +45,13 @@ class cmuxError(Exception):
|
|||
pass
|
||||
|
||||
|
||||
_LAST_SOCKET_PATH_FILE = "/tmp/cmux-last-socket-path"
|
||||
_APP_SUPPORT_DIR = os.path.expanduser("~/Library/Application Support/cmux")
|
||||
_STABLE_SOCKET_PATH = os.path.join(_APP_SUPPORT_DIR, "cmux.sock")
|
||||
_LEGACY_STABLE_SOCKET_PATH = "/tmp/cmux.sock"
|
||||
_LAST_SOCKET_PATH_FILES = [
|
||||
os.path.join(_APP_SUPPORT_DIR, "last-socket-path"),
|
||||
"/tmp/cmux-last-socket-path",
|
||||
]
|
||||
_DEFAULT_DEBUG_BUNDLE_ID = "com.cmuxterm.app.debug"
|
||||
|
||||
|
||||
|
|
@ -83,13 +89,14 @@ def _default_bundle_id() -> str:
|
|||
|
||||
|
||||
def _read_last_socket_path() -> Optional[str]:
|
||||
for marker_path in _LAST_SOCKET_PATH_FILES:
|
||||
try:
|
||||
with open(_LAST_SOCKET_PATH_FILE, "r", encoding="utf-8") as f:
|
||||
with open(marker_path, "r", encoding="utf-8") as f:
|
||||
path = f.read().strip()
|
||||
if path:
|
||||
return path
|
||||
except OSError:
|
||||
pass
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -134,8 +141,8 @@ def _default_socket_path() -> str:
|
|||
if override:
|
||||
if os.path.exists(override) and _can_connect(override):
|
||||
return override
|
||||
# Fall back to other heuristics if the override points at a stale socket file.
|
||||
if not os.path.exists(override):
|
||||
# Treat stable defaults as implicit so old env values still migrate cleanly.
|
||||
if not os.path.exists(override) and override not in {_STABLE_SOCKET_PATH, _LEGACY_STABLE_SOCKET_PATH}:
|
||||
return override
|
||||
|
||||
last_socket = _read_last_socket_path()
|
||||
|
|
@ -144,13 +151,14 @@ def _default_socket_path() -> str:
|
|||
return last_socket
|
||||
|
||||
# Prefer the non-tagged sockets when present.
|
||||
candidates = ["/tmp/cmux-debug.sock", "/tmp/cmux.sock"]
|
||||
candidates = ["/tmp/cmux-debug.sock", _STABLE_SOCKET_PATH, _LEGACY_STABLE_SOCKET_PATH]
|
||||
for path in candidates:
|
||||
if os.path.exists(path) and _can_connect(path):
|
||||
return path
|
||||
|
||||
# Otherwise, fall back to the newest tagged debug socket if there is one.
|
||||
# Otherwise, fall back to the newest discovered socket if there is one.
|
||||
tagged = glob.glob("/tmp/cmux-debug-*.sock")
|
||||
tagged.extend(glob.glob(os.path.join(_APP_SUPPORT_DIR, "cmux*.sock")))
|
||||
tagged = [p for p in tagged if os.path.exists(p)]
|
||||
if tagged:
|
||||
tagged.sort(key=lambda p: os.path.getmtime(p), reverse=True)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ Notes:
|
|||
|
||||
import base64
|
||||
import errno
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import select
|
||||
|
|
@ -32,16 +33,53 @@ class cmuxError(Exception):
|
|||
"""Exception raised for cmux errors."""
|
||||
|
||||
|
||||
_APP_SUPPORT_DIR = os.path.expanduser("~/Library/Application Support/cmux")
|
||||
_STABLE_SOCKET_PATH = os.path.join(_APP_SUPPORT_DIR, "cmux.sock")
|
||||
_LEGACY_STABLE_SOCKET_PATH = "/tmp/cmux.sock"
|
||||
_LAST_SOCKET_PATH_FILES = [
|
||||
os.path.join(_APP_SUPPORT_DIR, "last-socket-path"),
|
||||
"/tmp/cmux-last-socket-path",
|
||||
]
|
||||
|
||||
|
||||
def _read_last_socket_path() -> Optional[str]:
|
||||
for marker_path in _LAST_SOCKET_PATH_FILES:
|
||||
try:
|
||||
with open(marker_path, "r", encoding="utf-8") as f:
|
||||
path = f.read().strip()
|
||||
if path:
|
||||
return path
|
||||
except OSError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def _default_socket_path() -> str:
|
||||
# Backwards/forward compatibility: some scripts export CMUX_SOCKET,
|
||||
# while the client historically used CMUX_SOCKET_PATH.
|
||||
override = os.environ.get("CMUX_SOCKET_PATH") or os.environ.get("CMUX_SOCKET")
|
||||
if override:
|
||||
if os.path.exists(override):
|
||||
return override
|
||||
candidates = ["/tmp/cmux-debug.sock", "/tmp/cmux.sock"]
|
||||
if override not in {_STABLE_SOCKET_PATH, _LEGACY_STABLE_SOCKET_PATH}:
|
||||
return override
|
||||
|
||||
last_socket = _read_last_socket_path()
|
||||
if last_socket and os.path.exists(last_socket):
|
||||
return last_socket
|
||||
|
||||
candidates = ["/tmp/cmux-debug.sock", _STABLE_SOCKET_PATH, _LEGACY_STABLE_SOCKET_PATH]
|
||||
for path in candidates:
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
|
||||
discovered = glob.glob("/tmp/cmux-debug-*.sock")
|
||||
discovered.extend(glob.glob(os.path.join(_APP_SUPPORT_DIR, "cmux*.sock")))
|
||||
discovered = [path for path in discovered if os.path.exists(path)]
|
||||
if discovered:
|
||||
discovered.sort(key=os.path.getmtime, reverse=True)
|
||||
return discovered[0]
|
||||
|
||||
return candidates[0]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -227,7 +227,11 @@ def main():
|
|||
print(f"\nFound cmux process: PID {pid}")
|
||||
|
||||
# Try to connect to the socket
|
||||
socket_paths = ["/tmp/cmux.sock", "/tmp/cmux-debug.sock"]
|
||||
socket_paths = [
|
||||
os.path.expanduser("~/Library/Application Support/cmux/cmux.sock"),
|
||||
"/tmp/cmux.sock",
|
||||
"/tmp/cmux-debug.sock",
|
||||
]
|
||||
client = None
|
||||
for socket_path in socket_paths:
|
||||
if os.path.exists(socket_path):
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ def infer_app_name_for_osascript(socket_path: str) -> str:
|
|||
Examples:
|
||||
- /tmp/cmux-debug.sock -> "cmux DEV"
|
||||
- /tmp/cmux-debug-foo.sock -> "cmux DEV foo"
|
||||
- /tmp/cmux.sock -> "cmux"
|
||||
- ~/Library/Application Support/cmux/cmux.sock -> "cmux"
|
||||
- /tmp/cmux-foo.sock -> "cmux foo"
|
||||
"""
|
||||
base = Path(socket_path).name
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue