diff --git a/Sources/SocketControlSettings.swift b/Sources/SocketControlSettings.swift index 67b91682..c0fb35bb 100644 --- a/Sources/SocketControlSettings.swift +++ b/Sources/SocketControlSettings.swift @@ -38,6 +38,7 @@ enum SocketControlMode: String, CaseIterable, Identifiable { struct SocketControlSettings { static let appStorageKey = "socketControlMode" static let legacyEnabledKey = "socketControlEnabled" + static let allowSocketPathOverrideKey = "CMUX_ALLOW_SOCKET_OVERRIDE" /// Map old persisted rawValues to the new enum. static func migrateMode(_ raw: String) -> SocketControlMode { @@ -55,15 +56,83 @@ struct SocketControlSettings { return .cmuxOnly } - static func socketPath() -> String { - if let override = ProcessInfo.processInfo.environment["CMUX_SOCKET_PATH"], !override.isEmpty { + private static var isDebugBuild: Bool { +#if DEBUG + true +#else + false +#endif + } + + static func socketPath( + environment: [String: String] = ProcessInfo.processInfo.environment, + bundleIdentifier: String? = Bundle.main.bundleIdentifier, + isDebugBuild: Bool = SocketControlSettings.isDebugBuild + ) -> String { + let fallback = defaultSocketPath(bundleIdentifier: bundleIdentifier, isDebugBuild: isDebugBuild) + + guard let override = environment["CMUX_SOCKET_PATH"], !override.isEmpty else { + return fallback + } + + if shouldHonorSocketPathOverride( + environment: environment, + bundleIdentifier: bundleIdentifier, + isDebugBuild: isDebugBuild + ) { return override } -#if DEBUG - return "/tmp/cmux-debug.sock" -#else + + return fallback + } + + static func defaultSocketPath(bundleIdentifier: String?, isDebugBuild: Bool) -> String { + if bundleIdentifier == "com.cmuxterm.app.nightly" { + return "/tmp/cmux-nightly.sock" + } + if isDebugLikeBundleIdentifier(bundleIdentifier) || isDebugBuild { + return "/tmp/cmux-debug.sock" + } + if isStagingBundleIdentifier(bundleIdentifier) { + return "/tmp/cmux-staging.sock" + } return "/tmp/cmux.sock" -#endif + } + + static func shouldHonorSocketPathOverride( + environment: [String: String], + bundleIdentifier: String?, + isDebugBuild: Bool + ) -> Bool { + if isTruthy(environment[allowSocketPathOverrideKey]) { + return true + } + if isDebugLikeBundleIdentifier(bundleIdentifier) || isStagingBundleIdentifier(bundleIdentifier) { + return true + } + return isDebugBuild + } + + static func isDebugLikeBundleIdentifier(_ bundleIdentifier: String?) -> Bool { + guard let bundleIdentifier else { return false } + return bundleIdentifier == "com.cmuxterm.app.debug" + || bundleIdentifier.hasPrefix("com.cmuxterm.app.debug.") + } + + static func isStagingBundleIdentifier(_ bundleIdentifier: String?) -> Bool { + guard let bundleIdentifier else { return false } + return bundleIdentifier == "com.cmuxterm.app.staging" + || bundleIdentifier.hasPrefix("com.cmuxterm.app.staging.") + } + + static func isTruthy(_ raw: String?) -> Bool { + guard let raw else { return false } + switch raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { + case "1", "true", "yes", "on": + return true + default: + return false + } } static func envOverrideEnabled() -> Bool? { diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index 24841d43..f7da6258 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -2540,7 +2540,7 @@ struct SettingsView: View { SettingsCardDivider() SettingsCardNote("Controls access to the local Unix socket for programmatic control. In \"cmux processes only\" mode, only processes spawned inside cmux terminals can connect.") - SettingsCardNote("Overrides: CMUX_SOCKET_ENABLE, CMUX_SOCKET_MODE, and CMUX_SOCKET_PATH.") + SettingsCardNote("Overrides: CMUX_SOCKET_ENABLE, CMUX_SOCKET_MODE, and CMUX_SOCKET_PATH (set CMUX_ALLOW_SOCKET_OVERRIDE=1 for stable/nightly builds).") } SettingsCard { diff --git a/cmuxTests/GhosttyConfigTests.swift b/cmuxTests/GhosttyConfigTests.swift index e2978e55..6e57013e 100644 --- a/cmuxTests/GhosttyConfigTests.swift +++ b/cmuxTests/GhosttyConfigTests.swift @@ -316,3 +316,85 @@ final class TabManagerNotificationOrderingSourceTests: XCTestCase { return URL(fileURLWithPath: FileManager.default.currentDirectoryPath) } } + +final class SocketControlSettingsTests: XCTestCase { + func testStableReleaseIgnoresAmbientSocketOverrideByDefault() { + let path = SocketControlSettings.socketPath( + environment: [ + "CMUX_SOCKET_PATH": "/tmp/cmux-debug-issue-153-tmux-compat.sock", + ], + bundleIdentifier: "com.cmuxterm.app", + isDebugBuild: false + ) + + XCTAssertEqual(path, "/tmp/cmux.sock") + } + + func testNightlyReleaseUsesDedicatedDefaultAndIgnoresAmbientSocketOverride() { + let path = SocketControlSettings.socketPath( + environment: [ + "CMUX_SOCKET_PATH": "/tmp/cmux-debug-issue-153-tmux-compat.sock", + ], + bundleIdentifier: "com.cmuxterm.app.nightly", + isDebugBuild: false + ) + + XCTAssertEqual(path, "/tmp/cmux-nightly.sock") + } + + func testDebugBundleHonorsSocketOverrideWithoutOptInFlag() { + let path = SocketControlSettings.socketPath( + environment: [ + "CMUX_SOCKET_PATH": "/tmp/cmux-debug-my-tag.sock", + ], + bundleIdentifier: "com.cmuxterm.app.debug.my-tag", + isDebugBuild: false + ) + + XCTAssertEqual(path, "/tmp/cmux-debug-my-tag.sock") + } + + func testStagingBundleHonorsSocketOverrideWithoutOptInFlag() { + let path = SocketControlSettings.socketPath( + environment: [ + "CMUX_SOCKET_PATH": "/tmp/cmux-staging-my-tag.sock", + ], + bundleIdentifier: "com.cmuxterm.app.staging.my-tag", + isDebugBuild: false + ) + + XCTAssertEqual(path, "/tmp/cmux-staging-my-tag.sock") + } + + func testStableReleaseCanOptInToSocketOverride() { + let path = SocketControlSettings.socketPath( + environment: [ + "CMUX_SOCKET_PATH": "/tmp/cmux-debug-forced.sock", + "CMUX_ALLOW_SOCKET_OVERRIDE": "1", + ], + bundleIdentifier: "com.cmuxterm.app", + isDebugBuild: false + ) + + XCTAssertEqual(path, "/tmp/cmux-debug-forced.sock") + } + + func testDefaultSocketPathByChannel() { + XCTAssertEqual( + SocketControlSettings.defaultSocketPath(bundleIdentifier: "com.cmuxterm.app", isDebugBuild: false), + "/tmp/cmux.sock" + ) + XCTAssertEqual( + SocketControlSettings.defaultSocketPath(bundleIdentifier: "com.cmuxterm.app.nightly", isDebugBuild: false), + "/tmp/cmux-nightly.sock" + ) + XCTAssertEqual( + SocketControlSettings.defaultSocketPath(bundleIdentifier: "com.cmuxterm.app.debug.tag", isDebugBuild: false), + "/tmp/cmux-debug.sock" + ) + XCTAssertEqual( + SocketControlSettings.defaultSocketPath(bundleIdentifier: "com.cmuxterm.app.staging.tag", isDebugBuild: false), + "/tmp/cmux-staging.sock" + ) + } +}