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
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue