Skip to content

Commit 60be0fa

Browse files
committed
fix: stop settings hang caused by blocking XPC call on main thread
SMAppService.mainApp.status makes a synchronous XPC call to smd that can block for 5+ seconds. The async version was already written and used in init/bootstrapDefaultPreference, but onAppear was calling the sync refreshStatus() instead. Switch to refreshStatusAsync() so the XPC call runs off the main actor. Confirmed via `sample`: 100% of samples during the hang were blocked in mach_msg2_trap inside SMAppService's XPC pipe.
1 parent dea1d3a commit 60be0fa

2 files changed

Lines changed: 6 additions & 15 deletions

File tree

Dayflow/Dayflow/System/LaunchAtLoginManager.swift

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,9 @@ final class LaunchAtLoginManager: ObservableObject {
2727
}
2828

2929
/// Re-sync with System Settings, e.g. if the user adds/removes Dayflow manually.
30-
/// This is the synchronous version for use in setEnabled() after user action.
31-
func refreshStatus() {
32-
let status = SMAppService.mainApp.status
33-
let enabled = (status == .enabled)
34-
if isEnabled != enabled {
35-
logger.debug(
36-
"Launch at login status changed → \(enabled ? "enabled" : "disabled") [status=\(String(describing: status))]"
37-
)
38-
}
39-
isEnabled = enabled
40-
}
41-
42-
/// Async version that runs the XPC call off the main actor to avoid blocking
43-
private func refreshStatusAsync() async {
30+
/// Callers should fire-and-forget via `Task { await refreshStatusAsync() }` or
31+
/// the SwiftUI `.task {}` modifier — never call this directly from a synchronous context.
32+
func refreshStatusAsync() async {
4433
// Run XPC call on background thread
4534
let status = await Task.detached(priority: .utility) {
4635
SMAppService.mainApp.status

Dayflow/Dayflow/Views/UI/SettingsView.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@ struct SettingsView: View {
102102
otherViewModel.refreshAnalyticsState()
103103
storageViewModel.refreshStorageIfNeeded(isStorageTab: selectedTab == .storage)
104104
AnalyticsService.shared.capture("settings_opened")
105-
launchAtLoginManager.refreshStatus()
105+
}
106+
.task {
107+
await launchAtLoginManager.refreshStatusAsync()
106108
}
107109
.onChange(of: selectedTab) { _, newValue in
108110
if newValue == .storage {

0 commit comments

Comments
 (0)