Implement auto subtree watching of a directory
Allow recursively watching a directory while adding newly created subdirectories to the inotify watch list.
This commit is contained in:
@@ -24,7 +24,7 @@ public actor Inotify {
|
|||||||
guard wd >= 0 else {
|
guard wd >= 0 else {
|
||||||
throw InotifyError.addWatchFailed(path: path, errno: cinotify_get_errno())
|
throw InotifyError.addWatchFailed(path: path, errno: cinotify_get_errno())
|
||||||
}
|
}
|
||||||
watches.add(path, withId: wd)
|
watches.add(path, withId: wd, mask: mask)
|
||||||
return wd
|
return wd
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +39,13 @@ public actor Inotify {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
public func addWatchWithAutomaticSubtreeWatching(forDirectory path: String, mask: InotifyEventMask) async throws -> [CInt] {
|
||||||
|
let wds = try await self.addRecursiveWatch(forDirectory: path, mask: mask)
|
||||||
|
watches.enableAutomaticSubtreeWatching(forIds: wds)
|
||||||
|
return wds
|
||||||
|
}
|
||||||
|
|
||||||
public func removeWatch(_ wd: CInt) throws {
|
public func removeWatch(_ wd: CInt) throws {
|
||||||
guard inotify_rm_watch(self.fd, wd) == 0 else {
|
guard inotify_rm_watch(self.fd, wd) == 0 else {
|
||||||
throw InotifyError.removeWatchFailed(watchDescriptor: wd, errno: cinotify_get_errno())
|
throw InotifyError.removeWatchFailed(watchDescriptor: wd, errno: cinotify_get_errno())
|
||||||
@@ -50,11 +57,24 @@ public actor Inotify {
|
|||||||
cinotify_deinit(self.fd)
|
cinotify_deinit(self.fd)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func transform(_ rawEvent: RawInotifyEvent) -> InotifyEvent? {
|
private func transform(_ rawEvent: RawInotifyEvent) async -> InotifyEvent? {
|
||||||
guard let path = self.watches.path(forId: rawEvent.watchDescriptor) else { return nil }
|
guard let path = self.watches.path(forId: rawEvent.watchDescriptor) else { return nil }
|
||||||
|
let event = InotifyEvent.init(from: rawEvent, inDirectory: path)
|
||||||
|
await self.addWatchInCaseOfAutomaticSubtreeWatching(event)
|
||||||
return InotifyEvent.init(from: rawEvent, inDirectory: path)
|
return InotifyEvent.init(from: rawEvent, inDirectory: path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func addWatchInCaseOfAutomaticSubtreeWatching(_ event: InotifyEvent) async {
|
||||||
|
guard watches.isAutomaticSubtreeWatching(event.watchDescriptor),
|
||||||
|
event.mask.contains(.create),
|
||||||
|
event.mask.contains(.isDir) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let mask = self.watches.mask(forId: event.watchDescriptor) else { return }
|
||||||
|
let _ = try? await self.addWatchWithAutomaticSubtreeWatching(forDirectory: event.path.string, mask: mask)
|
||||||
|
}
|
||||||
|
|
||||||
private static func createEventReader(forFileDescriptor fd: CInt) -> (any DispatchSourceRead, AsyncStream<RawInotifyEvent>) {
|
private static func createEventReader(forFileDescriptor fd: CInt) -> (any DispatchSourceRead, AsyncStream<RawInotifyEvent>) {
|
||||||
let (stream, continuation) = AsyncStream<RawInotifyEvent>.makeStream(
|
let (stream, continuation) = AsyncStream<RawInotifyEvent>.makeStream(
|
||||||
of: RawInotifyEvent.self,
|
of: RawInotifyEvent.self,
|
||||||
|
|||||||
@@ -1,18 +1,46 @@
|
|||||||
struct InotifyWatchManager {
|
struct InotifyWatchManager {
|
||||||
private var watchPaths: [CInt: String] = [:]
|
private var watchPaths: [CInt: String] = [:]
|
||||||
|
private var watchMasks: [CInt: InotifyEventMask] = [:]
|
||||||
private var activeWatches: Set<CInt> = []
|
private var activeWatches: Set<CInt> = []
|
||||||
|
private var watchesWithAutomaticSubtreeWatching: Set<CInt> = []
|
||||||
|
|
||||||
mutating func add(_ path: String, withId watchDescriptor: CInt) {
|
mutating func add(_ path: String, withId watchDescriptor: CInt, mask: InotifyEventMask) {
|
||||||
self.watchPaths[watchDescriptor] = path
|
self.watchPaths[watchDescriptor] = path
|
||||||
|
self.watchMasks[watchDescriptor] = mask
|
||||||
self.activeWatches.insert(watchDescriptor)
|
self.activeWatches.insert(watchDescriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutating func enableAutomaticSubtreeWatching(forId watchDescriptor: CInt) {
|
||||||
|
assert(self.activeWatches.contains(watchDescriptor))
|
||||||
|
self.watchesWithAutomaticSubtreeWatching.insert(watchDescriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func enableAutomaticSubtreeWatching(forIds watchDescriptors: CInt...) {
|
||||||
|
self.enableAutomaticSubtreeWatching(forIds: watchDescriptors)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func enableAutomaticSubtreeWatching(forIds watchDescriptors: [CInt]) {
|
||||||
|
for watchDescriptor in watchDescriptors {
|
||||||
|
self.enableAutomaticSubtreeWatching(forId: watchDescriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mutating func remove(forId watchDescriptor: CInt) {
|
mutating func remove(forId watchDescriptor: CInt) {
|
||||||
self.watchPaths.removeValue(forKey: watchDescriptor)
|
self.watchPaths.removeValue(forKey: watchDescriptor)
|
||||||
|
self.watchMasks.removeValue(forKey: watchDescriptor)
|
||||||
self.activeWatches.remove(watchDescriptor)
|
self.activeWatches.remove(watchDescriptor)
|
||||||
|
self.watchesWithAutomaticSubtreeWatching.remove(watchDescriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func path(forId watchDescriptor: CInt) -> String? {
|
func path(forId watchDescriptor: CInt) -> String? {
|
||||||
return self.watchPaths[watchDescriptor]
|
return self.watchPaths[watchDescriptor]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mask(forId watchDescriptor: CInt) -> InotifyEventMask? {
|
||||||
|
return self.watchMasks[watchDescriptor]
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAutomaticSubtreeWatching(_ watchDescriptor: CInt) -> Bool {
|
||||||
|
return self.watchesWithAutomaticSubtreeWatching.contains(watchDescriptor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,31 @@ struct RecursiveEventTests {
|
|||||||
let events = try await getEventsForTrigger(
|
let events = try await getEventsForTrigger(
|
||||||
in: dir,
|
in: dir,
|
||||||
mask: [.create],
|
mask: [.create],
|
||||||
recursive: true
|
recursive: .recursive
|
||||||
) { _ in try createFile(at: "\(filepath)", contents: "hello") }
|
) { _ in try createFile(at: "\(filepath)", contents: "hello") }
|
||||||
|
|
||||||
let createEvent = events.first { $0.mask.contains(.create) && $0.path.string == filepath }
|
let createEvent = events.first { $0.mask.contains(.create) && $0.path.string == filepath }
|
||||||
#expect(createEvent != nil, "Expected CREATE for '\(filepath)', got: \(events)")
|
#expect(createEvent != nil, "Expected CREATE for '\(filepath)', got: \(events)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func newSubfoldersOfRecursiveWatchAreAutomaticallyWatchedToo() async throws {
|
||||||
|
try await withTempDir { dir in
|
||||||
|
let subDirectory = "\(dir)/Subfolder"
|
||||||
|
let filepath = "\(subDirectory)/modify-target.txt"
|
||||||
|
|
||||||
|
let events = try await getEventsForTrigger(
|
||||||
|
in: dir,
|
||||||
|
mask: [.create],
|
||||||
|
recursive: .withAutomaticSubtreeWatching
|
||||||
|
) { _ in
|
||||||
|
try FileManager.default.createDirectory(atPath: subDirectory, withIntermediateDirectories: true)
|
||||||
|
try await Task.sleep(for: .milliseconds(200))
|
||||||
|
try createFile(at: "\(filepath)", contents: "hello")
|
||||||
|
}
|
||||||
|
|
||||||
|
let createEvent = events.first { $0.mask.contains(.create) && $0.path.string == filepath }
|
||||||
|
#expect(createEvent != nil, "Expected CREATE for '\(filepath)', got: \(events)")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
import Inotify
|
import Inotify
|
||||||
|
|
||||||
|
enum RecursivKind {
|
||||||
|
case nonrecursive
|
||||||
|
case recursive
|
||||||
|
case withAutomaticSubtreeWatching
|
||||||
|
}
|
||||||
|
|
||||||
func getEventsForTrigger(
|
func getEventsForTrigger(
|
||||||
in dir: String,
|
in dir: String,
|
||||||
mask: InotifyEventMask,
|
mask: InotifyEventMask,
|
||||||
recursive: Bool = false,
|
recursive: RecursivKind = .nonrecursive,
|
||||||
trigger: @escaping (String) async throws -> Void,
|
trigger: @escaping (String) async throws -> Void,
|
||||||
) async throws -> [InotifyEvent] {
|
) async throws -> [InotifyEvent] {
|
||||||
let watcher = try Inotify()
|
let watcher = try Inotify()
|
||||||
if recursive {
|
switch recursive {
|
||||||
try await watcher.addRecursiveWatch(forDirectory: dir, mask: mask)
|
case .nonrecursive:
|
||||||
} else {
|
|
||||||
try await watcher.addWatch(path: dir, mask: mask)
|
try await watcher.addWatch(path: dir, mask: mask)
|
||||||
|
case .recursive:
|
||||||
|
try await watcher.addRecursiveWatch(forDirectory: dir, mask: mask)
|
||||||
|
case .withAutomaticSubtreeWatching:
|
||||||
|
try await watcher.addWatchWithAutomaticSubtreeWatching(forDirectory: dir, mask: mask)
|
||||||
}
|
}
|
||||||
|
|
||||||
let eventTask = Task {
|
let eventTask = Task {
|
||||||
|
|||||||
Reference in New Issue
Block a user