From f38bc3b7757726f9f7fe86b36675cd13799f3452 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:02:00 -0800 Subject: [PATCH] Move LaunchServices bundle registration off main thread (#513) Fixes CMUXTERM-MACOS-DS --- Sources/AppDelegate.swift | 59 +++++++++++++++++-- cmuxTests/CmuxWebViewKeyEquivalentTests.swift | 29 +++++++++ 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 73871cad..5f4bc8d2 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -734,6 +734,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent label: "com.cmuxterm.app.sessionPersistence", qos: .utility ) + private static let launchServicesRegistrationQueue = DispatchQueue( + label: "com.cmuxterm.app.launchServicesRegistration", + qos: .utility + ) + private static func enqueueLaunchServicesRegistrationWork(_ work: @escaping @Sendable () -> Void) { + launchServicesRegistrationQueue.async(execute: work) + } private var lastSessionAutosaveFingerprint: Int? private var lastSessionAutosavePersistedAt: Date = .distantPast private var didHandleExplicitOpenIntentAtStartup = false @@ -851,7 +858,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent if !isRunningUnderXCTest { DispatchQueue.main.async { [weak self] in guard let self else { return } - self.registerLaunchServicesBundle() + self.scheduleLaunchServicesBundleRegistration() self.enforceSingleInstance() self.observeDuplicateLaunches() } @@ -5678,14 +5685,54 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } } - private func registerLaunchServicesBundle() { - let bundleURL = Bundle.main.bundleURL.standardizedFileURL - let registerStatus = LSRegisterURL(bundleURL as CFURL, true) - if registerStatus != noErr { - NSLog("LaunchServices registration failed (status: \(registerStatus)) for \(bundleURL.path)") + private func scheduleLaunchServicesBundleRegistration( + bundleURL: URL = Bundle.main.bundleURL.standardizedFileURL, + scheduler: @escaping (@escaping @Sendable () -> Void) -> Void = AppDelegate.enqueueLaunchServicesRegistrationWork, + register: @escaping (CFURL) -> OSStatus = { url in + LSRegisterURL(url, true) + }, + breadcrumb: @escaping (_ message: String, _ data: [String: Any]) -> Void = { message, data in + sentryBreadcrumb(message, category: "startup", data: data) + } + ) { + let normalizedURL = bundleURL.standardizedFileURL + breadcrumb("launchservices.register.schedule", [ + "bundlePath": normalizedURL.path + ]) + + scheduler { + let startedAt = CFAbsoluteTimeGetCurrent() + let registerStatus = register(normalizedURL as CFURL) + let durationMs = Int(((CFAbsoluteTimeGetCurrent() - startedAt) * 1000).rounded()) + + breadcrumb("launchservices.register.complete", [ + "bundlePath": normalizedURL.path, + "status": Int(registerStatus), + "durationMs": durationMs + ]) + + if registerStatus != noErr { + NSLog("LaunchServices registration failed (status: \(registerStatus)) for \(normalizedURL.path)") + } } } +#if DEBUG + func scheduleLaunchServicesBundleRegistrationForTesting( + bundleURL: URL, + scheduler: @escaping (@escaping @Sendable () -> Void) -> Void, + register: @escaping (CFURL) -> OSStatus, + breadcrumb: @escaping (_ message: String, _ data: [String: Any]) -> Void = { _, _ in } + ) { + scheduleLaunchServicesBundleRegistration( + bundleURL: bundleURL, + scheduler: scheduler, + register: register, + breadcrumb: breadcrumb + ) + } +#endif + private func enforceSingleInstance() { guard let bundleId = Bundle.main.bundleIdentifier else { return } let currentPid = ProcessInfo.processInfo.processIdentifier diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index cc30b12f..806338a9 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -684,6 +684,35 @@ final class AppDelegateWindowContextRoutingTests: XCTestCase { } } +@MainActor +final class AppDelegateLaunchServicesRegistrationTests: XCTestCase { + func testScheduleLaunchServicesRegistrationDefersRegisterWork() { + _ = NSApplication.shared + let app = AppDelegate() + + var scheduledWork: (@Sendable () -> Void)? + var registerCallCount = 0 + + app.scheduleLaunchServicesBundleRegistrationForTesting( + bundleURL: URL(fileURLWithPath: "/tmp/../tmp/cmux-launch-services-test.app"), + scheduler: { work in + scheduledWork = work + }, + register: { _ in + registerCallCount += 1 + return noErr + } + ) + + XCTAssertEqual(registerCallCount, 0, "Registration should not run inline on the startup call path") + XCTAssertNotNil(scheduledWork, "Registration work should be handed to the scheduler") + + scheduledWork?() + + XCTAssertEqual(registerCallCount, 1) + } +} + final class FocusFlashPatternTests: XCTestCase { func testFocusFlashPatternMatchesTerminalDoublePulseShape() { XCTAssertEqual(FocusFlashPattern.values, [0, 1, 0, 1, 0])