File: FileWatcher\PollingDirectoryWatcher.cs
Web Access
Project: ..\..\..\src\BuiltInTools\dotnet-watch\dotnet-watch.csproj (dotnet-watch)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Immutable;
using System.Diagnostics;
 
namespace Microsoft.DotNet.Watch
{
    internal sealed class PollingDirectoryWatcher : DirectoryWatcher
    {
        // The minimum interval to rerun the scan
        private static readonly TimeSpan _minRunInternal = TimeSpan.FromSeconds(.5);
 
        private readonly DirectoryInfo _watchedDirectory;
 
        private Dictionary<string, DateTime> _currentSnapshot = new(PathUtilities.OSSpecificPathComparer);
 
        // The following are sets that are used to calculate new snapshot and cleared on eached use (pooled):
        private Dictionary<string, DateTime> _snapshotBuilder = new(PathUtilities.OSSpecificPathComparer);
        private readonly Dictionary<string, ChangeKind> _changesBuilder = new(PathUtilities.OSSpecificPathComparer);
 
        private Thread _pollingThread;
        private bool _raiseEvents;
 
        private volatile bool _disposed;
 
        public PollingDirectoryWatcher(string watchedDirectory, ImmutableHashSet<string> watchedFileNames, bool includeSubdirectories)
            : base(watchedDirectory, watchedFileNames, includeSubdirectories)
        {
            _watchedDirectory = new DirectoryInfo(watchedDirectory);
 
            _pollingThread = new Thread(new ThreadStart(PollingLoop))
            {
                IsBackground = true,
                Name = nameof(PollingDirectoryWatcher)
            };
 
            CaptureInitialSnapshot();
 
            _pollingThread.Start();
        }
 
        public override void Dispose()
        {
            EnableRaisingEvents = false;
            _disposed = true;
        }
 
        public override bool EnableRaisingEvents
        {
            get => _raiseEvents;
            set
            {
                ObjectDisposedException.ThrowIf(_disposed, this);
                _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 CaptureInitialSnapshot()
        {
            Debug.Assert(_currentSnapshot.Count == 0);
 
            ForeachEntityInDirectory(_watchedDirectory, (filePath, writeTime) =>
            {
                _currentSnapshot.Add(filePath, writeTime);
            });
        }
 
        private void CheckForChangedFiles()
        {
            Debug.Assert(_changesBuilder.Count == 0);
            Debug.Assert(_snapshotBuilder.Count == 0);
 
            ForeachEntityInDirectory(_watchedDirectory, (filePath, currentWriteTime) =>
            {
                if (!_currentSnapshot.TryGetValue(filePath, out var snapshotWriteTime))
                {
                    _changesBuilder.TryAdd(filePath, ChangeKind.Add);
                }
                else if (snapshotWriteTime != currentWriteTime)
                {
                    _changesBuilder.TryAdd(filePath, ChangeKind.Update);
                }
 
                _snapshotBuilder.Add(filePath, currentWriteTime);
            });
 
            foreach (var (filePath, _) in _currentSnapshot)
            {
                if (!_snapshotBuilder.ContainsKey(filePath))
                {
                    _changesBuilder.TryAdd(filePath, ChangeKind.Delete);
                }
            }
 
            NotifyChanges(_changesBuilder);
 
            // Swap the two dictionaries
            (_snapshotBuilder, _currentSnapshot) = (_currentSnapshot, _snapshotBuilder);
 
            _changesBuilder.Clear();
            _snapshotBuilder.Clear();
        }
 
        private void ForeachEntityInDirectory(DirectoryInfo dirInfo, Action<string, DateTime> fileAction)
        {
            if (!dirInfo.Exists)
            {
                return;
            }
 
            IEnumerable<FileSystemInfo> entities;
            try
            {
                entities = dirInfo.EnumerateFileSystemInfos("*.*", SearchOption.TopDirectoryOnly);
            }
            // If the directory is deleted after the exists check this will throw and could crash the process
            catch (DirectoryNotFoundException)
            {
                return;
            }
 
            foreach (var entity in entities)
            {
                if (entity is DirectoryInfo subdirInfo)
                {
                    if (IncludeSubdirectories)
                    {
                        ForeachEntityInDirectory(subdirInfo, fileAction);
                    }
                }
                else
                {
                    string filePath;
                    DateTime currentWriteTime;
                    try
                    {
                        filePath = entity.FullName;
                        currentWriteTime = entity.LastWriteTimeUtc;
                    }
                    catch (FileNotFoundException)
                    {
                        continue;
                    }
 
                    fileAction(filePath, currentWriteTime);
                }
            }
        }
 
        private void NotifyChanges(Dictionary<string, ChangeKind> changes)
        {
            foreach (var (path, kind) in changes)
            {
                if (_disposed || !_raiseEvents)
                {
                    break;
                }
 
                NotifyChange(path, kind);
            }
        }
    }
}