Compare commits
2
Commits
c79691cb6f
..
2.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8af25be549 | ||
|
|
442053eae2 |
+7
-1
@@ -8,7 +8,11 @@ let package = Package(
|
|||||||
.library(
|
.library(
|
||||||
name: "Inotify",
|
name: "Inotify",
|
||||||
targets: ["Inotify"]
|
targets: ["Inotify"]
|
||||||
)
|
),
|
||||||
|
.library(
|
||||||
|
name: "InotifyMask",
|
||||||
|
targets: ["InotifyMask"]
|
||||||
|
),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.7.1"),
|
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.7.1"),
|
||||||
@@ -20,10 +24,12 @@ let package = Package(
|
|||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.systemLibrary(name: "CInotify"),
|
.systemLibrary(name: "CInotify"),
|
||||||
|
.target(name: "InotifyMask"),
|
||||||
.target(
|
.target(
|
||||||
name: "Inotify",
|
name: "Inotify",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"CInotify",
|
"CInotify",
|
||||||
|
"InotifyMask",
|
||||||
.product(name: "Logging", package: "swift-log"),
|
.product(name: "Logging", package: "swift-log"),
|
||||||
.product(name: "_NIOFileSystem", package: "swift-nio"),
|
.product(name: "_NIOFileSystem", package: "swift-nio"),
|
||||||
.product(name: "SystemPackage", package: "swift-system")
|
.product(name: "SystemPackage", package: "swift-system")
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ When a watched directory is moved out of the tree, the watches on it and on its
|
|||||||
|
|
||||||
## Excluding Items
|
## Excluding Items
|
||||||
|
|
||||||
You can tell the `Inotify` actor to ignore certain file or directory names. Excluded names are skipped during recursive directory resolution (so no watch is installed on them) and silently dropped from the event stream:
|
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:
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
let inotify = try Inotify()
|
let inotify = try Inotify()
|
||||||
@@ -89,18 +89,23 @@ let inotify = try Inotify()
|
|||||||
// Ignore version-control and build directories
|
// Ignore version-control and build directories
|
||||||
await inotify.exclude(names: ".git", "node_modules", ".build")
|
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(
|
try await inotify.addWatchWithAutomaticSubtreeWatching(
|
||||||
forDirectory: "/home/user/project",
|
forDirectory: "/home/user/project",
|
||||||
mask: [.create, .modify, .delete]
|
mask: [.create, .modify, .delete]
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `isExcluded(_:)` to check whether a name is currently on the exclusion list.
|
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
|
## Event Masks
|
||||||
|
|
||||||
`InotifyEventMask` is an `OptionSet` that mirrors the native inotify flags. You can combine them freely.
|
`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 |
|
| Mask | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `.access` | File was read |
|
| `.access` | File was read |
|
||||||
|
|||||||
@@ -4,28 +4,28 @@ public struct DirectoryResolver {
|
|||||||
static let fileManager = FileSystem.shared
|
static let fileManager = FileSystem.shared
|
||||||
|
|
||||||
public static func resolve(_ paths: String..., excluding itemNames: Set<String> = []) async throws -> [FilePath] {
|
public static func resolve(_ paths: String..., excluding itemNames: Set<String> = []) async throws -> [FilePath] {
|
||||||
try await Self.resolve(paths, excluding: itemNames)
|
try await Self.resolve(paths, excluding: ExclusionList(names: itemNames))
|
||||||
}
|
}
|
||||||
|
|
||||||
static func resolve(_ paths: [String], excluding itemNames: Set<String> = []) async throws -> [FilePath] {
|
static func resolve(_ paths: [String], excluding exclusions: ExclusionList = ExclusionList()) async throws -> [FilePath] {
|
||||||
var resolved: [FilePath] = []
|
var resolved: [FilePath] = []
|
||||||
|
|
||||||
for path in paths {
|
for path in paths {
|
||||||
let path = FilePath(path)
|
let path = FilePath(path)
|
||||||
resolved.append(path)
|
resolved.append(path)
|
||||||
try await withSubdirectories(at: path, excluding: itemNames) { resolved.append($0) }
|
try await withSubdirectories(at: path, excluding: exclusions) { resolved.append($0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolved
|
return resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The direct children of `directory`, without the excluded names.
|
/// The direct children of `directory`, without the excluded items.
|
||||||
static func entries(of directory: FilePath, excluding itemNames: Set<String> = []) async throws -> [(name: String, isDirectory: Bool)] {
|
static func entries(of directory: FilePath, excluding exclusions: ExclusionList = ExclusionList()) async throws -> [(name: String, isDirectory: Bool)] {
|
||||||
let directoryHandle = try await fileManager.openDirectory(atPath: directory)
|
let directoryHandle = try await fileManager.openDirectory(atPath: directory)
|
||||||
var entries: [(name: String, isDirectory: Bool)] = []
|
var entries: [(name: String, isDirectory: Bool)] = []
|
||||||
for try await childContent in directoryHandle.listContents() {
|
for try await childContent in directoryHandle.listContents() {
|
||||||
guard let name = childContent.path.lastComponent?.string else { continue }
|
guard let name = childContent.path.lastComponent?.string else { continue }
|
||||||
guard !itemNames.contains(name) else { continue }
|
guard !exclusions.excludes(name) else { continue }
|
||||||
entries.append((name: name, isDirectory: childContent.type == .directory))
|
entries.append((name: name, isDirectory: childContent.type == .directory))
|
||||||
}
|
}
|
||||||
try await directoryHandle.close()
|
try await directoryHandle.close()
|
||||||
@@ -33,14 +33,14 @@ public struct DirectoryResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Calls `body` for every subdirectory below `path`, depth first. Excluded
|
/// Calls `body` for every subdirectory below `path`, depth first. Excluded
|
||||||
/// names are neither reported nor descended into.
|
/// directories are neither reported nor descended into.
|
||||||
private static func withSubdirectories(at path: FilePath, excluding itemNames: Set<String>, body: (FilePath) async throws -> Void) async throws {
|
private static func withSubdirectories(at path: FilePath, excluding exclusions: ExclusionList, body: (FilePath) async throws -> Void) async throws {
|
||||||
let directoryHandle = try await fileManager.openDirectory(atPath: path)
|
let directoryHandle = try await fileManager.openDirectory(atPath: path)
|
||||||
for try await childContent in directoryHandle.listContents() {
|
for try await childContent in directoryHandle.listContents() {
|
||||||
guard childContent.type == .directory else { continue }
|
guard childContent.type == .directory else { continue }
|
||||||
guard let name = childContent.path.lastComponent?.string, !itemNames.contains(name) else { continue }
|
guard let name = childContent.path.lastComponent?.string, !exclusions.excludes(name) else { continue }
|
||||||
try await body(childContent.path)
|
try await body(childContent.path)
|
||||||
try await withSubdirectories(at: childContent.path, excluding: itemNames, body: body)
|
try await withSubdirectories(at: childContent.path, excluding: exclusions, body: body)
|
||||||
}
|
}
|
||||||
try await directoryHandle.close()
|
try await directoryHandle.close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#if canImport(Musl)
|
||||||
|
import Musl
|
||||||
|
#else
|
||||||
|
import Glibc
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// The item names an ``Inotify`` instance skips: exact names and shell
|
||||||
|
/// patterns, both matched against an item's own name.
|
||||||
|
struct ExclusionList: Sendable {
|
||||||
|
private var names: Set<String> = []
|
||||||
|
private var patterns: [String] = []
|
||||||
|
|
||||||
|
init(names: Set<String> = [], patterns: [String] = []) {
|
||||||
|
self.names = names
|
||||||
|
self.patterns = patterns
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func add(name: String) {
|
||||||
|
self.names.insert(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func add(pattern: String) {
|
||||||
|
guard !self.patterns.contains(pattern) else { return }
|
||||||
|
self.patterns.append(pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Patterns are matched as the shell matches file names: `*` and `?`
|
||||||
|
/// stand for any characters, `[…]` for a set, and a leading dot needs
|
||||||
|
/// no special treatment.
|
||||||
|
func excludes(_ name: String) -> Bool {
|
||||||
|
self.names.contains(name) || self.patterns.contains { fnmatch($0, name, 0) == 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// The mask lives in its own module so that it is usable off Linux; users
|
||||||
|
// of `Inotify` keep seeing it as before.
|
||||||
|
@_exported import InotifyMask
|
||||||
@@ -20,7 +20,7 @@ Beyond single-directory watches, the library provides two higher-level methods f
|
|||||||
- ``Inotify/Inotify/addRecursiveWatch(forDirectory:mask:)`` installs watches on every existing subdirectory at setup time.
|
- ``Inotify/Inotify/addRecursiveWatch(forDirectory:mask:)`` installs watches on every existing subdirectory at setup time.
|
||||||
- ``Inotify/Inotify/addWatchWithAutomaticSubtreeWatching(forDirectory:mask:)`` does the same **and** automatically watches subdirectories that are created after setup.
|
- ``Inotify/Inotify/addWatchWithAutomaticSubtreeWatching(forDirectory:mask:)`` does the same **and** automatically watches subdirectories that are created after setup.
|
||||||
|
|
||||||
You can also exclude certain file or directory names so that they are skipped during directory resolution and silently dropped from the event stream. See ``Inotify/Inotify/exclude(names:)`` and <doc:WatchingDirectoryTrees> for details.
|
You can also exclude certain file or directory names, exactly or by shell pattern, so that they are skipped during directory resolution and silently dropped from the event stream. See ``Inotify/Inotify/exclude(names:)``, ``Inotify/Inotify/exclude(patterns:)`` and <doc:WatchingDirectoryTrees> for details.
|
||||||
|
|
||||||
All public types conform to `Sendable`, so they can be safely passed across concurrency boundaries.
|
All public types conform to `Sendable`, so they can be safely passed across concurrency boundaries.
|
||||||
|
|
||||||
|
|||||||
@@ -37,11 +37,12 @@ When a directory is moved out of the watched tree, the watches on it and on its
|
|||||||
|
|
||||||
### Excluding Directories
|
### Excluding Directories
|
||||||
|
|
||||||
When watching large trees you often want to skip certain subdirectories entirely — version-control metadata, build artefacts, dependency caches, and so on. Call ``Inotify/Inotify/exclude(names:)`` **before** adding a recursive or automatic-subtree watch:
|
When watching large trees you often want to skip certain subdirectories entirely — version-control metadata, build artefacts, dependency caches, and so on. Call ``Inotify/Inotify/exclude(names:)`` or ``Inotify/Inotify/exclude(patterns:)`` **before** adding a recursive or automatic-subtree watch:
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
let inotify = try Inotify()
|
let inotify = try Inotify()
|
||||||
await inotify.exclude(names: ".git", "node_modules", ".build")
|
await inotify.exclude(names: ".git", "node_modules", ".build")
|
||||||
|
await inotify.exclude(patterns: ".*", "*.tmp")
|
||||||
|
|
||||||
try await inotify.addWatchWithAutomaticSubtreeWatching(
|
try await inotify.addWatchWithAutomaticSubtreeWatching(
|
||||||
forDirectory: "/home/user/project",
|
forDirectory: "/home/user/project",
|
||||||
@@ -49,7 +50,7 @@ try await inotify.addWatchWithAutomaticSubtreeWatching(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Excluded names are matched against the last path component of each directory during resolution and are also filtered from the event stream, so you never receive events for items whose name is on the exclusion list.
|
Excluded names and patterns are matched against the last path component of each directory during resolution, against a directory that appears later before a watch is extended to it, and against every event, so you never receive events for excluded items. A pattern is matched the way the shell matches file names: `*` and `?` stand for any characters and `[…]` for a set of characters; a leading dot needs no special treatment.
|
||||||
|
|
||||||
### Choosing the Right Method
|
### Choosing the Right Method
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import SystemPackage
|
|||||||
|
|
||||||
public actor Inotify {
|
public actor Inotify {
|
||||||
private let fd: CInt
|
private let fd: CInt
|
||||||
private var excludedItemNames: Set<String> = []
|
private var exclusions = ExclusionList()
|
||||||
private var watches = InotifyWatchManager()
|
private var watches = InotifyWatchManager()
|
||||||
private nonisolated(unsafe) let eventReader: any DispatchSourceRead
|
private nonisolated(unsafe) let eventReader: any DispatchSourceRead
|
||||||
private nonisolated let eventStream: AsyncStream<RawInotifyEvent>
|
private nonisolated let eventStream: AsyncStream<RawInotifyEvent>
|
||||||
@@ -33,12 +33,14 @@ public actor Inotify {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether an item with this name is skipped, by an excluded name or
|
||||||
|
/// an excluded pattern.
|
||||||
public func isExcluded(_ name: String) -> Bool {
|
public func isExcluded(_ name: String) -> Bool {
|
||||||
self.excludedItemNames.contains(name)
|
self.exclusions.excludes(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func exclude(name: String) {
|
public func exclude(name: String) {
|
||||||
self.excludedItemNames.insert(name)
|
self.exclusions.add(name: name)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func exclude(names: String...) {
|
public func exclude(names: String...) {
|
||||||
@@ -47,7 +49,28 @@ public actor Inotify {
|
|||||||
|
|
||||||
public func exclude(names: [String]) {
|
public func exclude(names: [String]) {
|
||||||
for name in names {
|
for name in names {
|
||||||
self.excludedItemNames.insert(name)
|
self.exclusions.add(name: name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Excludes every item whose name matches a shell pattern such as
|
||||||
|
/// `*.tmp` or `@*`, with the same effect as an excluded name.
|
||||||
|
///
|
||||||
|
/// The pattern is matched against the item's own name, not its path,
|
||||||
|
/// as the shell matches file names: `*` and `?` stand for any
|
||||||
|
/// characters and `[…]` for a set of characters. A leading dot needs
|
||||||
|
/// no special treatment, so `.*` excludes hidden items.
|
||||||
|
public func exclude(pattern: String) {
|
||||||
|
self.exclusions.add(pattern: pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func exclude(patterns: String...) {
|
||||||
|
self.exclude(patterns: patterns)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func exclude(patterns: [String]) {
|
||||||
|
for pattern in patterns {
|
||||||
|
self.exclusions.add(pattern: pattern)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +86,7 @@ public actor Inotify {
|
|||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
public func addRecursiveWatch(forDirectory path: String, mask: InotifyEventMask) async throws -> [CInt] {
|
public func addRecursiveWatch(forDirectory path: String, mask: InotifyEventMask) async throws -> [CInt] {
|
||||||
let directoryPaths = try await DirectoryResolver.resolve(path, excluding: self.excludedItemNames)
|
let directoryPaths = try await DirectoryResolver.resolve([path], excluding: self.exclusions)
|
||||||
var result: [CInt] = []
|
var result: [CInt] = []
|
||||||
for path in directoryPaths {
|
for path in directoryPaths {
|
||||||
let wd = try self.addWatch(path: path.string, mask: mask)
|
let wd = try self.addWatch(path: path.string, mask: mask)
|
||||||
@@ -99,7 +122,7 @@ public actor Inotify {
|
|||||||
return InotifyEvent(from: rawEvent, inDirectory: "")
|
return InotifyEvent(from: rawEvent, inDirectory: "")
|
||||||
}
|
}
|
||||||
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.excludedItemNames.contains(rawEvent.name) else { return nil }
|
guard !self.exclusions.excludes(rawEvent.name) else { return nil }
|
||||||
let event = InotifyEvent.init(from: rawEvent, inDirectory: path)
|
let event = InotifyEvent.init(from: rawEvent, inDirectory: path)
|
||||||
self.forgetWatchInCaseTheKernelRemovedIt(event)
|
self.forgetWatchInCaseTheKernelRemovedIt(event)
|
||||||
self.removeWatchesInCaseADirectoryLeftTheTree(event)
|
self.removeWatchesInCaseADirectoryLeftTheTree(event)
|
||||||
@@ -152,7 +175,7 @@ public actor Inotify {
|
|||||||
private func synthesizeEvents(forContentOfWatches wds: [CInt], kind: InotifyEventMask, cookie: UInt32) async {
|
private func synthesizeEvents(forContentOfWatches wds: [CInt], kind: InotifyEventMask, cookie: UInt32) async {
|
||||||
for wd in wds {
|
for wd in wds {
|
||||||
guard let directory = self.watches.path(forId: wd) else { continue }
|
guard let directory = self.watches.path(forId: wd) else { continue }
|
||||||
guard let entries = try? await DirectoryResolver.entries(of: FilePath(directory), excluding: self.excludedItemNames) else { continue }
|
guard let entries = try? await DirectoryResolver.entries(of: FilePath(directory), excluding: self.exclusions) else { continue }
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
let mask: InotifyEventMask = entry.isDirectory ? [kind, .isDir] : kind
|
let mask: InotifyEventMask = entry.isDirectory ? [kind, .isDir] : kind
|
||||||
self.continuation.yield(RawInotifyEvent(
|
self.continuation.yield(RawInotifyEvent(
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import CInotify
|
|
||||||
|
|
||||||
public struct InotifyEventMask: OptionSet, Sendable, Hashable {
|
|
||||||
public let rawValue: CUnsignedInt
|
|
||||||
|
|
||||||
public init(rawValue: UInt32) {
|
|
||||||
self.rawValue = rawValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Watchable Events
|
|
||||||
|
|
||||||
public static let access = InotifyEventMask(rawValue: CUnsignedInt(IN_ACCESS))
|
|
||||||
public static let attrib = InotifyEventMask(rawValue: CUnsignedInt(IN_ATTRIB))
|
|
||||||
public static let closeWrite = InotifyEventMask(rawValue: CUnsignedInt(IN_CLOSE_WRITE))
|
|
||||||
public static let closeNoWrite = InotifyEventMask(rawValue: CUnsignedInt(IN_CLOSE_NOWRITE))
|
|
||||||
public static let create = InotifyEventMask(rawValue: CUnsignedInt(IN_CREATE))
|
|
||||||
public static let delete = InotifyEventMask(rawValue: CUnsignedInt(IN_DELETE))
|
|
||||||
public static let deleteSelf = InotifyEventMask(rawValue: CUnsignedInt(IN_DELETE_SELF))
|
|
||||||
public static let modify = InotifyEventMask(rawValue: CUnsignedInt(IN_MODIFY))
|
|
||||||
public static let moveSelf = InotifyEventMask(rawValue: CUnsignedInt(IN_MOVE_SELF))
|
|
||||||
public static let movedFrom = InotifyEventMask(rawValue: CUnsignedInt(IN_MOVED_FROM))
|
|
||||||
public static let movedTo = InotifyEventMask(rawValue: CUnsignedInt(IN_MOVED_TO))
|
|
||||||
public static let open = InotifyEventMask(rawValue: CUnsignedInt(IN_OPEN))
|
|
||||||
|
|
||||||
// MARK: - Combinations
|
|
||||||
|
|
||||||
public static let move: InotifyEventMask = [.movedFrom, .movedTo]
|
|
||||||
public static let close: InotifyEventMask = [.closeWrite, .closeNoWrite]
|
|
||||||
public static let allEvents: InotifyEventMask = [
|
|
||||||
.access, .attrib, .closeWrite, .closeNoWrite,
|
|
||||||
.create, .delete, .deleteSelf, .modify,
|
|
||||||
.moveSelf, .movedFrom, .movedTo, .open
|
|
||||||
]
|
|
||||||
|
|
||||||
// MARK: - Watch Flags
|
|
||||||
|
|
||||||
public static let dontFollow = InotifyEventMask(rawValue: CUnsignedInt(IN_DONT_FOLLOW))
|
|
||||||
public static let onlyDir = InotifyEventMask(rawValue: CUnsignedInt(IN_ONLYDIR))
|
|
||||||
public static let oneShot = InotifyEventMask(rawValue: CUnsignedInt(IN_ONESHOT))
|
|
||||||
|
|
||||||
// MARK: - Kernel-Only Flags
|
|
||||||
|
|
||||||
public static let isDir = InotifyEventMask(rawValue: CUnsignedInt(IN_ISDIR))
|
|
||||||
public static let ignored = InotifyEventMask(rawValue: CUnsignedInt(IN_IGNORED))
|
|
||||||
public static let queueOverflow = InotifyEventMask(rawValue: CUnsignedInt(IN_Q_OVERFLOW))
|
|
||||||
public static let unmount = InotifyEventMask(rawValue: CUnsignedInt(IN_UNMOUNT))
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/// The events and flags of an inotify watch or event, as bits.
|
||||||
|
///
|
||||||
|
/// The values are the constants of the Linux `<sys/inotify.h>` header,
|
||||||
|
/// which are part of the kernel's stable interface. Spelling them out here
|
||||||
|
/// keeps this module free of the C header, so it builds on every platform
|
||||||
|
/// and lets code that only stores or compares masks be tested off Linux.
|
||||||
|
public struct InotifyEventMask: OptionSet, Sendable, Hashable {
|
||||||
|
public let rawValue: UInt32
|
||||||
|
|
||||||
|
public init(rawValue: UInt32) {
|
||||||
|
self.rawValue = rawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Watchable Events
|
||||||
|
|
||||||
|
public static let access = InotifyEventMask(rawValue: 0x0000_0001)
|
||||||
|
public static let modify = InotifyEventMask(rawValue: 0x0000_0002)
|
||||||
|
public static let attrib = InotifyEventMask(rawValue: 0x0000_0004)
|
||||||
|
public static let closeWrite = InotifyEventMask(rawValue: 0x0000_0008)
|
||||||
|
public static let closeNoWrite = InotifyEventMask(rawValue: 0x0000_0010)
|
||||||
|
public static let open = InotifyEventMask(rawValue: 0x0000_0020)
|
||||||
|
public static let movedFrom = InotifyEventMask(rawValue: 0x0000_0040)
|
||||||
|
public static let movedTo = InotifyEventMask(rawValue: 0x0000_0080)
|
||||||
|
public static let create = InotifyEventMask(rawValue: 0x0000_0100)
|
||||||
|
public static let delete = InotifyEventMask(rawValue: 0x0000_0200)
|
||||||
|
public static let deleteSelf = InotifyEventMask(rawValue: 0x0000_0400)
|
||||||
|
public static let moveSelf = InotifyEventMask(rawValue: 0x0000_0800)
|
||||||
|
|
||||||
|
// MARK: - Combinations
|
||||||
|
|
||||||
|
public static let move: InotifyEventMask = [.movedFrom, .movedTo]
|
||||||
|
public static let close: InotifyEventMask = [.closeWrite, .closeNoWrite]
|
||||||
|
public static let allEvents: InotifyEventMask = [
|
||||||
|
.access, .attrib, .closeWrite, .closeNoWrite,
|
||||||
|
.create, .delete, .deleteSelf, .modify,
|
||||||
|
.moveSelf, .movedFrom, .movedTo, .open,
|
||||||
|
]
|
||||||
|
|
||||||
|
// MARK: - Watch Flags
|
||||||
|
|
||||||
|
public static let onlyDir = InotifyEventMask(rawValue: 0x0100_0000)
|
||||||
|
public static let dontFollow = InotifyEventMask(rawValue: 0x0200_0000)
|
||||||
|
public static let oneShot = InotifyEventMask(rawValue: 0x8000_0000)
|
||||||
|
|
||||||
|
// MARK: - Kernel-Only Flags
|
||||||
|
|
||||||
|
public static let unmount = InotifyEventMask(rawValue: 0x0000_2000)
|
||||||
|
public static let queueOverflow = InotifyEventMask(rawValue: 0x0000_4000)
|
||||||
|
public static let ignored = InotifyEventMask(rawValue: 0x0000_8000)
|
||||||
|
public static let isDir = InotifyEventMask(rawValue: 0x4000_0000)
|
||||||
|
}
|
||||||
@@ -24,4 +24,14 @@ struct DirectoryResolverTests {
|
|||||||
#expect(directories.map { $0.description } == [dir])
|
#expect(directories.map { $0.description } == [dir])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func doesNotDescendIntoDirectoriesMatchingAnExcludedPattern() async throws {
|
||||||
|
try await withTempDir { dir in
|
||||||
|
let excludedSubdirectory = "\(dir)/@eaDir/Inside"
|
||||||
|
try FileManager.default.createDirectory(atPath: excludedSubdirectory, withIntermediateDirectories: true)
|
||||||
|
let directories = try await DirectoryResolver.resolve([dir], excluding: ExclusionList(patterns: ["@*"]))
|
||||||
|
|
||||||
|
#expect(directories.map { $0.description } == [dir])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import CInotify
|
||||||
|
import Testing
|
||||||
|
@testable import Inotify
|
||||||
|
|
||||||
|
@Suite("Event Mask")
|
||||||
|
struct EventMaskTests {
|
||||||
|
@Test(arguments: [
|
||||||
|
(InotifyEventMask.access, UInt32(IN_ACCESS)),
|
||||||
|
(.attrib, UInt32(IN_ATTRIB)),
|
||||||
|
(.closeWrite, UInt32(IN_CLOSE_WRITE)),
|
||||||
|
(.closeNoWrite, UInt32(IN_CLOSE_NOWRITE)),
|
||||||
|
(.create, UInt32(IN_CREATE)),
|
||||||
|
(.delete, UInt32(IN_DELETE)),
|
||||||
|
(.deleteSelf, UInt32(IN_DELETE_SELF)),
|
||||||
|
(.modify, UInt32(IN_MODIFY)),
|
||||||
|
(.moveSelf, UInt32(IN_MOVE_SELF)),
|
||||||
|
(.movedFrom, UInt32(IN_MOVED_FROM)),
|
||||||
|
(.movedTo, UInt32(IN_MOVED_TO)),
|
||||||
|
(.open, UInt32(IN_OPEN)),
|
||||||
|
(.dontFollow, UInt32(IN_DONT_FOLLOW)),
|
||||||
|
(.onlyDir, UInt32(IN_ONLYDIR)),
|
||||||
|
(.oneShot, UInt32(IN_ONESHOT)),
|
||||||
|
(.isDir, UInt32(IN_ISDIR)),
|
||||||
|
(.ignored, UInt32(IN_IGNORED)),
|
||||||
|
(.queueOverflow, UInt32(IN_Q_OVERFLOW)),
|
||||||
|
(.unmount, UInt32(IN_UNMOUNT)),
|
||||||
|
] as [(InotifyEventMask, UInt32)])
|
||||||
|
func matchesTheKernelConstant(mask: InotifyEventMask, constant: UInt32) {
|
||||||
|
#expect(mask.rawValue == constant)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import Testing
|
||||||
|
@testable import Inotify
|
||||||
|
|
||||||
|
@Suite("Exclusion")
|
||||||
|
struct ExclusionTests {
|
||||||
|
@Test func excludesANameThatMatchesAPattern() async throws {
|
||||||
|
let inotify = try Inotify()
|
||||||
|
await inotify.exclude(patterns: "*.tmp", "@*")
|
||||||
|
|
||||||
|
#expect(await inotify.isExcluded("scan.tmp"))
|
||||||
|
#expect(await inotify.isExcluded("@eaDir"))
|
||||||
|
#expect(await !inotify.isExcluded("scan.pdf"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func excludesAnExactName() async throws {
|
||||||
|
let inotify = try Inotify()
|
||||||
|
await inotify.exclude(name: ".git")
|
||||||
|
|
||||||
|
#expect(await inotify.isExcluded(".git"))
|
||||||
|
#expect(await !inotify.isExcluded(".gitignore"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,44 @@ struct RecursiveEventTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func ignoresFileCreationInASubfolderMatchingAnExcludedPattern() async throws {
|
||||||
|
try await withTempDir { dir in
|
||||||
|
let subDirectory = "\(dir)/@eaDir"
|
||||||
|
let filepath = "\(subDirectory)/modify-target.txt"
|
||||||
|
try FileManager.default.createDirectory(atPath: subDirectory, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
let events = try await getEventsForTrigger(
|
||||||
|
in: dir,
|
||||||
|
mask: [.create],
|
||||||
|
recursive: .recursive,
|
||||||
|
excludePatterns: ["@*"]
|
||||||
|
) { _ in try createFile(at: "\(filepath)", contents: "hello") }
|
||||||
|
|
||||||
|
let createEvent = events.first { $0.mask.contains(.create) && $0.path.string == filepath }
|
||||||
|
#expect(createEvent == nil, "Did not expect CREATE for '\(filepath)', got: \(events)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func doesNotWatchANewSubfolderMatchingAnExcludedPattern() async throws {
|
||||||
|
try await withTempDir { dir in
|
||||||
|
let subDirectory = "\(dir)/@eaDir"
|
||||||
|
let filepath = "\(subDirectory)/modify-target.txt"
|
||||||
|
|
||||||
|
let events = try await getEventsForTrigger(
|
||||||
|
in: dir,
|
||||||
|
mask: [.create],
|
||||||
|
recursive: .withAutomaticSubtreeWatching,
|
||||||
|
excludePatterns: ["@*"]
|
||||||
|
) { _ in
|
||||||
|
try FileManager.default.createDirectory(atPath: subDirectory, withIntermediateDirectories: true)
|
||||||
|
try await Task.sleep(for: .milliseconds(400))
|
||||||
|
try createFile(at: "\(filepath)", contents: "hello")
|
||||||
|
}
|
||||||
|
|
||||||
|
#expect(events.isEmpty, "Did not expect any event, got: \(events)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test func newSubfoldersOfRecursiveWatchAreAutomaticallyWatchedToo() async throws {
|
@Test func newSubfoldersOfRecursiveWatchAreAutomaticallyWatchedToo() async throws {
|
||||||
try await withTempDir { dir in
|
try await withTempDir { dir in
|
||||||
let subDirectory = "\(dir)/Subfolder"
|
let subDirectory = "\(dir)/Subfolder"
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ func getEventsForTrigger(
|
|||||||
mask: InotifyEventMask,
|
mask: InotifyEventMask,
|
||||||
recursive: RecursivKind = .nonrecursive,
|
recursive: RecursivKind = .nonrecursive,
|
||||||
exclude: [String] = [],
|
exclude: [String] = [],
|
||||||
|
excludePatterns: [String] = [],
|
||||||
trigger: @escaping (String) async throws -> Void,
|
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)
|
||||||
|
await watcher.exclude(patterns: excludePatterns)
|
||||||
switch recursive {
|
switch recursive {
|
||||||
case .nonrecursive:
|
case .nonrecursive:
|
||||||
try await watcher.addWatch(path: dir, mask: mask)
|
try await watcher.addWatch(path: dir, mask: mask)
|
||||||
|
|||||||
Reference in New Issue
Block a user