Fix StackOverflowException caused when creating the watcher on directories from a network share

This commit is contained in:
Nate McMaster 2017-07-14 11:27:01 -07:00
parent 4cfeca184c
commit d058fe5a39
11 changed files with 104 additions and 31 deletions

25
shared/NullReporter.cs Normal file
View File

@ -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)
{ }
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Collections.Generic;
using System.Reflection; using System.Reflection;
using Microsoft.DotNet.Watcher.Tools; using Microsoft.DotNet.Watcher.Tools;
@ -19,6 +20,17 @@ namespace Microsoft.DotNet.Watcher
public IList<string> RemainingArguments { get; private set; } public IList<string> RemainingArguments { get; private set; }
public bool ListFiles { 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) public static CommandLineOptions Parse(string[] args, IConsole console)
{ {
Ensure.NotNull(args, nameof(args)); Ensure.NotNull(args, nameof(args));

View File

@ -51,7 +51,7 @@ namespace Microsoft.DotNet.Watcher
using (var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource( using (var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken, cancellationToken,
currentRunCancellationSource.Token)) currentRunCancellationSource.Token))
using (var fileSetWatcher = new FileSetWatcher(fileSet)) using (var fileSetWatcher = new FileSetWatcher(fileSet, _reporter))
{ {
var fileSetTask = fileSetWatcher.GetChangedFileAsync(combinedCancellationSource.Token); var fileSetTask = fileSetWatcher.GetChangedFileAsync(combinedCancellationSource.Token);
var processTask = _processRunner.RunAsync(processSpec, combinedCancellationSource.Token); var processTask = _processRunner.RunAsync(processSpec, combinedCancellationSource.Token);

View File

@ -11,14 +11,15 @@ namespace Microsoft.DotNet.Watcher.Internal
{ {
public class FileSetWatcher : IDisposable public class FileSetWatcher : IDisposable
{ {
private readonly FileWatcher _fileWatcher = new FileWatcher(); private readonly FileWatcher _fileWatcher;
private readonly IFileSet _fileSet; private readonly IFileSet _fileSet;
public FileSetWatcher(IFileSet fileSet) public FileSetWatcher(IFileSet fileSet, IReporter reporter)
{ {
Ensure.NotNull(fileSet, nameof(fileSet)); Ensure.NotNull(fileSet, nameof(fileSet));
_fileSet = fileSet; _fileSet = fileSet;
_fileWatcher = new FileWatcher(reporter);
} }
public async Task<string> GetChangedFileAsync(CancellationToken cancellationToken) public async Task<string> GetChangedFileAsync(CancellationToken cancellationToken)

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.Watcher.Internal namespace Microsoft.DotNet.Watcher.Internal
{ {
@ -12,7 +13,18 @@ namespace Microsoft.DotNet.Watcher.Internal
{ {
private bool _disposed; private bool _disposed;
private readonly IDictionary<string, IFileSystemWatcher> _watchers = new Dictionary<string, IFileSystemWatcher>(); private readonly IDictionary<string, IFileSystemWatcher> _watchers;
private readonly IReporter _reporter;
public FileWatcher()
: this(NullReporter.Singleton)
{ }
public FileWatcher(IReporter reporter)
{
_reporter = reporter ?? throw new ArgumentNullException(nameof(reporter));
_watchers = new Dictionary<string, IFileSystemWatcher>();
}
public event Action<string> OnFileChange; public event Action<string> OnFileChange;
@ -33,8 +45,11 @@ namespace Microsoft.DotNet.Watcher.Internal
foreach (var watcher in _watchers) foreach (var watcher in _watchers)
{ {
watcher.Value.OnFileChange -= WatcherChangedHandler;
watcher.Value.OnError -= WatcherErrorHandler;
watcher.Value.Dispose(); watcher.Value.Dispose();
} }
_watchers.Clear(); _watchers.Clear();
} }
@ -66,11 +81,20 @@ namespace Microsoft.DotNet.Watcher.Internal
var newWatcher = FileWatcherFactory.CreateWatcher(directory); var newWatcher = FileWatcherFactory.CreateWatcher(directory);
newWatcher.OnFileChange += WatcherChangedHandler; newWatcher.OnFileChange += WatcherChangedHandler;
newWatcher.OnError += WatcherErrorHandler;
newWatcher.EnableRaisingEvents = true; newWatcher.EnableRaisingEvents = true;
_watchers.Add(directory, newWatcher); _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) private void WatcherChangedHandler(object sender, string changedPath)
{ {
NotifyChange(changedPath); NotifyChange(changedPath);
@ -90,7 +114,9 @@ namespace Microsoft.DotNet.Watcher.Internal
_watchers.Remove(directory); _watchers.Remove(directory);
watcher.EnableRaisingEvents = false; watcher.EnableRaisingEvents = false;
watcher.OnFileChange -= WatcherChangedHandler; watcher.OnFileChange -= WatcherChangedHandler;
watcher.OnError -= WatcherErrorHandler;
watcher.Dispose(); watcher.Dispose();
} }

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.ComponentModel;
using System.IO; using System.IO;
using Microsoft.Extensions.Tools.Internal; using Microsoft.Extensions.Tools.Internal;
@ -10,7 +11,6 @@ namespace Microsoft.DotNet.Watcher.Internal
internal class DotnetFileWatcher : IFileSystemWatcher internal class DotnetFileWatcher : IFileSystemWatcher
{ {
private readonly Func<string, FileSystemWatcher> _watcherFactory; private readonly Func<string, FileSystemWatcher> _watcherFactory;
private readonly string _watchedDirectory;
private FileSystemWatcher _fileSystemWatcher; private FileSystemWatcher _fileSystemWatcher;
@ -26,14 +26,16 @@ namespace Microsoft.DotNet.Watcher.Internal
Ensure.NotNull(fileSystemWatcherFactory, nameof(fileSystemWatcherFactory)); Ensure.NotNull(fileSystemWatcherFactory, nameof(fileSystemWatcherFactory));
Ensure.NotNullOrEmpty(watchedDirectory, nameof(watchedDirectory)); Ensure.NotNullOrEmpty(watchedDirectory, nameof(watchedDirectory));
_watchedDirectory = watchedDirectory; BasePath = watchedDirectory;
_watcherFactory = fileSystemWatcherFactory; _watcherFactory = fileSystemWatcherFactory;
CreateFileSystemWatcher(); CreateFileSystemWatcher();
} }
public event EventHandler<string> OnFileChange; public event EventHandler<string> OnFileChange;
public event EventHandler OnError; public event EventHandler<Exception> OnError;
public string BasePath { get; }
private static FileSystemWatcher DefaultWatcherFactory(string watchedDirectory) private static FileSystemWatcher DefaultWatcherFactory(string watchedDirectory)
{ {
@ -44,10 +46,18 @@ namespace Microsoft.DotNet.Watcher.Internal
private void WatcherErrorHandler(object sender, ErrorEventArgs e) private void WatcherErrorHandler(object sender, ErrorEventArgs e)
{ {
// Recreate the watcher var exception = e.GetException();
CreateFileSystemWatcher();
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) private void WatcherRenameHandler(object sender, RenamedEventArgs e)
@ -99,7 +109,7 @@ namespace Microsoft.DotNet.Watcher.Internal
_fileSystemWatcher.Dispose(); _fileSystemWatcher.Dispose();
} }
_fileSystemWatcher = _watcherFactory(_watchedDirectory); _fileSystemWatcher = _watcherFactory(BasePath);
_fileSystemWatcher.IncludeSubdirectories = true; _fileSystemWatcher.IncludeSubdirectories = true;
_fileSystemWatcher.Created += WatcherChangeHandler; _fileSystemWatcher.Created += WatcherChangeHandler;
@ -114,8 +124,8 @@ namespace Microsoft.DotNet.Watcher.Internal
public bool EnableRaisingEvents public bool EnableRaisingEvents
{ {
get { return _fileSystemWatcher.EnableRaisingEvents; } get => _fileSystemWatcher.EnableRaisingEvents;
set { _fileSystemWatcher.EnableRaisingEvents = value; } set => _fileSystemWatcher.EnableRaisingEvents = value;
} }
public void Dispose() public void Dispose()

View File

@ -8,15 +8,7 @@ namespace Microsoft.DotNet.Watcher.Internal
public static class FileWatcherFactory public static class FileWatcherFactory
{ {
public static IFileSystemWatcher CreateWatcher(string watchedDirectory) public static IFileSystemWatcher CreateWatcher(string watchedDirectory)
{ => CreateWatcher(watchedDirectory, CommandLineOptions.IsPollingEnabled);
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);
}
public static IFileSystemWatcher CreateWatcher(string watchedDirectory, bool usePollingWatcher) public static IFileSystemWatcher CreateWatcher(string watchedDirectory, bool usePollingWatcher)
{ {

View File

@ -9,7 +9,9 @@ namespace Microsoft.DotNet.Watcher.Internal
{ {
event EventHandler<string> OnFileChange; event EventHandler<string> OnFileChange;
event EventHandler OnError; event EventHandler<Exception> OnError;
string BasePath { get; }
bool EnableRaisingEvents { get; set; } bool EnableRaisingEvents { get; set; }
} }

View File

@ -31,6 +31,7 @@ namespace Microsoft.DotNet.Watcher.Internal
Ensure.NotNullOrEmpty(watchedDirectory, nameof(watchedDirectory)); Ensure.NotNullOrEmpty(watchedDirectory, nameof(watchedDirectory));
_watchedDirectory = new DirectoryInfo(watchedDirectory); _watchedDirectory = new DirectoryInfo(watchedDirectory);
BasePath = _watchedDirectory.FullName;
_pollingThread = new Thread(new ThreadStart(PollingLoop)); _pollingThread = new Thread(new ThreadStart(PollingLoop));
_pollingThread.IsBackground = true; _pollingThread.IsBackground = true;
@ -44,15 +45,14 @@ namespace Microsoft.DotNet.Watcher.Internal
public event EventHandler<string> OnFileChange; public event EventHandler<string> OnFileChange;
#pragma warning disable CS0067 // not used #pragma warning disable CS0067 // not used
public event EventHandler OnError; public event EventHandler<Exception> OnError;
#pragma warning restore #pragma warning restore
public string BasePath { get; }
public bool EnableRaisingEvents public bool EnableRaisingEvents
{ {
get get => _raiseEvents;
{
return _raiseEvents;
}
set set
{ {
EnsureNotDisposed(); EnsureNotDisposed();
@ -125,7 +125,7 @@ namespace Microsoft.DotNet.Watcher.Internal
_knownEntities[fullFilePath] = new FileMeta(fileMeta.FileInfo, true); _knownEntities[fullFilePath] = new FileMeta(fileMeta.FileInfo, true);
} }
catch(FileNotFoundException) catch (FileNotFoundException)
{ {
_knownEntities[fullFilePath] = new FileMeta(fileMeta.FileInfo, false); _knownEntities[fullFilePath] = new FileMeta(fileMeta.FileInfo, false);
} }
@ -187,7 +187,7 @@ namespace Microsoft.DotNet.Watcher.Internal
{ {
return; return;
} }
var entities = dirInfo.EnumerateFileSystemInfos("*.*"); var entities = dirInfo.EnumerateFileSystemInfos("*.*");
foreach (var entity in entities) foreach (var entity in entities)
{ {

View File

@ -144,7 +144,7 @@ namespace Microsoft.DotNet.Watcher.Internal
var fileSet = new FileSet(new[] { _projectFile }); var fileSet = new FileSet(new[] { _projectFile });
using (var watcher = new FileSetWatcher(fileSet)) using (var watcher = new FileSetWatcher(fileSet, _reporter))
{ {
await watcher.GetChangedFileAsync(cancellationToken); await watcher.GetChangedFileAsync(cancellationToken);

View File

@ -160,6 +160,11 @@ namespace Microsoft.DotNet.Watcher
}, },
}; };
if (CommandLineOptions.IsPollingEnabled)
{
_reporter.Output("Polling file watcher is enabled");
}
await new DotNetWatcher(reporter) await new DotNetWatcher(reporter)
.WatchAsync(processInfo, fileSetFactory, cancellationToken); .WatchAsync(processInfo, fileSetFactory, cancellationToken);