From fba4de6ab3c4196e48d537d7ef83e336216afe70 Mon Sep 17 00:00:00 2001 From: KIT64 Date: Wed, 22 Apr 2026 15:10:39 +0700 Subject: [PATCH 1/2] test: add failing test for appIcon crash when launch finishes before store init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simulates macOS 26 (build 25D771280a+) where applicationDidFinishLaunching fires before cmuxApp.init() body completes, so isApplicationFinishedLaunching() returns true while CmuxSettingsFileStore is still initializing. The existing guard in AppIconSettings.applyIcon only blocks when the flag is false; on affected macOS 26 builds the flag is already true, so applyIcon proceeds to call startAppearanceObservation → applyIconForCurrentAppearance → NSApplication APIs that crash during App.init() re-entrancy. This test currently FAILS (startObservationCallCount > 0). Co-Authored-By: Claude Sonnet 4.6 --- cmuxTests/WorkspaceUnitTests.swift | 67 ++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/cmuxTests/WorkspaceUnitTests.swift b/cmuxTests/WorkspaceUnitTests.swift index f6c3708f4f0c..38fc47cdad4f 100644 --- a/cmuxTests/WorkspaceUnitTests.swift +++ b/cmuxTests/WorkspaceUnitTests.swift @@ -844,6 +844,73 @@ final class KeyboardShortcutSettingsFileStoreTests: XCTestCase { XCTAssertEqual(dockTileNotificationCount, 0) } + func testSettingsFileStoreDoesNotApplyAppIconDuringInitEvenIfLaunchIsAlreadyFinished() throws { + // Regression test for macOS 26 (build 25D771280a+) where applicationDidFinishLaunching + // fires before cmuxApp.init() body completes, causing isApplicationFinishedLaunching() + // to return true while CmuxSettingsFileStore.init() is still running. Without the + // isInitializationComplete guard, applyIcon(.automatic) would crash by calling + // NSApplication APIs that are unsafe during App.init() re-entrancy. + let defaults = UserDefaults.standard + let previousMode = defaults.object(forKey: AppIconSettings.modeKey) + let previousBackups = defaults.data(forKey: settingsFileBackupsDefaultsKey) + defer { + if let previousMode { + defaults.set(previousMode, forKey: AppIconSettings.modeKey) + } else { + defaults.removeObject(forKey: AppIconSettings.modeKey) + } + if let previousBackups { + defaults.set(previousBackups, forKey: settingsFileBackupsDefaultsKey) + } else { + defaults.removeObject(forKey: settingsFileBackupsDefaultsKey) + } + AppIconSettings.resetLiveEnvironmentProviderForTesting() + } + + defaults.removeObject(forKey: AppIconSettings.modeKey) + defaults.removeObject(forKey: settingsFileBackupsDefaultsKey) + + let directoryURL = try makeTemporaryDirectory() + defer { try? FileManager.default.removeItem(at: directoryURL) } + + let settingsFileURL = directoryURL.appendingPathComponent("settings.json", isDirectory: false) + try writeSettingsFile( + """ + { + "app": { + "appIcon": "automatic" + } + } + """, + to: settingsFileURL + ) + + var startObservationCallCount = 0 + var stopObservationCallCount = 0 + var runtimeIconSetCount = 0 + AppIconSettings.setLiveEnvironmentProviderForTesting { + AppIconSettings.Environment( + // Simulate macOS 26: launch is already "finished" before store init runs. + isApplicationFinishedLaunching: { true }, + imageForMode: { _ in nil }, + setApplicationIconImage: { _ in runtimeIconSetCount += 1 }, + startAppearanceObservation: { startObservationCallCount += 1 }, + stopAppearanceObservation: { stopObservationCallCount += 1 }, + notifyDockTilePlugin: {} + ) + } + + _ = KeyboardShortcutSettingsFileStore( + primaryPath: settingsFileURL.path, + fallbackPath: nil, + startWatching: false + ) + + XCTAssertEqual(startObservationCallCount, 0, "startAppearanceObservation must not be called during store init") + XCTAssertEqual(stopObservationCallCount, 0) + XCTAssertEqual(runtimeIconSetCount, 0) + } + func testSettingsFileStoreRejectsModifierFreeFirstStroke() throws { let directoryURL = try makeTemporaryDirectory() defer { try? FileManager.default.removeItem(at: directoryURL) } From 7ecb658a2ecdc13590976ccaffb8a6e769ee742f Mon Sep 17 00:00:00 2001 From: KIT64 Date: Wed, 22 Apr 2026 15:11:19 +0700 Subject: [PATCH 2/2] fix: skip appIcon apply during CmuxSettingsFileStore init to fix macOS 26 crash On macOS 26 (build 25D771280a+), applicationDidFinishLaunching fires before cmuxApp.init() body completes because @NSApplicationDelegateAdaptor stored properties are initialized before the init body runs. This causes isApplicationFinishedLaunching() to return true while CmuxSettingsFileStore is still initializing, bypassing the existing guard in AppIconSettings.applyIcon. The result is a deterministic crash at startup: AppIconAppearanceObserver.applyIconForCurrentAppearance() + 648 closure #3 in static AppIconSettings.Environment.live() CmuxSettingsFileStore.applyManagedUserDefaultsValue(_:for:) CmuxSettingsFileStore.init(...) Add isInitializationComplete flag to CmuxSettingsFileStore. Both applyManagedUserDefaultsValue and restoreUserDefaultsBackup now skip AppIconSettings.applyIcon while the flag is false (i.e. during init). AppDelegate.ensureApplicationIcon() handles the initial icon apply once applicationDidFinishLaunching runs, which is already the correct path. Fixes #3099 Co-Authored-By: Claude Sonnet 4.6 --- Sources/KeyboardShortcutSettingsFileStore.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Sources/KeyboardShortcutSettingsFileStore.swift b/Sources/KeyboardShortcutSettingsFileStore.swift index 7c055ffb4b60..5ba0d4c77efe 100644 --- a/Sources/KeyboardShortcutSettingsFileStore.swift +++ b/Sources/KeyboardShortcutSettingsFileStore.swift @@ -132,6 +132,13 @@ final class CmuxSettingsFileStore { private var activeManagedUserDefaults: [String: ManagedSettingsValue] = [:] private var activeManagedCustomSettings = ManagedCustomSettings() private var isApplyingManagedSettings = false + // Set to true after the first reload() in init() completes. Used to defer + // AppIcon application: on macOS 26 (build 25D771280a+), applicationDidFinishLaunching + // can fire before cmuxApp.init() body finishes, so isApplicationFinishedLaunching() + // returns true while we're still in App.init() re-entrancy — calling NSApplication + // APIs at that point crashes. AppDelegate.ensureApplicationIcon() handles the initial + // apply once it's truly safe. + private var isInitializationComplete = false private(set) var activeSourcePath: String? init( @@ -148,6 +155,7 @@ final class CmuxSettingsFileStore { bootstrapPrimaryTemplateIfNeeded() reload() + isInitializationComplete = true guard startWatching else { return } primaryWatcher = ShortcutSettingsFileWatcher(path: primaryPath, fileManager: fileManager) { [weak self] in @@ -1189,6 +1197,7 @@ final class CmuxSettingsFileStore { let language = AppLanguage(rawValue: UserDefaults.standard.string(forKey: defaultsKey) ?? "") ?? .system LanguageSettings.apply(language) case AppIconSettings.modeKey: + guard isInitializationComplete else { break } AppIconSettings.applyIcon(AppIconSettings.resolvedMode()) default: break @@ -1235,6 +1244,7 @@ final class CmuxSettingsFileStore { let language = AppLanguage(rawValue: UserDefaults.standard.string(forKey: defaultsKey) ?? "") ?? .system LanguageSettings.apply(language) case AppIconSettings.modeKey: + guard isInitializationComplete else { break } AppIconSettings.applyIcon(AppIconSettings.resolvedMode()) default: break