Skip to content

Commit 2f4c002

Browse files
committed
Fix crash: serialize file processing to prevent data race
UsageAggregator is an @observable class with Dictionary properties. Concurrent FSEvents callbacks spawned parallel Tasks that simultaneously mutated the same Dictionary (e.g. dailyUsage, sessions), causing swift_isUniquelyReferenced_nonNull_native to fail with SIGSEGV. Replace per-callback Task spawning with AsyncStream-based serial queue. All aggregator mutations now execute sequentially from a single consumer Task, eliminating the data race.
1 parent d7f2785 commit 2f4c002

1 file changed

Lines changed: 26 additions & 7 deletions

File tree

Sources/CCMonitor/App/AppState.swift

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,12 @@ final class AppState {
2828
private var watcher: FSEventsWatcher?
2929
private var refreshTask: Task<Void, Never>?
3030
private var saveTask: Task<Void, Never>?
31+
private var processingTask: Task<Void, Never>?
3132
private var usageStore: UsageStore?
3233

34+
/// 串行化文件处理队列,防止并发修改 aggregator 导致 crash
35+
private var fileChannel: AsyncStream<[String]>.Continuation?
36+
3337
init() {
3438
startPipeline()
3539
}
@@ -38,6 +42,8 @@ final class AppState {
3842
watcher?.stop()
3943
refreshTask?.cancel()
4044
saveTask?.cancel()
45+
processingTask?.cancel()
46+
fileChannel?.finish()
4147
// 退出时保存状态
4248
let reader = fileReader
4349
let agg = aggregator
@@ -79,7 +85,8 @@ final class AppState {
7985
return
8086
}
8187

82-
// 5. 启动 FSEvents 监控
88+
// 5. 启动串行文件处理队列 + FSEvents 监控
89+
startFileProcessingQueue()
8390
startWatcher(paths: projectDirs)
8491

8592
// 6. 启动定时刷新
@@ -160,17 +167,29 @@ final class AppState {
160167
: "Processed \(processedCount) files, \(totalEntries) new entries"
161168
}
162169

170+
/// 启动串行文件处理队列
171+
/// 所有对 aggregator 的写入都经过此队列,避免并发修改导致 crash
172+
private func startFileProcessingQueue() {
173+
let (stream, continuation) = AsyncStream<[String]>.makeStream()
174+
self.fileChannel = continuation
175+
176+
processingTask = Task { @MainActor [weak self] in
177+
for await paths in stream {
178+
guard let self else { break }
179+
for path in paths {
180+
await self.processFile(path)
181+
}
182+
self.updateViewModels()
183+
}
184+
}
185+
}
186+
163187
/// 启动 FSEvents 监控
164188
private func startWatcher(paths: [String]) {
165189
watcher = FSEventsWatcher(paths: paths) { [weak self] changedPaths in
166190
guard let self else { return }
167191
Self.logger.debug("🔄 FSEvents: \(changedPaths.count) files changed")
168-
Task { @MainActor in
169-
for path in changedPaths {
170-
await self.processFile(path)
171-
}
172-
self.updateViewModels()
173-
}
192+
self.fileChannel?.yield(changedPaths)
174193
}
175194
watcher?.start()
176195
Self.logger.info("👁️ FSEvents watcher started for \(paths.count) directories")

0 commit comments

Comments
 (0)