Skip to content

Instantly share code, notes, and snippets.

@moreaki
Created March 15, 2026 01:08
Show Gist options
  • Select an option

  • Save moreaki/79fb012a1349c3a860ed11d09efcfc43 to your computer and use it in GitHub Desktop.

Select an option

Save moreaki/79fb012a1349c3a860ed11d09efcfc43 to your computer and use it in GitHub Desktop.
Issue #480 investigation scripts: root-only DispatchSource watcher vs recursive FSEvents
import Foundation
import Dispatch
let fileManager = FileManager.default
let rootURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
.appendingPathComponent("latest-watch-test-\(UUID().uuidString)", isDirectory: true)
let nestedURL = rootURL.appendingPathComponent("Browsers", isDirectory: true)
try fileManager.createDirectory(at: nestedURL, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: rootURL) }
let descriptor = open((rootURL as NSURL).fileSystemRepresentation, O_EVTONLY)
guard descriptor != -1 else {
fputs("failed to open root directory\n", stderr)
exit(1)
}
enum Phase {
case nested
case root
}
var phase = Phase.nested
var nestedTriggered = false
var rootTriggered = false
let done = DispatchGroup()
done.enter()
let source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: descriptor, eventMask: .write, queue: .global())
source.setEventHandler {
switch phase {
case .nested:
nestedTriggered = true
case .root:
rootTriggered = true
}
}
source.setCancelHandler {
close(descriptor)
}
source.activate()
func waitForEvent(seconds: TimeInterval) {
let deadline = Date().addingTimeInterval(seconds)
while Date() < deadline {
usleep(50_000)
}
}
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
let nestedFile = nestedURL.appendingPathComponent("Firefox.txt")
fileManager.createFile(atPath: nestedFile.path, contents: Data("test".utf8))
waitForEvent(seconds: 1.0)
phase = .root
let rootFile = rootURL.appendingPathComponent("top-level.txt")
fileManager.createFile(atPath: rootFile.path, contents: Data("test".utf8))
waitForEvent(seconds: 1.0)
source.cancel()
done.leave()
}
done.wait()
print("nested_change_triggered=\(nestedTriggered)")
print("root_change_triggered=\(rootTriggered)")
import Foundation
import CoreServices
final class Watcher {
private var stream: FSEventStreamRef?
private let callback: ([String]) -> Void
init(path: String, callback: @escaping ([String]) -> Void) {
self.callback = callback
var context = FSEventStreamContext(
version: 0,
info: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()),
retain: nil,
release: nil,
copyDescription: nil
)
let flags = UInt32(kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagFileEvents)
stream = FSEventStreamCreate(nil, { _, info, _, pathsPointer, _, _ in
let watcher = Unmanaged<Watcher>.fromOpaque(info!).takeUnretainedValue()
let paths = unsafeBitCast(pathsPointer, to: CFArray.self) as! [String]
watcher.callback(paths)
}, &context, [path] as CFArray, FSEventStreamEventId(kFSEventStreamEventIdSinceNow), 0.1, flags)
FSEventStreamSetDispatchQueue(stream!, .global())
FSEventStreamStart(stream!)
}
deinit {
if let stream {
FSEventStreamStop(stream)
FSEventStreamInvalidate(stream)
FSEventStreamRelease(stream)
}
}
}
let fileManager = FileManager.default
let rootURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
.appendingPathComponent("fsevent-test-\(UUID().uuidString)", isDirectory: true)
let nestedURL = rootURL.appendingPathComponent("sub", isDirectory: true)
try fileManager.createDirectory(at: nestedURL, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: rootURL) }
let semaphore = DispatchSemaphore(value: 0)
let watcher = Watcher(path: rootURL.path) { paths in
print("paths=\(paths)")
semaphore.signal()
}
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
let nestedFile = nestedURL.appendingPathComponent("a.txt")
fileManager.createFile(atPath: nestedFile.path, contents: Data())
}
let result = semaphore.wait(timeout: .now() + 3.0)
print(result == .success ? "event" : "timeout")
_ = watcher
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment