Implement watching a path

Each inotify instance produces events for paths in its watch list. Each
item in the watch list is identified by its watch descriptor. Different
paths can be watched for different events.
This commit is contained in:
T. R. Bernstein
2026-03-11 17:58:06 +01:00
parent 098339f9d1
commit 564c409c15
5 changed files with 92 additions and 0 deletions

View File

@@ -2,6 +2,7 @@ import CInotify
public actor Inotify { public actor Inotify {
private let fd: Int32 private let fd: Int32
private var watches: [Int32: String] = [:]
public init() throws { public init() throws {
self.fd = inotify_init1(Int32(IN_NONBLOCK | IN_CLOEXEC)) 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 { deinit {
cinotify_deinit(self.fd) cinotify_deinit(self.fd)
} }

View File

@@ -2,11 +2,14 @@ import CInotify
public enum InotifyError: Error, Sendable, CustomStringConvertible { public enum InotifyError: Error, Sendable, CustomStringConvertible {
case initFailed(errno: Int32) case initFailed(errno: Int32)
case addWatchFailed(path: String, errno: Int32)
public var description: String { public var description: String {
switch self { switch self {
case .initFailed(let code): case .initFailed(let code):
"inotify_init1 failed: \(readableErrno(code))" "inotify_init1 failed: \(readableErrno(code))"
case .addWatchFailed(let path, let code):
"inotify_add_watch failed for '\(path)': \(readableErrno(code))"
} }
} }

View File

@@ -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))
}

View File

@@ -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)
}

View File

@@ -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)
}
}
}