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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user