Deliver an event enum with the queue overflow as its own case

The stream's element is now the enum InotifyEvent, and the struct that
describes a change to a watched item is FileSystemEvent. A queue
overflow was an event with descriptor -1 and an empty path that every
consumer had to know about; as a case, the compiler makes them handle
it. The enum is also where failed watches of a growing tree will be
reported, since no call site can catch them.
This commit is contained in:
T. R. Bernstein
2026-09-19 00:10:35 +02:00
parent f01e16e864
commit 3c852a565c
8 changed files with 94 additions and 52 deletions
+32
View File
@@ -0,0 +1,32 @@
import SystemPackage
/// A change to a watched file or directory, as delivered by an ``Inotify``
/// instance inside ``InotifyEvent/fileSystem(_:)``.
public struct FileSystemEvent: Sendable, Hashable, CustomStringConvertible {
public let watchDescriptor: Int32
public let mask: InotifyEventMask
public let cookie: UInt32
public let path: FilePath
/// Whether the event was produced by the library for an item that already
/// existed when its directory became watched, rather than by the kernel.
public let synthesized: Bool
public var description: String {
var parts = ["FileSystemEvent(wd: \(watchDescriptor), mask: \(mask), path: \"\(path)\""]
if cookie != 0 { parts.append("cookie: \(cookie)") }
return parts.joined(separator: ", ") + ")"
}
}
extension FileSystemEvent {
public init(from rawEvent: RawInotifyEvent, inDirectory path: String) {
let dirPath = FilePath(path)
self.init(
watchDescriptor: rawEvent.watchDescriptor,
mask: rawEvent.mask,
cookie: rawEvent.cookie,
path: dirPath.appending(rawEvent.name),
synthesized: rawEvent.synthesized
)
}
}
+8 -2
View File
@@ -4,14 +4,19 @@ Monitor filesystem events on Linux using modern Swift concurrency.
## Overview
The Inotify library wraps the Linux [inotify](https://man7.org/linux/man-pages/man7/inotify.7.html) API in a Swift-native interface built around actors and async sequences. You create an ``Inotify/Inotify`` actor, add watches for the paths you care about, and iterate over the ``Inotify/Inotify/events`` property to receive ``InotifyEvent`` values as they occur.
The Inotify library wraps the Linux [inotify](https://man7.org/linux/man-pages/man7/inotify.7.html) API in a Swift-native interface built around actors and async sequences. You create an ``Inotify/Inotify`` actor, add watches for the paths you care about, and iterate over the ``Inotify/Inotify/events`` property to receive ``InotifyEvent`` values as they occur. Most of them carry a ``FileSystemEvent`` describing a change to a watched item; the others tell you when the instance cannot deliver every change, such as after a kernel queue overflow.
```swift
let inotify = try Inotify()
try inotify.addWatch(path: "/tmp/inbox", mask: [.create, .modify])
for await event in await inotify.events {
print("\(event.mask) at \(event.path)")
switch event {
case .fileSystem(let change):
print("\(change.mask) at \(change.path)")
case .queueOverflow:
print("events were dropped, rescan")
}
}
```
@@ -30,6 +35,7 @@ All public types conform to `Sendable`, so they can be safely passed across conc
- ``Inotify/Inotify``
- ``InotifyEvent``
- ``FileSystemEvent``
- ``InotifyEventMask``
### Articles
@@ -31,7 +31,7 @@ let descriptors = try await inotify.addWatchWithAutomaticSubtreeWatching(
)
```
Internally this listens for `CREATE` and `MOVED_TO` events carrying the ``InotifyEventMask/isDir`` flag and installs new watches with the same mask on the subdirectory and its subtree whenever one appears. Items that already exist inside such a subdirectory are reported with ``InotifyEvent/synthesized`` set to `true`, since the kernel never produces events for them; a synthesized event may duplicate a kernel event for the same item.
Internally this listens for `CREATE` and `MOVED_TO` events carrying the ``InotifyEventMask/isDir`` flag and installs new watches with the same mask on the subdirectory and its subtree whenever one appears. Items that already exist inside such a subdirectory are reported with ``FileSystemEvent/synthesized`` set to `true`, since the kernel never produces events for them; a synthesized event may duplicate a kernel event for the same item.
When a directory is moved out of the watched tree, the watches on it and on its subdirectories are removed, so no events are reported under the stale path.
+6 -6
View File
@@ -119,22 +119,22 @@ public actor Inotify {
private func transform(_ rawEvent: RawInotifyEvent) async -> InotifyEvent? {
if rawEvent.mask.contains(.queueOverflow) {
return InotifyEvent(from: rawEvent, inDirectory: "")
return .queueOverflow
}
guard let path = self.watches.path(forId: rawEvent.watchDescriptor) else { return nil }
guard !self.exclusions.excludes(rawEvent.name) else { return nil }
let event = InotifyEvent.init(from: rawEvent, inDirectory: path)
let event = FileSystemEvent(from: rawEvent, inDirectory: path)
self.forgetWatchInCaseTheKernelRemovedIt(event)
self.removeWatchesInCaseADirectoryLeftTheTree(event)
await self.addWatchInCaseOfAutomaticSubtreeWatching(event)
return event
return .fileSystem(event)
}
/// The kernel reports `IN_IGNORED` once a watch is gone, whether it was
/// removed explicitly or because its item was deleted or unmounted.
/// Forgetting it keeps a reused descriptor number from mapping to a
/// stale path.
private func forgetWatchInCaseTheKernelRemovedIt(_ event: InotifyEvent) {
private func forgetWatchInCaseTheKernelRemovedIt(_ event: FileSystemEvent) {
guard event.mask.contains(.ignored) else { return }
self.watches.remove(forId: event.watchDescriptor)
}
@@ -142,7 +142,7 @@ public actor Inotify {
/// A directory moved out of a watched tree keeps its kernel watches,
/// which would then report events under the old path. Those watches
/// are removed instead.
private func removeWatchesInCaseADirectoryLeftTheTree(_ event: InotifyEvent) {
private func removeWatchesInCaseADirectoryLeftTheTree(_ event: FileSystemEvent) {
guard event.mask.contains(.movedFrom), event.mask.contains(.isDir) else { return }
for wd in self.watches.descriptors(under: event.path.string) {
inotify_rm_watch(self.fd, wd)
@@ -150,7 +150,7 @@ public actor Inotify {
}
}
private func addWatchInCaseOfAutomaticSubtreeWatching(_ event: InotifyEvent) async {
private func addWatchInCaseOfAutomaticSubtreeWatching(_ event: FileSystemEvent) async {
guard !event.synthesized,
watches.isAutomaticSubtreeWatching(event.watchDescriptor),
event.mask.contains(.isDir),
+8 -36
View File
@@ -1,37 +1,9 @@
import SystemPackage
/// A filesystem event delivered by an ``Inotify`` instance.
///
/// When the kernel's event queue overflows, it drops events and reports a
/// single event whose ``mask`` contains ``InotifyEventMask/queueOverflow``.
/// Such an event belongs to no watch: its ``watchDescriptor`` is `-1` and
/// its ``path`` is empty. Consumers that must not miss changes should
/// rescan the watched trees when they receive one.
public struct InotifyEvent: Sendable, Hashable, CustomStringConvertible {
public let watchDescriptor: Int32
public let mask: InotifyEventMask
public let cookie: UInt32
public let path: FilePath
/// Whether the event was produced by the library for an item that already
/// existed when its directory became watched, rather than by the kernel.
public let synthesized: Bool
public var description: String {
var parts = ["InotifyEvent(wd: \(watchDescriptor), mask: \(mask), path: \"\(path)\""]
if cookie != 0 { parts.append("cookie: \(cookie)") }
return parts.joined(separator: ", ") + ")"
}
}
extension InotifyEvent {
public init(from rawEvent: RawInotifyEvent, inDirectory path: String) {
let dirPath = FilePath(path)
self.init(
watchDescriptor: rawEvent.watchDescriptor,
mask: rawEvent.mask,
cookie: rawEvent.cookie,
path: dirPath.appending(rawEvent.name),
synthesized: rawEvent.synthesized
)
}
/// What an ``Inotify`` instance delivers: a change to a watched item, or a
/// condition that affects which changes it can deliver.
public enum InotifyEvent: Sendable, Hashable {
/// A change to a watched file or directory.
case fileSystem(FileSystemEvent)
/// The kernel's event queue was full, so it dropped events. Consumers
/// that must not miss changes should rescan the watched trees.
case queueOverflow
}