248 lines
6.9 KiB
C#
248 lines
6.9 KiB
C#
// 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;
|
|
using Microsoft.Extensions.Tools.Internal;
|
|
|
|
namespace Microsoft.DotNet.Watcher.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)
|
|
{
|
|
Ensure.NotNullOrEmpty(watchedDirectory, nameof(watchedDirectory));
|
|
|
|
_watchedDirectory = new DirectoryInfo(watchedDirectory);
|
|
|
|
_pollingThread = new Thread(new ThreadStart(PollingLoop));
|
|
_pollingThread.IsBackground = true;
|
|
_pollingThread.Name = nameof(PollingFileWatcher);
|
|
|
|
CreateKnownFilesSnapshot();
|
|
|
|
_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();
|
|
_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];
|
|
|
|
try
|
|
{
|
|
if (fileMeta.FileInfo.LastWriteTime != f.LastWriteTime)
|
|
{
|
|
// File changed
|
|
RecordChange(f);
|
|
}
|
|
|
|
_knownEntities[fullFilePath] = new FileMeta(fileMeta.FileInfo, true);
|
|
}
|
|
catch(FileNotFoundException)
|
|
{
|
|
_knownEntities[fullFilePath] = new FileMeta(fileMeta.FileInfo, false);
|
|
}
|
|
}
|
|
|
|
_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 (fileInfo == null ||
|
|
_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)
|
|
{
|
|
if (!dirInfo.Exists)
|
|
{
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|