1 //------------------------------------------------------------------------------
2 // <copyright file="FileChangesMonitor.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
5 //------------------------------------------------------------------------------
8 using System.Collections;
9 using System.Collections.Specialized;
10 using System.Diagnostics.CodeAnalysis;
11 using System.Globalization;
13 using System.Runtime.InteropServices;
14 using System.Security;
15 using System.Security.Permissions;
17 using System.Threading;
18 using System.Web.Configuration;
19 using System.Web.Hosting;
20 using System.Web.Util;
21 using Microsoft.Win32;
24 // Type of the callback to the subscriber of a file change event in FileChangesMonitor.StartMonitoringFile
25 delegate void FileChangeEventHandler(Object sender, FileChangeEvent e);
27 // The type of file change that occurred.
39 // Event data for a file change notification
40 sealed class FileChangeEvent : EventArgs {
41 internal FileAction Action; // the action
42 internal string FileName; // the file that caused the action
44 internal FileChangeEvent(FileAction action, string fileName) {
46 this.FileName = fileName;
50 // Contains information about the target of a file change notification
51 sealed class FileMonitorTarget {
52 internal readonly FileChangeEventHandler Callback; // the callback
53 internal readonly string Alias; // the filename used to name the file
54 internal readonly DateTime UtcStartMonitoring;// time we started monitoring
55 int _refs; // number of uses of callbacks
57 internal FileMonitorTarget(FileChangeEventHandler callback, string alias) {
60 UtcStartMonitoring = DateTime.UtcNow;
64 internal int AddRef() {
69 internal int Release() {
75 internal string DebugDescription(string indent) {
76 StringBuilder sb = new StringBuilder(200);
77 string i2 = indent + " ";
79 sb.Append(indent + "FileMonitorTarget\n");
80 sb.Append(i2 + " Callback: " + Callback.Target + "(HC=" + Callback.Target.GetHashCode().ToString("x", NumberFormatInfo.InvariantInfo) + ")\n");
81 sb.Append(i2 + " Alias: " + Alias + "\n");
82 sb.Append(i2 + "StartMonitoring: " + Debug.FormatUtcDate(UtcStartMonitoring) + "\n");
83 sb.Append(i2 + " _refs: " + _refs + "\n");
90 #if !FEATURE_PAL // FEATURE_PAL does not enable access control
91 sealed class FileSecurity {
92 const int DACL_INFORMATION =
93 UnsafeNativeMethods.DACL_SECURITY_INFORMATION |
94 UnsafeNativeMethods.GROUP_SECURITY_INFORMATION |
95 UnsafeNativeMethods.OWNER_SECURITY_INFORMATION;
97 static Hashtable s_interned;
98 static byte[] s_nullDacl;
100 class DaclComparer : IEqualityComparer {
101 // Compares two objects. An implementation of this method must return a
102 // value less than zero if x is less than y, zero if x is equal to y, or a
103 // value greater than zero if x is greater than y.
106 private int Compare(byte[] a, byte[] b) {
107 int result = a.Length - b.Length;
108 for (int i = 0; result == 0 && i < a.Length ; i++) {
109 result = a[i] - b[i];
115 bool IEqualityComparer.Equals(Object x, Object y) {
116 if (x == null && y == null) {
120 if (x == null || y == null) {
124 byte[] a = x as byte[];
125 byte[] b = y as byte[];
127 if (a == null || b == null) {
131 return Compare(a, b) == 0;
134 int IEqualityComparer.GetHashCode(Object obj) {
135 byte[] a = (byte[]) obj;
137 HashCodeCombiner combiner = new HashCodeCombiner();
138 foreach (byte b in a) {
139 combiner.AddObject(b);
142 return combiner.CombinedHash32;
146 static FileSecurity() {
147 s_interned = new Hashtable(0, 1.0f, new DaclComparer());
148 s_nullDacl = new byte[0];
151 [SuppressMessage("Microsoft.Interoperability", "CA1404:CallGetLastErrorImmediatelyAfterPInvoke",
152 Justification="Microsoft: Call to GetLastWin32Error() does follow P/Invoke call that is outside the if/else block.")]
153 static internal byte[] GetDacl(string filename) {
154 // DevDiv #322858 - allow skipping DACL step for perf gain
155 if (HostingEnvironment.FcnSkipReadAndCacheDacls) {
160 // Perf: Start with initial buffer size to minimize File IO
162 int lengthNeeded = 512;
163 byte[] dacl = new byte[lengthNeeded];
164 int fOK = UnsafeNativeMethods.GetFileSecurity(filename, DACL_INFORMATION, dacl, dacl.Length, ref lengthNeeded);
167 // If no size is needed, return a non-null marker
168 if (lengthNeeded == 0) {
169 Debug.Trace("GetDacl", "Returning null dacl");
173 // Shrink the buffer to fit the whole data
174 Array.Resize(ref dacl, lengthNeeded);
177 int hr = HttpException.HResultFromLastError(Marshal.GetLastWin32Error());
179 // Check if need to redo the call with larger buffer
180 if (hr != HResults.E_INSUFFICIENT_BUFFER) {
181 Debug.Trace("GetDacl", "Error in first call to GetFileSecurity: 0x" + hr.ToString("x", NumberFormatInfo.InvariantInfo));
185 // The buffer wasn't large enough. Try again
186 dacl = new byte[lengthNeeded];
187 fOK = UnsafeNativeMethods.GetFileSecurity(filename, DACL_INFORMATION, dacl, dacl.Length, ref lengthNeeded);
190 hr = HttpException.HResultFromLastError(Marshal.GetLastWin32Error());
191 Debug.Trace("GetDacl", "Error in second to GetFileSecurity: 0x" + hr.ToString("x", NumberFormatInfo.InvariantInfo));
198 byte[] interned = (byte[]) s_interned[dacl];
199 if (interned == null) {
200 lock (s_interned.SyncRoot) {
201 interned = (byte[]) s_interned[dacl];
202 if (interned == null) {
203 Debug.Trace("GetDacl", "Interning new dacl, length " + dacl.Length);
205 s_interned[interned] = interned;
210 Debug.Trace("GetDacl", "Returning dacl, length " + dacl.Length);
215 // holds information about a single file and the targets of change notification
216 sealed class FileMonitor {
217 internal readonly DirectoryMonitor DirectoryMonitor; // the parent
218 internal readonly HybridDictionary Aliases; // aliases for this file
219 string _fileNameLong; // long file name - if null, represents any file in this directory
220 string _fileNameShort; // short file name, may be null
221 HybridDictionary _targets; // targets of notification
222 bool _exists; // does the file exist?
223 FileAttributesData _fad; // file attributes
224 byte[] _dacl; // dacl
225 FileAction _lastAction; // last action that ocurred on this file
226 DateTime _utcLastCompletion; // date of the last RDCW completion
228 internal FileMonitor(
229 DirectoryMonitor dirMon, string fileNameLong, string fileNameShort,
230 bool exists, FileAttributesData fad, byte[] dacl) {
232 DirectoryMonitor = dirMon;
233 _fileNameLong = fileNameLong;
234 _fileNameShort = fileNameShort;
238 _targets = new HybridDictionary();
239 Aliases = new HybridDictionary(true);
242 internal string FileNameLong {get {return _fileNameLong;}}
243 internal string FileNameShort {get {return _fileNameShort;}}
244 internal bool Exists {get {return _exists;}}
245 internal bool IsDirectory {get {return (FileNameLong == null);}}
246 internal FileAction LastAction {
247 get {return _lastAction;}
248 set {_lastAction = value;}
251 internal DateTime UtcLastCompletion {
252 get {return _utcLastCompletion;}
253 set {_utcLastCompletion = value;}
256 // Returns the attributes of a file, updating them if the file has changed.
257 internal FileAttributesData Attributes {
261 internal byte[] Dacl {
265 internal void ResetCachedAttributes() {
270 internal void UpdateCachedAttributes() {
271 string path = Path.Combine(DirectoryMonitor.Directory, FileNameLong);
272 FileAttributesData.GetFileAttributes(path, out _fad);
273 _dacl = FileSecurity.GetDacl(path);
276 // Set new file information when a file comes into existence
277 internal void MakeExist(FindFileData ffd, byte[] dacl) {
278 _fileNameLong = ffd.FileNameLong;
279 _fileNameShort = ffd.FileNameShort;
280 _fad = ffd.FileAttributesData;
285 // Remove a file from existence
286 internal void MakeExtinct() {
292 internal void RemoveFileNameShort() {
293 _fileNameShort = null;
296 internal ICollection Targets {
297 get {return _targets.Values;}
300 // Add delegate for this file.
301 internal void AddTarget(FileChangeEventHandler callback, string alias, bool newAlias) {
302 FileMonitorTarget target = (FileMonitorTarget)_targets[callback.Target];
303 if (target != null) {
308 // Needs the lock to sync with DebugDescription
311 _targets.Add(callback.Target, new FileMonitorTarget(callback, alias));
318 Aliases[alias] = alias;
323 // Remove delegate for this file given the target object.
324 internal int RemoveTarget(object callbackTarget) {
325 FileMonitorTarget target = (FileMonitorTarget)_targets[callbackTarget];
327 if (FileChangesMonitor.s_enableRemoveTargetAssert) {
328 Debug.Assert(target != null, "removing file monitor target that was never added or already been removed");
331 if (target != null && target.Release() == 0) {
333 // Needs the lock to sync with DebugDescription
336 _targets.Remove(callbackTarget);
342 return _targets.Count;
346 internal string DebugDescription(string indent) {
347 StringBuilder sb = new StringBuilder(200);
348 string i2 = indent + " ";
349 string i3 = i2 + " ";
350 DictionaryEntryTypeComparer detcomparer = new DictionaryEntryTypeComparer();
352 sb.Append(indent + "System.Web.FileMonitor: ");
353 if (FileNameLong != null) {
354 sb.Append(FileNameLong);
355 if (FileNameShort != null) {
356 sb.Append("; ShortFileName=" + FileNameShort);
359 sb.Append("; FileExists="); sb.Append(_exists);
365 sb.Append(i2 + "LastAction="); sb.Append(_lastAction);
366 sb.Append("; LastCompletion="); sb.Append(Debug.FormatUtcDate(_utcLastCompletion));
370 sb.Append(_fad.DebugDescription(i2));
373 sb.Append(i2 + "FileAttributesData = <null>\n");
376 DictionaryEntry[] delegateEntries;
379 sb.Append(i2 + _targets.Count + " delegates...\n");
381 delegateEntries = new DictionaryEntry[_targets.Count];
382 _targets.CopyTo(delegateEntries, 0);
385 Array.Sort(delegateEntries, detcomparer);
387 foreach (DictionaryEntry d in delegateEntries) {
388 sb.Append(i3 + "Delegate " + d.Key.GetType() + "(HC=" + d.Key.GetHashCode().ToString("x", NumberFormatInfo.InvariantInfo) + ")\n");
391 return sb.ToString();
397 // Change notifications delegate from native code.
398 delegate void NativeFileChangeNotification(FileAction action, [In, MarshalAs(UnmanagedType.LPWStr)] string fileName, long ticks);
401 // Wraps N/Direct calls to native code that does completion port
402 // based ReadDirectoryChangesW().
403 // This needs to be a separate object so that a DirectoryMonitory
404 // can start monitoring while the old _rootCallback has not been
407 sealed class DirMonCompletion : IDisposable {
408 static int _activeDirMonCompletions = 0; // private counter used via reflection by FCN check-in suite
410 DirectoryMonitor _dirMon; // directory monitor
411 IntPtr _ndirMonCompletionPtr; // pointer to native dir mon as int (used only to construct HandleRef)
412 HandleRef _ndirMonCompletionHandle; // handleref of a pointer to native dir mon as int
413 GCHandle _rootCallback; // roots this callback to prevent collection
414 int _disposed; // set to 1 when we call DirMonClose
415 object _ndirMonCompletionHandleLock;
417 internal static int ActiveDirMonCompletions { get { return _activeDirMonCompletions; } }
419 internal DirMonCompletion(DirectoryMonitor dirMon, string dir, bool watchSubtree, uint notifyFilter) {
420 Debug.Trace("FileChangesMonitor", "DirMonCompletion::ctor " + dir + " " + watchSubtree.ToString() + " " + notifyFilter.ToString(NumberFormatInfo.InvariantInfo));
423 NativeFileChangeNotification myCallback;
426 myCallback = new NativeFileChangeNotification(this.OnFileChange);
427 _ndirMonCompletionHandleLock = new object();
431 // protected from ThreadAbortEx
432 lock(_ndirMonCompletionHandleLock) {
433 // Dev10 927846: The managed DirMonCompletion.ctor calls DirMonOpen to create and initialize the native DirMonCompletion.
434 // If this succeeds, the managed DirMonCompletion.ctor creates a GCHandle to root itself so the target of the callback
435 // stays alive. When the native DirMonCompletion is freed it invokes the managed callback with ACTION_DISPOSE to
436 // release the GCHandle. In order for the native DirMonCOmpletion to be freed, either DirMonOpen must fail or
437 // the managed DirMonCompletion.Dispose must be called and it must invoke DirMonClose. Waiting until the native
438 // DirMonCompletion.dtor is called to release the GCHandle ensures that the directory handle has been closed,
439 // the i/o completions have finished and there are no other threads calling the managed callback. This is because
440 // we AddRef when we initiate i/o and we Release when the i/o completion finishes.
442 // If I don't do this, myCallback will be collected by GC since its only reference is
443 // from the native code.
444 _rootCallback = GCHandle.Alloc(myCallback);
446 hr = UnsafeNativeMethods.DirMonOpen(dir, HttpRuntime.AppDomainAppId, watchSubtree, notifyFilter, dirMon.FcnMode, myCallback, out _ndirMonCompletionPtr);
447 if (hr != HResults.S_OK) {
448 _rootCallback.Free();
449 throw FileChangesMonitor.CreateFileMonitoringException(hr, dir);
452 _ndirMonCompletionHandle = new HandleRef(this, _ndirMonCompletionPtr);
453 Interlocked.Increment(ref _activeDirMonCompletions);
458 ~DirMonCompletion() {
462 void IDisposable.Dispose() {
464 System.GC.SuppressFinalize(this);
467 void Dispose(bool disposing) {
468 Debug.Trace("FileChangesMonitor", "DirMonCompletion::Dispose");
469 // this is here because callbacks from native code can cause
470 // this to be invoked from multiple threads concurrently, and
471 // I don't want contention on _ndirMonCompletionHandleLock,
472 // but also need to ensure that DirMonOpen returns and the
473 // _ndirMonCompletionHandle is set before DirMonClose is called.
474 if (Interlocked.Exchange(ref _disposed, 1) == 0) {
476 // Dev10 927846: There is a small window during which the .ctor has
477 // not returned from DirMonOpen yet but because we already started
478 // monitoring, we might receive a change notification which could
479 // potentially Dispose the instance, so we need to block until
480 // DirMonOpen returns and _ndirMonCompletionHandler is set
481 lock(_ndirMonCompletionHandleLock) {
482 // Dev11 - 364642: if Dispose is called while finalizing for AD unload then
483 // the native DirMonCompletion won't be able to call back into the appdomain.
484 // But it does not need to because _rootCallback is already reclaimed as part of AD unload
485 bool fNeedToSendFileActionDispose = !AppDomain.CurrentDomain.IsFinalizingForUnload();
486 HandleRef ndirMonCompletionHandle = _ndirMonCompletionHandle;
487 if (ndirMonCompletionHandle.Handle != IntPtr.Zero) {
488 _ndirMonCompletionHandle = new HandleRef(this, IntPtr.Zero);
489 UnsafeNativeMethods.DirMonClose(ndirMonCompletionHandle, fNeedToSendFileActionDispose);
495 void OnFileChange(FileAction action, string fileName, long ticks) {
496 DateTime utcCompletion;
498 utcCompletion = DateTime.MinValue;
501 utcCompletion = DateTimeUtil.FromFileTimeToUtc(ticks);
505 Debug.Trace("FileChangesMonitorOnFileChange", "Action=" + action + "; Dir=" + _dirMon.Directory + "; fileName=" + Debug.ToStringMaybeNull(fileName) + "; completion=" + Debug.FormatUtcDate(utcCompletion) + ";_ndirMonCompletionPtr=0x" + _ndirMonCompletionPtr.ToString("x"));
509 // The native DirMonCompletion sends FileAction.Dispose
510 // when there are no more outstanding calls on the
511 // delegate. Only then can _rootCallback be freed.
513 if (action == FileAction.Dispose) {
514 if (_rootCallback.IsAllocated) {
515 _rootCallback.Free();
517 Interlocked.Decrement(ref _activeDirMonCompletions);
520 using (new ApplicationImpersonationContext()) {
521 _dirMon.OnFileChange(action, fileName, utcCompletion);
527 internal string DebugDescription(string indent) {
528 int hc = ((Delegate)_rootCallback.Target).Target.GetHashCode();
529 string description = indent + "_ndirMonCompletionPtr=0x" + _ndirMonCompletionPtr.ToString("x") + "; callback=0x" + hc.ToString("x", NumberFormatInfo.InvariantInfo) + "\n";
535 sealed class NotificationQueueItem {
536 internal readonly FileChangeEventHandler Callback;
537 internal readonly string Filename;
538 internal readonly FileAction Action;
540 internal NotificationQueueItem(FileChangeEventHandler callback, FileAction action, string filename) {
548 // Monitor changes in a single directory.
550 sealed class DirectoryMonitor : IDisposable {
552 static Queue s_notificationQueue = new Queue();
553 static WorkItemCallback s_notificationCallback = new WorkItemCallback(FireNotifications);
554 static int s_inNotificationThread;
555 static int s_notificationBufferSizeIncreased = 0;
557 internal readonly string Directory; // directory being monitored
558 Hashtable _fileMons; // fileName -> FileMonitor
559 int _cShortNames; // number of file monitors that are added with their short name
560 FileMonitor _anyFileMon; // special file monitor to watch for any changes in directory
561 bool _watchSubtree; // watch subtree?
562 uint _notifyFilter; // the notify filter for the call to ReadDirectoryChangesW
563 bool _ignoreSubdirChange; // when a subdirectory is deleted or renamed, ignore the notification if we're not monitoring it
564 DirMonCompletion _dirMonCompletion; // dirmon completion
565 bool _isDirMonAppPathInternal; // special dirmon that monitors all files and subdirectories beneath the vroot (enabled via FCNMode registry key)
567 // FcnMode to pass to native code
568 internal int FcnMode {
573 // constructor for special dirmon that monitors all files and subdirectories beneath the vroot (enabled via FCNMode registry key)
574 internal DirectoryMonitor(string appPathInternal, int fcnMode): this(appPathInternal, true, UnsafeNativeMethods.RDCW_FILTER_FILE_AND_DIR_CHANGES, fcnMode) {
575 _isDirMonAppPathInternal = true;
578 internal DirectoryMonitor(string dir, bool watchSubtree, uint notifyFilter, int fcnMode): this(dir, watchSubtree, notifyFilter, false, fcnMode) {
581 internal DirectoryMonitor(string dir, bool watchSubtree, uint notifyFilter, bool ignoreSubdirChange, int fcnMode) {
583 _fileMons = new Hashtable(StringComparer.OrdinalIgnoreCase);
584 _watchSubtree = watchSubtree;
585 _notifyFilter = notifyFilter;
586 _ignoreSubdirChange = ignoreSubdirChange;
590 void IDisposable.Dispose() {
591 if (_dirMonCompletion != null) {
592 ((IDisposable)_dirMonCompletion).Dispose();
593 _dirMonCompletion = null;
597 // Remove aliases to this object in FileChangesMonitor so that
600 if (_anyFileMon != null) {
601 HttpRuntime.FileChangesMonitor.RemoveAliases(_anyFileMon);
605 foreach (DictionaryEntry e in _fileMons) {
606 string key = (string) e.Key;
607 FileMonitor fileMon = (FileMonitor) e.Value;
608 if (fileMon.FileNameLong == key) {
609 HttpRuntime.FileChangesMonitor.RemoveAliases(fileMon);
617 internal bool IsMonitoring() {
618 return GetFileMonitorsCount() > 0;
621 void StartMonitoring() {
622 if (_dirMonCompletion == null) {
623 _dirMonCompletion = new DirMonCompletion(this, Directory, _watchSubtree, _notifyFilter);
627 internal void StopMonitoring() {
629 ((IDisposable)this).Dispose();
633 FileMonitor FindFileMonitor(string file) {
637 fileMon = _anyFileMon;
640 fileMon = (FileMonitor)_fileMons[file];
646 FileMonitor AddFileMonitor(string file) {
649 FindFileData ffd = null;
652 if (String.IsNullOrEmpty(file)) {
653 // add as the <ANY> file monitor
654 fileMon = new FileMonitor(this, null, null, true, null, null);
655 _anyFileMon = fileMon;
658 // Get the long and short name of the file
659 path = Path.Combine(Directory, file);
660 if (_isDirMonAppPathInternal) {
661 hr = FindFileData.FindFile(path, Directory, out ffd);
664 hr = FindFileData.FindFile(path, out ffd);
666 if (hr == HResults.S_OK) {
667 // Unless this is FileChangesMonitor._dirMonAppPathInternal,
668 // don't monitor changes to a directory - this will not pickup changes to files in the directory.
669 if (!_isDirMonAppPathInternal
670 && (ffd.FileAttributesData.FileAttributes & FileAttributes.Directory) != 0) {
671 throw FileChangesMonitor.CreateFileMonitoringException(HResults.E_INVALIDARG, path);
674 byte[] dacl = FileSecurity.GetDacl(path);
675 fileMon = new FileMonitor(this, ffd.FileNameLong, ffd.FileNameShort, true, ffd.FileAttributesData, dacl);
676 _fileMons.Add(ffd.FileNameLong, fileMon);
678 // Update short name aliases to this file
679 UpdateFileNameShort(fileMon, null, ffd.FileNameShort);
681 else if (hr == HResults.E_PATHNOTFOUND || hr == HResults.E_FILENOTFOUND) {
682 // Don't allow possible short file names to be added as non-existant,
683 // because it is impossible to track them if they are indeed a short name since
684 // short file names may change.
690 if (file.IndexOf('~') != -1) {
691 throw FileChangesMonitor.CreateFileMonitoringException(HResults.E_INVALIDARG, path);
694 // Add as non-existent file
695 fileMon = new FileMonitor(this, file, null, false, null, null);
696 _fileMons.Add(file, fileMon);
699 throw FileChangesMonitor.CreateFileMonitoringException(hr, path);
707 // Update short names of a file
709 void UpdateFileNameShort(FileMonitor fileMon, string oldFileNameShort, string newFileNameShort) {
710 if (oldFileNameShort != null) {
711 FileMonitor oldFileMonShort = (FileMonitor)_fileMons[oldFileNameShort];
712 if (oldFileMonShort != null) {
713 // The old filemonitor no longer has this short file name.
714 // Update the monitor and _fileMons
715 if (oldFileMonShort != fileMon) {
716 oldFileMonShort.RemoveFileNameShort();
720 _fileMons.Remove(oldFileNameShort);
725 if (newFileNameShort != null) {
726 // Add the new short file name.
727 _fileMons.Add(newFileNameShort, fileMon);
732 void RemoveFileMonitor(FileMonitor fileMon) {
733 if (fileMon == _anyFileMon) {
737 _fileMons.Remove(fileMon.FileNameLong);
738 if (fileMon.FileNameShort != null) {
739 _fileMons.Remove(fileMon.FileNameShort);
744 HttpRuntime.FileChangesMonitor.RemoveAliases(fileMon);
747 int GetFileMonitorsCount() {
748 int c = _fileMons.Count - _cShortNames;
749 if (_anyFileMon != null) {
756 // The 4.0 CAS changes made the AppDomain homogenous, so we need to assert
757 // FileIOPermission. Currently this is only exposed publicly via CacheDependency, which
758 // already does a PathDiscover check for public callers.
759 [FileIOPermission(SecurityAction.Assert, Unrestricted = true)]
760 internal FileMonitor StartMonitoringFileWithAssert(string file, FileChangeEventHandler callback, string alias) {
761 FileMonitor fileMon = null;
762 bool firstFileMonAdded = false;
765 // Find existing file monitor
766 fileMon = FindFileMonitor(file);
767 if (fileMon == null) {
769 fileMon = AddFileMonitor(file);
770 if (GetFileMonitorsCount() == 1) {
771 firstFileMonAdded = true;
775 // Add callback to the file monitor
776 fileMon.AddTarget(callback, alias, true);
778 // Start directory monitoring when the first file gets added
779 if (firstFileMonAdded) {
788 // Request to stop monitoring a file.
790 internal void StopMonitoringFile(string file, object target) {
795 // Find existing file monitor
796 fileMon = FindFileMonitor(file);
797 if (fileMon != null) {
798 numTargets = fileMon.RemoveTarget(target);
799 if (numTargets == 0) {
800 RemoveFileMonitor(fileMon);
802 // last target for the file monitor gone
803 // -- remove the file monitor
804 if (GetFileMonitorsCount() == 0) {
805 ((IDisposable)this).Dispose();
812 if (fileMon != null) {
813 Debug.Dump("FileChangesMonitor", HttpRuntime.FileChangesMonitor);
819 internal bool GetFileAttributes(string file, out FileAttributesData fad) {
820 FileMonitor fileMon = null;
824 // Find existing file monitor
825 fileMon = FindFileMonitor(file);
826 if (fileMon != null) {
827 // Get the attributes
828 fad = fileMon.Attributes;
837 // Notes about file attributes:
839 // CreationTime is the time a file entry is added to a directory.
840 // If file q1 is copied to q2, q2's creation time is updated if it is new to the directory,
841 // else q2's old time is used.
843 // If a file is deleted, then added, its creation time is preserved from before the delete.
845 // LastWriteTime is the time a file was last written.
846 // If file q1 is copied to q2, q2's lastWrite time is the same as q1.
847 // Note that this implies that the LastWriteTime can be older than the LastCreationTime,
848 // and that a copy of a file can result in the LastWriteTime being earlier than
849 // its previous value.
851 // LastAccessTime is the time a file was last accessed, such as opened or written to.
852 // Note that if the attributes of a file are changed, its LastAccessTime is not necessarily updated.
854 // If the FileSize, CreationTime, or LastWriteTime have changed, then we know that the
855 // file has changed in a significant way, and that the LastAccessTime will be greater than
856 // or equal to that time.
858 // If the FileSize, CreationTime, or LastWriteTime have not changed, then the file's
859 // attributes may have changed without changing the LastAccessTime.
862 // Confirm that the changes occurred after we started monitoring,
863 // to handle the case where:
865 // 1. User creates a file.
866 // 2. User starts to monitor the file.
867 // 3. Change notification is made of the original creation of the file.
869 // Note that we can only approximate when the last change occurred by
870 // examining the LastAccessTime. The LastAccessTime will change if the
871 // contents of a file (but not necessarily its attributes) change.
872 // The drawback to using the LastAccessTime is that it will also be
873 // updated when a file is read.
875 // Note that we cannot make this confirmation when only the file's attributes
876 // or ACLs change, because changes to attributes and ACLs won't change the LastAccessTime.
878 bool IsChangeAfterStartMonitoring(FileAttributesData fad, FileMonitorTarget target, DateTime utcCompletion) {
879 // If the LastAccessTime is more than 60 seconds before we
880 // started monitoring, then the change likely did not update
881 // the LastAccessTime correctly.
882 if (fad.UtcLastAccessTime.AddSeconds(60) < target.UtcStartMonitoring) {
884 Debug.Trace("FileChangesMonitorIsChangeAfterStart", "LastAccessTime is more than 60 seconds before monitoring started.");
889 // Check if the notification of the change came after
890 // we started monitoring.
891 if (utcCompletion > target.UtcStartMonitoring) {
893 Debug.Trace("FileChangesMonitorIsChangeAfterStart", "Notification came after we started monitoring.");
898 // Make sure that the LastAccessTime is valid.
899 // It must be more recent than the LastWriteTime.
900 if (fad.UtcLastAccessTime < fad.UtcLastWriteTime) {
902 Debug.Trace("FileChangesMonitorIsChangeAfterStart", "UtcLastWriteTime is greater then UtcLastAccessTime.");
907 // If the LastAccessTime occurs exactly at midnight,
908 // then the system is FAT32 and LastAccessTime is unusable.
909 if (fad.UtcLastAccessTime.TimeOfDay == TimeSpan.Zero) {
911 Debug.Trace("FileChangesMonitorIsChangeAfterStart", "UtcLastAccessTime is midnight -- FAT32 likely.");
916 // Finally, compare LastAccessTime to the time we started monitoring.
917 // If the time of the last access was before we started monitoring, then
918 // we know a change did not occur to the file contents.
919 if (fad.UtcLastAccessTime >= target.UtcStartMonitoring) {
921 Debug.Trace("FileChangesMonitorIsChangeAfterStart", "UtcLastAccessTime is greater than UtcStartMonitoring.");
927 Debug.Trace("FileChangesMonitorIsChangeAfterStart", "Change is before start of monitoring. Data:\n FileAttributesData: \nUtcCreationTime: "
928 + fad.UtcCreationTime + " UtcLastAccessTime: " + fad.UtcLastAccessTime + " UtcLastWriteTime: " + fad.UtcLastWriteTime + "\n FileMonitorTarget:\n UtcStartMonitoring: "
929 + target.UtcStartMonitoring + "\nUtcCompletion: " + utcCompletion);
934 // If this is a special dirmon that monitors all files and subdirectories
935 // beneath the vroot (enabled via FCNMode registry key), then
936 // we need to special case how we lookup the FileMonitor. For example, nobody has called
937 // StartMonitorFile for specific files in the App_LocalResources directory,
938 // so we need to see if fileName is in App_LocalResources and then get the FileMonitor for
940 private bool GetFileMonitorForSpecialDirectory(string fileName, ref FileMonitor fileMon) {
942 // fileName should not be in short form (8.3 format)...it was converted to long form in
943 // DirMonCompletion::ProcessOneFileNotification
945 // first search for match within s_dirsToMonitor
946 for (int i = 0; i < FileChangesMonitor.s_dirsToMonitor.Length; i++) {
947 if (StringUtil.StringStartsWithIgnoreCase(fileName, FileChangesMonitor.s_dirsToMonitor[i])) {
948 fileMon = (FileMonitor)_fileMons[FileChangesMonitor.s_dirsToMonitor[i]];
949 return fileMon != null;
953 // if we did not find a match in s_dirsToMonitor, look for LocalResourcesDirectoryName anywhere within fileName
954 int indexStart = fileName.IndexOf(HttpRuntime.LocalResourcesDirectoryName, StringComparison.OrdinalIgnoreCase);
955 if (indexStart > -1) {
956 int dirNameLength = indexStart + HttpRuntime.LocalResourcesDirectoryName.Length;
958 // fileName should either end with LocalResourcesDirectoryName or include a trailing slash and more characters
959 if (fileName.Length == dirNameLength || fileName[dirNameLength] == Path.DirectorySeparatorChar) {
960 string dirName = fileName.Substring(0, dirNameLength);
961 fileMon = (FileMonitor)_fileMons[dirName];
962 return fileMon != null;
971 // Delegate callback from native code.
973 internal void OnFileChange(FileAction action, string fileName, DateTime utcCompletion) {
975 // Use try/catch to prevent runtime exceptions from propagating
979 FileMonitor fileMon = null;
980 ArrayList targets = null;
982 FileMonitorTarget target;
985 FileAttributesData fadOld = null;
986 FileAttributesData fadNew = null;
987 byte[] daclOld = null;
988 byte[] daclNew = null;
989 FileAction lastAction = FileAction.Error;
990 DateTime utcLastCompletion = DateTime.MinValue;
991 bool isSpecialDirectoryChange = false;
994 string reasonIgnore = string.Empty;
995 string reasonFire = string.Empty;
998 // We've already stopped monitoring, but a change completion was
999 // posted afterwards. Ignore it.
1000 if (_dirMonCompletion == null) {
1005 if (_fileMons.Count > 0) {
1006 if (action == FileAction.Error || action == FileAction.Overwhelming) {
1007 // Overwhelming change -- notify all file monitors
1008 Debug.Assert(fileName == null, "fileName == null");
1009 Debug.Assert(action != FileAction.Overwhelming, "action != FileAction.Overwhelming");
1011 if (action == FileAction.Overwhelming) {
1012 //increase file notification buffer size, but only once per app instance
1013 if (Interlocked.Increment(ref s_notificationBufferSizeIncreased) == 1) {
1014 UnsafeNativeMethods.GrowFileNotificationBuffer( HttpRuntime.AppDomainAppId, _watchSubtree );
1018 // Get targets for all files
1019 targets = new ArrayList();
1020 foreach (DictionaryEntry d in _fileMons) {
1021 key = (string) d.Key;
1022 fileMon = (FileMonitor) d.Value;
1023 if (fileMon.FileNameLong == key) {
1024 fileMon.ResetCachedAttributes();
1025 fileMon.LastAction = action;
1026 fileMon.UtcLastCompletion = utcCompletion;
1027 col = fileMon.Targets;
1028 targets.AddRange(col);
1035 Debug.Assert((int) action >= 1 && fileName != null && fileName.Length > 0,
1036 "(int) action >= 1 && fileName != null && fileName.Length > 0");
1038 // Find the file monitor
1039 fileMon = (FileMonitor)_fileMons[fileName];
1041 if (_isDirMonAppPathInternal && fileMon == null) {
1042 isSpecialDirectoryChange = GetFileMonitorForSpecialDirectory(fileName, ref fileMon);
1045 if (fileMon != null) {
1047 col = fileMon.Targets;
1048 targets = new ArrayList(col);
1050 fadOld = fileMon.Attributes;
1051 daclOld = fileMon.Dacl;
1052 lastAction = fileMon.LastAction;
1053 utcLastCompletion = fileMon.UtcLastCompletion;
1054 fileMon.LastAction = action;
1055 fileMon.UtcLastCompletion = utcCompletion;
1057 if (action == FileAction.Removed || action == FileAction.RenamedOldName) {
1058 // File not longer exists.
1059 fileMon.MakeExtinct();
1061 else if (fileMon.Exists) {
1062 // We only need to update the attributes if this is
1063 // a different completion, as we retreive the attributes
1064 // after the completion is received.
1065 if (utcLastCompletion != utcCompletion) {
1066 fileMon.UpdateCachedAttributes();
1070 // File now exists - update short name and attributes.
1071 FindFileData ffd = null;
1072 string path = Path.Combine(Directory, fileMon.FileNameLong);
1074 if (_isDirMonAppPathInternal) {
1075 hr = FindFileData.FindFile(path, Directory, out ffd);
1078 hr = FindFileData.FindFile(path, out ffd);
1080 if (hr == HResults.S_OK) {
1081 Debug.Assert(StringUtil.EqualsIgnoreCase(fileMon.FileNameLong, ffd.FileNameLong),
1082 "StringUtil.EqualsIgnoreCase(fileMon.FileNameLong, ffd.FileNameLong)");
1084 string oldFileNameShort = fileMon.FileNameShort;
1085 byte[] dacl = FileSecurity.GetDacl(path);
1086 fileMon.MakeExist(ffd, dacl);
1087 UpdateFileNameShort(fileMon, oldFileNameShort, ffd.FileNameShort);
1091 fadNew = fileMon.Attributes;
1092 daclNew = fileMon.Dacl;
1097 // Notify the delegate waiting for any changes
1098 if (_anyFileMon != null) {
1099 col = _anyFileMon.Targets;
1100 if (targets != null) {
1101 targets.AddRange(col);
1104 targets = new ArrayList(col);
1108 if (action == FileAction.Error) {
1110 ((IDisposable)this).Dispose();
1114 // Ignore Modified action for directories (VSWhidbey 295597)
1115 bool ignoreThisChangeNotification = false;
1117 if (!isSpecialDirectoryChange && fileName != null && action == FileAction.Modified) {
1118 // check if the file is a directory (reuse attributes if already obtained)
1119 FileAttributesData fad = fadNew;
1122 string path = Path.Combine(Directory, fileName);
1123 FileAttributesData.GetFileAttributes(path, out fad);
1126 if (fad != null && ((fad.FileAttributes & FileAttributes.Directory) != 0)) {
1127 // ignore if directory
1128 ignoreThisChangeNotification = true;
1132 // Dev10 440497: Don't unload AppDomain when a folder is deleted or renamed, unless we're monitoring files in it
1133 if (_ignoreSubdirChange && (action == FileAction.Removed || action == FileAction.RenamedOldName) && fileName != null) {
1134 string fullPath = Path.Combine(Directory, fileName);
1135 if (!HttpRuntime.FileChangesMonitor.IsDirNameMonitored(fullPath, fileName)) {
1137 Debug.Trace("FileChangesMonitorIgnoreSubdirChange",
1138 "*** Ignoring SubDirChange " + DateTime.Now.ToString("hh:mm:ss.fff", CultureInfo.InvariantCulture)
1139 + ": fullPath=" + fullPath + ", action=" + action.ToString());
1141 ignoreThisChangeNotification = true;
1145 Debug.Trace("FileChangesMonitorIgnoreSubdirChange",
1146 "*** SubDirChange " + DateTime.Now.ToString("hh:mm:ss.fff", CultureInfo.InvariantCulture)
1147 + ": fullPath=" + fullPath + ", action=" + action.ToString());
1153 if (targets != null && !ignoreThisChangeNotification) {
1154 Debug.Dump("FileChangesMonitor", HttpRuntime.FileChangesMonitor);
1156 lock (s_notificationQueue.SyncRoot) {
1157 for (i = 0, n = targets.Count; i < n; i++) {
1159 // Determine whether the change is significant, and if so, add it
1160 // to the notification queue.
1162 // - A change is significant if action is other than Added or Modified
1163 // - A change is significant if the action is Added and it occurred after
1164 // the target started monitoring.
1165 // - If the action is Modified:
1166 // -- A change is significant if the file contents were modified
1167 // and it occurred after the target started monitoring.
1168 // -- A change is significant if the DACL changed. We cannot check if
1169 // the change was made after the target started monitoring in this case,
1170 // as the LastAccess time may not be updated.
1172 target = (FileMonitorTarget)targets[i];
1173 bool isSignificantChange;
1174 if ((action != FileAction.Added && action != FileAction.Modified) || fadNew == null) {
1176 // Any change other than Added or Modified is significant.
1177 // If we have no attributes to examine, the change is significant.
1178 isSignificantChange = true;
1181 reasonFire = "(action != FileAction.Added && action != FileAction.Modified) || fadNew == null";
1184 else if (action == FileAction.Added) {
1185 // Added actions are significant if they occur after we started monitoring.
1186 isSignificantChange = IsChangeAfterStartMonitoring(fadNew, target, utcCompletion);
1189 reasonIgnore = "change occurred before started monitoring";
1190 reasonFire = "file added after start of monitoring";
1195 Debug.Assert(action == FileAction.Modified, "action == FileAction.Modified");
1196 if (utcCompletion == utcLastCompletion) {
1197 // File attributes and ACLs will not have changed if the completion is the same
1198 // as the last, since we get the attributes after all changes in the completion
1199 // have occurred. Therefore if the previous change was Modified, there
1200 // is no change that we can detect.
1202 // Notepad fires such spurious notifications when a file is saved.
1204 isSignificantChange = (lastAction != FileAction.Modified);
1207 reasonIgnore = "spurious FileAction.Modified";
1208 reasonFire = "spurious completion where action != modified";
1212 else if (fadOld == null) {
1213 // There were no attributes before this notification,
1214 // so assume the change is significant. We cannot check for
1215 // whether the change was after the start of monitoring,
1216 // because we don't know if the content changed, or just
1217 // DACL, in which case the LastAccessTime will not be updated.
1218 isSignificantChange = true;
1221 reasonFire = "no attributes before this notification";
1224 else if (daclOld == null || daclOld != daclNew) {
1225 // The change is significant if the DACL changed.
1226 // We cannot check if the change is after the start of monitoring,
1227 // as a change in the DACL does not necessarily update the
1228 // LastAccessTime of a file.
1229 // If we cannot access the DACL, then we must assume
1230 // that it is what has changed.
1231 isSignificantChange = true;
1234 if (daclOld == null) {
1235 reasonFire = "unable to access ACL";
1238 reasonFire = "ACL changed";
1244 // The file content was modified. We cannot guarantee that the
1245 // LastWriteTime or FileSize changed when the file changed, as
1246 // copying a file preserves the LastWriteTime, and the "touch"
1247 // command can reset the LastWriteTime of many files to the same
1250 // If the file content is modified, we can determine if the file
1251 // was not changed after the start of monitoring by looking at
1252 // the LastAccess time.
1253 isSignificantChange = IsChangeAfterStartMonitoring(fadNew, target, utcCompletion);
1256 reasonIgnore = "change occurred before started monitoring";
1257 reasonFire = "file content modified after start of monitoring";
1263 if (isSignificantChange) {
1265 Debug.Trace("FileChangesMonitorCallback", "Firing change event, reason=" + reasonFire +
1266 "\n\tArgs: Action=" + action + "; Completion=" + Debug.FormatUtcDate(utcCompletion) + "; fileName=" + fileName +
1267 "\n\t LastAction=" + lastAction + "; LastCompletion=" + Debug.FormatUtcDate(utcLastCompletion) +
1268 "\nfadOld=" + ((fadOld != null) ? fadOld.DebugDescription("\t") : "<null>") +
1269 "\nfileMon=" + ((fileMon != null) ? fileMon.DebugDescription("\t") : "<null>") +
1270 "\n" + target.DebugDescription("\t"));
1273 s_notificationQueue.Enqueue(new NotificationQueueItem(target.Callback, action, target.Alias));
1277 Debug.Trace("FileChangesMonitorCallback", "Ignoring change event, reason=" + reasonIgnore +
1278 "\n\tArgs: Action=" + action + "; Completion=" + Debug.FormatUtcDate(utcCompletion) + "; fileName=" + fileName +
1279 "\n\t LastAction=" + lastAction + "; LastCompletion=" + Debug.FormatUtcDate(utcLastCompletion) +
1280 "\nfadOld=" + ((fadOld != null) ? fadOld.DebugDescription("\t") : "<null>") +
1281 "\nfileMon=" + ((fileMon != null) ? fileMon.DebugDescription("\t") : "<null>") +
1282 "\n" + target.DebugDescription("\t"));
1289 if (s_notificationQueue.Count > 0 && s_inNotificationThread == 0 && Interlocked.Exchange(ref s_inNotificationThread, 1) == 0) {
1290 WorkItem.PostInternal(s_notificationCallback);
1294 catch (Exception ex) {
1295 Debug.Trace(Debug.TAG_INTERNAL,
1296 "Exception thrown processing file change notification" +
1297 " action=" + action.ToString() +
1298 " fileName" + fileName);
1300 Debug.TraceException(Debug.TAG_INTERNAL, ex);
1304 // Fire notifications on a separate thread from that which received the notifications,
1305 // so that we don't block notification collection.
1306 static void FireNotifications() {
1308 // Outer loop: test whether we need to fire notifications and grab the lock
1310 // Inner loop: fire notifications until the queue is emptied
1312 // Remove an item from the queue.
1313 NotificationQueueItem nqi = null;
1314 lock (s_notificationQueue.SyncRoot) {
1315 if (s_notificationQueue.Count > 0) {
1316 nqi = (NotificationQueueItem) s_notificationQueue.Dequeue();
1324 Debug.Trace("FileChangesMonitorFireNotification", "Firing change event" +
1325 "\n\tArgs: Action=" + nqi.Action + "; fileName=" + nqi.Filename + "; Target=" + nqi.Callback.Target + "(HC=" + nqi.Callback.Target.GetHashCode().ToString("x", NumberFormatInfo.InvariantInfo) + ")");
1327 // Call the callback
1328 nqi.Callback(null, new FileChangeEvent(nqi.Action, nqi.Filename));
1330 catch (Exception ex) {
1331 Debug.Trace(Debug.TAG_INTERNAL,
1332 "Exception thrown in file change callback" +
1333 " action=" + nqi.Action.ToString() +
1334 " fileName" + nqi.Filename);
1336 Debug.TraceException(Debug.TAG_INTERNAL, ex);
1341 Interlocked.Exchange(ref s_inNotificationThread, 0);
1343 // We need to test again to avoid ---- where a thread that receives notifications adds to the
1344 // queue, but does not spawn a thread because s_inNotificationThread = 1
1345 if (s_notificationQueue.Count == 0 || Interlocked.Exchange(ref s_inNotificationThread, 1) != 0)
1350 Interlocked.Exchange(ref s_inNotificationThread, 0);
1355 internal string DebugDescription(string indent) {
1356 StringBuilder sb = new StringBuilder(200);
1357 string i2 = indent + " ";
1358 DictionaryEntryCaseInsensitiveComparer decomparer = new DictionaryEntryCaseInsensitiveComparer();
1361 DictionaryEntry[] fileEntries = new DictionaryEntry[_fileMons.Count];
1362 _fileMons.CopyTo(fileEntries, 0);
1363 Array.Sort(fileEntries, decomparer);
1365 sb.Append(indent + "System.Web.DirectoryMonitor: " + Directory + "\n");
1366 if (_dirMonCompletion != null) {
1367 sb.Append(i2 + "_dirMonCompletion " + _dirMonCompletion.DebugDescription(String.Empty));
1370 sb.Append(i2 + "_dirMonCompletion = <null>\n");
1373 sb.Append(i2 + GetFileMonitorsCount() + " file monitors...\n");
1374 if (_anyFileMon != null) {
1375 sb.Append(_anyFileMon.DebugDescription(i2));
1378 foreach (DictionaryEntry d in fileEntries) {
1379 FileMonitor fileMon = (FileMonitor)d.Value;
1380 if (fileMon.FileNameShort == (string)d.Key)
1383 sb.Append(fileMon.DebugDescription(i2));
1387 return sb.ToString();
1391 #endif // !FEATURE_PAL
1394 // Manager for directory monitors.
1395 // Provides file change notification services in ASP.NET
1397 sealed class FileChangesMonitor {
1398 #if !FEATURE_PAL // FEATURE_PAL does not enable file change notification
1399 internal static string[] s_dirsToMonitor = new string[] {
1400 HttpRuntime.BinDirectoryName,
1401 HttpRuntime.ResourcesDirectoryName,
1402 HttpRuntime.CodeDirectoryName,
1403 HttpRuntime.WebRefDirectoryName,
1404 HttpRuntime.BrowsersDirectoryName
1407 internal const int MAX_PATH = 260;
1409 #pragma warning disable 0649
1410 ReadWriteSpinLock _lockDispose; // spinlock for coordinating dispose
1411 #pragma warning restore 0649
1413 bool _disposed; // have we disposed?
1414 Hashtable _aliases; // alias -> FileMonitor
1415 Hashtable _dirs; // dir -> DirectoryMonitor
1416 DirectoryMonitor _dirMonSubdirs; // subdirs monitor for renames
1417 Hashtable _subDirDirMons; // Hashtable of DirectoryMonitor used in ListenToSubdirectoryChanges
1418 ArrayList _dirMonSpecialDirs; // top level dirs we monitor
1419 FileChangeEventHandler _callbackRenameOrCriticaldirChange; // event handler for renames and bindir
1420 int _activeCallbackCount; // number of callbacks currently executing
1421 DirectoryMonitor _dirMonAppPathInternal; // watches all files and subdirectories (at any level) beneath HttpRuntime.AppDomainAppPathInternal
1422 String _appPathInternal; // HttpRuntime.AppDomainAppPathInternal
1423 int _FCNMode; // from registry, controls how we monitor directories
1426 internal static bool s_enableRemoveTargetAssert;
1429 // Dev10 927283: We were appending to HttpRuntime._shutdownMessage in DirectoryMonitor.OnFileChange when
1430 // we received overwhelming changes and errors, but not all overwhelming file change notifications result
1431 // in a shutdown. The fix is to only append to _shutdownMessage when the domain is being shutdown.
1432 internal static string GenerateErrorMessage(FileAction action, String fileName = null) {
1433 string message = null;
1434 if (action == FileAction.Overwhelming) {
1435 message = "Overwhelming Change Notification in ";
1437 else if (action == FileAction.Error) {
1438 message = "File Change Notification Error in ";
1443 return (fileName != null) ? message + Path.GetDirectoryName(fileName) : message;
1446 internal static HttpException CreateFileMonitoringException(int hr, string path) {
1448 bool logEvent = false;
1451 case HResults.E_FILENOTFOUND:
1452 case HResults.E_PATHNOTFOUND:
1453 message = SR.Directory_does_not_exist_for_monitoring;
1456 case HResults.E_ACCESSDENIED:
1457 message = SR.Access_denied_for_monitoring;
1461 case HResults.E_INVALIDARG:
1462 message = SR.Invalid_file_name_for_monitoring;
1465 case HResults.ERROR_TOO_MANY_CMDS:
1466 message = SR.NetBios_command_limit_reached;
1471 message = SR.Failed_to_start_monitoring;
1477 // Need to raise an eventlog too.
1478 UnsafeNativeMethods.RaiseFileMonitoringEventlogEvent(
1479 SR.GetString(message, HttpRuntime.GetSafePath(path)) +
1481 SR.GetString(SR.App_Virtual_Path, HttpRuntime.AppDomainAppVirtualPath),
1482 path, HttpRuntime.AppDomainAppVirtualPath, hr);
1485 return new HttpException(SR.GetString(message, HttpRuntime.GetSafePath(path)), hr);
1488 internal static string GetFullPath(string alias) {
1489 // Assert PathDiscovery before call to Path.GetFullPath
1491 new FileIOPermission(FileIOPermissionAccess.PathDiscovery, alias).Assert();
1494 throw CreateFileMonitoringException(HResults.E_INVALIDARG, alias);
1497 string path = Path.GetFullPath(alias);
1498 path = FileUtil.RemoveTrailingDirectoryBackSlash(path);
1503 private bool IsBeneathAppPathInternal(string fullPathName) {
1504 if (_appPathInternal != null
1505 && fullPathName.Length > _appPathInternal.Length+1
1506 && fullPathName.IndexOf(_appPathInternal, StringComparison.OrdinalIgnoreCase) > -1
1507 && fullPathName[_appPathInternal.Length] == Path.DirectorySeparatorChar) {
1513 private bool IsFCNDisabled { get { return _FCNMode == 1; } }
1515 internal FileChangesMonitor(FcnMode mode) {
1516 // Possible values for DWORD FCNMode:
1517 // does not exist == default behavior (create DirectoryMonitor for each subdir)
1518 // 0 or >2 == default behavior (create DirectoryMonitor for each subdir)
1519 // 1 == disable File Change Notifications (FCN)
1520 // 2 == create 1 DirectoryMonitor for AppPathInternal and watch subtrees
1522 case FcnMode.NotSet:
1523 // If the mode is not set, we use the registry key's value
1524 UnsafeNativeMethods.GetDirMonConfiguration(out _FCNMode);
1526 case FcnMode.Disabled:
1529 case FcnMode.Single:
1532 case FcnMode.Default:
1538 if (IsFCNDisabled) {
1542 _aliases = Hashtable.Synchronized(new Hashtable(StringComparer.OrdinalIgnoreCase));
1543 _dirs = new Hashtable(StringComparer.OrdinalIgnoreCase);
1544 _subDirDirMons = new Hashtable(StringComparer.OrdinalIgnoreCase);
1546 if (_FCNMode == 2 && HttpRuntime.AppDomainAppPathInternal != null) {
1547 _appPathInternal = GetFullPath(HttpRuntime.AppDomainAppPathInternal);
1548 _dirMonAppPathInternal = new DirectoryMonitor(_appPathInternal, _FCNMode);
1552 if ((int)Misc.GetAspNetRegValue(null /*subKey*/, "FCMRemoveTargetAssert", 0) > 0) {
1553 s_enableRemoveTargetAssert = true;
1559 internal bool IsDirNameMonitored(string fullPath, string dirName) {
1560 // is it one of the not-so-special directories we're monitoring?
1561 if (_dirs.ContainsKey(fullPath)) {
1564 // is it one of the special directories (bin, App_Code, etc) or a subfolder?
1565 foreach (string specialDirName in s_dirsToMonitor) {
1566 if (StringUtil.StringStartsWithIgnoreCase(dirName, specialDirName)) {
1567 // a special directory?
1568 if (dirName.Length == specialDirName.Length) {
1572 else if (dirName.Length > specialDirName.Length && dirName[specialDirName.Length] == Path.DirectorySeparatorChar) {
1578 if (dirName.IndexOf(HttpRuntime.LocalResourcesDirectoryName, StringComparison.OrdinalIgnoreCase) > -1) {
1581 // we're not monitoring it
1586 // Find the directory monitor. If not found, maybe add it.
1587 // If the directory is not actively monitoring, ensure that
1588 // it still represents an accessible directory.
1590 DirectoryMonitor FindDirectoryMonitor(string dir, bool addIfNotFound, bool throwOnError) {
1591 DirectoryMonitor dirMon;
1592 FileAttributesData fad = null;
1595 dirMon = (DirectoryMonitor)_dirs[dir];
1596 if (dirMon != null) {
1597 if (!dirMon.IsMonitoring()) {
1598 hr = FileAttributesData.GetFileAttributes(dir, out fad);
1599 if (hr != HResults.S_OK || (fad.FileAttributes & FileAttributes.Directory) == 0) {
1605 if (dirMon != null || !addIfNotFound) {
1609 lock (_dirs.SyncRoot) {
1610 // Check again, this time under synchronization.
1611 dirMon = (DirectoryMonitor)_dirs[dir];
1612 if (dirMon != null) {
1613 if (!dirMon.IsMonitoring()) {
1614 // Fail if it's not a directory or inaccessible.
1615 hr = FileAttributesData.GetFileAttributes(dir, out fad);
1616 if (hr == HResults.S_OK && (fad.FileAttributes & FileAttributes.Directory) == 0) {
1617 // Fail if it's not a directory.
1618 hr = HResults.E_INVALIDARG;
1621 if (hr != HResults.S_OK) {
1622 // Not accessible or a dir, so stop monitoring and remove.
1624 dirMon.StopMonitoring();
1625 if (addIfNotFound && throwOnError) {
1626 throw FileChangesMonitor.CreateFileMonitoringException(hr, dir);
1633 else if (addIfNotFound) {
1634 // Fail if it's not a directory or inaccessible.
1635 hr = FileAttributesData.GetFileAttributes(dir, out fad);
1636 if (hr == HResults.S_OK && (fad.FileAttributes & FileAttributes.Directory) == 0) {
1637 hr = HResults.E_INVALIDARG;
1640 if (hr == HResults.S_OK) {
1641 // Add a new directory monitor.
1642 dirMon = new DirectoryMonitor(dir, false, UnsafeNativeMethods.RDCW_FILTER_FILE_AND_DIR_CHANGES, _FCNMode);
1643 _dirs.Add(dir, dirMon);
1645 else if (throwOnError) {
1646 throw FileChangesMonitor.CreateFileMonitoringException(hr, dir);
1654 // Remove the aliases of a file monitor.
1655 internal void RemoveAliases(FileMonitor fileMon) {
1656 if (IsFCNDisabled) {
1660 foreach (DictionaryEntry entry in fileMon.Aliases) {
1661 if (_aliases[entry.Key] == fileMon) {
1662 _aliases.Remove(entry.Key);
1668 // Request to monitor a file, which may or may not exist.
1670 internal DateTime StartMonitoringFile(string alias, FileChangeEventHandler callback) {
1671 Debug.Trace("FileChangesMonitor", "StartMonitoringFile\n" + "\tArgs: File=" + alias + "; Callback=" + callback.Target + "(HC=" + callback.Target.GetHashCode().ToString("x", NumberFormatInfo.InvariantInfo) + ")");
1673 FileMonitor fileMon;
1674 DirectoryMonitor dirMon;
1675 string fullPathName, dir, file;
1676 bool addAlias = false;
1678 if (alias == null) {
1679 throw CreateFileMonitoringException(HResults.E_INVALIDARG, alias);
1682 if (IsFCNDisabled) {
1683 fullPathName = GetFullPath(alias);
1684 FindFileData ffd = null;
1685 int hr = FindFileData.FindFile(fullPathName, out ffd);
1686 if (hr == HResults.S_OK) {
1687 return ffd.FileAttributesData.UtcLastWriteTime;
1690 return DateTime.MinValue;
1694 using (new ApplicationImpersonationContext()) {
1695 _lockDispose.AcquireReaderLock();
1697 // Don't start monitoring if disposed.
1699 return DateTime.MinValue;
1702 fileMon = (FileMonitor)_aliases[alias];
1703 if (fileMon != null) {
1704 // Used the cached directory monitor and file name.
1705 dirMon = fileMon.DirectoryMonitor;
1706 file = fileMon.FileNameLong;
1711 if (alias.Length == 0 || !UrlPath.IsAbsolutePhysicalPath(alias)) {
1712 throw CreateFileMonitoringException(HResults.E_INVALIDARG, alias);
1716 // Get the directory and file name, and lookup
1717 // the directory monitor.
1719 fullPathName = GetFullPath(alias);
1721 if (IsBeneathAppPathInternal(fullPathName)) {
1722 dirMon = _dirMonAppPathInternal;
1723 file = fullPathName.Substring(_appPathInternal.Length+1);
1726 dir = UrlPath.GetDirectoryOrRootName(fullPathName);
1727 file = Path.GetFileName(fullPathName);
1728 if (String.IsNullOrEmpty(file)) {
1730 throw CreateFileMonitoringException(HResults.E_INVALIDARG, alias);
1732 dirMon = FindDirectoryMonitor(dir, true /*addIfNotFound*/, true /*throwOnError*/);
1736 fileMon = dirMon.StartMonitoringFileWithAssert(file, callback, alias);
1738 _aliases[alias] = fileMon;
1742 _lockDispose.ReleaseReaderLock();
1745 FileAttributesData fad;
1746 fileMon.DirectoryMonitor.GetFileAttributes(file, out fad);
1748 Debug.Dump("FileChangesMonitor", this);
1751 return fad.UtcLastWriteTime;
1754 return DateTime.MinValue;
1760 // Request to monitor a path, which may be file, directory, or non-existent
1763 internal DateTime StartMonitoringPath(string alias, FileChangeEventHandler callback, out FileAttributesData fad) {
1764 Debug.Trace("FileChangesMonitor", "StartMonitoringPath\n" + "\tArgs: File=" + alias + "; Callback=" + callback.Target + "(HC=" + callback.Target.GetHashCode().ToString("x", NumberFormatInfo.InvariantInfo) + ")");
1766 FileMonitor fileMon = null;
1767 DirectoryMonitor dirMon = null;
1768 string fullPathName, dir, file = null;
1769 bool addAlias = false;
1773 if (alias == null) {
1774 throw new HttpException(SR.GetString(SR.Invalid_file_name_for_monitoring, String.Empty));
1777 if (IsFCNDisabled) {
1778 fullPathName = GetFullPath(alias);
1779 FindFileData ffd = null;
1780 int hr = FindFileData.FindFile(fullPathName, out ffd);
1781 if (hr == HResults.S_OK) {
1782 fad = ffd.FileAttributesData;
1783 return ffd.FileAttributesData.UtcLastWriteTime;
1786 return DateTime.MinValue;
1790 using (new ApplicationImpersonationContext()) {
1791 _lockDispose.AcquireReaderLock();
1794 return DateTime.MinValue;
1797 // do/while loop once to make breaking out easy
1799 fileMon = (FileMonitor)_aliases[alias];
1800 if (fileMon != null) {
1801 // Used the cached directory monitor and file name.
1802 file = fileMon.FileNameLong;
1803 fileMon = fileMon.DirectoryMonitor.StartMonitoringFileWithAssert(file, callback, alias);
1809 if (alias.Length == 0 || !UrlPath.IsAbsolutePhysicalPath(alias)) {
1810 throw new HttpException(SR.GetString(SR.Invalid_file_name_for_monitoring, HttpRuntime.GetSafePath(alias)));
1813 fullPathName = GetFullPath(alias);
1815 // see if the path is beneath HttpRuntime.AppDomainAppPathInternal
1816 if (IsBeneathAppPathInternal(fullPathName)) {
1817 dirMon = _dirMonAppPathInternal;
1818 file = fullPathName.Substring(_appPathInternal.Length+1);
1819 fileMon = dirMon.StartMonitoringFileWithAssert(file, callback, alias);
1823 // try treating the path as a directory
1824 dirMon = FindDirectoryMonitor(fullPathName, false, false);
1825 if (dirMon != null) {
1826 fileMon = dirMon.StartMonitoringFileWithAssert(null, callback, alias);
1830 // try treaing the path as a file
1831 dir = UrlPath.GetDirectoryOrRootName(fullPathName);
1832 file = Path.GetFileName(fullPathName);
1833 if (!String.IsNullOrEmpty(file)) {
1834 dirMon = FindDirectoryMonitor(dir, false, false);
1835 if (dirMon != null) {
1836 // try to add it - a file is the common case,
1837 // and we avoid hitting the disk twice
1839 fileMon = dirMon.StartMonitoringFileWithAssert(file, callback, alias);
1844 if (fileMon != null) {
1850 // We aren't monitoring this path or its parent directory yet.
1851 // Hit the disk to determine if it's a directory or file.
1852 dirMon = FindDirectoryMonitor(fullPathName, true, false);
1853 if (dirMon != null) {
1854 // It's a directory, so monitor all changes in it
1858 // It's not a directory, so treat as file
1859 if (String.IsNullOrEmpty(file)) {
1860 throw CreateFileMonitoringException(HResults.E_INVALIDARG, alias);
1863 dirMon = FindDirectoryMonitor(dir, true, true);
1866 fileMon = dirMon.StartMonitoringFileWithAssert(file, callback, alias);
1869 if (!fileMon.IsDirectory) {
1870 fileMon.DirectoryMonitor.GetFileAttributes(file, out fad);
1874 _aliases[alias] = fileMon;
1878 _lockDispose.ReleaseReaderLock();
1881 Debug.Dump("FileChangesMonitor", this);
1884 return fad.UtcLastWriteTime;
1887 return DateTime.MinValue;
1893 // Request to monitor the bin directory and directory renames anywhere under app
1896 internal void StartMonitoringDirectoryRenamesAndBinDirectory(string dir, FileChangeEventHandler callback) {
1897 Debug.Trace("FileChangesMonitor", "StartMonitoringDirectoryRenamesAndBinDirectory\n" + "\tArgs: File=" + dir + "; Callback=" + callback.Target + "(HC=" + callback.Target.GetHashCode().ToString("x", NumberFormatInfo.InvariantInfo) + ")");
1899 if (String.IsNullOrEmpty(dir)) {
1900 throw new HttpException(SR.GetString(SR.Invalid_file_name_for_monitoring, String.Empty));
1903 if (IsFCNDisabled) {
1908 Debug.Assert(_dirs.Count == 0, "This function must be called before monitoring other directories, otherwise monitoring of UNC directories will be unreliable on Windows2000 Server.");
1910 using (new ApplicationImpersonationContext()) {
1911 _lockDispose.AcquireReaderLock();
1917 _callbackRenameOrCriticaldirChange = callback;
1919 string dirRoot = GetFullPath(dir);
1921 // Monitor bin directory and app directory (for renames only) separately
1922 // to avoid overwhelming changes when the user writes to a subdirectory
1923 // of the app directory.
1925 _dirMonSubdirs = new DirectoryMonitor(dirRoot, true, UnsafeNativeMethods.RDCW_FILTER_DIR_RENAMES, true, _FCNMode);
1927 _dirMonSubdirs.StartMonitoringFileWithAssert(null, new FileChangeEventHandler(this.OnSubdirChange), dirRoot);
1930 ((IDisposable)_dirMonSubdirs).Dispose();
1931 _dirMonSubdirs = null;
1935 _dirMonSpecialDirs = new ArrayList();
1936 for (int i=0; i<s_dirsToMonitor.Length; i++) {
1937 _dirMonSpecialDirs.Add(ListenToSubdirectoryChanges(dirRoot, s_dirsToMonitor[i]));
1941 _lockDispose.ReleaseReaderLock();
1947 // Monitor a directory that causes an appdomain shutdown when it changes
1949 internal void StartListeningToLocalResourcesDirectory(VirtualPath virtualDir) {
1950 Debug.Trace("FileChangesMonitor", "StartListeningToVirtualSubdirectory\n" + "\tArgs: virtualDir=" + virtualDir);
1952 if (IsFCNDisabled) {
1956 // In some situation (not well understood yet), we get here with either
1957 // _callbackRenameOrCriticaldirChange or _dirMonSpecialDirs being null (VSWhidbey #215040).
1958 // When that happens, just return.
1959 //Debug.Assert(_callbackRenameOrCriticaldirChange != null);
1960 //Debug.Assert(_dirMonSpecialDirs != null);
1961 if (_callbackRenameOrCriticaldirChange == null || _dirMonSpecialDirs == null)
1964 using (new ApplicationImpersonationContext()) {
1965 _lockDispose.AcquireReaderLock();
1971 // Get the physical path, and split it into the parent dir and the dir name
1972 string dir = virtualDir.MapPath();
1973 dir = FileUtil.RemoveTrailingDirectoryBackSlash(dir);
1974 string name = Path.GetFileName(dir);
1975 dir = Path.GetDirectoryName(dir);
1977 // If the physical parent directory doesn't exist, don't do anything.
1978 // This could happen when using a non-file system based VirtualPathProvider
1979 if (!Directory.Exists(dir))
1982 _dirMonSpecialDirs.Add(ListenToSubdirectoryChanges(dir, name));
1985 _lockDispose.ReleaseReaderLock();
1990 DirectoryMonitor ListenToSubdirectoryChanges(string dirRoot, string dirToListenTo) {
1992 string dirRootSubDir;
1993 DirectoryMonitor dirMonSubDir;
1995 if (StringUtil.StringEndsWith(dirRoot, '\\')) {
1996 dirRootSubDir = dirRoot + dirToListenTo;
1999 dirRootSubDir = dirRoot + "\\" + dirToListenTo;
2002 if (IsBeneathAppPathInternal(dirRootSubDir)) {
2003 dirMonSubDir = _dirMonAppPathInternal;
2005 dirToListenTo = dirRootSubDir.Substring(_appPathInternal.Length+1);
2006 Debug.Trace("ListenToSubDir", dirRoot + " " + dirToListenTo);
2007 dirMonSubDir.StartMonitoringFileWithAssert(dirToListenTo, new FileChangeEventHandler(this.OnCriticaldirChange), dirRootSubDir);
2009 else if (Directory.Exists(dirRootSubDir)) {
2010 dirMonSubDir = new DirectoryMonitor(dirRootSubDir, true, UnsafeNativeMethods.RDCW_FILTER_FILE_CHANGES, _FCNMode);
2012 dirMonSubDir.StartMonitoringFileWithAssert(null, new FileChangeEventHandler(this.OnCriticaldirChange), dirRootSubDir);
2015 ((IDisposable)dirMonSubDir).Dispose();
2016 dirMonSubDir = null;
2021 dirMonSubDir = (DirectoryMonitor)_subDirDirMons[dirRoot];
2022 if (dirMonSubDir == null) {
2023 dirMonSubDir = new DirectoryMonitor(dirRoot, false, UnsafeNativeMethods.RDCW_FILTER_FILE_AND_DIR_CHANGES, _FCNMode);
2024 _subDirDirMons[dirRoot] = dirMonSubDir;
2028 dirMonSubDir.StartMonitoringFileWithAssert(dirToListenTo, new FileChangeEventHandler(this.OnCriticaldirChange), dirRootSubDir);
2031 ((IDisposable)dirMonSubDir).Dispose();
2032 dirMonSubDir = null;
2037 return dirMonSubDir;
2040 void OnSubdirChange(Object sender, FileChangeEvent e) {
2042 Interlocked.Increment(ref _activeCallbackCount);
2048 Debug.Trace("FileChangesMonitor", "OnSubdirChange\n" + "\tArgs: Action=" + e.Action + "; fileName=" + e.FileName);
2049 FileChangeEventHandler handler = _callbackRenameOrCriticaldirChange;
2050 if ( handler != null &&
2051 (e.Action == FileAction.Error || e.Action == FileAction.Overwhelming || e.Action == FileAction.RenamedOldName || e.Action == FileAction.Removed)) {
2052 Debug.Trace("FileChangesMonitor", "Firing subdir change event\n" + "\tArgs: Action=" + e.Action + "; fileName=" + e.FileName + "; Target=" + handler.Target + "(HC=" + handler.Target.GetHashCode().ToString("x", NumberFormatInfo.InvariantInfo) + ")");
2054 HttpRuntime.SetShutdownMessage(
2055 SR.GetString(SR.Directory_rename_notification, e.FileName));
2061 Interlocked.Decrement(ref _activeCallbackCount);
2065 void OnCriticaldirChange(Object sender, FileChangeEvent e) {
2067 Interlocked.Increment(ref _activeCallbackCount);
2073 Debug.Trace("FileChangesMonitor", "OnCriticaldirChange\n" + "\tArgs: Action=" + e.Action + "; fileName=" + e.FileName);
2074 HttpRuntime.SetShutdownMessage(SR.GetString(SR.Change_notification_critical_dir));
2075 FileChangeEventHandler handler = _callbackRenameOrCriticaldirChange;
2076 if (handler != null) {
2081 Interlocked.Decrement(ref _activeCallbackCount);
2086 // Request to stop monitoring a file.
2088 internal void StopMonitoringFile(string alias, object target) {
2089 Debug.Trace("FileChangesMonitor", "StopMonitoringFile\n" + "File=" + alias + "; Callback=" + target);
2091 if (IsFCNDisabled) {
2095 FileMonitor fileMon;
2096 DirectoryMonitor dirMon = null;
2097 string fullPathName, file = null, dir;
2099 if (alias == null) {
2100 throw new HttpException(SR.GetString(SR.Invalid_file_name_for_monitoring, String.Empty));
2103 using (new ApplicationImpersonationContext()) {
2104 _lockDispose.AcquireReaderLock();
2110 fileMon = (FileMonitor)_aliases[alias];
2111 if (fileMon != null && !fileMon.IsDirectory) {
2112 // Used the cached directory monitor and file name
2113 dirMon = fileMon.DirectoryMonitor;
2114 file = fileMon.FileNameLong;
2117 if (alias.Length == 0 || !UrlPath.IsAbsolutePhysicalPath(alias)) {
2118 throw new HttpException(SR.GetString(SR.Invalid_file_name_for_monitoring, HttpRuntime.GetSafePath(alias)));
2121 // Lookup the directory monitor
2122 fullPathName = GetFullPath(alias);
2123 dir = UrlPath.GetDirectoryOrRootName(fullPathName);
2124 file = Path.GetFileName(fullPathName);
2125 if (String.IsNullOrEmpty(file)) {
2127 throw new HttpException(SR.GetString(SR.Invalid_file_name_for_monitoring, HttpRuntime.GetSafePath(alias)));
2130 dirMon = FindDirectoryMonitor(dir, false, false);
2133 if (dirMon != null) {
2134 dirMon.StopMonitoringFile(file, target);
2138 _lockDispose.ReleaseReaderLock();
2144 // Request to stop monitoring a file.
2146 internal void StopMonitoringPath(String alias, object target) {
2147 Debug.Trace("FileChangesMonitor", "StopMonitoringFile\n" + "File=" + alias + "; Callback=" + target);
2149 if (IsFCNDisabled) {
2153 FileMonitor fileMon;
2154 DirectoryMonitor dirMon = null;
2155 string fullPathName, file = null, dir;
2157 if (alias == null) {
2158 throw new HttpException(SR.GetString(SR.Invalid_file_name_for_monitoring, String.Empty));
2161 using (new ApplicationImpersonationContext()) {
2162 _lockDispose.AcquireReaderLock();
2168 fileMon = (FileMonitor)_aliases[alias];
2169 if (fileMon != null) {
2170 // Used the cached directory monitor and file name.
2171 dirMon = fileMon.DirectoryMonitor;
2172 file = fileMon.FileNameLong;
2175 if (alias.Length == 0 || !UrlPath.IsAbsolutePhysicalPath(alias)) {
2176 throw new HttpException(SR.GetString(SR.Invalid_file_name_for_monitoring, HttpRuntime.GetSafePath(alias)));
2179 // try treating the path as a directory
2180 fullPathName = GetFullPath(alias);
2181 dirMon = FindDirectoryMonitor(fullPathName, false, false);
2182 if (dirMon == null) {
2183 // try treaing the path as a file
2184 dir = UrlPath.GetDirectoryOrRootName(fullPathName);
2185 file = Path.GetFileName(fullPathName);
2186 if (!String.IsNullOrEmpty(file)) {
2187 dirMon = FindDirectoryMonitor(dir, false, false);
2192 if (dirMon != null) {
2193 dirMon.StopMonitoringFile(file, target);
2197 _lockDispose.ReleaseReaderLock();
2203 // Returns the last modified time of the file. If the
2204 // file does not exist, returns DateTime.MinValue.
2206 internal FileAttributesData GetFileAttributes(string alias) {
2207 FileMonitor fileMon;
2208 DirectoryMonitor dirMon = null;
2209 string fullPathName, file = null, dir;
2210 FileAttributesData fad = null;
2212 if (alias == null) {
2213 throw FileChangesMonitor.CreateFileMonitoringException(HResults.E_INVALIDARG, alias);
2216 if (IsFCNDisabled) {
2217 if (alias.Length == 0 || !UrlPath.IsAbsolutePhysicalPath(alias)) {
2218 throw FileChangesMonitor.CreateFileMonitoringException(HResults.E_INVALIDARG, alias);
2221 fullPathName = GetFullPath(alias);
2222 FindFileData ffd = null;
2223 int hr = FindFileData.FindFile(fullPathName, out ffd);
2224 if (hr == HResults.S_OK) {
2225 return ffd.FileAttributesData;
2232 using (new ApplicationImpersonationContext()) {
2233 _lockDispose.AcquireReaderLock();
2236 fileMon = (FileMonitor)_aliases[alias];
2237 if (fileMon != null && !fileMon.IsDirectory) {
2238 // Used the cached directory monitor and file name.
2239 dirMon = fileMon.DirectoryMonitor;
2240 file = fileMon.FileNameLong;
2243 if (alias.Length == 0 || !UrlPath.IsAbsolutePhysicalPath(alias)) {
2244 throw FileChangesMonitor.CreateFileMonitoringException(HResults.E_INVALIDARG, alias);
2247 // Lookup the directory monitor
2248 fullPathName = GetFullPath(alias);
2249 dir = UrlPath.GetDirectoryOrRootName(fullPathName);
2250 file = Path.GetFileName(fullPathName);
2251 if (!String.IsNullOrEmpty(file)) {
2252 dirMon = FindDirectoryMonitor(dir, false, false);
2258 _lockDispose.ReleaseReaderLock();
2261 // If we're not monitoring the file, get the attributes.
2262 if (dirMon == null || !dirMon.GetFileAttributes(file, out fad)) {
2263 FileAttributesData.GetFileAttributes(alias, out fad);
2271 // Request to stop monitoring everything -- release all native resources
2273 internal void Stop() {
2274 Debug.Trace("FileChangesMonitor", "Stop!");
2276 if (IsFCNDisabled) {
2280 using (new ApplicationImpersonationContext()) {
2281 _lockDispose.AcquireWriterLock();
2286 _lockDispose.ReleaseWriterLock();
2289 // wait for executing callbacks to complete
2290 while(_activeCallbackCount != 0) {
2294 if (_dirMonSubdirs != null) {
2295 _dirMonSubdirs.StopMonitoring();
2296 _dirMonSubdirs = null;
2299 if (_dirMonSpecialDirs != null) {
2300 foreach (DirectoryMonitor dirMon in _dirMonSpecialDirs) {
2301 if (dirMon != null) {
2302 dirMon.StopMonitoring();
2306 _dirMonSpecialDirs = null;
2309 _callbackRenameOrCriticaldirChange = null;
2311 if (_dirs != null) {
2312 IDictionaryEnumerator e = _dirs.GetEnumerator();
2313 while (e.MoveNext()) {
2314 DirectoryMonitor dirMon = (DirectoryMonitor)e.Value;
2315 dirMon.StopMonitoring();
2322 // Don't allow the AppDomain to unload while we have
2323 // active DirMonCompletions
2324 while (DirMonCompletion.ActiveDirMonCompletions != 0) {
2329 Debug.Dump("FileChangesMonitor", this);
2333 internal string DebugDescription(string indent) {
2334 StringBuilder sb = new StringBuilder(200);
2335 string i2 = indent + " ";
2336 DictionaryEntryCaseInsensitiveComparer decomparer = new DictionaryEntryCaseInsensitiveComparer();
2338 sb.Append(indent + "System.Web.FileChangesMonitor\n");
2339 if (_dirMonSubdirs != null) {
2340 sb.Append(indent + "_dirMonSubdirs\n");
2341 sb.Append(_dirMonSubdirs.DebugDescription(i2));
2344 if (_dirMonSpecialDirs != null) {
2345 for (int i=0; i<s_dirsToMonitor.Length; i++) {
2346 if (_dirMonSpecialDirs[i] != null) {
2347 sb.Append(indent + "_dirMon" + s_dirsToMonitor[i] + "\n");
2348 sb.Append(((DirectoryMonitor)_dirMonSpecialDirs[i]).DebugDescription(i2));
2353 sb.Append(indent + "_dirs " + _dirs.Count + " directory monitors...\n");
2355 DictionaryEntry[] dirEntries = new DictionaryEntry[_dirs.Count];
2356 _dirs.CopyTo(dirEntries, 0);
2357 Array.Sort(dirEntries, decomparer);
2359 foreach (DictionaryEntry d in dirEntries) {
2360 DirectoryMonitor dirMon = (DirectoryMonitor)d.Value;
2361 sb.Append(dirMon.DebugDescription(i2));
2364 return sb.ToString();
2368 #else // !FEATURE_PAL stubbing
2370 internal static string[] s_dirsToMonitor = new string[] {
2373 internal DateTime StartMonitoringFile(string alias, FileChangeEventHandler callback)
2375 return DateTime.Now;
2378 internal DateTime StartMonitoringPath(string alias, FileChangeEventHandler callback)
2380 return DateTime.Now;
2383 internal void StopMonitoringPath(String alias, object target)
2387 internal void StartMonitoringDirectoryRenamesAndBinDirectory(string dir, FileChangeEventHandler callback)
2391 internal void Stop()
2395 #endif // !FEATURE_PAL
2399 internal sealed class DictionaryEntryCaseInsensitiveComparer : IComparer {
2400 IComparer _cicomparer = StringComparer.OrdinalIgnoreCase;
2402 internal DictionaryEntryCaseInsensitiveComparer() {}
2404 int IComparer.Compare(object x, object y) {
2405 string a = (string) ((DictionaryEntry) x).Key;
2406 string b = (string) ((DictionaryEntry) y).Key;
2408 if (a != null && b != null) {
2409 return _cicomparer.Compare(a, b);
2412 return InvariantComparer.Default.Compare(a, b);
2419 internal sealed class DictionaryEntryTypeComparer : IComparer {
2420 IComparer _cicomparer = StringComparer.OrdinalIgnoreCase;
2422 internal DictionaryEntryTypeComparer() {}
2424 int IComparer.Compare(object x, object y) {
2425 object a = ((DictionaryEntry) x).Key;
2426 object b = ((DictionaryEntry) y).Key;
2428 string i = null, j = null;
2430 i = a.GetType().ToString();
2434 j = b.GetType().ToString();
2437 if (i != null && j != null) {
2438 return _cicomparer.Compare(i, j);
2441 return InvariantComparer.Default.Compare(i, j);