File: System\IO\FileSystemWatcher.cs
Web Access
Project: src\src\libraries\System.IO.FileSystem.Watcher\src\System.IO.FileSystem.Watcher.csproj (System.IO.FileSystem.Watcher)
// 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;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO.Enumeration;
using System.Threading;
using System.Threading.Tasks;
 
namespace System.IO
{
    /// <devdoc>
    ///    Listens to the system directory change notifications and
    ///    raises events when a directory or file within a directory changes.
    /// </devdoc>
    public partial class FileSystemWatcher : Component, ISupportInitialize
    {
        // Filters collection
        private readonly NormalizedFilterCollection _filters = new NormalizedFilterCollection();
 
        // Directory being monitored
        private string _directory;
 
        // The watch filter for the API call.
        private const NotifyFilters c_defaultNotifyFilters = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName;
        private NotifyFilters _notifyFilters = c_defaultNotifyFilters;
 
        // Flag to watch subtree of this directory
        private bool _includeSubdirectories;
 
        // Flag to note whether we are attached to the thread pool and responding to changes
        private bool _enabled;
 
        // Are we in init?
        private bool _initializing;
 
        // Buffer size
        private uint _internalBufferSize = 8192;
 
        // Used for synchronization
        private bool _disposed;
 
        // Event handlers
        private FileSystemEventHandler? _onChangedHandler;
        private FileSystemEventHandler? _onCreatedHandler;
        private FileSystemEventHandler? _onDeletedHandler;
        private RenamedEventHandler? _onRenamedHandler;
        private ErrorEventHandler? _onErrorHandler;
 
        private const int c_notifyFiltersValidMask = (int)(NotifyFilters.Attributes |
                                                           NotifyFilters.CreationTime |
                                                           NotifyFilters.DirectoryName |
                                                           NotifyFilters.FileName |
                                                           NotifyFilters.LastAccess |
                                                           NotifyFilters.LastWrite |
                                                           NotifyFilters.Security |
                                                           NotifyFilters.Size);
 
#if DEBUG
        static FileSystemWatcher()
        {
            int s_notifyFiltersValidMask = 0;
            foreach (int enumValue in Enum.GetValues<NotifyFilters>())
                s_notifyFiltersValidMask |= enumValue;
            Debug.Assert(c_notifyFiltersValidMask == s_notifyFiltersValidMask, "The NotifyFilters enum has changed. The c_notifyFiltersValidMask must be updated to reflect the values of the NotifyFilters enum.");
        }
#endif
 
        /// <devdoc>
        ///    Initializes a new instance of the <see cref='System.IO.FileSystemWatcher'/> class.
        /// </devdoc>
        public FileSystemWatcher()
        {
            _directory = string.Empty;
        }
 
        /// <devdoc>
        ///    Initializes a new instance of the <see cref='System.IO.FileSystemWatcher'/> class,
        ///    given the specified directory to monitor.
        /// </devdoc>
        public FileSystemWatcher(string path)
        {
            CheckPathValidity(path);
            _directory = path;
        }
 
        /// <devdoc>
        ///    Initializes a new instance of the <see cref='System.IO.FileSystemWatcher'/> class,
        ///    given the specified directory and type of files to monitor.
        /// </devdoc>
        public FileSystemWatcher(string path, string filter)
        {
            CheckPathValidity(path);
            ArgumentNullException.ThrowIfNull(filter);
 
            _directory = path;
            Filter = filter;
        }
 
        /// <devdoc>
        ///    Gets or sets the type of changes to watch for.
        /// </devdoc>
        public NotifyFilters NotifyFilter
        {
            get
            {
                return _notifyFilters;
            }
            set
            {
                if (((int)value & ~c_notifyFiltersValidMask) != 0)
                    throw new ArgumentException(SR.Format(SR.InvalidEnumArgument, nameof(value), (int)value, nameof(NotifyFilters)));
 
                if (_notifyFilters != value)
                {
                    _notifyFilters = value;
 
                    Restart();
                }
            }
        }
 
        public Collection<string> Filters => _filters;
 
        /// <devdoc>
        ///    Gets or sets a value indicating whether the component is enabled.
        /// </devdoc>
        public bool EnableRaisingEvents
        {
            get
            {
                return _enabled;
            }
            set
            {
                if (_enabled == value)
                {
                    return;
                }
 
                if (IsSuspended())
                {
                    _enabled = value; // Alert the Component to start watching for events when EndInit is called.
                }
                else
                {
                    if (value)
                    {
                        StartRaisingEventsIfNotDisposed(); // will set _enabled to true once successfully started
                    }
                    else
                    {
                        StopRaisingEvents(); // will set _enabled to false
                    }
                }
            }
        }
 
