Add a polling watcher
This commit is contained in:
parent
46d4c6edcb
commit
8f1f3c0772
|
|
@ -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<string, FileSystemWatcher> _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<string, FileSystemWatcher> fileSystemWatcherFactory)
|
||||
{
|
||||
if (string.IsNullOrEmpty(watchedDirectory))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(watchedDirectory));
|
||||
}
|
||||
|
||||
_watchedDirectory = watchedDirectory;
|
||||
_watcherFactory = fileSystemWatcherFactory;
|
||||
CreateFileSystemWatcher();
|
||||
}
|
||||
|
||||
public event EventHandler<string> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string> OnFileChange;
|
||||
|
||||
event EventHandler OnError;
|
||||
|
||||
bool EnableRaisingEvents { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, FileMeta> _knownEntities = new Dictionary<string, FileMeta>();
|
||||
private Dictionary<string, FileMeta> _tempDictionary = new Dictionary<string, FileMeta>();
|
||||
private HashSet<string> _changes = new HashSet<string>();
|
||||
|
||||
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<string> 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<FileSystemInfo> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, FileSystemWatcher> _watchers = new Dictionary<string, FileSystemWatcher>();
|
||||
private readonly IDictionary<string, IFileSystemWatcher> _watchers = new Dictionary<string, IFileSystemWatcher>();
|
||||
|
||||
public event Action<string> 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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string>();
|
||||
|
||||
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<string>();
|
||||
|
||||
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<string>();
|
||||
|
||||
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<string>();
|
||||
|
||||
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<string>();
|
||||
|
||||
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<string>();
|
||||
|
||||
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<string>();
|
||||
|
||||
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<string> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<string, string> envVariables = null;
|
||||
if (UsePollingWatcher)
|
||||
{
|
||||
envVariables = new Dictionary<string, string>()
|
||||
{
|
||||
["USE_POLLING_FILE_WATCHER"] = "true"
|
||||
};
|
||||
}
|
||||
|
||||
WatcherProcess = _scenario.ExecuteDotnet("watch " + arguments, workingFolder, envVariables);
|
||||
}
|
||||
|
||||
public virtual void Dispose()
|
||||
|
|
|
|||
|
|
@ -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<string, string> 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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue