Files
swift-inotify/Sources/Inotify/DirectoryResolver.swift
T
T. R. Bernstein c037302c01
Docs / docs (push) Canceled after 0s
Docs / deploy (push) Canceled after 0s
Report the directories a growing tree could not watch
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.
2026-09-19 00:56:31 +02:00

94 lines
3.8 KiB
Swift

import CInotify
import _NIOFileSystem
public struct DirectoryResolver {
static let fileManager = FileSystem.shared
public static func resolve(_ paths: String..., excluding itemNames: Set<String> = []) async throws -> [FilePath] {
try await Self.resolve(paths, excluding: ExclusionList(names: itemNames))
}
static func resolve(_ paths: [String], excluding exclusions: ExclusionList = ExclusionList()) async throws -> [FilePath] {
var resolved: [FilePath] = []
for path in paths {
let path = FilePath(path)
resolved.append(path)
try await withSubdirectories(at: path, excluding: exclusions) { resolved.append($0) }
}
return resolved
}
/// Resolves `path` like ``resolve(_:excluding:)``, but a directory that
/// cannot be listed is recorded with its errno and skipped together with
/// its subtree, instead of failing the whole resolution.
static func resolveTolerantly(_ path: FilePath, excluding exclusions: ExclusionList) async -> TolerantResolution {
var resolution = TolerantResolution()
await collectDirectories(at: path, excluding: exclusions, into: &resolution)
return resolution
}
private static func collectDirectories(at path: FilePath, excluding exclusions: ExclusionList, into resolution: inout TolerantResolution) async {
let subdirectories: [FilePath]
do {
subdirectories = try await entries(of: path, excluding: exclusions)
.filter(\.isDirectory)
.map { path.appending($0.name) }
} catch {
resolution.unreadable.append((path: path, errno: errno(of: error)))
return
}
resolution.directories.append(path)
for subdirectory in subdirectories {
await collectDirectories(at: subdirectory, excluding: exclusions, into: &resolution)
}
}
/// The errno behind a file system error; the error's code when the
/// system call is unknown.
private static func errno(of error: any Error) -> Int32 {
guard let fileSystemError = error as? FileSystemError else { return EIO }
if let systemCall = fileSystemError.cause as? FileSystemError.SystemCallError {
return systemCall.errno.rawValue
}
return switch fileSystemError.code {
case .permissionDenied: EACCES
case .notFound: ENOENT
default: EIO
}
}
/// The direct children of `directory`, without the excluded items.
static func entries(of directory: FilePath, excluding exclusions: ExclusionList = ExclusionList()) 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 !exclusions.excludes(name) else { continue }
entries.append((name: name, isDirectory: childContent.type == .directory))
}
try await directoryHandle.close()
return entries
}
/// Calls `body` for every subdirectory below `path`, depth first. Excluded
/// directories are neither reported nor descended into.
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)
for try await childContent in directoryHandle.listContents() {
guard childContent.type == .directory else { continue }
guard let name = childContent.path.lastComponent?.string, !exclusions.excludes(name) else { continue }
try await body(childContent.path)
try await withSubdirectories(at: childContent.path, excluding: exclusions, body: body)
}
try await directoryHandle.close()
}
}
struct TolerantResolution {
/// The directories that could be listed, each before its subdirectories.
var directories: [FilePath] = []
var unreadable: [(path: FilePath, errno: Int32)] = []
}