fix: a directory that stops being readable no longer throws at whoever lists it

SaferEnumerateFiles returned a lazy sequence, so an I/O error was raised where
the sequence was walked rather than where it was created - past the try/catch
callers had wrapped around it. IgnoreInaccessible did not help either: it only
forgives permissions, not a volume that has stopped answering.

Walk the enumerator defensively instead, keeping what was read and reporting
the reason, and let a caller ask whether a directory can be read at all.

See issue #1984.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
This commit is contained in:
Cursor Agentandrmcrackan committed 2026-08-24 14:21:58 +00:00
1 parent 819a0960be
commit 5ef3f3e11e
5 files changed
+464 -14

No files matched your search

+25 -11
View File
@@ -69,19 +69,33 @@ public class BackgroundFileSystem : IDisposable
fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
}
directoryChangesEvents = new BlockingCollection<FileSystemEventArgs>();
fileSystemWatcher = new FileSystemWatcher(RootDirectory)
try
{
IncludeSubdirectories = true,
EnableRaisingEvents = true
};
fileSystemWatcher.Created += FileSystemWatcher_Changed;
fileSystemWatcher.Deleted += FileSystemWatcher_Changed;
fileSystemWatcher.Renamed += FileSystemWatcher_Changed;
fileSystemWatcher.Error += FileSystemWatcher_Error;
directoryChangesEvents = new BlockingCollection<FileSystemEventArgs>();
fileSystemWatcher = new FileSystemWatcher(RootDirectory)
{
IncludeSubdirectories = true,
EnableRaisingEvents = true
};
fileSystemWatcher.Created += FileSystemWatcher_Changed;
fileSystemWatcher.Deleted += FileSystemWatcher_Changed;
fileSystemWatcher.Renamed += FileSystemWatcher_Changed;
fileSystemWatcher.Error += FileSystemWatcher_Error;
backgroundScanner = new Task(BackgroundScanner);
backgroundScanner.Start();
backgroundScanner = new Task(BackgroundScanner);
backgroundScanner.Start();
}
// Watching a directory fails for the same reasons reading one does, and a removable drive can be pulled
// between the listing above and this. Constructing this type happens inside the static initializer that
// builds the Books file cache, and the runtime caches a failed initializer for the life of the process:
// one throw here would be rethrown at every later reader of the Books directory. Give up the live
// updates instead. Dropping RootDirectory has the owner rebuild this once the directory works again.
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Could not watch a directory for changes, so its file cache was abandoned: {@DebugText}", new { path = (string?)RootDirectory });
Stop();
RootDirectory = null;
}
}
private void Stop()
{