        /// <devdoc>
        ///    Gets or sets the filter string, used to determine what files are monitored in a directory.
        /// </devdoc>
        public string Filter
        {
            get
            {
                return Filters.Count == 0 ? "*" : Filters[0];
            }
            set
            {
                Filters.Clear();
                Filters.Add(value);
            }
        }
 
        /// <devdoc>
        ///    Gets or sets a value indicating whether subdirectories within the specified path should be monitored.
        /// </devdoc>
        public bool IncludeSubdirectories
        {
            get
            {
                return _includeSubdirectories;
            }
            set
            {
                if (_includeSubdirectories != value)
                {
                    _includeSubdirectories = value;
 
                    Restart();
                }
            }
        }
 
        /// <devdoc>
        ///    Gets or sets the size of the internal buffer.
        /// </devdoc>
        public int InternalBufferSize
        {
            get
            {
                return (int)_internalBufferSize;
            }
            set
            {
                if (_internalBufferSize != value)
                {
                    if (value < 4096)
                    {
                        _internalBufferSize = 4096;
                    }
                    else
                    {
                        _internalBufferSize = (uint)value;
                    }
 
                    Restart();
                }
            }
        }
 
        /// <summary>Allocates a buffer of the requested internal buffer size.</summary>
        /// <returns>The allocated buffer.</returns>
        private byte[] AllocateBuffer()
        {
            try
            {
                return new byte[_internalBufferSize];
            }
            catch (OutOfMemoryException)
            {
                throw new OutOfMemoryException(SR.Format(SR.BufferSizeTooLarge, _internalBufferSize));
            }
        }
 
        /// <devdoc>
        ///    Gets or sets the path of the directory to watch.
        /// </devdoc>
        [Editor("System.Diagnostics.Design.FSWPathEditor, System.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a",
                "System.Drawing.Design.UITypeEditor, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
        public string Path
        {
            get
            {
                return _directory;
            }
            set
            {
                value ??= string.Empty;
                if (!string.Equals(_directory, value, PathInternal.StringComparison))
                {
                    if (value.Length == 0)
                        throw new ArgumentException(SR.Format(SR.InvalidDirName, value), nameof(Path));
 
                    if (!Directory.Exists(value))
                        throw new ArgumentException(SR.Format(SR.InvalidDirName_NotExists, value), nameof(Path));
 
                    _directory = value;
                    Restart();
                }
            }
        }
 
        /// <devdoc>
        ///    Occurs when a file or directory in the specified <see cref='System.IO.FileSystemWatcher.Path'/> is changed.
        /// </devdoc>
        public event FileSystemEventHandler? Changed
        {
            add
            {
                _onChangedHandler += value;
            }
            remove
            {
                _onChangedHandler -= value;
            }
        }
 
        /// <devdoc>
        ///    Occurs when a file or directory in the specified <see cref='System.IO.FileSystemWatcher.Path'/> is created.
        /// </devdoc>
        public event FileSystemEventHandler? Created
        {
            add
            {
                _onCreatedHandler += value;
            }
            remove
            {
                _onCreatedHandler -= value;
            }
        }
 
        /// <devdoc>
        ///    Occurs when a file or directory in the specified <see cref='System.IO.FileSystemWatcher.Path'/> is deleted.
        /// </devdoc>
        public event FileSystemEventHandler? Deleted
        {
            add
            {
                _onDeletedHandler += value;
            }
            remove
            {
                _onDeletedHandler -= value;
            }
        }
 
        /// <devdoc>
        ///    Occurs when the internal buffer overflows.
        /// </devdoc>
        public event ErrorEventHandler? Error
        {
            add
            {
                _onErrorHandler += value;
            }
            remove
            {
                _onErrorHandler -= value;
            }
        }
 
        /// <devdoc>
        ///    Occurs when a file or directory in the specified <see cref='System.IO.FileSystemWatcher.Path'/>
        ///    is renamed.
        /// </devdoc>
        public event RenamedEventHandler? Renamed
        {
            add
            {
                _onRenamedHandler += value;
            }
            remove
            {
                _onRenamedHandler -= value;
            }
        }
 
        protected override void Dispose(bool disposing)
        {
            try
            {
                if (disposing)
                {
                    //Stop raising events cleans up managed and
                    //unmanaged resources.
                    StopRaisingEvents();
 
                    // Clean up managed resources
                    _onChangedHandler = null;
                    _onCreatedHandler = null;
                    _onDeletedHandler = null;
                    _onRenamedHandler = null;
                    _onErrorHandler = null;
                }
                else
                {
                    FinalizeDispose();
                }
            }
            finally
            {
                _disposed = true;
                base.Dispose(disposing);
            }
        }
 
        private static void CheckPathValidity(string path)
        {
            ArgumentNullException.ThrowIfNull(path);
 
            // Early check for directory parameter so that an exception can be thrown as early as possible.
            if (path.Length == 0)
                throw new ArgumentException(SR.Format(SR.InvalidDirName, path), nameof(path));
 
            if (!Directory.Exists(path))
                throw new ArgumentException(SR.Format(SR.InvalidDirName_NotExists, path), nameof(path));
        }
 
        /// <summary>
        /// Sees if the name given matches the name filter we have.
        /// </summary>
        private bool MatchPattern(ReadOnlySpan<char> relativePath)
        {
            ReadOnlySpan<char> name = IO.Path.GetFileName(relativePath);
            if (name.Length == 0)
                return false;
 
            string[] filters = _filters.GetFilters();
            if (filters.Length == 0)
                return true;
 
            foreach (string filter in filters)
            {
                if (FileSystemName.MatchesSimpleExpression(filter, name, ignoreCase: !PathInternal.IsCaseSensitive))
                    return true;
            }
 
            return false;
        }
 
        /// <summary>
        /// Raises the event to each handler in the list.
        /// </summary>
        private void NotifyInternalBufferOverflowEvent()
        {
            if (_onErrorHandler != null)
            {
                OnError(new ErrorEventArgs(
                        new InternalBufferOverflowException(SR.Format(SR.FSW_BufferOverflow, _directory))));
            }
        }
 
        /// <summary>
        /// Raises the event to each handler in the list.
        /// </summary>
        private void NotifyRenameEventArgs(WatcherChangeTypes action, ReadOnlySpan<char> name, ReadOnlySpan<char> oldName)
        {
            // filter if there's no handler or neither new name or old name match a specified pattern
            if (_onRenamedHandler != null &&
                (MatchPattern(name) || MatchPattern(oldName)))
            {
                OnRenamed(new RenamedEventArgs(action, _directory, name.IsEmpty ? null : name.ToString(), oldName.IsEmpty ? null : oldName.ToString()));
            }
        }
 
        private FileSystemEventHandler? GetHandler(WatcherChangeTypes changeType)
        {
            switch (changeType)
            {
                case WatcherChangeTypes.Created:
                    return _onCreatedHandler;
                case WatcherChangeTypes.Deleted:
                    return _onDeletedHandler;
                case WatcherChangeTypes.Changed:
                    return _onChangedHandler;
            }
 
            Debug.Fail("Unknown FileSystemEvent change type!  Value: " + changeType);
            return null;
        }
 
        /// <summary>
        /// Raises the event to each handler in the list.
        /// </summary>
        private void NotifyFileSystemEventArgs(WatcherChangeTypes changeType, ReadOnlySpan<char> name)
        {
            FileSystemEventHandler? handler = GetHandler(changeType);
 
            if (handler != null && MatchPattern(name.IsEmpty ? _directory : name))
            {
                InvokeOn(new FileSystemEventArgs(changeType, _directory, name.IsEmpty ? null : name.ToString()), handler);
            }
        }
 
        /// <summary>
        /// Raises the event to each handler in the list.
        /// </summary>
        private void NotifyFileSystemEventArgs(WatcherChangeTypes changeType, string name)
        {
            FileSystemEventHandler? handler = GetHandler(changeType);
 
            if (handler != null && MatchPattern(string.IsNullOrEmpty(name) ? _directory : name))
            {
                InvokeOn(new FileSystemEventArgs(changeType, _directory, name), handler);
            }
        }
 
        /// <devdoc>
        ///    Raises the <see cref='System.IO.FileSystemWatcher.Changed'/> event.
        /// </devdoc>
        protected void OnChanged(FileSystemEventArgs e)
        {
            InvokeOn(e, _onChangedHandler);
        }
 
        /// <devdoc>
        ///    Raises the <see cref='System.IO.FileSystemWatcher.Created'/> event.
        /// </devdoc>
        protected void OnCreated(FileSystemEventArgs e)
        {
            InvokeOn(e, _onCreatedHandler);
        }
 
        /// <devdoc>
        ///    Raises the <see cref='System.IO.FileSystemWatcher.Deleted'/> event.
        /// </devdoc>
        protected void OnDeleted(FileSystemEventArgs e)
        {
            InvokeOn(e, _onDeletedHandler);
        }
 
        private void InvokeOn(FileSystemEventArgs e, FileSystemEventHandler? handler)
        {
            if (handler != null)
            {
                ISynchronizeInvoke? syncObj = SynchronizingObject;
                if (syncObj != null && syncObj.InvokeRequired)
                    syncObj.BeginInvoke(handler, new object[] { this, e });
                else
                    handler(this, e);
            }
        }
 
        /// <devdoc>
        ///    Raises the <see cref='System.IO.FileSystemWatcher.Error'/> event.
        /// </devdoc>
        protected void OnError(ErrorEventArgs e)
        {
            ErrorEventHandler? handler = _onErrorHandler;
            if (handler != null)
            {
                ISynchronizeInvoke? syncObj = SynchronizingObject;
                if (syncObj != null && syncObj.InvokeRequired)
                    syncObj.BeginInvoke(handler, new object[] { this, e });
                else
                    handler(this, e);
            }
        }
 
        /// <devdoc>
        ///    Raises the <see cref='System.IO.FileSystemWatcher.Renamed'/> event.
        /// </devdoc>
        protected void OnRenamed(RenamedEventArgs e)
        {
            RenamedEventHandler? handler = _onRenamedHandler;
            if (handler != null)
            {
                ISynchronizeInvoke? syncObj = SynchronizingObject;
                if (syncObj != null && syncObj.InvokeRequired)
                    syncObj.BeginInvoke(handler, new object[] { this, e });
                else
                    handler(this, e);
            }
        }
 
        public WaitForChangedResult WaitForChanged(WatcherChangeTypes changeType) =>
            WaitForChanged(changeType, Timeout.Infinite);
 
        public WaitForChangedResult WaitForChanged(WatcherChangeTypes changeType, int timeout)
        {
            // The full framework implementation doesn't do any argument validation, so
            // none is done here, either.
 
            var tcs = new TaskCompletionSource<WaitForChangedResult>();
            FileSystemEventHandler? fseh = null;
            RenamedEventHandler? reh = null;
 
            // Register the event handlers based on what events are desired.  The full framework
            // doesn't register for the Error event, so this doesn't either.
            if ((changeType & (WatcherChangeTypes.Created | WatcherChangeTypes.Deleted | WatcherChangeTypes.Changed)) != 0)
            {
                fseh = (s, e) =>
                {
                    if ((e.ChangeType & changeType) != 0)
                    {
                        tcs.TrySetResult(new WaitForChangedResult(e.ChangeType, e.Name, oldName: null, timedOut: false));
                    }
                };
                if ((changeType & WatcherChangeTypes.Created) != 0)
                    Created += fseh;
                if ((changeType & WatcherChangeTypes.Deleted) != 0)
                    Deleted += fseh;
                if ((changeType & WatcherChangeTypes.Changed) != 0)
                    Changed += fseh;
            }
            if ((changeType & WatcherChangeTypes.Renamed) != 0)
            {
                reh = (s, e) =>
                {
                    if ((e.ChangeType & changeType) != 0)
                    {
                        tcs.TrySetResult(new WaitForChangedResult(e.ChangeType, e.Name, e.OldName, timedOut: false));
                    }
                };
                Renamed += reh;
            }
            try
            {
                // Enable the FSW if it wasn't already.
                bool wasEnabled = EnableRaisingEvents;
                if (!wasEnabled)
                {
                    EnableRaisingEvents = true;
                }
 
                // Block until an appropriate event arrives or until we timeout.
                Debug.Assert(EnableRaisingEvents, "Expected EnableRaisingEvents to be true");
                tcs.Task.Wait(timeout);
 
                // Reset the enabled state to what it was.
                EnableRaisingEvents = wasEnabled;
            }
            finally
            {
                // Unregister the event handlers.
                if (reh != null)
                {
                    Renamed -= reh;
                }
                if (fseh != null)
                {
                    if ((changeType & WatcherChangeTypes.Changed) != 0)
                        Changed -= fseh;
                    if ((changeType & WatcherChangeTypes.Deleted) != 0)
                        Deleted -= fseh;
                    if ((changeType & WatcherChangeTypes.Created) != 0)
                        Created -= fseh;
                }
            }
 
            // Return the results.
            return tcs.Task.IsCompletedSuccessfully ?
                tcs.Task.Result :
                WaitForChangedResult.TimedOutResult;
        }
 
        public WaitForChangedResult WaitForChanged(WatcherChangeTypes changeType, TimeSpan timeout) =>
            WaitForChanged(changeType, ToTimeoutMilliseconds(timeout));
 
        private static int ToTimeoutMilliseconds(TimeSpan timeout)
        {
            long totalMilliseconds = (long)timeout.TotalMilliseconds;
            ArgumentOutOfRangeException.ThrowIfLessThan(totalMilliseconds, -1, nameof(timeout));
            ArgumentOutOfRangeException.ThrowIfGreaterThan(totalMilliseconds, int.MaxValue, nameof(timeout));
            return (int)totalMilliseconds;
        }
 
        /// <devdoc>
        ///     Stops and starts this object.
        /// </devdoc>
        /// <internalonly/>
        private void Restart()
        {
            if ((!IsSuspended()) && _enabled)
            {
                StopRaisingEvents();
                StartRaisingEventsIfNotDisposed();
            }
        }
 
        private void StartRaisingEventsIfNotDisposed()
        {
            //Cannot allocate the directoryHandle and the readBuffer if the object has been disposed; finalization has been suppressed.
            ObjectDisposedException.ThrowIf(_disposed, this);
            StartRaisingEvents();
        }
 
        public override ISite? Site
        {
            get
            {
                return base.Site;
            }
            set
            {
                base.Site = value;
 
                // set EnableRaisingEvents to true at design time so the user
                // doesn't have to manually.
                if (Site != null && Site.DesignMode)
                    EnableRaisingEvents = true;
            }
        }
 
        public ISynchronizeInvoke? SynchronizingObject { get; set; }
 
        public void BeginInit()
        {
            bool oldEnabled = _enabled;
            StopRaisingEvents();
            _enabled = oldEnabled;
            _initializing = true;
        }
 
        public void EndInit()
        {
            _initializing = false;
            // Start listening to events if _enabled was set to true at some point.
            if (_directory.Length != 0 && _enabled)
                StartRaisingEvents();
        }
 
        private bool IsSuspended()
        {
            return _initializing || DesignMode;
        }
 
        private sealed class NormalizedFilterCollection : Collection<string>
        {
            internal NormalizedFilterCollection() : base(new ImmutableStringList())
            {
            }
 
            protected override void InsertItem(int index, string item)
            {
                base.InsertItem(index, string.IsNullOrEmpty(item) || item == "*.*" ? "*" : item);
            }
 
            protected override void SetItem(int index, string item)
            {
                base.SetItem(index, string.IsNullOrEmpty(item) || item == "*.*" ? "*" : item);
            }
 
            internal string[] GetFilters() => ((ImmutableStringList)Items).Items;
 
            /// <summary>
            /// List that maintains its underlying data in an immutable array, such that the list
            /// will never modify an array returned from its Items property. This is to allow
            /// the array to be enumerated safely while another thread might be concurrently mutating
            /// the collection.
            /// </summary>
            private sealed class ImmutableStringList : IList<string>
            {
                public string[] Items = Array.Empty<string>();
 
                public string this[int index]
                {
                    get
                    {
                        string[] items = Items;
                        ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)items.Length, nameof(index));
                        return items[index];
                    }
                    set
                    {
                        string[] clone = (string[])Items.Clone();
                        clone[index] = value;
                        Items = clone;
                    }
                }
 
                public int Count => Items.Length;
 
                public bool IsReadOnly => false;
 
                public void Add(string item)
                {
                    // Collection<T> doesn't use this method.
                    throw new NotSupportedException();
                }
 
                public void Clear() => Items = Array.Empty<string>();
 
                public bool Contains(string item) => Array.IndexOf(Items, item) != -1;
 
                public void CopyTo(string[] array, int arrayIndex) => Items.CopyTo(array, arrayIndex);
 
                public IEnumerator<string> GetEnumerator() => ((IEnumerable<string>)Items).GetEnumerator();
 
                public int IndexOf(string item) => Array.IndexOf(Items, item);
 
                public void Insert(int index, string item)
                {
                    string[] items = Items;
                    string[] newItems = new string[items.Length + 1];
                    items.AsSpan(0, index).CopyTo(newItems);
                    items.AsSpan(index).CopyTo(newItems.AsSpan(index + 1));
                    newItems[index] = item;
                    Items = newItems;
                }
 
                public bool Remove(string item)
                {
                    // Collection<T> doesn't use this method.
                    throw new NotSupportedException();
                }
 
                public void RemoveAt(int index)
                {
                    string[] items = Items;
                    string[] newItems = new string[items.Length - 1];
                    items.AsSpan(0, index).CopyTo(newItems);
                    items.AsSpan(index + 1).CopyTo(newItems.AsSpan(index));
                    Items = newItems;
                }
 
                IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
            }
        }
    }
}