diff --git a/shared/NullReporter.cs b/shared/NullReporter.cs new file mode 100644 index 0000000000..5d80aeac91 --- /dev/null +++ b/shared/NullReporter.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Extensions.Tools.Internal +{ + public class NullReporter : IReporter + { + private NullReporter() + { } + + public static IReporter Singleton { get; } = new NullReporter(); + + public void Verbose(string message) + { } + + public void Output(string message) + { } + + public void Warn(string message) + { } + + public void Error(string message) + { } + } +} diff --git a/src/Microsoft.DotNet.Watcher.Tools/CommandLineOptions.cs b/src/Microsoft.DotNet.Watcher.Tools/CommandLineOptions.cs index af158d0e53..22705c9265 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/CommandLineOptions.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/CommandLineOptions.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; using System.Reflection; using Microsoft.DotNet.Watcher.Tools; @@ -19,6 +20,17 @@ namespace Microsoft.DotNet.Watcher public IList RemainingArguments { get; private set; } public bool ListFiles { get; private set; } + public static bool IsPollingEnabled + { + get + { + var envVar = Environment.GetEnvironmentVariable("DOTNET_USE_POLLING_FILE_WATCHER"); + return envVar != null && + (envVar.Equals("1", StringComparison.OrdinalIgnoreCase) || + envVar.Equals("true", StringComparison.OrdinalIgnoreCase)); + } + } + public static CommandLineOptions Parse(string[] args, IConsole console) { Ensure.NotNull(args, nameof(args)); diff --git a/src/Microsoft.DotNet.Watcher.Tools/DotNetWatcher.cs b/src/Microsoft.DotNet.Watcher.Tools/DotNetWatcher.cs index 66a213a043..258ba0ab1e 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/DotNetWatcher.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/DotNetWatcher.cs @@ -51,7 +51,7 @@ namespace Microsoft.DotNet.Watcher using (var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken, currentRunCancellationSource.Token)) - using (var fileSetWatcher = new FileSetWatcher(fileSet)) + using (var fileSetWatcher = new FileSetWatcher(fileSet, _reporter)) { var fileSetTask = fileSetWatcher.GetChangedFileAsync(combinedCancellationSource.Token); var processTask = _processRunner.RunAsync(processSpec, combinedCancellationSource.Token); diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/FileSetWatcher.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/FileSetWatcher.cs index 597d13cdd1..3dc56cc452 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/FileSetWatcher.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Internal/FileSetWatcher.cs @@ -11,14 +11,15 @@ namespace Microsoft.DotNet.Watcher.Internal { public class FileSetWatcher : IDisposable { - private readonly FileWatcher _fileWatcher = new FileWatcher(); + private readonly FileWatcher _fileWatcher; private readonly IFileSet _fileSet; - public FileSetWatcher(IFileSet fileSet) + public FileSetWatcher(IFileSet fileSet, IReporter reporter) { Ensure.NotNull(fileSet, nameof(fileSet)); _fileSet = fileSet; + _fileWatcher = new FileWatcher(reporter); } public async Task GetChangedFileAsync(CancellationToken cancellationToken) diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher.cs index 20840f40b9..65c2ff9285 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Microsoft.Extensions.Tools.Internal; namespace Microsoft.DotNet.Watcher.Internal { @@ -12,7 +13,18 @@ namespace Microsoft.DotNet.Watcher.Internal { private bool _disposed; - private readonly IDictionary _watchers = new Dictionary(); + private readonly IDictionary _watchers; + private readonly IReporter _reporter; + + public FileWatcher() + : this(NullReporter.Singleton) + { } + + public FileWatcher(IReporter reporter) + { + _reporter = reporter ?? throw new ArgumentNullException(nameof(reporter)); + _watchers = new Dictionary(); + } public event Action OnFileChange; @@ -33,8 +45,11 @@ namespace Microsoft.DotNet.Watcher.Internal foreach (var watcher in _watchers) { + watcher.Value.OnFileChange -= WatcherChangedHandler; + watcher.Value.OnError -= WatcherErrorHandler; watcher.Value.Dispose(); } + _watchers.Clear(); } @@ -66,11 +81,20 @@ namespace Microsoft.DotNet.Watcher.Internal var newWatcher = FileWatcherFactory.CreateWatcher(directory); newWatcher.OnFileChange += WatcherChangedHandler; + newWatcher.OnError += WatcherErrorHandler; newWatcher.EnableRaisingEvents = true; _watchers.Add(directory, newWatcher); } + private void WatcherErrorHandler(object sender, Exception error) + { + if (sender is IFileSystemWatcher watcher) + { + _reporter.Warn($"The file watcher observing '{watcher.BasePath}' encountered an error: {error.Message}"); + } + } + private void WatcherChangedHandler(object sender, string changedPath) { NotifyChange(changedPath); @@ -90,7 +114,9 @@ namespace Microsoft.DotNet.Watcher.Internal _watchers.Remove(directory); watcher.EnableRaisingEvents = false; + watcher.OnFileChange -= WatcherChangedHandler; + watcher.OnError -= WatcherErrorHandler; watcher.Dispose(); } diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/DotnetFileWatcher.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/DotnetFileWatcher.cs index 5064239746..d1103c41e6 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/DotnetFileWatcher.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/DotnetFileWatcher.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.ComponentModel; using System.IO; using Microsoft.Extensions.Tools.Internal; @@ -10,7 +11,6 @@ namespace Microsoft.DotNet.Watcher.Internal internal class DotnetFileWatcher : IFileSystemWatcher { private readonly Func _watcherFactory; - private readonly string _watchedDirectory; private FileSystemWatcher _fileSystemWatcher; @@ -26,14 +26,16 @@ namespace Microsoft.DotNet.Watcher.Internal Ensure.NotNull(fileSystemWatcherFactory, nameof(fileSystemWatcherFactory)); Ensure.NotNullOrEmpty(watchedDirectory, nameof(watchedDirectory)); - _watchedDirectory = watchedDirectory; + BasePath = watchedDirectory; _watcherFactory = fileSystemWatcherFactory; CreateFileSystemWatcher(); } public event EventHandler OnFileChange; - public event EventHandler OnError; + public event EventHandler OnError; + + public string BasePath { get; } private static FileSystemWatcher DefaultWatcherFactory(string watchedDirectory) { @@ -44,10 +46,18 @@ namespace Microsoft.DotNet.Watcher.Internal private void WatcherErrorHandler(object sender, ErrorEventArgs e) { - // Recreate the watcher - CreateFileSystemWatcher(); + var exception = e.GetException(); - OnError?.Invoke(this, null); + // Win32Exception may be triggered when setting EnableRaisingEvents on a file system type + // that is not supported, such as a network share. Don't attempt to recreate the watcher + // in this case as it will cause a StackOverflowException + if (!(exception is Win32Exception)) + { + // Recreate the watcher if it is a recoverable error. + CreateFileSystemWatcher(); + } + + OnError?.Invoke(this, exception); } private void WatcherRenameHandler(object sender, RenamedEventArgs e) @@ -99,7 +109,7 @@ namespace Microsoft.DotNet.Watcher.Internal _fileSystemWatcher.Dispose(); } - _fileSystemWatcher = _watcherFactory(_watchedDirectory); + _fileSystemWatcher = _watcherFactory(BasePath); _fileSystemWatcher.IncludeSubdirectories = true; _fileSystemWatcher.Created += WatcherChangeHandler; @@ -114,8 +124,8 @@ namespace Microsoft.DotNet.Watcher.Internal public bool EnableRaisingEvents { - get { return _fileSystemWatcher.EnableRaisingEvents; } - set { _fileSystemWatcher.EnableRaisingEvents = value; } + get => _fileSystemWatcher.EnableRaisingEvents; + set => _fileSystemWatcher.EnableRaisingEvents = value; } public void Dispose() diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/FileWatcherFactory.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/FileWatcherFactory.cs index 248342571f..cbb1c12a74 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/FileWatcherFactory.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/FileWatcherFactory.cs @@ -8,15 +8,7 @@ namespace Microsoft.DotNet.Watcher.Internal public static class FileWatcherFactory { public static IFileSystemWatcher CreateWatcher(string watchedDirectory) - { - var envVar = Environment.GetEnvironmentVariable("DOTNET_USE_POLLING_FILE_WATCHER"); - var usePollingWatcher = - envVar != null && - (envVar.Equals("1", StringComparison.OrdinalIgnoreCase) || - envVar.Equals("true", StringComparison.OrdinalIgnoreCase)); - - return CreateWatcher(watchedDirectory, usePollingWatcher); - } + => CreateWatcher(watchedDirectory, CommandLineOptions.IsPollingEnabled); public static IFileSystemWatcher CreateWatcher(string watchedDirectory, bool usePollingWatcher) { diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/IFileSystemWatcher.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/IFileSystemWatcher.cs index 4bfc6bac6d..aaf5773449 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/IFileSystemWatcher.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/IFileSystemWatcher.cs @@ -9,7 +9,9 @@ namespace Microsoft.DotNet.Watcher.Internal { event EventHandler OnFileChange; - event EventHandler OnError; + event EventHandler OnError; + + string BasePath { get; } bool EnableRaisingEvents { get; set; } } diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/PollingFileWatcher.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/PollingFileWatcher.cs index 25e4f6f76f..1b503af774 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/PollingFileWatcher.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Internal/FileWatcher/PollingFileWatcher.cs @@ -31,6 +31,7 @@ namespace Microsoft.DotNet.Watcher.Internal Ensure.NotNullOrEmpty(watchedDirectory, nameof(watchedDirectory)); _watchedDirectory = new DirectoryInfo(watchedDirectory); + BasePath = _watchedDirectory.FullName; _pollingThread = new Thread(new ThreadStart(PollingLoop)); _pollingThread.IsBackground = true; @@ -44,15 +45,14 @@ namespace Microsoft.DotNet.Watcher.Internal public event EventHandler OnFileChange; #pragma warning disable CS0067 // not used - public event EventHandler OnError; + public event EventHandler OnError; #pragma warning restore + public string BasePath { get; } + public bool EnableRaisingEvents { - get - { - return _raiseEvents; - } + get => _raiseEvents; set { EnsureNotDisposed(); @@ -125,7 +125,7 @@ namespace Microsoft.DotNet.Watcher.Internal _knownEntities[fullFilePath] = new FileMeta(fileMeta.FileInfo, true); } - catch(FileNotFoundException) + catch (FileNotFoundException) { _knownEntities[fullFilePath] = new FileMeta(fileMeta.FileInfo, false); } @@ -187,7 +187,7 @@ namespace Microsoft.DotNet.Watcher.Internal { return; } - + var entities = dirInfo.EnumerateFileSystemInfos("*.*"); foreach (var entity in entities) { diff --git a/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildFileSetFactory.cs b/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildFileSetFactory.cs index 01b8f4a996..0190b089ca 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildFileSetFactory.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Internal/MsBuildFileSetFactory.cs @@ -144,7 +144,7 @@ namespace Microsoft.DotNet.Watcher.Internal var fileSet = new FileSet(new[] { _projectFile }); - using (var watcher = new FileSetWatcher(fileSet)) + using (var watcher = new FileSetWatcher(fileSet, _reporter)) { await watcher.GetChangedFileAsync(cancellationToken); diff --git a/src/Microsoft.DotNet.Watcher.Tools/Program.cs b/src/Microsoft.DotNet.Watcher.Tools/Program.cs index c8832649fb..019c95a329 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Program.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Program.cs @@ -160,6 +160,11 @@ namespace Microsoft.DotNet.Watcher }, }; + if (CommandLineOptions.IsPollingEnabled) + { + _reporter.Output("Polling file watcher is enabled"); + } + await new DotNetWatcher(reporter) .WatchAsync(processInfo, fileSetFactory, cancellationToken);