Extending an automatically watched tree to a new directory swallowed every error, so a reached watch limit or an unreadable directory left part of the tree unwatched without any sign. No call of the consumer is running at that moment, so the failures now arrive in the event stream as InotifyEvent.watchFailed, after the event that triggered the extension. The library watches what it can first: a reached limit ends the attempt, an unreadable directory is skipped with its subtree, and a directory that vanished in between is not reported. The buffer now carries the library's own events next to the kernel's, which keeps them in order and hides the stream's element type. The test runner drops root's DAC capabilities so that an unreadable directory can be tested.
Inotify
A Swift wrapper around the Linux inotify API, built on modern Swift concurrency. It lets you watch individual files or directories for filesystem events, recursively monitor entire subtrees, and optionally have newly created subdirectories watched automatically.
Events are delivered as an AsyncSequence, so you can consume them with a simple for await loop.
Adding Inotify to Your Project
Add the package dependency in your Package.swift:
dependencies: [
.package(url: "https://github.com/astzweig/swift-inotify.git", from: "1.0.0")
]
Then add Inotify to your target's dependencies:
.target(
...
dependencies: [
.product(name: "Inotify", package: "swift-inotify")
]
...
)
Quick Start
import Inotify
let inotify = try Inotify()
// Watch a single file for modifications
try inotify.addWatch(path: "/tmp/some-existing-file.txt", mask: [.modify])
// Watch a single directory for file creations and modifications
try inotify.addWatch(path: "/tmp/watched", mask: [.create, .modify])
// Consume events as they arrive
for await event in await inotify.events {
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.")
case .watchFailed(let path, let error):
print("Changes below \(path) go unreported: \(error)")
}
}
Watching Subtrees
Inotify operates on individual watch descriptors, so monitoring a directory does not automatically cover its children. This library provides two convenience methods that handle the recursion for you.
Recursive Watch
addRecursiveWatch walks the directory tree at setup time and installs a watch on every existing subdirectory:
try await inotify.addRecursiveWatch(
forDirectory: "/home/user/project",
mask: [.create, .modify, .delete]
)
Subdirectories created after the call are not watched.
Automatic Subtree Watching
addWatchWithAutomaticSubtreeWatching does everything addRecursiveWatch does, and additionally listens for CREATE and MOVED_TO events with the isDir flag. Whenever a subdirectory appears, whether created or moved in, a watch is installed on it and on its subdirectories automatically:
try await inotify.addWatchWithAutomaticSubtreeWatching(
forDirectory: "/home/user/project",
mask: [.create, .modify, .delete]
)
This is the most convenient option when you need full coverage of a growing directory tree.
Items that already exist inside a directory that appears this way never produce kernel events. The library reports them as if they had just appeared, using the same kind of event (CREATE or MOVED_TO), with synthesized set to true. A synthesized event may duplicate a kernel event for the same item, so consumers that act on events should tolerate seeing an item twice.
When a watched directory is moved out of the tree, the watches on it and on its subdirectories are removed, so no events are reported under the stale path.
Extending the watch to a new directory can fail, typically because the user's watch limit (fs.inotify.max_user_watches) is reached or the directory is not readable. The library then watches what it can and delivers InotifyEvent.watchFailed(path:error:) for each directory it could not watch, so changes below that path are known to go unreported. A directory that vanished before it could be watched is not reported. The explicit addRecursiveWatch and addWatchWithAutomaticSubtreeWatching calls, by contrast, either watch the whole tree or throw and leave no watch behind.
Excluding Items
You can tell the Inotify actor to ignore certain file or directory names, either exactly or by a shell pattern. Excluded items are skipped during recursive directory resolution (so no watch is installed on them), never get a watch when they appear later, and are silently dropped from the event stream:
let inotify = try Inotify()
// Ignore version-control and build directories
await inotify.exclude(names: ".git", "node_modules", ".build")
// Ignore every hidden item and every metadata directory of a NAS
await inotify.exclude(patterns: ".*", "@eaDir")
try await inotify.addWatchWithAutomaticSubtreeWatching(
forDirectory: "/home/user/project",
mask: [.create, .modify, .delete]
)
A pattern is matched against an item's own name, not its path, the way the shell matches file names: * and ? stand for any characters and […] for a set of characters. Use isExcluded(_:) to check whether a name is currently excluded.
Event Masks
InotifyEventMask is an OptionSet that mirrors the native inotify flags. You can combine them freely.
The mask lives in the separate InotifyMask product, which has no Linux dependency. Depend on it alone where code only stores or compares masks and must build or be tested on other platforms; Inotify re-exports it.
| Mask | Description |
|---|---|
.access |
File was read |
.attrib |
Metadata changed (permissions, timestamps, ...) |
.closeWrite |
File opened for writing was closed |
.closeNoWrite |
File not opened for writing was closed |
.create |
File or directory created in watched directory |
.delete |
File or directory deleted in watched directory |
.deleteSelf |
Watched item itself was deleted |
.modify |
File was written to |
.moveSelf |
Watched item itself was moved |
.movedFrom |
File moved out of watched directory |
.movedTo |
File moved into watched directory |
.open |
File was opened |
Convenience combinations: .move (.movedFrom + .movedTo), .close (.closeWrite + .closeNoWrite), .allEvents.
Watch flags: .dontFollow, .onlyDir, .oneShot.
Kernel-only flags returned in events: .isDir, .ignored, .unmount.
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
Every addWatch variant returns one or more watch descriptors that you can use to remove the watch later:
let wd = try inotify.addWatch(path: "/tmp/watched", mask: .create)
// ... later
try inotify.removeWatch(wd)
Build Tool
The package ships with a task executable (the TaskCLI target) that serves as the project's build tool. It automates running tests and generating documentation inside Linux Docker containers, so you can validate everything on the correct platform even when developing on macOS.
Because of a Swift Package Manager Bug in the package dependency resolution, the executable needs to be run using the task.sh shell script.
Tests
./task.sh test
Use -v, -vv, or -vvv to increase log verbosity. The command runs two passes: first all tests except InotifyLimitTests, then only InotifyLimitTests (which manipulate system-level inotify limits and need to run in isolation).
Docker must be installed and running on your machine.
Documentation
Full API documentation is available as DocC catalogs bundled with the package. Generate them locally with:
./task.sh generate-docs
Then open the files in the newly created public folder.
Or preview in Xcode by selecting Product > Build Documentation.
Requirements
- Swift 6.0+
- Linux (inotify is a Linux-only API)
- Docker (for running the test suite via
swift run task test)
License
See LICENSE for details.