From 8f1f3c0772953c95e95b1b614f8ecf6e20cf14c0 Mon Sep 17 00:00:00 2001 From: moozzyk Date: Mon, 28 Mar 2016 16:47:18 -0700 Subject: [PATCH] Add a polling watcher --- .../Internal/FileWatcher/DotnetFileWatcher.cs | 136 +++++++ .../FileWatcher/FileWatcherFactory.cs | 28 ++ .../FileWatcher/IFileSystemWatcher.cs | 16 + .../FileWatcher/PollingFileWatcher.cs | 243 ++++++++++++ .../Internal/Implementation/FileWatcher.cs | 29 +- .../FileWatcherTests.cs | 349 ++++++++++++++++++ .../GlobbingAppTests.cs | 34 +- .../Scenario/DotNetWatchScenario.cs | 14 +- .../Scenario/ProjectToolScenario.cs | 20 +- .../WaitForFileToChange.cs | 19 +- 10 files changed, 848 insertions(+), 40 deletions(-) create mode 100644 src/Microsoft.DotNet.Watcher.Core/Internal/FileWatcher/DotnetFileWatcher.cs create mode 100644 src/Microsoft.DotNet.Watcher.Core/Internal/FileWatcher/FileWatcherFactory.cs create mode 100644 src/Microsoft.DotNet.Watcher.Core/Internal/FileWatcher/IFileSystemWatcher.cs create mode 100644 src/Microsoft.DotNet.Watcher.Core/Internal/FileWatcher/PollingFileWatcher.cs create mode 100644 test/dotnet-watch.FunctionalTests/FileWatcherTests.cs diff --git a/src/Microsoft.DotNet.Watcher.Core/Internal/FileWatcher/DotnetFileWatcher.cs b/src/Microsoft.DotNet.Watcher.Core/Internal/FileWatcher/DotnetFileWatcher.cs new file mode 100644 index 0000000000..8f2e44b5e7 --- /dev/null +++ b/src/Microsoft.DotNet.Watcher.Core/Internal/FileWatcher/DotnetFileWatcher.cs @@ -0,0 +1,136 @@ +// 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.IO; + +namespace Microsoft.DotNet.Watcher.Core.Internal +{ + internal class DotnetFileWatcher : IFileSystemWatcher + { + private readonly Func _watcherFactory; + private readonly string _watchedDirectory; + + private FileSystemWatcher _fileSystemWatcher; + + private readonly object _createLock = new object(); + + public DotnetFileWatcher(string watchedDirectory) + : this(watchedDirectory, DefaultWatcherFactory) + { + } + + internal DotnetFileWatcher(string watchedDirectory, Func fileSystemWatcherFactory) + { + if (string.IsNullOrEmpty(watchedDirectory)) + { + throw new ArgumentNullException(nameof(watchedDirectory)); + } + + _watchedDirectory = watchedDirectory; + _watcherFactory = fileSystemWatcherFactory; + CreateFileSystemWatcher(); + } + + public event EventHandler OnFileChange; + + public event EventHandler OnError; + + private static FileSystemWatcher DefaultWatcherFactory(string watchedDirectory) + { + if (string.IsNullOrEmpty(watchedDirectory)) + { + throw new ArgumentNullException(nameof(watchedDirectory)); + } + + return new FileSystemWatcher(watchedDirectory); + } + + private void WatcherErrorHandler(object sender, ErrorEventArgs e) + { + // Recreate the watcher + CreateFileSystemWatcher(); + + if (OnError != null) + { + OnError(this, null); + } + } + + private void WatcherRenameHandler(object sender, RenamedEventArgs e) + { + NotifyChange(e.OldFullPath); + NotifyChange(e.FullPath); + + if (Directory.Exists(e.FullPath)) + { + 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)); + NotifyChange(oldLocation); + NotifyChange(newLocation); + } + } + } + + private void WatcherChangeHandler(object sender, FileSystemEventArgs e) + { + NotifyChange(e.FullPath); + } + + private void NotifyChange(string fullPath) + { + if (OnFileChange != null) + { + // Only report file changes + OnFileChange(this, fullPath); + } + } + + private void CreateFileSystemWatcher() + { + lock (_createLock) + { + bool enableEvents = false; + + if (_fileSystemWatcher != null) + { + enableEvents = _fileSystemWatcher.EnableRaisingEvents; + + _fileSystemWatcher.EnableRaisingEvents = false; + + _fileSystemWatcher.Created -= WatcherChangeHandler; + _fileSystemWatcher.Deleted -= WatcherChangeHandler; + _fileSystemWatcher.Changed -= WatcherChangeHandler; + _fileSystemWatcher.Renamed -= WatcherRenameHandler; + _fileSystemWatcher.Error -= WatcherErrorHandler; + + _fileSystemWatcher.Dispose(); + } + + _fileSystemWatcher = _watcherFactory(_watchedDirectory); + _fileSystemWatcher.IncludeSubdirectories = true; + + _fileSystemWatcher.Created += WatcherChangeHandler; + _fileSystemWatcher.Deleted += WatcherChangeHandler; + _fileSystemWatcher.Changed += WatcherChangeHandler; + _fileSystemWatcher.Renamed += WatcherRenameHandler; + _fileSystemWatcher.Error += WatcherErrorHandler; + + _fileSystemWatcher.EnableRaisingEvents = enableEvents; + } + } + + public bool EnableRaisingEvents + { + get { return _fileSystemWatcher.EnableRaisingEvents; } + set { _fileSystemWatcher.EnableRaisingEvents = value; } + } + + public void Dispose() + { + _fileSystemWatcher.Dispose(); + } + } +} diff --git a/src/Microsoft.DotNet.Watcher.Core/Internal/FileWatcher/FileWatcherFactory.cs b/src/Microsoft.DotNet.Watcher.Core/Internal/FileWatcher/FileWatcherFactory.cs new file mode 100644 index 0000000000..35e7e7c739 --- /dev/null +++ b/src/Microsoft.DotNet.Watcher.Core/Internal/FileWatcher/FileWatcherFactory.cs @@ -0,0 +1,28 @@ +// 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; + +namespace Microsoft.DotNet.Watcher.Core.Internal +{ + public static class FileWatcherFactory + { + public static IFileSystemWatcher CreateWatcher(string watchedDirectory) + { + var envVar = Environment.GetEnvironmentVariable("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) + { + return usePollingWatcher ? + new PollingFileWatcher(watchedDirectory) : + new DotnetFileWatcher(watchedDirectory) as IFileSystemWatcher; + } + } +} diff --git a/src/Microsoft.DotNet.Watcher.Core/Internal/FileWatcher/IFileSystemWatcher.cs b/src/Microsoft.DotNet.Watcher.Core/Internal/FileWatcher/IFileSystemWatcher.cs new file mode 100644 index 0000000000..c944bb9d64 --- /dev/null +++ b/src/Microsoft.DotNet.Watcher.Core/Internal/FileWatcher/IFileSystemWatcher.cs @@ -0,0 +1,16 @@ +// 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; + +namespace Microsoft.DotNet.Watcher.Core.Internal +{ + public interface IFileSystemWatcher : IDisposable + { + event EventHandler OnFileChange; + + event EventHandler OnError; + + bool EnableRaisingEvents { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Watcher.Core/Internal/FileWatcher/PollingFileWatcher.cs b/src/Microsoft.DotNet.Watcher.Core/Internal/FileWatcher/PollingFileWatcher.cs new file mode 100644 index 0000000000..b7d6866f69 --- /dev/null +++ b/src/Microsoft.DotNet.Watcher.Core/Internal/FileWatcher/PollingFileWatcher.cs @@ -0,0 +1,243 @@ +// 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.Diagnostics; +using System.IO; +using System.Threading; + +namespace Microsoft.DotNet.Watcher.Core.Internal +{ + internal class PollingFileWatcher : IFileSystemWatcher + { + // The minimum interval to rerun the scan + private static readonly TimeSpan _minRunInternal = TimeSpan.FromSeconds(.5); + + private readonly DirectoryInfo _watchedDirectory; + + private Dictionary _knownEntities = new Dictionary(); + private Dictionary _tempDictionary = new Dictionary(); + private HashSet _changes = new HashSet(); + + private Thread _pollingThread; + private bool _raiseEvents; + + private bool _disposed; + + public PollingFileWatcher(string watchedDirectory) + { + if (string.IsNullOrEmpty(watchedDirectory)) + { + throw new ArgumentNullException(nameof(watchedDirectory)); + } + + _watchedDirectory = new DirectoryInfo(watchedDirectory); + + _pollingThread = new Thread(new ThreadStart(PollingLoop)); + _pollingThread.IsBackground = true; + _pollingThread.Name = nameof(PollingFileWatcher); + _pollingThread.Start(); + } + + public event EventHandler OnFileChange; + +#pragma warning disable CS0067 // not used + public event EventHandler OnError; +#pragma warning restore + + public bool EnableRaisingEvents + { + get + { + return _raiseEvents; + } + set + { + EnsureNotDisposed(); + + if (value == true) + { + CreateKnownFilesSnapshot(); + + if (_pollingThread.ThreadState == System.Threading.ThreadState.Unstarted) + { + // Start the loop the first time events are enabled + _pollingThread.Start(); + } + } + _raiseEvents = value; + } + } + + private void PollingLoop() + { + var stopwatch = Stopwatch.StartNew(); + stopwatch.Start(); + + while (!_disposed) + { + if (stopwatch.Elapsed < _minRunInternal) + { + // Don't run too often + // The min wait time here can be double + // the value of the variable (FYI) + Thread.Sleep(_minRunInternal); + } + + stopwatch.Reset(); + + if (!_raiseEvents) + { + continue; + } + + CheckForChangedFiles(); + } + + stopwatch.Stop(); + } + + private void CreateKnownFilesSnapshot() + { + _knownEntities.Clear(); + + ForeachEntityInDirectory(_watchedDirectory, f => + { + _knownEntities.Add(f.FullName, new FileMeta(f)); + }); + } + + private void CheckForChangedFiles() + { + _changes.Clear(); + + ForeachEntityInDirectory(_watchedDirectory, f => + { + var fullFilePath = f.FullName; + + if (!_knownEntities.ContainsKey(fullFilePath)) + { + // New file + RecordChange(f); + } + else + { + var fileMeta = _knownEntities[fullFilePath]; + if (fileMeta.FileInfo.LastWriteTime != f.LastWriteTime) + { + // File changed + RecordChange(f); + } + + _knownEntities[fullFilePath] = new FileMeta(fileMeta.FileInfo, true); + } + + _tempDictionary.Add(f.FullName, new FileMeta(f)); + }); + + foreach (var file in _knownEntities) + { + if (!file.Value.FoundAgain) + { + // File deleted + RecordChange(file.Value.FileInfo); + } + } + + NotifyChanges(); + + // Swap the two dictionaries + var swap = _knownEntities; + _knownEntities = _tempDictionary; + _tempDictionary = swap; + + _tempDictionary.Clear(); + } + + private void RecordChange(FileSystemInfo fileInfo) + { + if (_changes.Contains(fileInfo.FullName) || + fileInfo.FullName.Equals(_watchedDirectory.FullName, StringComparison.Ordinal)) + { + return; + } + + _changes.Add(fileInfo.FullName); + if (fileInfo.FullName != _watchedDirectory.FullName) + { + var file = fileInfo as FileInfo; + if (file != null) + { + RecordChange(file.Directory); + } + else + { + var dir = fileInfo as DirectoryInfo; + if (dir != null) + { + RecordChange(dir.Parent); + } + } + } + } + + private void ForeachEntityInDirectory(DirectoryInfo dirInfo, Action fileAction) + { + var entities = dirInfo.EnumerateFileSystemInfos("*.*"); + foreach (var entity in entities) + { + fileAction(entity); + + var subdirInfo = entity as DirectoryInfo; + if (subdirInfo != null) + { + ForeachEntityInDirectory(subdirInfo, fileAction); + } + } + } + + private void NotifyChanges() + { + foreach (var path in _changes) + { + if (_disposed || !_raiseEvents) + { + break; + } + + if (OnFileChange != null) + { + OnFileChange(this, path); + } + } + } + + private void EnsureNotDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(PollingFileWatcher)); + } + } + + public void Dispose() + { + EnableRaisingEvents = false; + _disposed = true; + } + + private struct FileMeta + { + public FileMeta(FileSystemInfo fileInfo, bool foundAgain = false) + { + FileInfo = fileInfo; + FoundAgain = foundAgain; + } + + public FileSystemInfo FileInfo; + + public bool FoundAgain; + } + } +} diff --git a/src/Microsoft.DotNet.Watcher.Core/Internal/Implementation/FileWatcher.cs b/src/Microsoft.DotNet.Watcher.Core/Internal/Implementation/FileWatcher.cs index 57f082a8d0..388f7f52ab 100644 --- a/src/Microsoft.DotNet.Watcher.Core/Internal/Implementation/FileWatcher.cs +++ b/src/Microsoft.DotNet.Watcher.Core/Internal/Implementation/FileWatcher.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; namespace Microsoft.DotNet.Watcher.Core.Internal @@ -12,7 +11,7 @@ namespace Microsoft.DotNet.Watcher.Core.Internal { private bool _disposed; - private readonly IDictionary _watchers = new Dictionary(); + private readonly IDictionary _watchers = new Dictionary(); public event Action OnFileChange; @@ -62,28 +61,16 @@ namespace Microsoft.DotNet.Watcher.Core.Internal } } - var newWatcher = new FileSystemWatcher(directory); - newWatcher.IncludeSubdirectories = true; - - newWatcher.Changed += WatcherChangedHandler; - newWatcher.Created += WatcherChangedHandler; - newWatcher.Deleted += WatcherChangedHandler; - newWatcher.Renamed += WatcherRenamedHandler; - + var newWatcher = FileWatcherFactory.CreateWatcher(directory); + newWatcher.OnFileChange += WatcherChangedHandler; newWatcher.EnableRaisingEvents = true; _watchers.Add(directory, newWatcher); } - private void WatcherRenamedHandler(object sender, RenamedEventArgs e) + private void WatcherChangedHandler(object sender, string changedPath) { - NotifyChange(e.OldFullPath); - NotifyChange(e.FullPath); - } - - private void WatcherChangedHandler(object sender, FileSystemEventArgs e) - { - NotifyChange(e.FullPath); + NotifyChange(changedPath); } private void NotifyChange(string path) @@ -100,11 +87,7 @@ namespace Microsoft.DotNet.Watcher.Core.Internal _watchers.Remove(directory); watcher.EnableRaisingEvents = false; - - watcher.Changed -= WatcherChangedHandler; - watcher.Created -= WatcherChangedHandler; - watcher.Deleted -= WatcherChangedHandler; - watcher.Renamed -= WatcherRenamedHandler; + watcher.OnFileChange -= WatcherChangedHandler; watcher.Dispose(); } diff --git a/test/dotnet-watch.FunctionalTests/FileWatcherTests.cs b/test/dotnet-watch.FunctionalTests/FileWatcherTests.cs new file mode 100644 index 0000000000..d31cce667d --- /dev/null +++ b/test/dotnet-watch.FunctionalTests/FileWatcherTests.cs @@ -0,0 +1,349 @@ +// 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.IO; +using System.Linq; +using System.Threading; +using Microsoft.DotNet.Watcher.Core.Internal; +using Xunit; + +namespace Microsoft.DotNet.Watcher.FunctionalTests +{ + public class FileWatcherTests + { + private const int DefaultTimeout = 10 * 1000; // 10 sec + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void NewFile(bool usePolling) + { + UsingTempDirectory(dir => + { + using (var changedEv = new ManualResetEvent(false)) + using (var watcher = FileWatcherFactory.CreateWatcher(dir, usePolling)) + { + var filesChanged = new HashSet(); + + watcher.OnFileChange += (_, f) => + { + filesChanged.Add(f); + changedEv.Set(); + }; + watcher.EnableRaisingEvents = true; + + var testFileFullPath = Path.Combine(dir, "foo"); + File.WriteAllText(testFileFullPath, string.Empty); + + Assert.True(changedEv.WaitOne(DefaultTimeout)); + Assert.Equal(testFileFullPath, filesChanged.Single()); + } + }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ChangeFile(bool usePolling) + { + UsingTempDirectory(dir => + { + var testFileFullPath = Path.Combine(dir, "foo"); + File.WriteAllText(testFileFullPath, string.Empty); + + using (var changedEv = new ManualResetEvent(false)) + using (var watcher = FileWatcherFactory.CreateWatcher(dir, usePolling)) + { + var filesChanged = new HashSet(); + + watcher.OnFileChange += (_, f) => + { + filesChanged.Add(f); + changedEv.Set(); + }; + watcher.EnableRaisingEvents = true; + + File.WriteAllText(testFileFullPath, string.Empty); + + Assert.True(changedEv.WaitOne(DefaultTimeout)); + Assert.Equal(testFileFullPath, filesChanged.Single()); + } + }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MoveFile(bool usePolling) + { + UsingTempDirectory(dir => + { + var srcFile = Path.Combine(dir, "foo"); + var dstFile = Path.Combine(dir, "foo2"); + + File.WriteAllText(srcFile, string.Empty); + + using (var changedEv = new ManualResetEvent(false)) + using (var watcher = FileWatcherFactory.CreateWatcher(dir, usePolling)) + { + var filesChanged = new HashSet(); + + var changeCount = 0; + watcher.OnFileChange += (_, f) => + { + filesChanged.Add(f); + + changeCount++; + + if (changeCount >= 2) + { + changedEv.Set(); + } + }; + watcher.EnableRaisingEvents = true; + + File.Move(srcFile, dstFile); + + Assert.True(changedEv.WaitOne(DefaultTimeout)); + Assert.True(filesChanged.Contains(srcFile)); + Assert.True(filesChanged.Contains(dstFile)); + } + }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void FileInSubdirectory(bool usePolling) + { + UsingTempDirectory(dir => + { + var subdir = Path.Combine(dir, "subdir"); + Directory.CreateDirectory(subdir); + + var testFileFullPath = Path.Combine(subdir, "foo"); + File.WriteAllText(testFileFullPath, string.Empty); + + using (var changedEv = new ManualResetEvent(false)) + using (var watcher = FileWatcherFactory.CreateWatcher(dir, usePolling)) + { + var filesChanged = new HashSet(); + + var totalChanges = 0; + watcher.OnFileChange += (_, f) => + { + filesChanged.Add(f); + + totalChanges++; + if (totalChanges >= 2) + { + changedEv.Set(); + } + }; + watcher.EnableRaisingEvents = true; + + File.WriteAllText(testFileFullPath, string.Empty); + + Assert.True(changedEv.WaitOne(DefaultTimeout)); + Assert.True(filesChanged.Contains(subdir)); + Assert.True(filesChanged.Contains(testFileFullPath)); + } + }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void NoNotificationIfDisabled(bool usePolling) + { + UsingTempDirectory(dir => + { + using (var watcher = FileWatcherFactory.CreateWatcher(dir, usePolling)) + using (var changedEv = new ManualResetEvent(false)) + { + watcher.OnFileChange += (_, f) => changedEv.Set(); + + // Disable + watcher.EnableRaisingEvents = false; + + var testFileFullPath = Path.Combine(dir, "foo"); + File.WriteAllText(testFileFullPath, string.Empty); + + Assert.False(changedEv.WaitOne(DefaultTimeout / 2)); + } + }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DisposedNoEvents(bool usePolling) + { + UsingTempDirectory(dir => + { + using (var changedEv = new ManualResetEvent(false)) + { + using (var watcher = FileWatcherFactory.CreateWatcher(dir, usePolling)) + { + watcher.OnFileChange += (_, f) => changedEv.Set(); + watcher.EnableRaisingEvents = true; + } + + var testFileFullPath = Path.Combine(dir, "foo"); + File.WriteAllText(testFileFullPath, string.Empty); + + Assert.False(changedEv.WaitOne(DefaultTimeout / 2)); + } + }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MultipleFiles(bool usePolling) + { + UsingTempDirectory(dir => + { + File.WriteAllText(Path.Combine(dir, "foo1"), string.Empty); + File.WriteAllText(Path.Combine(dir, "foo2"), string.Empty); + File.WriteAllText(Path.Combine(dir, "foo3"), string.Empty); + File.WriteAllText(Path.Combine(dir, "foo4"), string.Empty); + File.WriteAllText(Path.Combine(dir, "foo4"), string.Empty); + + var testFileFullPath = Path.Combine(dir, "foo3"); + + using (var changedEv = new ManualResetEvent(false)) + using (var watcher = FileWatcherFactory.CreateWatcher(dir, usePolling)) + { + var filesChanged = new HashSet(); + + watcher.OnFileChange += (_, f) => + { + filesChanged.Add(f); + changedEv.Set(); + }; + watcher.EnableRaisingEvents = true; + + File.WriteAllText(testFileFullPath, string.Empty); + + Assert.True(changedEv.WaitOne(DefaultTimeout)); + Assert.Equal(testFileFullPath, filesChanged.Single()); + } + }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MultipleTriggers(bool usePolling) + { + UsingTempDirectory(dir => + { + using (var changedEv = new AutoResetEvent(false)) + using (var watcher = FileWatcherFactory.CreateWatcher(dir, usePolling)) + { + var filesChanged = new HashSet(); + + watcher.OnFileChange += (_, f) => + { + filesChanged.Add(f); + changedEv.Set(); + }; + watcher.EnableRaisingEvents = true; + + var testFileFullPath = Path.Combine(dir, "foo1"); + File.WriteAllText(testFileFullPath, string.Empty); + Assert.True(changedEv.WaitOne(DefaultTimeout)); + Assert.Equal(testFileFullPath, filesChanged.Single()); + filesChanged.Clear(); + + testFileFullPath = Path.Combine(dir, "foo2"); + File.WriteAllText(testFileFullPath, string.Empty); + Assert.True(changedEv.WaitOne(DefaultTimeout)); + Assert.Equal(testFileFullPath, filesChanged.Single()); + filesChanged.Clear(); + + testFileFullPath = Path.Combine(dir, "foo3"); + File.WriteAllText(testFileFullPath, string.Empty); + Assert.True(changedEv.WaitOne(DefaultTimeout)); + Assert.Equal(testFileFullPath, filesChanged.Single()); + filesChanged.Clear(); + + File.WriteAllText(testFileFullPath, string.Empty); + Assert.True(changedEv.WaitOne(DefaultTimeout)); + Assert.Equal(testFileFullPath, filesChanged.Single()); + } + }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DeleteSubfolder(bool usePolling) + { + UsingTempDirectory(dir => + { + var subdir = Path.Combine(dir, "subdir"); + Directory.CreateDirectory(subdir); + + var f1 = Path.Combine(subdir, "foo1"); + var f2 = Path.Combine(subdir, "foo2"); + var f3 = Path.Combine(subdir, "foo3"); + + File.WriteAllText(f1, string.Empty); + File.WriteAllText(f2, string.Empty); + File.WriteAllText(f3, string.Empty); + + using (var changedEv = new AutoResetEvent(false)) + using (var watcher = FileWatcherFactory.CreateWatcher(dir, usePolling)) + { + var filesChanged = new HashSet(); + + var totalChanges = 0; + watcher.OnFileChange += (_, f) => + { + filesChanged.Add(f); + + totalChanges++; + if (totalChanges >= 4) + { + changedEv.Set(); + } + }; + watcher.EnableRaisingEvents = true; + + Directory.Delete(subdir, recursive: true); + + Assert.True(changedEv.WaitOne(DefaultTimeout)); + + Assert.True(filesChanged.Contains(f1)); + Assert.True(filesChanged.Contains(f2)); + Assert.True(filesChanged.Contains(f3)); + Assert.True(filesChanged.Contains(subdir)); + } + }); + } + + private static void UsingTempDirectory(Action action) + { + var tempFolder = Path.Combine(Path.GetTempPath(), $"{nameof(FileWatcherTests)}-{Guid.NewGuid().ToString("N")}"); + if (Directory.Exists(tempFolder)) + { + Directory.Delete(tempFolder, recursive: true); + } + + Directory.CreateDirectory(tempFolder); + + try + { + action(tempFolder); + } + finally + { + Directory.Delete(tempFolder, recursive: true); + } + } + } +} diff --git a/test/dotnet-watch.FunctionalTests/GlobbingAppTests.cs b/test/dotnet-watch.FunctionalTests/GlobbingAppTests.cs index 839ce62426..9df0d466f3 100644 --- a/test/dotnet-watch.FunctionalTests/GlobbingAppTests.cs +++ b/test/dotnet-watch.FunctionalTests/GlobbingAppTests.cs @@ -15,13 +15,26 @@ namespace Microsoft.DotNet.Watcher.FunctionalTests private static readonly TimeSpan _negativeTestWaitTime = TimeSpan.FromSeconds(10); - // Change a file included in compilation [Fact] - public void ChangeCompiledFile() + public void ChangeCompiledFile_PollingWatcher() + { + ChangeCompiledFile(usePollingWatcher: true); + } + + [Fact] + public void ChangeCompiledFile_DotNetWatcher() + { + ChangeCompiledFile(usePollingWatcher: false); + } + + // Change a file included in compilation + private void ChangeCompiledFile(bool usePollingWatcher) { using (var scenario = new GlobbingAppScenario()) using (var wait = new WaitForFileToChange(scenario.StartedFile)) { + scenario.UsePollingWatcher = usePollingWatcher; + scenario.Start(); var fileToChange = Path.Combine(scenario.TestAppFolder, "include", "Foo.cs"); @@ -91,12 +104,25 @@ namespace Microsoft.DotNet.Watcher.FunctionalTests } } - // Add a file that's in a included folder but not matching the globbing pattern [Fact] - public void ChangeNonCompiledFile() + public void ChangeNonCompiledFile_PollingWatcher() { + ChangeNonCompiledFile(usePollingWatcher: true); + } + + [Fact] + public void ChangeNonCompiledFile_DotNetWatcher() + { + ChangeNonCompiledFile(usePollingWatcher: false); + } + + // Add a file that's in a included folder but not matching the globbing pattern + private void ChangeNonCompiledFile(bool usePollingWatcher) + { using (var scenario = new GlobbingAppScenario()) { + scenario.UsePollingWatcher = usePollingWatcher; + scenario.Start(); var ids = File.ReadAllLines(scenario.StatusFile); diff --git a/test/dotnet-watch.FunctionalTests/Scenario/DotNetWatchScenario.cs b/test/dotnet-watch.FunctionalTests/Scenario/DotNetWatchScenario.cs index f143e8f734..2a0b2e441c 100644 --- a/test/dotnet-watch.FunctionalTests/Scenario/DotNetWatchScenario.cs +++ b/test/dotnet-watch.FunctionalTests/Scenario/DotNetWatchScenario.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.Collections.Generic; using System.Diagnostics; using System.IO; using Microsoft.Extensions.Internal; @@ -25,9 +26,20 @@ namespace Microsoft.DotNet.Watcher.FunctionalTests public Process WatcherProcess { get; private set; } + public bool UsePollingWatcher { get; set; } + protected void RunDotNetWatch(string arguments, string workingFolder) { - WatcherProcess = _scenario.ExecuteDotnet("watch " + arguments, workingFolder); + IDictionary envVariables = null; + if (UsePollingWatcher) + { + envVariables = new Dictionary() + { + ["USE_POLLING_FILE_WATCHER"] = "true" + }; + } + + WatcherProcess = _scenario.ExecuteDotnet("watch " + arguments, workingFolder, envVariables); } public virtual void Dispose() diff --git a/test/dotnet-watch.FunctionalTests/Scenario/ProjectToolScenario.cs b/test/dotnet-watch.FunctionalTests/Scenario/ProjectToolScenario.cs index 22ea8f5cac..c4976ef3ac 100644 --- a/test/dotnet-watch.FunctionalTests/Scenario/ProjectToolScenario.cs +++ b/test/dotnet-watch.FunctionalTests/Scenario/ProjectToolScenario.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.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -114,16 +115,31 @@ namespace Microsoft.DotNet.Watcher.FunctionalTests File.Copy(nugetConfigFilePath, tempNugetConfigFile); } - public Process ExecuteDotnet(string arguments, string workDir) + public Process ExecuteDotnet(string arguments, string workDir, IDictionary environmentVariables = null) { Console.WriteLine($"Running dotnet {arguments} in {workDir}"); var psi = new ProcessStartInfo("dotnet", arguments) { UseShellExecute = false, - WorkingDirectory = workDir + WorkingDirectory = workDir, }; + if (environmentVariables != null) + { + foreach (var newEnvVar in environmentVariables) + { + if (psi.Environment.ContainsKey(newEnvVar.Key)) + { + psi.Environment[newEnvVar.Key] = newEnvVar.Value; + } + else + { + psi.Environment.Add(newEnvVar.Key, newEnvVar.Value); + } + } + } + return Process.Start(psi); } diff --git a/test/dotnet-watch.FunctionalTests/WaitForFileToChange.cs b/test/dotnet-watch.FunctionalTests/WaitForFileToChange.cs index eb964f85d2..64c59c61dc 100644 --- a/test/dotnet-watch.FunctionalTests/WaitForFileToChange.cs +++ b/test/dotnet-watch.FunctionalTests/WaitForFileToChange.cs @@ -4,30 +4,30 @@ using System; using System.IO; using System.Threading; +using Microsoft.DotNet.Watcher.Core.Internal; namespace Microsoft.DotNet.Watcher.FunctionalTests { public class WaitForFileToChange : IDisposable { - private readonly FileSystemWatcher _watcher; + private readonly IFileSystemWatcher _watcher; private readonly string _expectedFile; private ManualResetEvent _changed = new ManualResetEvent(false); public WaitForFileToChange(string file) { - _watcher = new FileSystemWatcher(Path.GetDirectoryName(file), "*" + Path.GetExtension(file)); + _watcher = FileWatcherFactory.CreateWatcher(Path.GetDirectoryName(file), usePollingWatcher: true); _expectedFile = file; - _watcher.Changed += WatcherEvent; - _watcher.Created += WatcherEvent; - + _watcher.OnFileChange += WatcherEvent; + _watcher.EnableRaisingEvents = true; } - private void WatcherEvent(object sender, FileSystemEventArgs e) + private void WatcherEvent(object sender, string file) { - if (e.FullPath.Equals(_expectedFile, StringComparison.Ordinal)) + if (file.Equals(_expectedFile, StringComparison.Ordinal)) { Waiters.WaitForFileToBeReadable(_expectedFile, TimeSpan.FromSeconds(10)); _changed?.Set(); @@ -50,9 +50,8 @@ namespace Microsoft.DotNet.Watcher.FunctionalTests { _watcher.EnableRaisingEvents = false; - _watcher.Changed -= WatcherEvent; - _watcher.Created -= WatcherEvent; - + _watcher.OnFileChange -= WatcherEvent; + _watcher.Dispose(); _changed.Dispose();