Skip to content

Commit

Permalink
Simplify file watching
Browse files Browse the repository at this point in the history
Cleanup

Fixes

Cleanup

Use unique ports in tests
  • Loading branch information
tmat committed Nov 6, 2024
1 parent c3d59b9 commit a6c6ff3
Show file tree
Hide file tree
Showing 14 changed files with 296 additions and 357 deletions.
12 changes: 8 additions & 4 deletions src/BuiltInTools/dotnet-watch/DotNetWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke

using var currentRunCancellationSource = new CancellationTokenSource();
using var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(shutdownCancellationToken, currentRunCancellationSource.Token);
using var fileSetWatcher = new FileWatcher(evaluationResult.Files, Context.Reporter);
using var fileSetWatcher = new FileWatcher(Context.Reporter);

fileSetWatcher.WatchDirectories(evaluationResult.Files.Keys);

var processTask = ProcessRunner.RunAsync(processSpec, Context.Reporter, isUserApplication: true, launchResult: null, combinedCancellationSource.Token);

Expand All @@ -89,7 +91,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke

while (true)
{
fileSetTask = fileSetWatcher.GetChangedFileAsync(startedWatching: null, combinedCancellationSource.Token);
fileSetTask = fileSetWatcher.WaitForFileChangeAsync(evaluationResult.Files, startedWatching: null, combinedCancellationSource.Token);
finishedTask = await Task.WhenAny(processTask, fileSetTask, cancelledTaskSource.Task);

if (staticFileHandler != null && finishedTask == fileSetTask && fileSetTask.Result.HasValue)
Expand Down Expand Up @@ -119,9 +121,11 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
{
// Process exited. Redo evalulation
buildEvaluator.RequiresRevaluation = true;

// Now wait for a file to change before restarting process
changedFile = await fileSetWatcher.GetChangedFileAsync(
() => Context.Reporter.Report(MessageDescriptor.WaitingForFileChangeBeforeRestarting),
changedFile = await fileSetWatcher.WaitForFileChangeAsync(
evaluationResult.Files,
startedWatching: () => Context.Reporter.Report(MessageDescriptor.WaitingForFileChangeBeforeRestarting),
shutdownCancellationToken);
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ async Task<ImmutableArray<string>> ConnectAsync()
Reporter.Verbose($"Capabilities: '{capabilities}'");
return capabilities.Split(' ').ToImmutableArray();
}
catch (EndOfStreamException)
{
// process terminated before capabilities sent:
return [];
}
catch (Exception e) when (e is not OperationCanceledException)
{
// pipe might throw another exception when forcibly closed on process termination:
Expand Down
191 changes: 160 additions & 31 deletions src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs

Large diffs are not rendered by default.

47 changes: 32 additions & 15 deletions src/BuiltInTools/dotnet-watch/Internal/FileWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace Microsoft.DotNet.Watcher.Internal
{
internal sealed class FileWatcher(IReadOnlyDictionary<string, FileItem> fileSet, IReporter reporter) : IDisposable
internal sealed class FileWatcher(IReporter reporter) : IDisposable
{
// Directory watcher for each watched directory
private readonly Dictionary<string, IDirectoryWatcher> _watchers = [];
Expand All @@ -30,13 +30,19 @@ public void Dispose()
}
}

public void StartWatching()
public bool WatchingDirectories
=> _watchers.Count > 0;

public void WatchContainingDirectories(IEnumerable<string> filePaths)
=> WatchDirectories(filePaths.Select(path => Path.GetDirectoryName(path)!));

public void WatchDirectories(IEnumerable<string> directories)
{
ObjectDisposedException.ThrowIf(_disposed, this);

foreach (var (filePath, _) in fileSet)
foreach (var dir in directories)
{
var directory = EnsureTrailingSlash(Path.GetDirectoryName(filePath)!);
var directory = EnsureTrailingSlash(dir);

var alreadyWatched = _watchers
.Where(d => directory.StartsWith(d.Key))
Expand Down Expand Up @@ -70,7 +76,7 @@ private void WatcherErrorHandler(object? sender, Exception error)
{
if (sender is IDirectoryWatcher watcher)
{
reporter.Warn($"The file watcher observing '{watcher.Directory}' encountered an error: {error.Message}");
reporter.Warn($"The file watcher observing '{watcher.WatchedDirectory}' encountered an error: {error.Message}");
}
}

Expand All @@ -94,18 +100,22 @@ private void DisposeWatcher(string directory)
private static string EnsureTrailingSlash(string path)
=> (path is [.., var last] && last != Path.DirectorySeparatorChar) ? path + Path.DirectorySeparatorChar : path;

public async Task<ChangedFile?> GetChangedFileAsync(Action? startedWatching, CancellationToken cancellationToken)
{
StartWatching();
public Task<ChangedFile?> WaitForFileChangeAsync(IReadOnlyDictionary<string, FileItem> fileSet, Action? startedWatching, CancellationToken cancellationToken)
=> WaitForFileChangeAsync(
changeFilter: (path, kind) => fileSet.TryGetValue(path, out var fileItem) ? new ChangedFile(fileItem, kind) : null,
startedWatching,
cancellationToken);

public async Task<ChangedFile?> WaitForFileChangeAsync(Func<string, ChangeKind, ChangedFile?> changeFilter, Action? startedWatching, CancellationToken cancellationToken)
{
var fileChangedSource = new TaskCompletionSource<ChangedFile?>(TaskCreationOptions.RunContinuationsAsynchronously);
cancellationToken.Register(() => fileChangedSource.TrySetResult(null));

void FileChangedCallback(string path, ChangeKind kind)
{
if (fileSet.TryGetValue(path, out var fileItem))
if (changeFilter(path, kind) is { } changedFile)
{
fileChangedSource.TrySetResult(new ChangedFile(fileItem, kind));
fileChangedSource.TrySetResult(changedFile);
}
}

Expand All @@ -125,14 +135,21 @@ void FileChangedCallback(string path, ChangeKind kind)
return changedFile;
}

public static async ValueTask WaitForFileChangeAsync(string path, IReporter reporter, CancellationToken cancellationToken)
public static async ValueTask WaitForFileChangeAsync(string filePath, IReporter reporter, CancellationToken cancellationToken)
{
var fileSet = new Dictionary<string, FileItem>() { { path, new FileItem { FilePath = path } } };
using var watcher = new FileWatcher(reporter);

using var watcher = new FileWatcher(fileSet, reporter);
await watcher.GetChangedFileAsync(startedWatching: null, cancellationToken);
watcher.WatchDirectories([Path.GetDirectoryName(filePath)!]);

reporter.Output($"File changed: {path}");
var fileChange = await watcher.WaitForFileChangeAsync(
changeFilter: (path, kind) => path == filePath ? new ChangedFile(new FileItem { FilePath = path }, kind) : null,
startedWatching:
null, cancellationToken);

if (fileChange != null)
{
reporter.Output($"File changed: {filePath}");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ internal sealed class EventBasedDirectoryWatcher : IDirectoryWatcher

public event EventHandler<Exception>? OnError;

public string Directory { get; }
public string WatchedDirectory { get; }

internal Action<string>? Logger { get; set; }
internal Action<string>? Logger { get; set; }

private volatile bool _disposed;

Expand All @@ -24,10 +24,16 @@ internal sealed class EventBasedDirectoryWatcher : IDirectoryWatcher

internal EventBasedDirectoryWatcher(string watchedDirectory)
{
Directory = watchedDirectory;
WatchedDirectory = watchedDirectory;
CreateFileSystemWatcher();
}

public void Dispose()
{
_disposed = true;
DisposeInnerWatcher();
}

private void WatcherErrorHandler(object sender, ErrorEventArgs e)
{
if (_disposed)
Expand Down Expand Up @@ -65,9 +71,9 @@ private void WatcherRenameHandler(object sender, RenamedEventArgs e)
NotifyChange(e.OldFullPath, ChangeKind.Delete);
NotifyChange(e.FullPath, ChangeKind.Add);

if (System.IO.Directory.Exists(e.FullPath))
if (Directory.Exists(e.FullPath))
{
foreach (var newLocation in System.IO.Directory.EnumerateFileSystemEntries(e.FullPath, "*", SearchOption.AllDirectories))
foreach (var newLocation in Directory.EnumerateFileSystemEntries(e.FullPath, "*", SearchOption.AllDirectories))
{
// Calculated previous path of this moved item.
var oldLocation = Path.Combine(e.OldFullPath, newLocation.Substring(e.FullPath.Length + 1));
Expand Down Expand Up @@ -129,7 +135,7 @@ private void CreateFileSystemWatcher()
DisposeInnerWatcher();
}

_fileSystemWatcher = new FileSystemWatcher(Directory)
_fileSystemWatcher = new FileSystemWatcher(WatchedDirectory)
{
IncludeSubdirectories = true
};
Expand All @@ -146,7 +152,7 @@ private void CreateFileSystemWatcher()

private void DisposeInnerWatcher()
{
if ( _fileSystemWatcher != null )
if (_fileSystemWatcher != null)
{
_fileSystemWatcher.EnableRaisingEvents = false;

Expand All @@ -165,11 +171,5 @@ public bool EnableRaisingEvents
get => _fileSystemWatcher!.EnableRaisingEvents;
set => _fileSystemWatcher!.EnableRaisingEvents = value;
}

public void Dispose()
{
_disposed = true;
DisposeInnerWatcher();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ internal interface IDirectoryWatcher : IDisposable

event EventHandler<Exception> OnError;

string Directory { get; }
string WatchedDirectory { get; }

bool EnableRaisingEvents { get; set; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,29 @@ internal sealed class PollingDirectoryWatcher : IDirectoryWatcher

private readonly DirectoryInfo _watchedDirectory;

private Dictionary<string, FileMeta> _knownEntities = new();
private Dictionary<string, FileMeta> _tempDictionary = new();
private Dictionary<string, ChangeKind> _changes = new();
private Dictionary<string, FileMeta> _knownEntities = [];
private Dictionary<string, FileMeta> _tempDictionary = [];
private readonly Dictionary<string, ChangeKind> _changes = [];

private Thread _pollingThread;
private bool _raiseEvents;

private bool _disposed;
private volatile bool _disposed;

public event EventHandler<(string filePath, ChangeKind kind)>? OnFileChange;

#pragma warning disable CS0067 // not used
public event EventHandler<Exception>? OnError;
#pragma warning restore

public string WatchedDirectory { get; }

public PollingDirectoryWatcher(string watchedDirectory)
{
Ensure.NotNullOrEmpty(watchedDirectory, nameof(watchedDirectory));

_watchedDirectory = new DirectoryInfo(watchedDirectory);
Directory = _watchedDirectory.FullName;
WatchedDirectory = _watchedDirectory.FullName;

_pollingThread = new Thread(new ThreadStart(PollingLoop))
{
Expand All @@ -40,20 +48,18 @@ public PollingDirectoryWatcher(string watchedDirectory)
_pollingThread.Start();
}

public event EventHandler<(string filePath, ChangeKind kind)>? OnFileChange;

#pragma warning disable CS0067 // not used
public event EventHandler<Exception>? OnError;
#pragma warning restore

public string Directory { get; }
public void Dispose()
{
EnableRaisingEvents = false;
_disposed = true;
}

public bool EnableRaisingEvents
{
get => _raiseEvents;
set
{
EnsureNotDisposed();
ObjectDisposedException.ThrowIf(_disposed, this);
_raiseEvents = value;
}
}
Expand Down Expand Up @@ -90,24 +96,24 @@ private void CreateKnownFilesSnapshot()
{
_knownEntities.Clear();

ForeachEntityInDirectory(_watchedDirectory, f =>
ForeachEntityInDirectory(_watchedDirectory, fileInfo =>
{
_knownEntities.Add(f.FullName, new FileMeta(f));
_knownEntities.Add(fileInfo.FullName, new FileMeta(fileInfo, foundAgain: false));
});
}

private void CheckForChangedFiles()
{
_changes.Clear();

ForeachEntityInDirectory(_watchedDirectory, f =>
ForeachEntityInDirectory(_watchedDirectory, fileInfo =>
{
var fullFilePath = f.FullName;
var fullFilePath = fileInfo.FullName;

if (!_knownEntities.ContainsKey(fullFilePath))
{
// New file or directory
RecordChange(f, ChangeKind.Add);
RecordChange(fileInfo, ChangeKind.Add);
}
else
{
Expand All @@ -116,10 +122,10 @@ private void CheckForChangedFiles()
try
{
if (!fileMeta.FileInfo.Attributes.HasFlag(FileAttributes.Directory) &&
fileMeta.FileInfo.LastWriteTime != f.LastWriteTime)
fileMeta.FileInfo.LastWriteTime != fileInfo.LastWriteTime)
{
// File changed
RecordChange(f, ChangeKind.Update);
RecordChange(fileInfo, ChangeKind.Update);
}

_knownEntities[fullFilePath] = new FileMeta(fileMeta.FileInfo, foundAgain: true);
Expand All @@ -130,7 +136,7 @@ private void CheckForChangedFiles()
}
}

_tempDictionary.Add(f.FullName, new FileMeta(f));
_tempDictionary.Add(fileInfo.FullName, new FileMeta(fileInfo, foundAgain: false));
});

foreach (var file in _knownEntities)
Expand Down Expand Up @@ -211,31 +217,10 @@ private void NotifyChanges()
}
}

private void EnsureNotDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(PollingDirectoryWatcher));
}
}

public void Dispose()
{
EnableRaisingEvents = false;
_disposed = true;
}

private struct FileMeta
private readonly struct FileMeta(FileSystemInfo fileInfo, bool foundAgain)
{
public FileMeta(FileSystemInfo fileInfo, bool foundAgain = false)
{
FileInfo = fileInfo;
FoundAgain = foundAgain;
}

public FileSystemInfo FileInfo;

public bool FoundAgain;
public readonly FileSystemInfo FileInfo = fileInfo;
public readonly bool FoundAgain = foundAgain;
}
}
}
Loading

0 comments on commit a6c6ff3

Please sign in to comment.