From 693a8d024e91c10aca6f5054866c975fdb848a15 Mon Sep 17 00:00:00 2001 From: Divyansh Date: Fri, 28 Aug 2026 09:56:41 +0530 Subject: [PATCH] Isolate XPCHelperClient to the main actor XPCHelperClient held mutable connection state on a non-isolated class while every method hopped through `await MainActor.run` to reach it. That sent both `self` and the resulting `RemoteXPCService` across an isolation boundary on each call, which accounted for 56 of the strict-concurrency diagnostics: 19 `sending 'self'`, 19 non-Sendable captures of `self` in `@Sendable` closures, 15 `RemoteXPCService` Sendable violations, and the rest. Isolating the class to the main actor removes the boundary rather than annotating each crossing. The hops disappear, so `self` and the service never leave the actor, and the type becomes implicitly Sendable. - `ensureRemoteService()` is now called directly instead of via `MainActor.run` - `startMonitoringAccessibilityAuthorization` uses `Task` rather than `Task.detached`, so it stays on the actor - `shared` and `init` stay `nonisolated`, which is safe now that the type is Sendable, preserving the existing call contract for non-isolated callers - `requestAccessibilityAuthorization()` stays `nonisolated` since it is fire-and-forget and its work hops onto the actor internally - the singleton's `deinit` is removed and `init` made private; a process-lifetime singleton never deinits, and teardown is already explicit in `applicationWillTerminate` Relates to #1054. --- .../XPCHelperClient/XPCHelperClient.swift | 123 ++++++------------ 1 file changed, 43 insertions(+), 80 deletions(-) diff --git a/boringNotch/XPCHelperClient/XPCHelperClient.swift b/boringNotch/XPCHelperClient/XPCHelperClient.swift index e3ad4b0fc..1c9bce831 100644 --- a/boringNotch/XPCHelperClient/XPCHelperClient.swift +++ b/boringNotch/XPCHelperClient/XPCHelperClient.swift @@ -2,9 +2,14 @@ import Foundation import Cocoa import AsyncXPCConnection +@MainActor final class XPCHelperClient: NSObject { nonisolated static let shared = XPCHelperClient() + nonisolated private override init() { + super.init() + } + private let serviceName = "theboringteam.boringnotch.BoringNotchXPCHelper" private var remoteService: RemoteXPCService? @@ -14,14 +19,8 @@ final class XPCHelperClient: NSObject { private var lunarListener: BoringNotchXPCHelperLunarListener? private var hasLunarListener: Bool = false - deinit { - connection?.invalidate() - stopMonitoringAccessibilityAuthorization() - } - // MARK: - Connection Management (Main Actor Isolated) - @MainActor private func ensureRemoteService(needsListener: Bool = false) -> RemoteXPCService { if let existing = remoteService, (!needsListener || hasLunarListener) { return existing @@ -72,7 +71,6 @@ final class XPCHelperClient: NSObject { return service } - @MainActor private func getRemoteService() -> RemoteXPCService? { remoteService } @@ -88,7 +86,6 @@ final class XPCHelperClient: NSObject { return interface } - @MainActor private func notifyAuthorizationChange(_ granted: Bool) { guard lastKnownAuthorization != granted else { return } lastKnownAuthorization = granted @@ -100,10 +97,10 @@ final class XPCHelperClient: NSObject { } // MARK: - Monitoring - nonisolated func startMonitoringAccessibilityAuthorization(every interval: TimeInterval = 3.0) { + func startMonitoringAccessibilityAuthorization(every interval: TimeInterval = 3.0) { // Ensure only one monitor exists stopMonitoringAccessibilityAuthorization() - monitoringTask = Task.detached { [weak self] in + monitoringTask = Task { [weak self] in guard let self = self else { return } while !Task.isCancelled { // Call the helper method periodically which will notify on change @@ -115,7 +112,7 @@ final class XPCHelperClient: NSObject { } } - nonisolated func stopMonitoringAccessibilityAuthorization() { + func stopMonitoringAccessibilityAuthorization() { monitoringTask?.cancel() monitoringTask = nil } @@ -127,49 +124,41 @@ final class XPCHelperClient: NSObject { // MARK: - Accessibility + // Fire-and-forget: callers invoke this from non-isolated contexts, and the work + // itself hops onto the main actor. nonisolated func requestAccessibilityAuthorization() { - Task { - let service = await MainActor.run { - ensureRemoteService() - } + Task { @MainActor in + let service = ensureRemoteService() try? await service.withService { service in service.requestAccessibilityAuthorization() } } } - nonisolated func isAccessibilityAuthorized() async -> Bool { + func isAccessibilityAuthorized() async -> Bool { do { - let service = await MainActor.run { - ensureRemoteService() - } + let service = ensureRemoteService() let result: Bool = try await service.withContinuation { service, continuation in service.isAccessibilityAuthorized { authorized in continuation.resume(returning: authorized) } } - await MainActor.run { - notifyAuthorizationChange(result) - } + notifyAuthorizationChange(result) return result } catch { return false } } - nonisolated func ensureAccessibilityAuthorization(promptIfNeeded: Bool) async -> Bool { + func ensureAccessibilityAuthorization(promptIfNeeded: Bool) async -> Bool { do { - let service = await MainActor.run { - ensureRemoteService() - } + let service = ensureRemoteService() let result: Bool = try await service.withContinuation { service, continuation in service.ensureAccessibilityAuthorization(promptIfNeeded) { authorized in continuation.resume(returning: authorized) } } - await MainActor.run { - notifyAuthorizationChange(result) - } + notifyAuthorizationChange(result) return result } catch { return false @@ -178,11 +167,9 @@ final class XPCHelperClient: NSObject { // MARK: - Keyboard Brightness - nonisolated func isKeyboardBrightnessAvailable() async -> Bool { + func isKeyboardBrightnessAvailable() async -> Bool { do { - let service = await MainActor.run { - ensureRemoteService() - } + let service = ensureRemoteService() return try await service.withContinuation { service, continuation in service.isKeyboardBrightnessAvailable { available in continuation.resume(returning: available) @@ -193,11 +180,9 @@ final class XPCHelperClient: NSObject { } } - nonisolated func currentKeyboardBrightness() async -> Float? { + func currentKeyboardBrightness() async -> Float? { do { - let service = await MainActor.run { - ensureRemoteService() - } + let service = ensureRemoteService() let result: NSNumber? = try await service.withContinuation { service, continuation in service.currentKeyboardBrightness { value in continuation.resume(returning: value) @@ -209,11 +194,9 @@ final class XPCHelperClient: NSObject { } } - nonisolated func setKeyboardBrightness(_ value: Float) async -> Bool { + func setKeyboardBrightness(_ value: Float) async -> Bool { do { - let service = await MainActor.run { - ensureRemoteService() - } + let service = ensureRemoteService() return try await service.withContinuation { service, continuation in service.setKeyboardBrightness(value) { success in continuation.resume(returning: success) @@ -226,11 +209,9 @@ final class XPCHelperClient: NSObject { // MARK: - Screen Brightness - nonisolated func isScreenBrightnessAvailable() async -> Bool { + func isScreenBrightnessAvailable() async -> Bool { do { - let service = await MainActor.run { - ensureRemoteService() - } + let service = ensureRemoteService() return try await service.withContinuation { service, continuation in service.isScreenBrightnessAvailable { available in continuation.resume(returning: available) @@ -241,11 +222,9 @@ final class XPCHelperClient: NSObject { } } - nonisolated func currentScreenBrightness() async -> Float? { + func currentScreenBrightness() async -> Float? { do { - let service = await MainActor.run { - ensureRemoteService() - } + let service = ensureRemoteService() let result: NSNumber? = try await service.withContinuation { service, continuation in service.currentScreenBrightness { value in continuation.resume(returning: value) @@ -257,11 +236,9 @@ final class XPCHelperClient: NSObject { } } - nonisolated func displayIDForBrightness() async -> CGDirectDisplayID? { + func displayIDForBrightness() async -> CGDirectDisplayID? { do { - let service = await MainActor.run { - ensureRemoteService() - } + let service = ensureRemoteService() let result: NSNumber? = try await service.withContinuation { service, continuation in service.displayIDForBrightness(with: { value in continuation.resume(returning: value) @@ -274,11 +251,9 @@ final class XPCHelperClient: NSObject { } } - nonisolated func setScreenBrightness(_ value: Float) async -> Bool { + func setScreenBrightness(_ value: Float) async -> Bool { do { - let service = await MainActor.run { - ensureRemoteService() - } + let service = ensureRemoteService() return try await service.withContinuation { service, continuation in service.setScreenBrightness(value) { success in continuation.resume(returning: success) @@ -288,11 +263,9 @@ final class XPCHelperClient: NSObject { return false } } - nonisolated func adjustScreenBrightness(by value: Float) async -> Bool { + func adjustScreenBrightness(by value: Float) async -> Bool { do { - let service = await MainActor.run { - ensureRemoteService() - } + let service = ensureRemoteService() return try await service.withContinuation { service, continuation in service.adjustScreenBrightness(by: value) { success in continuation.resume(returning: success) @@ -305,11 +278,9 @@ final class XPCHelperClient: NSObject { // MARK: - Lunar Events - nonisolated func isLunarAvailable() async -> Bool { + func isLunarAvailable() async -> Bool { do { - let service = await MainActor.run { - ensureRemoteService() - } + let service = ensureRemoteService() return try await service.withContinuation { service, continuation in service.isLunarAvailable { available in continuation.resume(returning: available) @@ -320,14 +291,10 @@ final class XPCHelperClient: NSObject { } } - nonisolated func startLunarEventStream(listener: BoringNotchXPCHelperLunarListener) async -> Bool { - await MainActor.run { - lunarListener = listener - } + func startLunarEventStream(listener: BoringNotchXPCHelperLunarListener) async -> Bool { + lunarListener = listener do { - let service = await MainActor.run { - ensureRemoteService(needsListener: true) - } + let service = ensureRemoteService(needsListener: true) return try await service.withContinuation { service, continuation in service.startLunarEventStream { started in continuation.resume(returning: started) @@ -338,11 +305,9 @@ final class XPCHelperClient: NSObject { } } - nonisolated func stopLunarEventStream() async { + func stopLunarEventStream() async { do { - let service = await MainActor.run { - ensureRemoteService(needsListener: true) - } + let service = ensureRemoteService(needsListener: true) try await service.withService { service in service.stopLunarEventStream() } @@ -351,11 +316,9 @@ final class XPCHelperClient: NSObject { } } - nonisolated func setLunarOSDHidden(_ hide: Bool) async -> Bool { + func setLunarOSDHidden(_ hide: Bool) async -> Bool { do { - let service = await MainActor.run { - ensureRemoteService() - } + let service = ensureRemoteService() return try await service.withContinuation { service, continuation in service.setLunarOSDHidden(hide) { ok in continuation.resume(returning: ok)