diff --git a/Sources/Inotify/Inotify.swift b/Sources/Inotify/Inotify.swift index 298c899..7e078cd 100644 --- a/Sources/Inotify/Inotify.swift +++ b/Sources/Inotify/Inotify.swift @@ -2,6 +2,7 @@ import CInotify public actor Inotify { private let fd: Int32 + private var watches: [Int32: String] = [:] public init() throws { self.fd = inotify_init1(Int32(IN_NONBLOCK | IN_CLOEXEC)) @@ -10,6 +11,16 @@ public actor Inotify { } } + @discardableResult + public func addWatch(path: String, mask: InotifyEventMask) throws -> Int32 { + let wd = inotify_add_watch(self.fd, path, mask.rawValue) + guard wd >= 0 else { + throw InotifyError.addWatchFailed(path: path, errno: cinotify_get_errno()) + } + watches[wd] = path + return wd + } + deinit { cinotify_deinit(self.fd) } diff --git a/Sources/Inotify/InotifyError.swift b/Sources/Inotify/InotifyError.swift index 3859856..824a336 100644 --- a/Sources/Inotify/InotifyError.swift +++ b/Sources/Inotify/InotifyError.swift @@ -2,11 +2,14 @@ import CInotify public enum InotifyError: Error, Sendable, CustomStringConvertible { case initFailed(errno: Int32) + case addWatchFailed(path: String, errno: Int32) public var description: String { switch self { case .initFailed(let code): "inotify_init1 failed: \(readableErrno(code))" + case .addWatchFailed(let path, let code): + "inotify_add_watch failed for '\(path)': \(readableErrno(code))" } } diff --git a/Sources/Inotify/InotifyEventMask.swift b/Sources/Inotify/InotifyEventMask.swift new file mode 100644 index 0000000..6cbd439 --- /dev/null +++ b/Sources/Inotify/InotifyEventMask.swift @@ -0,0 +1,47 @@ +import CInotify + +public struct InotifyEventMask: OptionSet, Sendable, Hashable { + public let rawValue: UInt32 + + public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + // MARK: - Watchable Events + + public static let access = InotifyEventMask(rawValue: UInt32(IN_ACCESS)) + public static let attrib = InotifyEventMask(rawValue: UInt32(IN_ATTRIB)) + public static let closeWrite = InotifyEventMask(rawValue: UInt32(IN_CLOSE_WRITE)) + public static let closeNoWrite = InotifyEventMask(rawValue: UInt32(IN_CLOSE_NOWRITE)) + public static let create = InotifyEventMask(rawValue: UInt32(IN_CREATE)) + public static let delete = InotifyEventMask(rawValue: UInt32(IN_DELETE)) + public static let deleteSelf = InotifyEventMask(rawValue: UInt32(IN_DELETE_SELF)) + public static let modify = InotifyEventMask(rawValue: UInt32(IN_MODIFY)) + public static let moveSelf = InotifyEventMask(rawValue: UInt32(IN_MOVE_SELF)) + public static let movedFrom = InotifyEventMask(rawValue: UInt32(IN_MOVED_FROM)) + public static let movedTo = InotifyEventMask(rawValue: UInt32(IN_MOVED_TO)) + public static let open = InotifyEventMask(rawValue: UInt32(IN_OPEN)) + + // MARK: - Combinations + + public static let move: InotifyEventMask = [.movedFrom, .movedTo] + public static let close: InotifyEventMask = [.closeWrite, .closeNoWrite] + public static let allEvents: InotifyEventMask = [ + .access, .attrib, .closeWrite, .closeNoWrite, + .create, .delete, .deleteSelf, .modify, + .moveSelf, .movedFrom, .movedTo, .open + ] + + // MARK: - Watch Flags + + public static let dontFollow = InotifyEventMask(rawValue: UInt32(IN_DONT_FOLLOW)) + public static let onlyDir = InotifyEventMask(rawValue: UInt32(IN_ONLYDIR)) + public static let oneShot = InotifyEventMask(rawValue: UInt32(IN_ONESHOT)) + + // MARK: - Kernel-Only Flags + + public static let isDir = InotifyEventMask(rawValue: UInt32(IN_ISDIR)) + public static let ignored = InotifyEventMask(rawValue: UInt32(IN_IGNORED)) + public static let queueOverflow = InotifyEventMask(rawValue: UInt32(IN_Q_OVERFLOW)) + public static let unmount = InotifyEventMask(rawValue: UInt32(IN_UNMOUNT)) +} diff --git a/Tests/InotifyIntegrationTests/Utilities.swift b/Tests/InotifyIntegrationTests/Utilities.swift new file mode 100644 index 0000000..1c08b65 --- /dev/null +++ b/Tests/InotifyIntegrationTests/Utilities.swift @@ -0,0 +1,10 @@ +import Foundation + +func withTempDir(_ body: (String) async throws -> Void) async throws { + let dir = FileManager.default.temporaryDirectory + .appendingPathComponent("InotifyIntegrationTests-\(UUID().uuidString)") + .path + try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(atPath: dir) } + try await body(dir) +} diff --git a/Tests/InotifyIntegrationTests/WatchTests.swift b/Tests/InotifyIntegrationTests/WatchTests.swift new file mode 100644 index 0000000..178e13d --- /dev/null +++ b/Tests/InotifyIntegrationTests/WatchTests.swift @@ -0,0 +1,21 @@ +import Testing +import Foundation +@testable import Inotify + +@Suite("Watch Management") +struct WatchTests { + @Test func addWatchReturnsValidDescriptor() async throws { + try await withTempDir { dir in + let watcher = try Inotify() + let wd = try await watcher.addWatch(path: dir, mask: .allEvents) + #expect(wd >= 0) + } + } + + @Test func addWatchOnInvalidPathThrows() async throws { + let watcher = try Inotify() + await #expect(throws: InotifyError.self) { + try await watcher.addWatch(path: "/nonexistent-\(UUID())", mask: .allEvents) + } + } +}