Skip to content
This repository has been archived by the owner on Aug 2, 2023. It is now read-only.

Commit

Permalink
FileSystemWatcher consistency (#2235)
Browse files Browse the repository at this point in the history
* Rename class

* Remove dependency on TraceSource

* Expose fields as properties

* Recurse subdirectories

* Use proper events

* Standardize naming

* Add error event

* CR changes

* Fix test build

* Refactor to single constructor

* CR changes

* Add license

* Separate the files again
  • Loading branch information
MisinformedDNA authored and ahsonkhan committed May 2, 2018
1 parent 3635104 commit 311f528
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 239 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ struct FileChangeList

public bool IsEmpty { get { return _changes == null || _count == 0; } }

public int Count { get { return _count; } }

internal void AddAdded(string directory, string path)
{
Debug.Assert(path != null);
Expand Down Expand Up @@ -92,8 +90,8 @@ struct FileState
internal byte _version; // removal notification are implemented something similar to "mark and sweep". This value is incremented in the mark phase
public string Path;
public string Directory;
public DateTimeOffset LastWrite;
public long FileSize;
public DateTimeOffset LastWriteTimeUtc;
public long Length;

public FileState(string directory, string path) : this()
{
Expand All @@ -106,10 +104,5 @@ public override string ToString()
{
return Path;
}

internal bool IsEmpty
{
get { return Path == null; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ internal class FileSystemChangeEnumerator: FileSystemEnumerator<string>
{
private FileChangeList _changes = new FileChangeList();
private string _currentDirectory;
private PollingWatcher _watcher;
private PollingFileSystemWatcher _watcher;

public FileSystemChangeEnumerator(PollingWatcher watcher, string directory, EnumerationOptions options = null)
public FileSystemChangeEnumerator(PollingFileSystemWatcher watcher, string directory, EnumerationOptions options = null)
: base(directory, options)
{
_watcher = watcher;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,19 @@

namespace System.IO.FileSystem
{
// this is a quick an dirty hashtable optimized for the PollingWatcher
// this is a quick an dirty hashtable optimized for the PollingFileSystemWatcher
// It allows mutating struct values (FileState) contained in the hashtable
// It has optimized Equals and GetHasCode
// It implements removals by marking values as "removed" (Path==null) and then garbage collecting them when table is resized
class PathToFileStateHashtable
{
TraceSource _trace;
int _count;
int _nextValuesIndex = 1; // the first Values slot is reserved so that default(Bucket) knows that it is not pointing to any value.
public FileState[] Values { get; private set; }
private Bucket[] Buckets;

public PathToFileStateHashtable(TraceSource trace, int capacity = 4)
public PathToFileStateHashtable(int capacity = 4)
{
_trace = trace;
Values = new FileState[capacity];

// +1 is needed so that there are always more buckets than values.
Expand Down Expand Up @@ -142,12 +140,7 @@ private void Resize()
// this is because sometimes we just need to garbade collect instead of increase size
var newSize = Math.Max(_count * 2, 4);

if (_trace.Switch.ShouldTrace(TraceEventType.Verbose))
{
_trace.TraceEvent(TraceEventType.Verbose, 5, "Resizing hashtable from {0} to {1}", Values.Length, newSize);
}

var bigger = new PathToFileStateHashtable(_trace, newSize);
var bigger = new PathToFileStateHashtable(newSize);

foreach (var existingValue in this)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace System.IO.FileSystem
{
public class PollingFileSystemEventArgs : EventArgs
{
public PollingFileSystemEventArgs(FileChange[] changes)
{
Changes = changes;
}

public FileChange[] Changes { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace System.IO.FileSystem
{
public delegate void PollingFileSystemEventHandler(object sender, PollingFileSystemEventArgs e);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.IO.Enumeration;
using System.Runtime.InteropServices;
using System.Threading;

namespace System.IO.FileSystem
{
/// <summary>
/// PollingFileSystemWatcher can be used to monitor changes to a file system directory
/// </summary>
/// <remarks>
/// This type is similar to FileSystemWatcher, but unlike FileSystemWatcher it is fully reliable,
/// at the cost of some performance overhead.
/// Instead of relying on Win32 file notification APIs, it periodically scans the watched directory to discover changes.
/// This means that sooner or later it will discover every change.
/// FileSystemWatcher's Win32 APIs can drop some events in rare circumstances, which is often an acceptable compromise.
/// In scenarios where events cannot be missed, PollingFileSystemWatcher should be used.
/// Note: When a watched file is renamed, one or two notifications will be made.
/// Note: When no changes are detected, PollingFileSystemWatcher will not allocate memory on the GC heap.
/// </remarks>
public class PollingFileSystemWatcher : IDisposable
{
Timer _timer;
PathToFileStateHashtable _state; // stores state of the directory
byte _version; // this is used to keep track of removals. // TODO: describe the algorithm

/// <summary>
/// Creates an instance of a watcher
/// </summary>
/// <param name="path">The path to watch.</param>
/// <param name="filter">The type of files to watch. For example, "*.txt" watches for changes to all text files.</param>
public PollingFileSystemWatcher(string path, string filter = "*.*", EnumerationOptions options = null)
{
if (!Directory.Exists(path))
throw new ArgumentException("Path not found.", nameof(path));

_state = new PathToFileStateHashtable();
Path = path;
Filter = filter;
EnumerationOptions = null ?? new EnumerationOptions();
}

public EnumerationOptions EnumerationOptions { get; set; } = new EnumerationOptions();
public string Filter { get; set; } = "*.*";
public string Path { get; set; }
public int PollingIntervalInMilliseconds { get; set; } = 1000;

public void Start()
{
ComputeChangesAndUpdateState(); // captures the initial state
_timer = new Timer(new TimerCallback(TimerHandler), null, PollingIntervalInMilliseconds, Timeout.Infinite);
}

// This function walks all watched files, collects changes, and updates state
private FileChangeList ComputeChangesAndUpdateState()
{
_version++;

var enumerator = new FileSystemChangeEnumerator(this, Path, EnumerationOptions);
while (enumerator.MoveNext())
{
// Ignore `.Current`
}
var changes = enumerator.Changes;

foreach (var value in _state)
{
if (value._version != _version)
{
changes.AddRemoved(value.Directory, value.Path);
_state.Remove(value.Directory, value.Path);
}
}

return changes;
}

internal bool IsWatched(ref FileSystemEntry entry)
{
if (entry.IsDirectory) return false;
if (Filter == null) return true;

bool ignoreCase = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
if (FileSystemName.MatchesSimpleExpression(Filter, entry.FileName, ignoreCase: ignoreCase))
return true;

return false;
}

internal void UpdateState(string directory, ref FileChangeList changes, ref FileSystemEntry file)
{
int index = _state.IndexOf(directory, file.FileName);
if (index == -1) // file added
{
string path = file.FileName.ToString();

changes.AddAdded(directory, path.ToString());

var newFileState = new FileState(directory, path);
newFileState.LastWriteTimeUtc = file.LastWriteTimeUtc;
newFileState.Length = file.Length;
newFileState._version = _version;
_state.Add(directory, path, newFileState);
return;
}

_state.Values[index]._version = _version;

var previousState = _state.Values[index];
if (file.LastWriteTimeUtc != previousState.LastWriteTimeUtc || file.Length != previousState.Length)
{
changes.AddChanged(directory, previousState.Path);
_state.Values[index].LastWriteTimeUtc = file.LastWriteTimeUtc;
_state.Values[index].Length = file.Length;
}
}

/// <summary>
/// This callback is called when any change (Created, Deleted, Changed) is detected in any watched file.
/// </summary>
public event EventHandler Changed;

public event PollingFileSystemEventHandler ChangedDetailed;

public event ErrorEventHandler Error;

/// <summary>
/// Disposes the timer used for polling.
/// </summary>
public void Dispose()
{
_timer.Dispose();
}

private void TimerHandler(object context)
{
try
{
var changes = ComputeChangesAndUpdateState();

if (!changes.IsEmpty)
{
Changed?.Invoke(this, EventArgs.Empty);
ChangedDetailed?.Invoke(this, new PollingFileSystemEventArgs(changes.ToArray()));
}
}
catch (Exception e)
{
Error?.Invoke(this, new ErrorEventArgs(e));
}

_timer.Change(PollingIntervalInMilliseconds, Timeout.Infinite);
}
}
}
Loading

0 comments on commit 311f528

Please sign in to comment.