




















@@ -2,11 +2,101 @@ import Foundation
2233final class CanvasFileWatcher: @unchecked Sendable, SimpleFileWatcherOwner {
44let watcher: SimpleFileWatcher
5+private let pollingWatcher: PollingDirectoryWatcher
5667init(url: URL, onChange: @escaping () -> Void) {
78self.watcher = SimpleFileWatcher(CoalescingFSEventsWatcher(
89 paths: [url.path],
910 queueLabel: "ai.openclaw.canvaswatcher",
1011 onChange: onChange))
12+self.pollingWatcher = PollingDirectoryWatcher(
13+ url: url,
14+ queueLabel: "ai.openclaw.canvaswatcher.poll",
15+ onChange: onChange)
16+}
17+18+func start() {
19+self.watcher.start()
20+self.pollingWatcher.start()
21+}
22+23+func stop() {
24+self.watcher.stop()
25+self.pollingWatcher.stop()
26+}
27+}
28+29+private final class PollingDirectoryWatcher: @unchecked Sendable {
30+private struct FileSignature: Equatable {
31+let modifiedAt: TimeInterval
32+let size: Int
33+}
34+35+private let url: URL
36+private let queue: DispatchQueue
37+private let onChange: () -> Void
38+private var timer: DispatchSourceTimer?
39+private var lastSnapshot: [String: FileSignature] = [:]
40+41+init(url: URL, queueLabel: String, onChange: @escaping () -> Void) {
42+self.url = url
43+self.queue = DispatchQueue(label: queueLabel)
44+self.onChange = onChange
45+}
46+47+deinit {
48+self.stop()
49+}
50+51+func start() {
52+self.queue.sync {
53+guard self.timer == nil else { return }
54+self.lastSnapshot = self.snapshot()
55+56+let timer = DispatchSource.makeTimerSource(queue: self.queue)
57+ timer.schedule(deadline: .now() + 0.15, repeating: 0.25)
58+ timer.setEventHandler { [weak self] in
59+self?.poll()
60+}
61+self.timer = timer
62+ timer.resume()
63+}
64+}
65+66+func stop() {
67+self.queue.sync {
68+self.timer?.cancel()
69+self.timer = nil
70+self.lastSnapshot = [:]
71+}
72+}
73+74+private func poll() {
75+let next = self.snapshot()
76+guard next != self.lastSnapshot else { return }
77+self.lastSnapshot = next
78+self.onChange()
79+}
80+81+private func snapshot() -> [String: FileSignature] {
82+let keys: [URLResourceKey] = [.contentModificationDateKey, .fileSizeKey, .isRegularFileKey]
83+guard let enumerator = FileManager.default.enumerator(
84+ at: self.url,
85+ includingPropertiesForKeys: keys,
86+ options: [.skipsPackageDescendants])
87+else { return [:] }
88+89+var result: [String: FileSignature] = [:]
90+for case let fileURL as URL in enumerator {
91+guard let values = try? fileURL.resourceValues(forKeys: Set(keys)),
92+ values.isRegularFile == true
93+else { continue }
94+95+let relativePath = String(fileURL.path.dropFirst(self.url.path.count + 1))
96+result[relativePath] = FileSignature(
97+ modifiedAt: values.contentModificationDate?.timeIntervalSinceReferenceDate ?? 0,
98+ size: values.fileSize ?? 0)
99+}
100+return result
11101}
12102}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。