Skip to content

Commit b810aa4

Browse files
authored
File watcher improvements (#45131)
1 parent 79f2919 commit b810aa4

File tree

67 files changed

+1792
-368
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+1792
-368
lines changed

src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public bool TryGetRefreshServer(ProjectGraphNode projectNode, [NotNullWhen(true)
9696
{
9797
lock (_serversGuard)
9898
{
99-
return _servers.TryGetValue(projectNode, out server);
99+
return _servers.TryGetValue(projectNode, out server) && server != null;
100100
}
101101
}
102102

src/BuiltInTools/dotnet-watch/FileItem.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,12 @@ internal readonly record struct FileItem
1010

1111
/// <summary>
1212
/// List of all projects that contain this file (does not contain duplicates).
13-
/// Empty if <see cref="Change"/> is <see cref="ChangeKind.Add"/> and the
14-
/// item has not been assigned to a project yet.
13+
/// Empty if the item is added but not been assigned to a project yet.
1514
/// </summary>
1615
public required List<string> ContainingProjectPaths { get; init; }
1716

1817
public string? StaticWebAssetPath { get; init; }
1918

20-
public ChangeKind Change { get; init; }
21-
2219
public bool IsStaticFile => StaticWebAssetPath != null;
2320
}
2421
}

src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ private async ValueTask DisplayResultsAsync(WatchHotReloadService.Updates update
364364
switch (updates.Status)
365365
{
366366
case ModuleUpdateStatus.None:
367-
_reporter.Output("No C# changes to apply.");
367+
_reporter.Report(MessageDescriptor.NoHotReloadChangesToApply);
368368
break;
369369

370370
case ModuleUpdateStatus.Ready:

src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs

Lines changed: 209 additions & 69 deletions
Large diffs are not rendered by default.

src/BuiltInTools/dotnet-watch/Internal/FileWatcher.cs

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ internal sealed class FileWatcher(IReporter reporter) : IDisposable
99
private readonly Dictionary<string, IDirectoryWatcher> _watchers = [];
1010

1111
private bool _disposed;
12-
public event Action<string, ChangeKind>? OnFileChange;
12+
public event Action<ChangedPath>? OnFileChange;
13+
14+
public bool SuppressEvents { get; set; }
1315

1416
public void Dispose()
1517
{
@@ -62,6 +64,11 @@ public void WatchDirectories(IEnumerable<string> directories)
6264
}
6365

6466
var newWatcher = FileWatcherFactory.CreateWatcher(directory);
67+
if (newWatcher is EventBasedDirectoryWatcher eventBasedWatcher)
68+
{
69+
eventBasedWatcher.Logger = message => reporter.Verbose(message);
70+
}
71+
6572
newWatcher.OnFileChange += WatcherChangedHandler;
6673
newWatcher.OnError += WatcherErrorHandler;
6774
newWatcher.EnableRaisingEvents = true;
@@ -78,9 +85,12 @@ private void WatcherErrorHandler(object? sender, Exception error)
7885
}
7986
}
8087

81-
private void WatcherChangedHandler(object? sender, (string changedPath, ChangeKind kind) args)
88+
private void WatcherChangedHandler(object? sender, ChangedPath change)
8289
{
83-
OnFileChange?.Invoke(args.changedPath, args.kind);
90+
if (!SuppressEvents)
91+
{
92+
OnFileChange?.Invoke(change);
93+
}
8494
}
8595

8696
private void DisposeWatcher(string directory)
@@ -98,45 +108,43 @@ private void DisposeWatcher(string directory)
98108
private static string EnsureTrailingSlash(string path)
99109
=> (path is [.., var last] && last != Path.DirectorySeparatorChar) ? path + Path.DirectorySeparatorChar : path;
100110

101-
public Task<ChangedFile?> WaitForFileChangeAsync(Action? startedWatching, CancellationToken cancellationToken)
102-
=> WaitForFileChangeAsync(
103-
changeFilter: (path, kind) => new ChangedFile(new FileItem() { FilePath = path, ContainingProjectPaths = [] }, kind),
104-
startedWatching,
105-
cancellationToken);
106-
107-
public Task<ChangedFile?> WaitForFileChangeAsync(IReadOnlyDictionary<string, FileItem> fileSet, Action? startedWatching, CancellationToken cancellationToken)
108-
=> WaitForFileChangeAsync(
109-
changeFilter: (path, kind) => fileSet.TryGetValue(path, out var fileItem) ? new ChangedFile(fileItem, kind) : null,
111+
public async Task<ChangedFile?> WaitForFileChangeAsync(IReadOnlyDictionary<string, FileItem> fileSet, Action? startedWatching, CancellationToken cancellationToken)
112+
{
113+
var changedPath = await WaitForFileChangeAsync(
114+
acceptChange: change => fileSet.ContainsKey(change.Path),
110115
startedWatching,
111116
cancellationToken);
112117

113-
public async Task<ChangedFile?> WaitForFileChangeAsync(Func<string, ChangeKind, ChangedFile?> changeFilter, Action? startedWatching, CancellationToken cancellationToken)
118+
return changedPath.HasValue ? new ChangedFile(fileSet[changedPath.Value.Path], changedPath.Value.Kind) : null;
119+
}
120+
121+
public async Task<ChangedPath?> WaitForFileChangeAsync(Predicate<ChangedPath> acceptChange, Action? startedWatching, CancellationToken cancellationToken)
114122
{
115-
var fileChangedSource = new TaskCompletionSource<ChangedFile?>(TaskCreationOptions.RunContinuationsAsynchronously);
123+
var fileChangedSource = new TaskCompletionSource<ChangedPath?>(TaskCreationOptions.RunContinuationsAsynchronously);
116124
cancellationToken.Register(() => fileChangedSource.TrySetResult(null));
117125

118-
void FileChangedCallback(string path, ChangeKind kind)
126+
void FileChangedCallback(ChangedPath change)
119127
{
120-
if (changeFilter(path, kind) is { } changedFile)
128+
if (acceptChange(change))
121129
{
122-
fileChangedSource.TrySetResult(changedFile);
130+
fileChangedSource.TrySetResult(change);
123131
}
124132
}
125133

126-
ChangedFile? changedFile;
134+
ChangedPath? change;
127135

128136
OnFileChange += FileChangedCallback;
129137
try
130138
{
131139
startedWatching?.Invoke();
132-
changedFile = await fileChangedSource.Task;
140+
change = await fileChangedSource.Task;
133141
}
134142
finally
135143
{
136144
OnFileChange -= FileChangedCallback;
137145
}
138146

139-
return changedFile;
147+
return change;
140148
}
141149

142150
public static async ValueTask WaitForFileChangeAsync(string filePath, IReporter reporter, Action? startedWatching, CancellationToken cancellationToken)
@@ -146,7 +154,7 @@ public static async ValueTask WaitForFileChangeAsync(string filePath, IReporter
146154
watcher.WatchDirectories([Path.GetDirectoryName(filePath)!]);
147155

148156
var fileChange = await watcher.WaitForFileChangeAsync(
149-
changeFilter: (path, kind) => path == filePath ? new ChangedFile(new FileItem { FilePath = path, ContainingProjectPaths = [] }, kind) : null,
157+
acceptChange: change => change.Path == filePath,
150158
startedWatching,
151159
cancellationToken);
152160

src/BuiltInTools/dotnet-watch/Internal/FileWatcher/ChangeKind.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ internal enum ChangeKind
1010
Delete
1111
}
1212

13-
internal readonly record struct ChangedFile(FileItem Item, ChangeKind Change);
13+
internal readonly record struct ChangedFile(FileItem Item, ChangeKind Kind);
14+
15+
internal readonly record struct ChangedPath(string Path, ChangeKind Kind);

src/BuiltInTools/dotnet-watch/Internal/FileWatcher/EventBasedDirectoryWatcher.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Microsoft.DotNet.Watch
77
{
88
internal sealed class EventBasedDirectoryWatcher : IDirectoryWatcher
99
{
10-
public event EventHandler<(string filePath, ChangeKind kind)>? OnFileChange;
10+
public event EventHandler<ChangedPath>? OnFileChange;
1111

1212
public event EventHandler<Exception>? OnError;
1313

@@ -118,7 +118,7 @@ private void WatcherAddedHandler(object sender, FileSystemEventArgs e)
118118
private void NotifyChange(string fullPath, ChangeKind kind)
119119
{
120120
// Only report file changes
121-
OnFileChange?.Invoke(this, (fullPath, kind));
121+
OnFileChange?.Invoke(this, new ChangedPath(fullPath, kind));
122122
}
123123

124124
private void CreateFileSystemWatcher()

src/BuiltInTools/dotnet-watch/Internal/FileWatcher/IDirectoryWatcher.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Microsoft.DotNet.Watch
55
{
66
internal interface IDirectoryWatcher : IDisposable
77
{
8-
event EventHandler<(string filePath, ChangeKind kind)> OnFileChange;
8+
event EventHandler<ChangedPath> OnFileChange;
99

1010
event EventHandler<Exception> OnError;
1111

src/BuiltInTools/dotnet-watch/Internal/FileWatcher/PollingDirectoryWatcher.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ internal sealed class PollingDirectoryWatcher : IDirectoryWatcher
2121

2222
private volatile bool _disposed;
2323

24-
public event EventHandler<(string filePath, ChangeKind kind)>? OnFileChange;
24+
public event EventHandler<ChangedPath>? OnFileChange;
2525

2626
#pragma warning disable CS0067 // not used
2727
public event EventHandler<Exception>? OnError;
@@ -212,7 +212,7 @@ private void NotifyChanges()
212212
break;
213213
}
214214

215-
OnFileChange?.Invoke(this, (path, kind));
215+
OnFileChange?.Invoke(this, new ChangedPath(path, kind));
216216
}
217217
}
218218

src/BuiltInTools/dotnet-watch/Internal/IReporter.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ public bool TryGetMessage(string? prefix, object?[] args, [NotNullWhen(true)] ou
7272
public static readonly MessageDescriptor ApplyUpdate_FileContentDoesNotMatchBuiltSource = new("{0} Expected if a source file is updated that is linked to project whose build is not up-to-date.", "⌚", MessageSeverity.Verbose, s_id++);
7373
public static readonly MessageDescriptor ConfiguredToLaunchBrowser = new("dotnet-watch is configured to launch a browser on ASP.NET Core application startup.", "⌚", MessageSeverity.Verbose, s_id++);
7474
public static readonly MessageDescriptor ConfiguredToUseBrowserRefresh = new("Configuring the app to use browser-refresh middleware", "⌚", MessageSeverity.Verbose, s_id++);
75+
public static readonly MessageDescriptor IgnoringChangeInHiddenDirectory = new("Ignoring change in hidden directory '{0}': {1} '{2}'", "⌚", MessageSeverity.Verbose, s_id++);
76+
public static readonly MessageDescriptor IgnoringChangeInOutputDirectory = new("Ignoring change in output directory: {0} '{1}'", "⌚", MessageSeverity.Verbose, s_id++);
77+
public static readonly MessageDescriptor FileAdditionTriggeredReEvaluation = new("File addition triggered re-evaluation.", "⌚", MessageSeverity.Verbose, s_id++);
78+
public static readonly MessageDescriptor NoHotReloadChangesToApply = new ("No C# changes to apply.", "⌚", MessageSeverity.Output, s_id++);
7579
}
7680

7781
internal interface IReporter

0 commit comments

Comments
 (0)