Skip to content

Isolate XPCHelperClient to the main actor - #1495

Merged
Alexander5015 merged 1 commit into
TheBoredTeam:devfrom
Dbhardwaj99:chore/xpc-client-concurrency
Aug 29, 2026
Merged

Isolate XPCHelperClient to the main actor#1495
Alexander5015 merged 1 commit into
TheBoredTeam:devfrom
Dbhardwaj99:chore/xpc-client-concurrency

Conversation

@Dbhardwaj99

@Dbhardwaj99 Dbhardwaj99 commented Aug 28, 2026

Copy link
Copy Markdown

Second step of the Swift 6 migration tracked in #1054, following #1430.

The problem

XPCHelperClient kept its connection state (remoteService, connection, lunarListener,
monitoringTask) as mutable properties on a non-isolated class, while every method reached that
state through await MainActor.run { ensureRemoteService() } and then used the returned service
outside the actor.

That sent two things across an isolation boundary on every call, which is where 56 of the
project's strict-concurrency diagnostics came from:

Count Diagnostic
19 sending 'self' risks causing data races
19 capture of 'self' with non-Sendable type 'XPCHelperClient' in a '@Sendable' closure
15 type 'RemoteXPCService<any BoringNotchXPCHelperProtocol>' does not conform to 'Sendable'
2 passing closure as a 'sending' parameter
1 static property 'shared' is not concurrency-safe

The change

Isolating the class to the main actor removes the boundary instead of annotating each crossing.
The MainActor.run hops go away, so neither self nor the service ever leaves the actor, and the
type becomes implicitly Sendable.

  • ensureRemoteService() is called directly rather than through MainActor.run
  • startMonitoringAccessibilityAuthorization uses Task instead of Task.detached so it stays
    on the actor
  • shared and init remain nonisolated, which is safe now that the type is Sendable. This
    keeps the existing call contract, so non-isolated callers such as MediaKeyInterceptor compile
    unchanged
  • requestAccessibilityAuthorization() remains nonisolated because it is fire and forget, and
    its work hops onto the actor internally
  • the deinit is removed and init made private. A main-actor class has a non-isolated deinit
    that cannot touch isolated stored properties, and for a process-lifetime singleton it was
    unreachable regardless. Teardown is already explicit in applicationWillTerminate, which calls
    stopMonitoringAccessibilityAuthorization() directly

Net effect on the file is 43 insertions and 80 deletions.

Results

Project-wide diagnostics that the compiler marks as this is an error in the Swift 6 language mode, measured on clean Debug builds before and after:

Before After
Project total 227 164
XPCHelperClient.swift 56 0
components/OSD/Managers/XPC/BrightnessManager.swift 18 10
components/OSD/Managers/LunarManager.swift 13 14

BrightnessManager improves without being touched, because it calls into this client.

LunarManager gains one: sending value of non-Sendable type 'LunarEventListener'. Now that
startLunarEventStream(listener:) is main-actor isolated, passing a non-Sendable listener into it
from a detached task is a crossing. That is a pre-existing problem in LunarManager which this
change surfaces rather than causes, and that file is already on the list for its own pass.

One practical note for planning: per-file counts shift as these land, so the remaining split is
worth re-measuring before each PR rather than working from the original table in #1054.

Testing

Tested on a locally signed build, since the ad-hoc signed helper in a stock Debug build can't be
granted Accessibility. That signing change isn't part of this PR.

Accessibility prompt works, brightness, volume and keyboard backlight all show in the notch and
take effect, toggling Replace System OSD off and on restarts the monitor fine, and quitting from
the menu exits cleanly. Didn't test the Lunar paths since I don't have Lunar installed.

Relates to #1054. Further steps remain.

@Dbhardwaj99
Dbhardwaj99 marked this pull request as draft August 28, 2026 05:29
@Dbhardwaj99
Dbhardwaj99 marked this pull request as ready for review August 28, 2026 05:41
@theboringhumane

Copy link
Copy Markdown
Member

fixed in #1496

@Alexander5015 Alexander5015 reopened this Aug 29, 2026
@Alexander5015
Alexander5015 force-pushed the chore/xpc-client-concurrency branch from 8b0833d to 6fb85db Compare August 29, 2026 16:04
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 TheBoredTeam#1054.
@Alexander5015
Alexander5015 force-pushed the chore/xpc-client-concurrency branch from 6fb85db to 693a8d0 Compare August 29, 2026 16:43
@Alexander5015
Alexander5015 merged commit 951d66d into TheBoredTeam:dev Aug 29, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants