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:
@@ -41,7 +41,12 @@ try inotify.addWatch(path: "/tmp/watched", mask: [.create, .modify])
|
||||
|
||||
// Consume events as they arrive
|
||||
for await event in await inotify.events {
|
||||
print("Event at \(event.path): \(event.mask)")
|
||||
switch event {
|
||||
case .fileSystem(let change):
|
||||
print("Event at \(change.path): \(change.mask)")
|
||||
case .queueOverflow:
|
||||
print("The kernel dropped events; rescan if you must not miss changes.")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -125,9 +130,9 @@ Convenience combinations: `.move` (`.movedFrom` + `.movedTo`), `.close` (`.close
|
||||
|
||||
Watch flags: `.dontFollow`, `.onlyDir`, `.oneShot`.
|
||||
|
||||
Kernel-only flags returned in events: `.isDir`, `.ignored`, `.queueOverflow`, `.unmount`.
|
||||
Kernel-only flags returned in events: `.isDir`, `.ignored`, `.unmount`.
|
||||
|
||||
When the kernel queue overflows, events are lost and a single event with `.queueOverflow` is delivered instead. It has no path and a watch descriptor of `-1`; rescan the watched directories if you must not miss changes.
|
||||
When the kernel queue overflows, events are lost and `InotifyEvent.queueOverflow` is delivered instead of a file system event; rescan the watched directories if you must not miss changes.
|
||||
|
||||
## Removing a Watch
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ struct InotifyLimitTests {
|
||||
var received = 0
|
||||
for await event in await watcher.events {
|
||||
received += 1
|
||||
if event.mask.contains(.queueOverflow) { return (event, received) }
|
||||
if case .queueOverflow = event { return (event, received) }
|
||||
}
|
||||
return (nil, received)
|
||||
}
|
||||
@@ -65,9 +65,7 @@ struct InotifyLimitTests {
|
||||
overflowTask.cancel()
|
||||
let (overflow, received) = await overflowTask.value
|
||||
|
||||
#expect(overflow != nil, "Expected a queue overflow event after \(index) file creations and \(received) received events")
|
||||
#expect(overflow?.watchDescriptor == -1)
|
||||
#expect(overflow?.path == "")
|
||||
#expect(overflow == .queueOverflow, "Expected a queue overflow event after \(index) file creations and \(received) received events")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ enum RecursivKind {
|
||||
case withAutomaticSubtreeWatching
|
||||
}
|
||||
|
||||
/// The file system events an instance delivers around `trigger`.
|
||||
func getEventsForTrigger(
|
||||
in dir: String,
|
||||
mask: InotifyEventMask,
|
||||
@@ -13,6 +14,27 @@ func getEventsForTrigger(
|
||||
exclude: [String] = [],
|
||||
excludePatterns: [String] = [],
|
||||
trigger: @escaping (String) async throws -> Void,
|
||||
) async throws -> [FileSystemEvent] {
|
||||
let events = try await getInotifyEventsForTrigger(
|
||||
in: dir,
|
||||
mask: mask,
|
||||
recursive: recursive,
|
||||
exclude: exclude,
|
||||
excludePatterns: excludePatterns,
|
||||
trigger: trigger
|
||||
)
|
||||
return events.compactMap(\.fileSystemEvent)
|
||||
}
|
||||
|
||||
/// Everything an instance delivers around `trigger`, including the
|
||||
/// events that are not about a file system item.
|
||||
func getInotifyEventsForTrigger(
|
||||
in dir: String,
|
||||
mask: InotifyEventMask,
|
||||
recursive: RecursivKind = .nonrecursive,
|
||||
exclude: [String] = [],
|
||||
excludePatterns: [String] = [],
|
||||
trigger: @escaping (String) async throws -> Void,
|
||||
) async throws -> [InotifyEvent] {
|
||||
let watcher = try Inotify()
|
||||
await watcher.exclude(names: exclude)
|
||||
@@ -41,3 +63,10 @@ func getEventsForTrigger(
|
||||
eventTask.cancel()
|
||||
return await eventTask.value
|
||||
}
|
||||
|
||||
extension InotifyEvent {
|
||||
var fileSystemEvent: FileSystemEvent? {
|
||||
if case .fileSystem(let event) = self { return event }
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user