diff --git a/Sources/Inotify/Inotify.swift b/Sources/Inotify/Inotify.swift index 3a85681..c65ed25 100644 --- a/Sources/Inotify/Inotify.swift +++ b/Sources/Inotify/Inotify.swift @@ -24,7 +24,7 @@ public actor Inotify { guard wd >= 0 else { throw InotifyError.addWatchFailed(path: path, errno: cinotify_get_errno()) } - watches.add(path, withId: wd) + watches.add(path, withId: wd, mask: mask) return wd } @@ -39,6 +39,13 @@ public actor Inotify { 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 { guard inotify_rm_watch(self.fd, wd) == 0 else { throw InotifyError.removeWatchFailed(watchDescriptor: wd, errno: cinotify_get_errno()) @@ -50,11 +57,24 @@ public actor Inotify { 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 } + let event = InotifyEvent.init(from: rawEvent, inDirectory: path) + await self.addWatchInCaseOfAutomaticSubtreeWatching(event) 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) { let (stream, continuation) = AsyncStream.makeStream( of: RawInotifyEvent.self, diff --git a/Sources/Inotify/InotifyWatchManager.swift b/Sources/Inotify/InotifyWatchManager.swift index 4d1d926..db69f56 100644 --- a/Sources/Inotify/InotifyWatchManager.swift +++ b/Sources/Inotify/InotifyWatchManager.swift @@ -1,18 +1,46 @@ struct InotifyWatchManager { private var watchPaths: [CInt: String] = [:] + private var watchMasks: [CInt: InotifyEventMask] = [:] private var activeWatches: Set = [] + private var watchesWithAutomaticSubtreeWatching: Set = [] - mutating func add(_ path: String, withId watchDescriptor: CInt) { + mutating func add(_ path: String, withId watchDescriptor: CInt, mask: InotifyEventMask) { self.watchPaths[watchDescriptor] = path + self.watchMasks[watchDescriptor] = mask 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) { self.watchPaths.removeValue(forKey: watchDescriptor) + self.watchMasks.removeValue(forKey: watchDescriptor) self.activeWatches.remove(watchDescriptor) + self.watchesWithAutomaticSubtreeWatching.remove(watchDescriptor) } func path(forId watchDescriptor: CInt) -> String? { return self.watchPaths[watchDescriptor] } + + func mask(forId watchDescriptor: CInt) -> InotifyEventMask? { + return self.watchMasks[watchDescriptor] + } + + func isAutomaticSubtreeWatching(_ watchDescriptor: CInt) -> Bool { + return self.watchesWithAutomaticSubtreeWatching.contains(watchDescriptor) + } } diff --git a/Tests/InotifyIntegrationTests/RecursiveEventTests.swift b/Tests/InotifyIntegrationTests/RecursiveEventTests.swift index e330e29..5a012a2 100644 --- a/Tests/InotifyIntegrationTests/RecursiveEventTests.swift +++ b/Tests/InotifyIntegrationTests/RecursiveEventTests.swift @@ -13,11 +13,31 @@ struct RecursiveEventTests { let events = try await getEventsForTrigger( in: dir, mask: [.create], - recursive: true + recursive: .recursive ) { _ in 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)") } } + + @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)") + } + } } diff --git a/Tests/InotifyIntegrationTests/Utilities/getEventsForTrigger.swift b/Tests/InotifyIntegrationTests/Utilities/getEventsForTrigger.swift index 78a20e9..1f54767 100644 --- a/Tests/InotifyIntegrationTests/Utilities/getEventsForTrigger.swift +++ b/Tests/InotifyIntegrationTests/Utilities/getEventsForTrigger.swift @@ -1,16 +1,25 @@ import Inotify +enum RecursivKind { + case nonrecursive + case recursive + case withAutomaticSubtreeWatching +} + func getEventsForTrigger( in dir: String, mask: InotifyEventMask, - recursive: Bool = false, + recursive: RecursivKind = .nonrecursive, trigger: @escaping (String) async throws -> Void, ) async throws -> [InotifyEvent] { let watcher = try Inotify() - if recursive { - try await watcher.addRecursiveWatch(forDirectory: dir, mask: mask) - } else { + switch recursive { + case .nonrecursive: 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 {