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.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import CInotify
|
||||
import _NIOFileSystem
|
||||
|
||||
public struct DirectoryResolver {
|
||||
@@ -19,6 +20,45 @@ public struct DirectoryResolver {
|
||||
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)
|
||||
@@ -45,3 +85,9 @@ public struct DirectoryResolver {
|
||||
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)] = []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user