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:
Lawrence Chen 2026-03-13 17:37:01 -07:00 committed by GitHub
parent 126c6c6e56
commit 8a0934b801
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 479 additions and 83 deletions

View file

@ -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() {