Use non-recursive watcher in PhysicalFilesWatcher for missing parent directories#126026
Use non-recursive watcher in PhysicalFilesWatcher for missing parent directories#126026
Conversation
…t directories When a watched file's parent directory does not exist, PhysicalFilesWatcher now uses a cascading non-recursive FileSystemWatcher (PendingCreationWatcher) that advances through intermediate directory levels as they are created. This avoids adding recursive watches on the entire ancestor tree and avoids spurious token fires for intermediate directory creations. Key changes: - PendingCreationWatcher cascades through missing directory levels using a non-recursive FileSystemWatcher, only firing when the target file is created - HasMissingParentDirectory detects when the watched path's parent does not exist - FileConfigurationSource.ResolveFileProvider walks up to the nearest existing ancestor - ArraySegmentExtensions added for netstandard2.0/net462 compatibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR addresses excessive recursive OS file watches (e.g., inotify) triggered when watching a file whose parent directory doesn’t exist yet, by introducing a non-recursive “cascading” watcher approach and adjusting configuration/file-provider resolution to work with missing directories.
Changes:
- Add a
PendingCreationWatcherpath-component–by–path-component watcher for missing-parent scenarios, avoiding recursive subtree watches. - Allow
PhysicalFileProviderconstruction when the root directory doesn’t exist and adjustFileSystemWatcherinitialization accordingly. - Update
FileConfigurationSource.ResolveFileProviderbehavior and add tests covering missing-directory watch behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs | Adds pending-creation watcher logic and changes when the main FileSystemWatcher is enabled. |
| src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFileProvider.cs | Removes root existence requirement; creates FileSystemWatcher without setting Path up-front. |
| src/libraries/Microsoft.Extensions.Configuration.FileExtensions/src/FileConfigurationSource.cs | Changes resolution to create a provider for the file’s immediate directory and reduce the config path to the filename. |
| src/libraries/Microsoft.Extensions.FileProviders.Physical/src/ArraySegmentExtensions.cs | Adds ArraySegment<T>.At and a Slice polyfill for non-NET TFMs. |
| src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFilesWatcherTests.cs | Adds unit tests for pending-creation watcher behavior and main watcher enablement. |
| src/libraries/Microsoft.Extensions.Configuration.FileExtensions/tests/FileConfigurationProviderTest.cs | Adds an integration-style test for config reload token firing when missing directory/file is created. |
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs
Outdated
Show resolved
Hide resolved
...ies/Microsoft.Extensions.Configuration.FileExtensions/tests/FileConfigurationProviderTest.cs
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs
Outdated
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs
Outdated
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.Configuration.FileExtensions/src/FileConfigurationSource.cs
Show resolved
Hide resolved
...ies/Microsoft.Extensions.Configuration.FileExtensions/tests/FileConfigurationProviderTest.cs
Outdated
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFilesWatcherTests.cs
Outdated
Show resolved
Hide resolved
...ies/Microsoft.Extensions.Configuration.FileExtensions/tests/FileConfigurationProviderTest.cs
Outdated
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs
Outdated
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs
Outdated
Show resolved
Hide resolved
...ies/Microsoft.Extensions.Configuration.FileExtensions/tests/FileConfigurationProviderTest.cs
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs
Outdated
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs
Outdated
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFilesWatcherTests.cs
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs
Outdated
Show resolved
Hide resolved
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Petr Onderka <gsvick@gmail.com>
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs
Outdated
Show resolved
Hide resolved
f590fea to
7963837
Compare
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs
Outdated
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs
Outdated
Show resolved
Hide resolved
...ies/Microsoft.Extensions.Configuration.FileExtensions/tests/FileConfigurationProviderTest.cs
Show resolved
Hide resolved
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFilesWatcherTests.cs
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PollingFileChangeToken.cs
Outdated
Show resolved
Hide resolved
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs
Outdated
Show resolved
Hide resolved
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
|
||
| private static readonly char[] _pathSeparators = new[] | ||
| {Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar}; | ||
| internal static char[] PathSeparators => |
There was a problem hiding this comment.
PathSeparators currently creates a new char[] on every access ([ ... ]). This introduces avoidable allocations on hot paths like GetFileInfo, Watch, and PathNavigatesAboveRoot. Consider making this a cached immutable instance (e.g., internal static readonly char[] PathSeparators = { ... }; or a get-only property backed by a static field) so callers can reuse it without per-call allocations.
| internal static char[] PathSeparators => | |
| internal static readonly char[] PathSeparators = |
| try | ||
| { | ||
| if (string.IsNullOrEmpty(_fileWatcher.Path)) | ||
| { | ||
| _fileWatcher.Path = _root; | ||
| } | ||
|
|
||
| _fileWatcher.EnableRaisingEvents = true; | ||
|
|
||
| // Only scan for existing entries if the FSW was enabled after _root | ||
| // was initially missing (i.e. we went through the PCW path). In the | ||
| // normal case where _root always existed, there is no gap to cover. | ||
| justEnabledAfterRootCreated = _rootWasUnavailable; | ||
| _rootWasUnavailable = false; | ||
| } |
There was a problem hiding this comment.
After the root directory is successfully created and the main FileSystemWatcher is enabled, _rootCreationWatcher is left referenced (with a cancelled token) until PhysicalFilesWatcher.Dispose() or another missing-root cycle. Consider disposing and clearing _rootCreationWatcher once _fileWatcher.EnableRaisingEvents = true succeeds to avoid keeping the CancellationTokenSource/registration alive for the lifetime of the provider.
| private static PhysicalFilesWatcher CreateWatcher(string rootPath, bool useActivePolling) | ||
| { | ||
| FileSystemWatcher? fsw = useActivePolling ? null : new FileSystemWatcher(rootPath); | ||
| var watcher = new PhysicalFilesWatcher(rootPath, fsw, pollForChanges: useActivePolling); | ||
| if (useActivePolling) | ||
| { | ||
| watcher.UseActivePolling = true; | ||
| } | ||
| return watcher; |
There was a problem hiding this comment.
CreateWatcher constructs new FileSystemWatcher(rootPath) when useActivePolling == false, which requires the root to exist and doesn’t exercise the new production path where PhysicalFileProvider creates new FileSystemWatcher() with an empty Path (to support missing roots). To better cover the missing-root scenario and the _fileWatcher.Path initialization in TryEnableFileSystemWatcher, consider constructing new FileSystemWatcher() here as well (and letting PhysicalFilesWatcher set the path when enabling).
🤖 Copilot Code Review — PR #126026Holistic AssessmentMotivation: The problem is real and well-motivated. Approach: The approach — deferring Summary: Detailed Findings
|
Fixes #116713
Problem
When
PhysicalFileProvideris constructed with a root directory that does not yet exist (e.g., a configuration file path whose parent directory hasn't been created),Watch()fails becauseFileSystemWatchercannot watch a non-existent directory. This commonly occurs withAddJsonFilewhen the config file's parent directory is missing at startup.Solution
PhysicalFilesWatchernow gracefully handles a missing root directory by deferringFileSystemWatcheractivation until the root appears. APendingCreationWatchermonitors the nearest existing ancestor directory using a non-recursiveFileSystemWatcherand cascades through intermediate directory levels as they are created. Once the root directory exists, the main recursiveFileSystemWatcheris enabled and any already-existing watched entries are reported.Callers always receive normal FSW-backed change tokens — no re-registration is needed when the root directory appears later.
Changes
PhysicalFilesWatcher.csPendingCreationWatcherinner class that watches for a non-existent directory to be created.TryEnableFileSystemWatcherdefers toEnsureRootCreationWatcherwhen_rootdoesn't exist, with a callback to retry once it appears.ReportExistingWatchedEntriesfires tokens for entries created before the FSW was active. The constructor normalizes_rootto always have a trailing separator.OnFileSystemEntryChangenow usesDirectoryInfofor directory paths so exclusion filters work correctly.PhysicalFileProvider.csDirectoryNotFoundExceptionfor a missing root — thePhysicalFilesWatcherhandles it. UpdatedWatchdoc comments to include directories. Removed duplicate_pathSeparatorsfield in favor ofPathUtils.PathSeparators.FileConfigurationSource.csResolveFileProvidercreates thePhysicalFileProviderwith the file's immediate parent directory (even if missing), relying on the watcher to handle the non-existent case.PollingFileChangeToken.csGetLastWriteTimeUtcnow falls back to checkingDirectoryInfowhenFileInfo.Existsis false, so polling correctly detects directory changes.PathUtils.csPathSeparatorsmadeinternalfor reuse across files.PhysicalFilesWatcherTests.csFileConfigurationProviderTest.csPhysicalFileProviderTests.csTokenFiredForGlobbingPatternsPointingToSubDirectorypath length issue on .NET Framework. FixedTokensWithForwardAndBackwardSlashesAreSameandTokensFiredForNewDirectoryContentsOnRename.