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
|
// Consume events as they arrive
|
||||||
for await event in await inotify.events {
|
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`.
|
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
|
## 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
|
## 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
|
```swift
|
||||||
let inotify = try Inotify()
|
let inotify = try Inotify()
|
||||||
try inotify.addWatch(path: "/tmp/inbox", mask: [.create, .modify])
|
try inotify.addWatch(path: "/tmp/inbox", mask: [.create, .modify])
|
||||||
|
|
||||||
for await event in await inotify.events {
|
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``
|
- ``Inotify/Inotify``
|
||||||
- ``InotifyEvent``
|
- ``InotifyEvent``
|
||||||
|
- ``FileSystemEvent``
|
||||||
- ``InotifyEventMask``
|
- ``InotifyEventMask``
|
||||||
|
|
||||||
### Articles
|
### 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.
|
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? {
|
private func transform(_ rawEvent: RawInotifyEvent) async -> InotifyEvent? {
|
||||||
if rawEvent.mask.contains(.queueOverflow) {
|
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 let path = self.watches.path(forId: rawEvent.watchDescriptor) else { return nil }
|
||||||
guard !self.exclusions.excludes(rawEvent.name) 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.forgetWatchInCaseTheKernelRemovedIt(event)
|
||||||
self.removeWatchesInCaseADirectoryLeftTheTree(event)
|
self.removeWatchesInCaseADirectoryLeftTheTree(event)
|
||||||
await self.addWatchInCaseOfAutomaticSubtreeWatching(event)
|
await self.addWatchInCaseOfAutomaticSubtreeWatching(event)
|
||||||
return event
|
return .fileSystem(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The kernel reports `IN_IGNORED` once a watch is gone, whether it was
|
/// The kernel reports `IN_IGNORED` once a watch is gone, whether it was
|
||||||
/// removed explicitly or because its item was deleted or unmounted.
|
/// removed explicitly or because its item was deleted or unmounted.
|
||||||
/// Forgetting it keeps a reused descriptor number from mapping to a
|
/// Forgetting it keeps a reused descriptor number from mapping to a
|
||||||
/// stale path.
|
/// stale path.
|
||||||
private func forgetWatchInCaseTheKernelRemovedIt(_ event: InotifyEvent) {
|
private func forgetWatchInCaseTheKernelRemovedIt(_ event: FileSystemEvent) {
|
||||||
guard event.mask.contains(.ignored) else { return }
|
guard event.mask.contains(.ignored) else { return }
|
||||||
self.watches.remove(forId: event.watchDescriptor)
|
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,
|
/// A directory moved out of a watched tree keeps its kernel watches,
|
||||||
/// which would then report events under the old path. Those watches
|
/// which would then report events under the old path. Those watches
|
||||||
/// are removed instead.
|
/// are removed instead.
|
||||||
private func removeWatchesInCaseADirectoryLeftTheTree(_ event: InotifyEvent) {
|
private func removeWatchesInCaseADirectoryLeftTheTree(_ event: FileSystemEvent) {
|
||||||
guard event.mask.contains(.movedFrom), event.mask.contains(.isDir) else { return }
|
guard event.mask.contains(.movedFrom), event.mask.contains(.isDir) else { return }
|
||||||
for wd in self.watches.descriptors(under: event.path.string) {
|
for wd in self.watches.descriptors(under: event.path.string) {
|
||||||
inotify_rm_watch(self.fd, wd)
|
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,
|
guard !event.synthesized,
|
||||||
watches.isAutomaticSubtreeWatching(event.watchDescriptor),
|
watches.isAutomaticSubtreeWatching(event.watchDescriptor),
|
||||||
event.mask.contains(.isDir),
|
event.mask.contains(.isDir),
|
||||||
|
|||||||
@@ -1,37 +1,9 @@
|
|||||||
import SystemPackage
|
/// What an ``Inotify`` instance delivers: a change to a watched item, or a
|
||||||
|
/// condition that affects which changes it can deliver.
|
||||||
/// A filesystem event delivered by an ``Inotify`` instance.
|
public enum InotifyEvent: Sendable, Hashable {
|
||||||
///
|
/// A change to a watched file or directory.
|
||||||
/// When the kernel's event queue overflows, it drops events and reports a
|
case fileSystem(FileSystemEvent)
|
||||||
/// single event whose ``mask`` contains ``InotifyEventMask/queueOverflow``.
|
/// The kernel's event queue was full, so it dropped events. Consumers
|
||||||
/// Such an event belongs to no watch: its ``watchDescriptor`` is `-1` and
|
/// that must not miss changes should rescan the watched trees.
|
||||||
/// its ``path`` is empty. Consumers that must not miss changes should
|
case queueOverflow
|
||||||
/// 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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ struct InotifyLimitTests {
|
|||||||
var received = 0
|
var received = 0
|
||||||
for await event in await watcher.events {
|
for await event in await watcher.events {
|
||||||
received += 1
|
received += 1
|
||||||
if event.mask.contains(.queueOverflow) { return (event, received) }
|
if case .queueOverflow = event { return (event, received) }
|
||||||
}
|
}
|
||||||
return (nil, received)
|
return (nil, received)
|
||||||
}
|
}
|
||||||
@@ -65,9 +65,7 @@ struct InotifyLimitTests {
|
|||||||
overflowTask.cancel()
|
overflowTask.cancel()
|
||||||
let (overflow, received) = await overflowTask.value
|
let (overflow, received) = await overflowTask.value
|
||||||
|
|
||||||
#expect(overflow != nil, "Expected a queue overflow event after \(index) file creations and \(received) received events")
|
#expect(overflow == .queueOverflow, "Expected a queue overflow event after \(index) file creations and \(received) received events")
|
||||||
#expect(overflow?.watchDescriptor == -1)
|
|
||||||
#expect(overflow?.path == "")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ enum RecursivKind {
|
|||||||
case withAutomaticSubtreeWatching
|
case withAutomaticSubtreeWatching
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The file system events an instance delivers around `trigger`.
|
||||||
func getEventsForTrigger(
|
func getEventsForTrigger(
|
||||||
in dir: String,
|
in dir: String,
|
||||||
mask: InotifyEventMask,
|
mask: InotifyEventMask,
|
||||||
@@ -13,6 +14,27 @@ func getEventsForTrigger(
|
|||||||
exclude: [String] = [],
|
exclude: [String] = [],
|
||||||
excludePatterns: [String] = [],
|
excludePatterns: [String] = [],
|
||||||
trigger: @escaping (String) async throws -> Void,
|
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] {
|
) async throws -> [InotifyEvent] {
|
||||||
let watcher = try Inotify()
|
let watcher = try Inotify()
|
||||||
await watcher.exclude(names: exclude)
|
await watcher.exclude(names: exclude)
|
||||||
@@ -41,3 +63,10 @@ func getEventsForTrigger(
|
|||||||
eventTask.cancel()
|
eventTask.cancel()
|
||||||
return await eventTask.value
|
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