Watch directories moved into the tree and report their content
Automatic subtree watching only reacted to `CREATE`, so a directory moved in from elsewhere stayed unwatched. It is now handled like a created one. Items that already exist in such a directory never produce kernel events; they are reported with the same event kind and `synthesized` set to `true`, so consumers can treat them as newly appeared.
This commit is contained in:
@@ -23,6 +23,19 @@ public struct DirectoryResolver {
|
||||
return resolved
|
||||
}
|
||||
|
||||
/// The direct children of `directory`, without the excluded names.
|
||||
static func entries(of directory: FilePath, excluding itemNames: Set<String> = []) async throws -> [(name: String, isDirectory: Bool)] {
|
||||
let directoryHandle = try await fileManager.openDirectory(atPath: directory)
|
||||
var entries: [(name: String, isDirectory: Bool)] = []
|
||||
for try await childContent in directoryHandle.listContents() {
|
||||
guard let name = childContent.path.lastComponent?.string else { continue }
|
||||
guard !itemNames.contains(name) else { continue }
|
||||
entries.append((name: name, isDirectory: childContent.type == .directory))
|
||||
}
|
||||
try await directoryHandle.close()
|
||||
return entries
|
||||
}
|
||||
|
||||
private static func withSubdirectories(at path: FilePath, recursive: Bool = false, body: (FilePath) async throws -> Void) async throws {
|
||||
let directoryHandle = try await fileManager.openDirectory(atPath: path)
|
||||
for try await childContent in directoryHandle.listContents() {
|
||||
|
||||
@@ -31,7 +31,7 @@ let descriptors = try await inotify.addWatchWithAutomaticSubtreeWatching(
|
||||
)
|
||||
```
|
||||
|
||||
Internally this listens for `CREATE` events carrying the ``InotifyEventMask/isDir`` flag and installs a new watch with the same mask whenever a subdirectory appears.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Dispatch
|
||||
import CInotify
|
||||
import SystemPackage
|
||||
|
||||
public actor Inotify {
|
||||
private let fd: CInt
|
||||
@@ -7,6 +8,7 @@ public actor Inotify {
|
||||
private var watches = InotifyWatchManager()
|
||||
private nonisolated(unsafe) let eventReader: any DispatchSourceRead
|
||||
private nonisolated let eventStream: AsyncStream<RawInotifyEvent>
|
||||
private nonisolated let continuation: AsyncStream<RawInotifyEvent>.Continuation
|
||||
public nonisolated var events: AsyncCompactMapSequence<AsyncStream<RawInotifyEvent>, InotifyEvent> {
|
||||
self.eventStream.compactMap(self.transform(_:))
|
||||
}
|
||||
@@ -25,7 +27,7 @@ public actor Inotify {
|
||||
guard self.fd >= 0 else {
|
||||
throw InotifyError.initFailed(errno: cinotify_get_errno())
|
||||
}
|
||||
(self.eventReader, self.eventStream) = Self.createEventReader(
|
||||
(self.eventReader, self.eventStream, self.continuation) = Self.createEventReader(
|
||||
forFileDescriptor: fd,
|
||||
bufferingPolicy: bufferingPolicy
|
||||
)
|
||||
@@ -126,20 +128,48 @@ public actor Inotify {
|
||||
}
|
||||
|
||||
private func addWatchInCaseOfAutomaticSubtreeWatching(_ event: InotifyEvent) async {
|
||||
guard watches.isAutomaticSubtreeWatching(event.watchDescriptor),
|
||||
event.mask.contains(.create),
|
||||
event.mask.contains(.isDir) else {
|
||||
guard !event.synthesized,
|
||||
watches.isAutomaticSubtreeWatching(event.watchDescriptor),
|
||||
event.mask.contains(.isDir),
|
||||
let kind = Self.subtreeTrigger(in: event.mask) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let mask = self.watches.mask(forId: event.watchDescriptor) else { return }
|
||||
let _ = try? await self.addWatchWithAutomaticSubtreeWatching(forDirectory: event.path.string, mask: mask)
|
||||
guard let wds = try? await self.addWatchWithAutomaticSubtreeWatching(forDirectory: event.path.string, mask: mask) else { return }
|
||||
await self.synthesizeEvents(forContentOfWatches: wds, kind: kind, cookie: event.cookie)
|
||||
}
|
||||
|
||||
private static func subtreeTrigger(in mask: InotifyEventMask) -> InotifyEventMask? {
|
||||
if mask.contains(.create) { return .create }
|
||||
if mask.contains(.movedTo) { return .movedTo }
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Items that already exist when a directory becomes watched never
|
||||
/// produce kernel events, so they are reported as if they had just
|
||||
/// appeared, marked as synthesized.
|
||||
private func synthesizeEvents(forContentOfWatches wds: [CInt], kind: InotifyEventMask, cookie: UInt32) async {
|
||||
for wd in wds {
|
||||
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 }
|
||||
for entry in entries {
|
||||
let mask: InotifyEventMask = entry.isDirectory ? [kind, .isDir] : kind
|
||||
self.continuation.yield(RawInotifyEvent(
|
||||
watchDescriptor: wd,
|
||||
mask: mask,
|
||||
cookie: cookie,
|
||||
name: entry.name,
|
||||
synthesized: true
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func createEventReader(
|
||||
forFileDescriptor fd: CInt,
|
||||
bufferingPolicy: AsyncStream<RawInotifyEvent>.Continuation.BufferingPolicy
|
||||
) -> (any DispatchSourceRead, AsyncStream<RawInotifyEvent>) {
|
||||
) -> (any DispatchSourceRead, AsyncStream<RawInotifyEvent>, AsyncStream<RawInotifyEvent>.Continuation) {
|
||||
let (stream, continuation) = AsyncStream<RawInotifyEvent>.makeStream(
|
||||
of: RawInotifyEvent.self,
|
||||
bufferingPolicy: bufferingPolicy
|
||||
@@ -161,6 +191,6 @@ public actor Inotify {
|
||||
}
|
||||
reader.activate()
|
||||
|
||||
return (reader, stream)
|
||||
return (reader, stream, continuation)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ public struct InotifyEvent: Sendable, Hashable, CustomStringConvertible {
|
||||
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)\""]
|
||||
@@ -27,7 +30,8 @@ extension InotifyEvent {
|
||||
watchDescriptor: rawEvent.watchDescriptor,
|
||||
mask: rawEvent.mask,
|
||||
cookie: rawEvent.cookie,
|
||||
path: dirPath.appending(rawEvent.name)
|
||||
path: dirPath.appending(rawEvent.name),
|
||||
synthesized: rawEvent.synthesized
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ struct InotifyEventParser {
|
||||
watchDescriptor: rawEvent.wd,
|
||||
mask: InotifyEventMask(rawValue: rawEvent.mask),
|
||||
cookie: rawEvent.cookie,
|
||||
name: Self.extractName(from: eventPointer, nameLength: rawEvent.len)
|
||||
name: Self.extractName(from: eventPointer, nameLength: rawEvent.len),
|
||||
synthesized: false
|
||||
))
|
||||
|
||||
offset += Self.eventSize(nameLength: rawEvent.len)
|
||||
|
||||
@@ -3,6 +3,9 @@ public struct RawInotifyEvent: Sendable, Hashable, CustomStringConvertible {
|
||||
public let mask: InotifyEventMask
|
||||
public let cookie: UInt32
|
||||
public let name: String
|
||||
/// 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 = ["RawInotifyEvent(wd: \(watchDescriptor), mask: \(mask), name: \"\(name)\""]
|
||||
|
||||
Reference in New Issue
Block a user