//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // //------------------------------------------------------------------------------ namespace System.Web { using System.Collections; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Runtime.InteropServices; using System.Security; using System.Security.Permissions; using System.Text; using System.Threading; using System.Web.Configuration; using System.Web.Hosting; using System.Web.Util; using Microsoft.Win32; // Type of the callback to the subscriber of a file change event in FileChangesMonitor.StartMonitoringFile delegate void FileChangeEventHandler(Object sender, FileChangeEvent e); // The type of file change that occurred. enum FileAction { Dispose = -2, Error = -1, Overwhelming = 0, Added = 1, Removed = 2, Modified = 3, RenamedOldName = 4, RenamedNewName = 5 } // Event data for a file change notification sealed class FileChangeEvent : EventArgs { internal FileAction Action; // the action internal string FileName; // the file that caused the action internal FileChangeEvent(FileAction action, string fileName) { this.Action = action; this.FileName = fileName; } } // Contains information about the target of a file change notification sealed class FileMonitorTarget { internal readonly FileChangeEventHandler Callback; // the callback internal readonly string Alias; // the filename used to name the file internal readonly DateTime UtcStartMonitoring;// time we started monitoring int _refs; // number of uses of callbacks internal FileMonitorTarget(FileChangeEventHandler callback, string alias) { Callback = callback; Alias = alias; UtcStartMonitoring = DateTime.UtcNow; _refs = 1; } internal int AddRef() { _refs++; return _refs; } internal int Release() { _refs--; return _refs; } #if DBG internal string DebugDescription(string indent) { StringBuilder sb = new StringBuilder(200); string i2 = indent + " "; sb.Append(indent + "FileMonitorTarget\n"); sb.Append(i2 + " Callback: " + Callback.Target + "(HC=" + Callback.Target.GetHashCode().ToString("x", NumberFormatInfo.InvariantInfo) + ")\n"); sb.Append(i2 + " Alias: " + Alias + "\n"); sb.Append(i2 + "StartMonitoring: " + Debug.FormatUtcDate(UtcStartMonitoring) + "\n"); sb.Append(i2 + " _refs: " + _refs + "\n"); return sb.ToString(); } #endif } #if !FEATURE_PAL // FEATURE_PAL does not enable access control sealed class FileSecurity { const int DACL_INFORMATION = UnsafeNativeMethods.DACL_SECURITY_INFORMATION | UnsafeNativeMethods.GROUP_SECURITY_INFORMATION | UnsafeNativeMethods.OWNER_SECURITY_INFORMATION; static Hashtable s_interned; static byte[] s_nullDacl; class DaclComparer : IEqualityComparer { // Compares two objects. An implementation of this method must return a // value less than zero if x is less than y, zero if x is equal to y, or a // value greater than zero if x is greater than y. // private int Compare(byte[] a, byte[] b) { int result = a.Length - b.Length; for (int i = 0; result == 0 && i < a.Length ; i++) { result = a[i] - b[i]; } return result; } bool IEqualityComparer.Equals(Object x, Object y) { if (x == null && y == null) { return true; } if (x == null || y == null) { return false; } byte[] a = x as byte[]; byte[] b = y as byte[]; if (a == null || b == null) { return false; } return Compare(a, b) == 0; } int IEqualityComparer.GetHashCode(Object obj) { byte[] a = (byte[]) obj; HashCodeCombiner combiner = new HashCodeCombiner(); foreach (byte b in a) { combiner.AddObject(b); } return combiner.CombinedHash32; } } static FileSecurity() { s_interned = new Hashtable(0, 1.0f, new DaclComparer()); s_nullDacl = new byte[0]; } [SuppressMessage("Microsoft.Interoperability", "CA1404:CallGetLastErrorImmediatelyAfterPInvoke", Justification="[....]: Call to GetLastWin32Error() does follow P/Invoke call that is outside the if/else block.")] static internal byte[] GetDacl(string filename) { // DevDiv #322858 - allow skipping DACL step for perf gain if (HostingEnvironment.FcnSkipReadAndCacheDacls) { return s_nullDacl; } // DevDiv #246973 // Perf: Start with initial buffer size to minimize File IO // int lengthNeeded = 512; byte[] dacl = new byte[lengthNeeded]; int fOK = UnsafeNativeMethods.GetFileSecurity(filename, DACL_INFORMATION, dacl, dacl.Length, ref lengthNeeded); if (fOK != 0) { // If no size is needed, return a non-null marker if (lengthNeeded == 0) { Debug.Trace("GetDacl", "Returning null dacl"); return s_nullDacl; } // Shrink the buffer to fit the whole data Array.Resize(ref dacl, lengthNeeded); } else { int hr = HttpException.HResultFromLastError(Marshal.GetLastWin32Error()); // Check if need to redo the call with larger buffer if (hr != HResults.E_INSUFFICIENT_BUFFER) { Debug.Trace("GetDacl", "Error in first call to GetFileSecurity: 0x" + hr.ToString("x", NumberFormatInfo.InvariantInfo)); return null; } // The buffer wasn't large enough. Try again dacl = new byte[lengthNeeded]; fOK = UnsafeNativeMethods.GetFileSecurity(filename, DACL_INFORMATION, dacl, dacl.Length, ref lengthNeeded); if (fOK == 0) { #if DBG hr = HttpException.HResultFromLastError(Marshal.GetLastWin32Error()); Debug.Trace("GetDacl", "Error in second to GetFileSecurity: 0x" + hr.ToString("x", NumberFormatInfo.InvariantInfo)); #endif return null; } } byte[] interned = (byte[]) s_interned[dacl]; if (interned == null) { lock (s_interned.SyncRoot) { interned = (byte[]) s_interned[dacl]; if (interned == null) { Debug.Trace("GetDacl", "Interning new dacl, length " + dacl.Length); interned = dacl; s_interned[interned] = interned; } } } Debug.Trace("GetDacl", "Returning dacl, length " + dacl.Length); return interned; } } // holds information about a single file and the targets of change notification sealed class FileMonitor { internal readonly DirectoryMonitor DirectoryMonitor; // the parent internal readonly HybridDictionary Aliases; // aliases for this file string _fileNameLong; // long file name - if null, represents any file in this directory string _fileNameShort; // short file name, may be null HybridDictionary _targets; // targets of notification bool _exists; // does the file exist? FileAttributesData _fad; // file attributes byte[] _dacl; // dacl FileAction _lastAction; // last action that ocurred on this file DateTime _utcLastCompletion; // date of the last RDCW completion internal FileMonitor( DirectoryMonitor dirMon, string fileNameLong, string fileNameShort, bool exists, FileAttributesData fad, byte[] dacl) { DirectoryMonitor = dirMon; _fileNameLong = fileNameLong; _fileNameShort = fileNameShort; _exists = exists; _fad = fad; _dacl = dacl; _targets = new HybridDictionary(); Aliases = new HybridDictionary(true); } internal string FileNameLong {get {return _fileNameLong;}} internal string FileNameShort {get {return _fileNameShort;}} internal bool Exists {get {return _exists;}} internal bool IsDirectory {get {return (FileNameLong == null);}} internal FileAction LastAction { get {return _lastAction;} set {_lastAction = value;} } internal DateTime UtcLastCompletion { get {return _utcLastCompletion;} set {_utcLastCompletion = value;} } // Returns the attributes of a file, updating them if the file has changed. internal FileAttributesData Attributes { get {return _fad;} } internal byte[] Dacl { get {return _dacl;} } internal void ResetCachedAttributes() { _fad = null; _dacl = null; } internal void UpdateCachedAttributes() { string path = Path.Combine(DirectoryMonitor.Directory, FileNameLong); FileAttributesData.GetFileAttributes(path, out _fad); _dacl = FileSecurity.GetDacl(path); } // Set new file information when a file comes into existence internal void MakeExist(FindFileData ffd, byte[] dacl) { _fileNameLong = ffd.FileNameLong; _fileNameShort = ffd.FileNameShort; _fad = ffd.FileAttributesData; _dacl = dacl; _exists = true; } // Remove a file from existence internal void MakeExtinct() { _fad = null; _dacl = null; _exists = false; } internal void RemoveFileNameShort() { _fileNameShort = null; } internal ICollection Targets { get {return _targets.Values;} } // Add delegate for this file. internal void AddTarget(FileChangeEventHandler callback, string alias, bool newAlias) { FileMonitorTarget target = (FileMonitorTarget)_targets[callback.Target]; if (target != null) { target.AddRef(); } else { #if DBG // Needs the lock to [....] with DebugDescription lock (_targets) { #endif _targets.Add(callback.Target, new FileMonitorTarget(callback, alias)); #if DBG } #endif } if (newAlias) { Aliases[alias] = alias; } } // Remove delegate for this file given the target object. internal int RemoveTarget(object callbackTarget) { FileMonitorTarget target = (FileMonitorTarget)_targets[callbackTarget]; #if DBG if (FileChangesMonitor.s_enableRemoveTargetAssert) { Debug.Assert(target != null, "removing file monitor target that was never added or already been removed"); } #endif if (target != null && target.Release() == 0) { #if DBG // Needs the lock to [....] with DebugDescription lock (_targets) { #endif _targets.Remove(callbackTarget); #if DBG } #endif } return _targets.Count; } #if DBG internal string DebugDescription(string indent) { StringBuilder sb = new StringBuilder(200); string i2 = indent + " "; string i3 = i2 + " "; DictionaryEntryTypeComparer detcomparer = new DictionaryEntryTypeComparer(); sb.Append(indent + "System.Web.FileMonitor: "); if (FileNameLong != null) { sb.Append(FileNameLong); if (FileNameShort != null) { sb.Append("; ShortFileName=" + FileNameShort); } sb.Append("; FileExists="); sb.Append(_exists); } else { sb.Append(""); } sb.Append("\n"); sb.Append(i2 + "LastAction="); sb.Append(_lastAction); sb.Append("; LastCompletion="); sb.Append(Debug.FormatUtcDate(_utcLastCompletion)); sb.Append("\n"); if (_fad != null) { sb.Append(_fad.DebugDescription(i2)); } else { sb.Append(i2 + "FileAttributesData = \n"); } DictionaryEntry[] delegateEntries; lock (_targets) { sb.Append(i2 + _targets.Count + " delegates...\n"); delegateEntries = new DictionaryEntry[_targets.Count]; _targets.CopyTo(delegateEntries, 0); } Array.Sort(delegateEntries, detcomparer); foreach (DictionaryEntry d in delegateEntries) { sb.Append(i3 + "Delegate " + d.Key.GetType() + "(HC=" + d.Key.GetHashCode().ToString("x", NumberFormatInfo.InvariantInfo) + ")\n"); } return sb.ToString(); } #endif } // Change notifications delegate from native code. delegate void NativeFileChangeNotification(FileAction action, [In, MarshalAs(UnmanagedType.LPWStr)] string fileName, long ticks); // // Wraps N/Direct calls to native code that does completion port // based ReadDirectoryChangesW(). // This needs to be a separate object so that a DirectoryMonitory // can start monitoring while the old _rootCallback has not been // disposed. // sealed class DirMonCompletion : IDisposable { static int _activeDirMonCompletions = 0; // private counter used via reflection by FCN check-in suite DirectoryMonitor _dirMon; // directory monitor IntPtr _ndirMonCompletionPtr; // pointer to native dir mon as int (used only to construct HandleRef) HandleRef _ndirMonCompletionHandle; // handleref of a pointer to native dir mon as int GCHandle _rootCallback; // roots this callback to prevent collection int _disposed; // set to 1 when we call DirMonClose object _ndirMonCompletionHandleLock; internal static int ActiveDirMonCompletions { get { return _activeDirMonCompletions; } } internal DirMonCompletion(DirectoryMonitor dirMon, string dir, bool watchSubtree, uint notifyFilter) { Debug.Trace("FileChangesMonitor", "DirMonCompletion::ctor " + dir + " " + watchSubtree.ToString() + " " + notifyFilter.ToString(NumberFormatInfo.InvariantInfo)); int hr; NativeFileChangeNotification myCallback; _dirMon = dirMon; myCallback = new NativeFileChangeNotification(this.OnFileChange); _ndirMonCompletionHandleLock = new object(); try { } finally { // protected from ThreadAbortEx lock(_ndirMonCompletionHandleLock) { // Dev10 927846: The managed DirMonCompletion.ctor calls DirMonOpen to create and initialize the native DirMonCompletion. // If this succeeds, the managed DirMonCompletion.ctor creates a GCHandle to root itself so the target of the callback // stays alive. When the native DirMonCompletion is freed it invokes the managed callback with ACTION_DISPOSE to // release the GCHandle. In order for the native DirMonCOmpletion to be freed, either DirMonOpen must fail or // the managed DirMonCompletion.Dispose must be called and it must invoke DirMonClose. Waiting until the native // DirMonCompletion.dtor is called to release the GCHandle ensures that the directory handle has been closed, // the i/o completions have finished and there are no other threads calling the managed callback. This is because // we AddRef when we initiate i/o and we Release when the i/o completion finishes. // If I don't do this, myCallback will be collected by GC since its only reference is // from the native code. _rootCallback = GCHandle.Alloc(myCallback); hr = UnsafeNativeMethods.DirMonOpen(dir, HttpRuntime.AppDomainAppId, watchSubtree, notifyFilter, dirMon.FcnMode, myCallback, out _ndirMonCompletionPtr); if (hr != HResults.S_OK) { _rootCallback.Free(); throw FileChangesMonitor.CreateFileMonitoringException(hr, dir); } _ndirMonCompletionHandle = new HandleRef(this, _ndirMonCompletionPtr); Interlocked.Increment(ref _activeDirMonCompletions); } } } ~DirMonCompletion() { Dispose(false); } void IDisposable.Dispose() { Dispose(true); System.GC.SuppressFinalize(this); } void Dispose(bool disposing) { Debug.Trace("FileChangesMonitor", "DirMonCompletion::Dispose"); // this is here because callbacks from native code can cause // this to be invoked from multiple threads concurrently, and // I don't want contention on _ndirMonCompletionHandleLock, // but also need to ensure that DirMonOpen returns and the // _ndirMonCompletionHandle is set before DirMonClose is called. if (Interlocked.Exchange(ref _disposed, 1) == 0) { // Dev10 927846: There is a small window during which the .ctor has // not returned from DirMonOpen yet but because we already started // monitoring, we might receive a change notification which could // potentially Dispose the instance, so we need to block until // DirMonOpen returns and _ndirMonCompletionHandler is set lock(_ndirMonCompletionHandleLock) { // Dev11 - 364642: if Dispose is called while finalizing for AD unload then // the native DirMonCompletion won't be able to call back into the appdomain. // But it does not need to because _rootCallback is already reclaimed as part of AD unload bool fNeedToSendFileActionDispose = !AppDomain.CurrentDomain.IsFinalizingForUnload(); HandleRef ndirMonCompletionHandle = _ndirMonCompletionHandle; if (ndirMonCompletionHandle.Handle != IntPtr.Zero) { _ndirMonCompletionHandle = new HandleRef(this, IntPtr.Zero); UnsafeNativeMethods.DirMonClose(ndirMonCompletionHandle, fNeedToSendFileActionDispose); } } } } void OnFileChange(FileAction action, string fileName, long ticks) { DateTime utcCompletion; if (ticks == 0) { utcCompletion = DateTime.MinValue; } else { utcCompletion = DateTimeUtil.FromFileTimeToUtc(ticks); } #if DBG Debug.Trace("FileChangesMonitorOnFileChange", "Action=" + action + "; Dir=" + _dirMon.Directory + "; fileName=" + Debug.ToStringMaybeNull(fileName) + "; completion=" + Debug.FormatUtcDate(utcCompletion) + ";_ndirMonCompletionPtr=0x" + _ndirMonCompletionPtr.ToString("x")); #endif // // The native DirMonCompletion sends FileAction.Dispose // when there are no more outstanding calls on the // delegate. Only then can _rootCallback be freed. // if (action == FileAction.Dispose) { if (_rootCallback.IsAllocated) { _rootCallback.Free(); } Interlocked.Decrement(ref _activeDirMonCompletions); } else { using (new ApplicationImpersonationContext()) { _dirMon.OnFileChange(action, fileName, utcCompletion); } } } #if DBG internal string DebugDescription(string indent) { int hc = ((Delegate)_rootCallback.Target).Target.GetHashCode(); string description = indent + "_ndirMonCompletionPtr=0x" + _ndirMonCompletionPtr.ToString("x") + "; callback=0x" + hc.ToString("x", NumberFormatInfo.InvariantInfo) + "\n"; return description; } #endif } sealed class NotificationQueueItem { internal readonly FileChangeEventHandler Callback; internal readonly string Filename; internal readonly FileAction Action; internal NotificationQueueItem(FileChangeEventHandler callback, FileAction action, string filename) { Callback = callback; Action = action; Filename = filename; } } // // Monitor changes in a single directory. // sealed class DirectoryMonitor : IDisposable { static Queue s_notificationQueue = new Queue(); static WorkItemCallback s_notificationCallback = new WorkItemCallback(FireNotifications); static int s_inNotificationThread; static int s_notificationBufferSizeIncreased = 0; internal readonly string Directory; // directory being monitored Hashtable _fileMons; // fileName -> FileMonitor int _cShortNames; // number of file monitors that are added with their short name FileMonitor _anyFileMon; // special file monitor to watch for any changes in directory bool _watchSubtree; // watch subtree? uint _notifyFilter; // the notify filter for the call to ReadDirectoryChangesW bool _ignoreSubdirChange; // when a subdirectory is deleted or renamed, ignore the notification if we're not monitoring it DirMonCompletion _dirMonCompletion; // dirmon completion bool _isDirMonAppPathInternal; // special dirmon that monitors all files and subdirectories beneath the vroot (enabled via FCNMode registry key) // FcnMode to pass to native code internal int FcnMode { get; set; } // constructor for special dirmon that monitors all files and subdirectories beneath the vroot (enabled via FCNMode registry key) internal DirectoryMonitor(string appPathInternal, int fcnMode): this(appPathInternal, true, UnsafeNativeMethods.RDCW_FILTER_FILE_AND_DIR_CHANGES, fcnMode) { _isDirMonAppPathInternal = true; } internal DirectoryMonitor(string dir, bool watchSubtree, uint notifyFilter, int fcnMode): this(dir, watchSubtree, notifyFilter, false, fcnMode) { } internal DirectoryMonitor(string dir, bool watchSubtree, uint notifyFilter, bool ignoreSubdirChange, int fcnMode) { Directory = dir; _fileMons = new Hashtable(StringComparer.OrdinalIgnoreCase); _watchSubtree = watchSubtree; _notifyFilter = notifyFilter; _ignoreSubdirChange = ignoreSubdirChange; FcnMode = fcnMode; } void IDisposable.Dispose() { if (_dirMonCompletion != null) { ((IDisposable)_dirMonCompletion).Dispose(); _dirMonCompletion = null; } // // Remove aliases to this object in FileChangesMonitor so that // it is not rooted. // if (_anyFileMon != null) { HttpRuntime.FileChangesMonitor.RemoveAliases(_anyFileMon); _anyFileMon = null; } foreach (DictionaryEntry e in _fileMons) { string key = (string) e.Key; FileMonitor fileMon = (FileMonitor) e.Value; if (fileMon.FileNameLong == key) { HttpRuntime.FileChangesMonitor.RemoveAliases(fileMon); } } _fileMons.Clear(); _cShortNames = 0; } internal bool IsMonitoring() { return GetFileMonitorsCount() > 0; } void StartMonitoring() { if (_dirMonCompletion == null) { _dirMonCompletion = new DirMonCompletion(this, Directory, _watchSubtree, _notifyFilter); } } internal void StopMonitoring() { lock (this) { ((IDisposable)this).Dispose(); } } FileMonitor FindFileMonitor(string file) { FileMonitor fileMon; if (file == null) { fileMon = _anyFileMon; } else { fileMon = (FileMonitor)_fileMons[file]; } return fileMon; } FileMonitor AddFileMonitor(string file) { string path; FileMonitor fileMon; FindFileData ffd = null; int hr; if (String.IsNullOrEmpty(file)) { // add as the file monitor fileMon = new FileMonitor(this, null, null, true, null, null); _anyFileMon = fileMon; } else { // Get the long and short name of the file path = Path.Combine(Directory, file); if (_isDirMonAppPathInternal) { hr = FindFileData.FindFile(path, Directory, out ffd); } else { hr = FindFileData.FindFile(path, out ffd); } if (hr == HResults.S_OK) { // Unless this is FileChangesMonitor._dirMonAppPathInternal, // don't monitor changes to a directory - this will not pickup changes to files in the directory. if (!_isDirMonAppPathInternal && (ffd.FileAttributesData.FileAttributes & FileAttributes.Directory) != 0) { throw FileChangesMonitor.CreateFileMonitoringException(HResults.E_INVALIDARG, path); } byte[] dacl = FileSecurity.GetDacl(path); fileMon = new FileMonitor(this, ffd.FileNameLong, ffd.FileNameShort, true, ffd.FileAttributesData, dacl); _fileMons.Add(ffd.FileNameLong, fileMon); // Update short name aliases to this file UpdateFileNameShort(fileMon, null, ffd.FileNameShort); } else if (hr == HResults.E_PATHNOTFOUND || hr == HResults.E_FILENOTFOUND) { // Don't allow possible short file names to be added as non-existant, // because it is impossible to track them if they are indeed a short name since // short file names may change. // FEATURE_PAL if (file.IndexOf('~') != -1) { throw FileChangesMonitor.CreateFileMonitoringException(HResults.E_INVALIDARG, path); } // Add as non-existent file fileMon = new FileMonitor(this, file, null, false, null, null); _fileMons.Add(file, fileMon); } else { throw FileChangesMonitor.CreateFileMonitoringException(hr, path); } } return fileMon; } // // Update short names of a file // void UpdateFileNameShort(FileMonitor fileMon, string oldFileNameShort, string newFileNameShort) { if (oldFileNameShort != null) { FileMonitor oldFileMonShort = (FileMonitor)_fileMons[oldFileNameShort]; if (oldFileMonShort != null) { // The old filemonitor no longer has this short file name. // Update the monitor and _fileMons if (oldFileMonShort != fileMon) { oldFileMonShort.RemoveFileNameShort(); } _fileMons.Remove(oldFileNameShort); _cShortNames--; } } if (newFileNameShort != null) { // Add the new short file name. _fileMons.Add(newFileNameShort, fileMon); _cShortNames++; } } void RemoveFileMonitor(FileMonitor fileMon) { if (fileMon == _anyFileMon) { _anyFileMon = null; } else { _fileMons.Remove(fileMon.FileNameLong); if (fileMon.FileNameShort != null) { _fileMons.Remove(fileMon.FileNameShort); _cShortNames--; } } HttpRuntime.FileChangesMonitor.RemoveAliases(fileMon); } int GetFileMonitorsCount() { int c = _fileMons.Count - _cShortNames; if (_anyFileMon != null) { c++; } return c; } // The 4.0 CAS changes made the AppDomain homogenous, so we need to assert // FileIOPermission. Currently this is only exposed publicly via CacheDependency, which // already does a PathDiscover check for public callers. [FileIOPermission(SecurityAction.Assert, Unrestricted = true)] internal FileMonitor StartMonitoringFileWithAssert(string file, FileChangeEventHandler callback, string alias) { FileMonitor fileMon = null; bool firstFileMonAdded = false; lock (this) { // Find existing file monitor fileMon = FindFileMonitor(file); if (fileMon == null) { // Add a new monitor fileMon = AddFileMonitor(file); if (GetFileMonitorsCount() == 1) { firstFileMonAdded = true; } } // Add callback to the file monitor fileMon.AddTarget(callback, alias, true); // Start directory monitoring when the first file gets added if (firstFileMonAdded) { StartMonitoring(); } } return fileMon; } // // Request to stop monitoring a file. // internal void StopMonitoringFile(string file, object target) { FileMonitor fileMon; int numTargets; lock (this) { // Find existing file monitor fileMon = FindFileMonitor(file); if (fileMon != null) { numTargets = fileMon.RemoveTarget(target); if (numTargets == 0) { RemoveFileMonitor(fileMon); // last target for the file monitor gone // -- remove the file monitor if (GetFileMonitorsCount() == 0) { ((IDisposable)this).Dispose(); } } } } #if DBG if (fileMon != null) { Debug.Dump("FileChangesMonitor", HttpRuntime.FileChangesMonitor); } #endif } internal bool GetFileAttributes(string file, out FileAttributesData fad) { FileMonitor fileMon = null; fad = null; lock (this) { // Find existing file monitor fileMon = FindFileMonitor(file); if (fileMon != null) { // Get the attributes fad = fileMon.Attributes; return true; } } return false; } // // Notes about file attributes: // // CreationTime is the time a file entry is added to a directory. // If file q1 is copied to q2, q2's creation time is updated if it is new to the directory, // else q2's old time is used. // // If a file is deleted, then added, its creation time is preserved from before the delete. // // LastWriteTime is the time a file was last written. // If file q1 is copied to q2, q2's lastWrite time is the same as q1. // Note that this implies that the LastWriteTime can be older than the LastCreationTime, // and that a copy of a file can result in the LastWriteTime being earlier than // its previous value. // // LastAccessTime is the time a file was last accessed, such as opened or written to. // Note that if the attributes of a file are changed, its LastAccessTime is not necessarily updated. // // If the FileSize, CreationTime, or LastWriteTime have changed, then we know that the // file has changed in a significant way, and that the LastAccessTime will be greater than // or equal to that time. // // If the FileSize, CreationTime, or LastWriteTime have not changed, then the file's // attributes may have changed without changing the LastAccessTime. // // Confirm that the changes occurred after we started monitoring, // to handle the case where: // // 1. User creates a file. // 2. User starts to monitor the file. // 3. Change notification is made of the original creation of the file. // // Note that we can only approximate when the last change occurred by // examining the LastAccessTime. The LastAccessTime will change if the // contents of a file (but not necessarily its attributes) change. // The drawback to using the LastAccessTime is that it will also be // updated when a file is read. // // Note that we cannot make this confirmation when only the file's attributes // or ACLs change, because changes to attributes and ACLs won't change the LastAccessTime. // bool IsChangeAfterStartMonitoring(FileAttributesData fad, FileMonitorTarget target, DateTime utcCompletion) { // If the LastAccessTime is more than 60 seconds before we // started monitoring, then the change likely did not update // the LastAccessTime correctly. if (fad.UtcLastAccessTime.AddSeconds(60) < target.UtcStartMonitoring) { #if DBG Debug.Trace("FileChangesMonitorIsChangeAfterStart", "LastAccessTime is more than 60 seconds before monitoring started."); #endif return true; } // Check if the notification of the change came after // we started monitoring. if (utcCompletion > target.UtcStartMonitoring) { #if DBG Debug.Trace("FileChangesMonitorIsChangeAfterStart", "Notification came after we started monitoring."); #endif return true; } // Make sure that the LastAccessTime is valid. // It must be more recent than the LastWriteTime. if (fad.UtcLastAccessTime < fad.UtcLastWriteTime) { #if DBG Debug.Trace("FileChangesMonitorIsChangeAfterStart", "UtcLastWriteTime is greater then UtcLastAccessTime."); #endif return true; } // If the LastAccessTime occurs exactly at midnight, // then the system is FAT32 and LastAccessTime is unusable. if (fad.UtcLastAccessTime.TimeOfDay == TimeSpan.Zero) { #if DBG Debug.Trace("FileChangesMonitorIsChangeAfterStart", "UtcLastAccessTime is midnight -- FAT32 likely."); #endif return true; } // Finally, compare LastAccessTime to the time we started monitoring. // If the time of the last access was before we started monitoring, then // we know a change did not occur to the file contents. if (fad.UtcLastAccessTime >= target.UtcStartMonitoring) { #if DBG Debug.Trace("FileChangesMonitorIsChangeAfterStart", "UtcLastAccessTime is greater than UtcStartMonitoring."); #endif return true; } #if DBG Debug.Trace("FileChangesMonitorIsChangeAfterStart", "Change is before start of monitoring. Data:\n FileAttributesData: \nUtcCreationTime: " + fad.UtcCreationTime + " UtcLastAccessTime: " + fad.UtcLastAccessTime + " UtcLastWriteTime: " + fad.UtcLastWriteTime + "\n FileMonitorTarget:\n UtcStartMonitoring: " + target.UtcStartMonitoring + "\nUtcCompletion: " + utcCompletion); #endif return false; } // If this is a special dirmon that monitors all files and subdirectories // beneath the vroot (enabled via FCNMode registry key), then // we need to special case how we lookup the FileMonitor. For example, nobody has called // StartMonitorFile for specific files in the App_LocalResources directory, // so we need to see if fileName is in App_LocalResources and then get the FileMonitor for // the directory. private bool GetFileMonitorForSpecialDirectory(string fileName, ref FileMonitor fileMon) { // fileName should not be in short form (8.3 format)...it was converted to long form in // DirMonCompletion::ProcessOneFileNotification // first search for match within s_dirsToMonitor for (int i = 0; i < FileChangesMonitor.s_dirsToMonitor.Length; i++) { if (StringUtil.StringStartsWithIgnoreCase(fileName, FileChangesMonitor.s_dirsToMonitor[i])) { fileMon = (FileMonitor)_fileMons[FileChangesMonitor.s_dirsToMonitor[i]]; return fileMon != null; } } // if we did not find a match in s_dirsToMonitor, look for LocalResourcesDirectoryName anywhere within fileName int indexStart = fileName.IndexOf(HttpRuntime.LocalResourcesDirectoryName, StringComparison.OrdinalIgnoreCase); if (indexStart > -1) { int dirNameLength = indexStart + HttpRuntime.LocalResourcesDirectoryName.Length; // fileName should either end with LocalResourcesDirectoryName or include a trailing slash and more characters if (fileName.Length == dirNameLength || fileName[dirNameLength] == Path.DirectorySeparatorChar) { string dirName = fileName.Substring(0, dirNameLength); fileMon = (FileMonitor)_fileMons[dirName]; return fileMon != null; } } return false; } // // Delegate callback from native code. // internal void OnFileChange(FileAction action, string fileName, DateTime utcCompletion) { // // Use try/catch to prevent runtime exceptions from propagating // into native code. // try { FileMonitor fileMon = null; ArrayList targets = null; int i, n; FileMonitorTarget target; ICollection col; string key; FileAttributesData fadOld = null; FileAttributesData fadNew = null; byte[] daclOld = null; byte[] daclNew = null; FileAction lastAction = FileAction.Error; DateTime utcLastCompletion = DateTime.MinValue; bool isSpecialDirectoryChange = false; #if DBG string reasonIgnore = string.Empty; string reasonFire = string.Empty; #endif // We've already stopped monitoring, but a change completion was // posted afterwards. Ignore it. if (_dirMonCompletion == null) { return; } lock (this) { if (_fileMons.Count > 0) { if (action == FileAction.Error || action == FileAction.Overwhelming) { // Overwhelming change -- notify all file monitors Debug.Assert(fileName == null, "fileName == null"); Debug.Assert(action != FileAction.Overwhelming, "action != FileAction.Overwhelming"); if (action == FileAction.Overwhelming) { //increase file notification buffer size, but only once per app instance if (Interlocked.Increment(ref s_notificationBufferSizeIncreased) == 1) { UnsafeNativeMethods.GrowFileNotificationBuffer( HttpRuntime.AppDomainAppId, _watchSubtree ); } } // Get targets for all files targets = new ArrayList(); foreach (DictionaryEntry d in _fileMons) { key = (string) d.Key; fileMon = (FileMonitor) d.Value; if (fileMon.FileNameLong == key) { fileMon.ResetCachedAttributes(); fileMon.LastAction = action; fileMon.UtcLastCompletion = utcCompletion; col = fileMon.Targets; targets.AddRange(col); } } fileMon = null; } else { Debug.Assert((int) action >= 1 && fileName != null && fileName.Length > 0, "(int) action >= 1 && fileName != null && fileName.Length > 0"); // Find the file monitor fileMon = (FileMonitor)_fileMons[fileName]; if (_isDirMonAppPathInternal && fileMon == null) { isSpecialDirectoryChange = GetFileMonitorForSpecialDirectory(fileName, ref fileMon); } if (fileMon != null) { // Get the targets col = fileMon.Targets; targets = new ArrayList(col); fadOld = fileMon.Attributes; daclOld = fileMon.Dacl; lastAction = fileMon.LastAction; utcLastCompletion = fileMon.UtcLastCompletion; fileMon.LastAction = action; fileMon.UtcLastCompletion = utcCompletion; if (action == FileAction.Removed || action == FileAction.RenamedOldName) { // File not longer exists. fileMon.MakeExtinct(); } else if (fileMon.Exists) { // We only need to update the attributes if this is // a different completion, as we retreive the attributes // after the completion is received. if (utcLastCompletion != utcCompletion) { fileMon.UpdateCachedAttributes(); } } else { // File now exists - update short name and attributes. FindFileData ffd = null; string path = Path.Combine(Directory, fileMon.FileNameLong); int hr; if (_isDirMonAppPathInternal) { hr = FindFileData.FindFile(path, Directory, out ffd); } else { hr = FindFileData.FindFile(path, out ffd); } if (hr == HResults.S_OK) { Debug.Assert(StringUtil.EqualsIgnoreCase(fileMon.FileNameLong, ffd.FileNameLong), "StringUtil.EqualsIgnoreCase(fileMon.FileNameLong, ffd.FileNameLong)"); string oldFileNameShort = fileMon.FileNameShort; byte[] dacl = FileSecurity.GetDacl(path); fileMon.MakeExist(ffd, dacl); UpdateFileNameShort(fileMon, oldFileNameShort, ffd.FileNameShort); } } fadNew = fileMon.Attributes; daclNew = fileMon.Dacl; } } } // Notify the delegate waiting for any changes if (_anyFileMon != null) { col = _anyFileMon.Targets; if (targets != null) { targets.AddRange(col); } else { targets = new ArrayList(col); } } if (action == FileAction.Error) { // Stop monitoring. ((IDisposable)this).Dispose(); } } // Ignore Modified action for directories (VSWhidbey 295597) bool ignoreThisChangeNotification = false; if (!isSpecialDirectoryChange && fileName != null && action == FileAction.Modified) { // check if the file is a directory (reuse attributes if already obtained) FileAttributesData fad = fadNew; if (fad == null) { string path = Path.Combine(Directory, fileName); FileAttributesData.GetFileAttributes(path, out fad); } if (fad != null && ((fad.FileAttributes & FileAttributes.Directory) != 0)) { // ignore if directory ignoreThisChangeNotification = true; } } // Dev10 440497: Don't unload AppDomain when a folder is deleted or renamed, unless we're monitoring files in it if (_ignoreSubdirChange && (action == FileAction.Removed || action == FileAction.RenamedOldName) && fileName != null) { string fullPath = Path.Combine(Directory, fileName); if (!HttpRuntime.FileChangesMonitor.IsDirNameMonitored(fullPath, fileName)) { #if DBG Debug.Trace("FileChangesMonitorIgnoreSubdirChange", "*** Ignoring SubDirChange " + DateTime.Now.ToString("hh:mm:ss.fff", CultureInfo.InvariantCulture) + ": fullPath=" + fullPath + ", action=" + action.ToString()); #endif ignoreThisChangeNotification = true; } #if DBG else { Debug.Trace("FileChangesMonitorIgnoreSubdirChange", "*** SubDirChange " + DateTime.Now.ToString("hh:mm:ss.fff", CultureInfo.InvariantCulture) + ": fullPath=" + fullPath + ", action=" + action.ToString()); } #endif } // Fire the event if (targets != null && !ignoreThisChangeNotification) { Debug.Dump("FileChangesMonitor", HttpRuntime.FileChangesMonitor); lock (s_notificationQueue.SyncRoot) { for (i = 0, n = targets.Count; i < n; i++) { // // Determine whether the change is significant, and if so, add it // to the notification queue. // // - A change is significant if action is other than Added or Modified // - A change is significant if the action is Added and it occurred after // the target started monitoring. // - If the action is Modified: // -- A change is significant if the file contents were modified // and it occurred after the target started monitoring. // -- A change is significant if the DACL changed. We cannot check if // the change was made after the target started monitoring in this case, // as the LastAccess time may not be updated. // target = (FileMonitorTarget)targets[i]; bool isSignificantChange; if ((action != FileAction.Added && action != FileAction.Modified) || fadNew == null) { // Any change other than Added or Modified is significant. // If we have no attributes to examine, the change is significant. isSignificantChange = true; #if DBG reasonFire = "(action != FileAction.Added && action != FileAction.Modified) || fadNew == null"; #endif } else if (action == FileAction.Added) { // Added actions are significant if they occur after we started monitoring. isSignificantChange = IsChangeAfterStartMonitoring(fadNew, target, utcCompletion); #if DBG reasonIgnore = "change occurred before started monitoring"; reasonFire = "file added after start of monitoring"; #endif } else { Debug.Assert(action == FileAction.Modified, "action == FileAction.Modified"); if (utcCompletion == utcLastCompletion) { // File attributes and ACLs will not have changed if the completion is the same // as the last, since we get the attributes after all changes in the completion // have occurred. Therefore if the previous change was Modified, there // is no change that we can detect. // // Notepad fires such spurious notifications when a file is saved. // isSignificantChange = (lastAction != FileAction.Modified); #if DBG reasonIgnore = "spurious FileAction.Modified"; reasonFire = "spurious completion where action != modified"; #endif } else if (fadOld == null) { // There were no attributes before this notification, // so assume the change is significant. We cannot check for // whether the change was after the start of monitoring, // because we don't know if the content changed, or just // DACL, in which case the LastAccessTime will not be updated. isSignificantChange = true; #if DBG reasonFire = "no attributes before this notification"; #endif } else if (daclOld == null || daclOld != daclNew) { // The change is significant if the DACL changed. // We cannot check if the change is after the start of monitoring, // as a change in the DACL does not necessarily update the // LastAccessTime of a file. // If we cannot access the DACL, then we must assume // that it is what has changed. isSignificantChange = true; #if DBG if (daclOld == null) { reasonFire = "unable to access ACL"; } else { reasonFire = "ACL changed"; } #endif } else { // The file content was modified. We cannot guarantee that the // LastWriteTime or FileSize changed when the file changed, as // copying a file preserves the LastWriteTime, and the "touch" // command can reset the LastWriteTime of many files to the same // time. // // If the file content is modified, we can determine if the file // was not changed after the start of monitoring by looking at // the LastAccess time. isSignificantChange = IsChangeAfterStartMonitoring(fadNew, target, utcCompletion); #if DBG reasonIgnore = "change occurred before started monitoring"; reasonFire = "file content modified after start of monitoring"; #endif } } if (isSignificantChange) { #if DBG Debug.Trace("FileChangesMonitorCallback", "Firing change event, reason=" + reasonFire + "\n\tArgs: Action=" + action + "; Completion=" + Debug.FormatUtcDate(utcCompletion) + "; fileName=" + fileName + "\n\t LastAction=" + lastAction + "; LastCompletion=" + Debug.FormatUtcDate(utcLastCompletion) + "\nfadOld=" + ((fadOld != null) ? fadOld.DebugDescription("\t") : "") + "\nfileMon=" + ((fileMon != null) ? fileMon.DebugDescription("\t") : "") + "\n" + target.DebugDescription("\t")); #endif s_notificationQueue.Enqueue(new NotificationQueueItem(target.Callback, action, target.Alias)); } #if DBG else { Debug.Trace("FileChangesMonitorCallback", "Ignoring change event, reason=" + reasonIgnore + "\n\tArgs: Action=" + action + "; Completion=" + Debug.FormatUtcDate(utcCompletion) + "; fileName=" + fileName + "\n\t LastAction=" + lastAction + "; LastCompletion=" + Debug.FormatUtcDate(utcLastCompletion) + "\nfadOld=" + ((fadOld != null) ? fadOld.DebugDescription("\t") : "") + "\nfileMon=" + ((fileMon != null) ? fileMon.DebugDescription("\t") : "") + "\n" + target.DebugDescription("\t")); } #endif } } if (s_notificationQueue.Count > 0 && s_inNotificationThread == 0 && Interlocked.Exchange(ref s_inNotificationThread, 1) == 0) { WorkItem.PostInternal(s_notificationCallback); } } } catch (Exception ex) { Debug.Trace(Debug.TAG_INTERNAL, "Exception thrown processing file change notification" + " action=" + action.ToString() + " fileName" + fileName); Debug.TraceException(Debug.TAG_INTERNAL, ex); } } // Fire notifications on a separate thread from that which received the notifications, // so that we don't block notification collection. static void FireNotifications() { try { // Outer loop: test whether we need to fire notifications and grab the lock for (;;) { // Inner loop: fire notifications until the queue is emptied for (;;) { // Remove an item from the queue. NotificationQueueItem nqi = null; lock (s_notificationQueue.SyncRoot) { if (s_notificationQueue.Count > 0) { nqi = (NotificationQueueItem) s_notificationQueue.Dequeue(); } } if (nqi == null) break; try { Debug.Trace("FileChangesMonitorFireNotification", "Firing change event" + "\n\tArgs: Action=" + nqi.Action + "; fileName=" + nqi.Filename + "; Target=" + nqi.Callback.Target + "(HC=" + nqi.Callback.Target.GetHashCode().ToString("x", NumberFormatInfo.InvariantInfo) + ")"); // Call the callback nqi.Callback(null, new FileChangeEvent(nqi.Action, nqi.Filename)); } catch (Exception ex) { Debug.Trace(Debug.TAG_INTERNAL, "Exception thrown in file change callback" + " action=" + nqi.Action.ToString() + " fileName" + nqi.Filename); Debug.TraceException(Debug.TAG_INTERNAL, ex); } } // Release the lock Interlocked.Exchange(ref s_inNotificationThread, 0); // We need to test again to avoid ---- where a thread that receives notifications adds to the // queue, but does not spawn a thread because s_inNotificationThread = 1 if (s_notificationQueue.Count == 0 || Interlocked.Exchange(ref s_inNotificationThread, 1) != 0) break; } } catch { Interlocked.Exchange(ref s_inNotificationThread, 0); } } #if DBG internal string DebugDescription(string indent) { StringBuilder sb = new StringBuilder(200); string i2 = indent + " "; DictionaryEntryCaseInsensitiveComparer decomparer = new DictionaryEntryCaseInsensitiveComparer(); lock (this) { DictionaryEntry[] fileEntries = new DictionaryEntry[_fileMons.Count]; _fileMons.CopyTo(fileEntries, 0); Array.Sort(fileEntries, decomparer); sb.Append(indent + "System.Web.DirectoryMonitor: " + Directory + "\n"); if (_dirMonCompletion != null) { sb.Append(i2 + "_dirMonCompletion " + _dirMonCompletion.DebugDescription(String.Empty)); } else { sb.Append(i2 + "_dirMonCompletion = \n"); } sb.Append(i2 + GetFileMonitorsCount() + " file monitors...\n"); if (_anyFileMon != null) { sb.Append(_anyFileMon.DebugDescription(i2)); } foreach (DictionaryEntry d in fileEntries) { FileMonitor fileMon = (FileMonitor)d.Value; if (fileMon.FileNameShort == (string)d.Key) continue; sb.Append(fileMon.DebugDescription(i2)); } } return sb.ToString(); } #endif } #endif // !FEATURE_PAL // // Manager for directory monitors. // Provides file change notification services in ASP.NET // sealed class FileChangesMonitor { #if !FEATURE_PAL // FEATURE_PAL does not enable file change notification internal static string[] s_dirsToMonitor = new string[] { HttpRuntime.BinDirectoryName, HttpRuntime.ResourcesDirectoryName, HttpRuntime.CodeDirectoryName, HttpRuntime.WebRefDirectoryName, HttpRuntime.BrowsersDirectoryName }; internal const int MAX_PATH = 260; #pragma warning disable 0649 ReadWriteSpinLock _lockDispose; // spinlock for coordinating dispose #pragma warning restore 0649 bool _disposed; // have we disposed? Hashtable _aliases; // alias -> FileMonitor Hashtable _dirs; // dir -> DirectoryMonitor DirectoryMonitor _dirMonSubdirs; // subdirs monitor for renames Hashtable _subDirDirMons; // Hashtable of DirectoryMonitor used in ListenToSubdirectoryChanges ArrayList _dirMonSpecialDirs; // top level dirs we monitor FileChangeEventHandler _callbackRenameOrCriticaldirChange; // event handler for renames and bindir int _activeCallbackCount; // number of callbacks currently executing DirectoryMonitor _dirMonAppPathInternal; // watches all files and subdirectories (at any level) beneath HttpRuntime.AppDomainAppPathInternal String _appPathInternal; // HttpRuntime.AppDomainAppPathInternal int _FCNMode; // from registry, controls how we monitor directories #if DBG internal static bool s_enableRemoveTargetAssert; #endif // Dev10 927283: We were appending to HttpRuntime._shutdownMessage in DirectoryMonitor.OnFileChange when // we received overwhelming changes and errors, but not all overwhelming file change notifications result // in a shutdown. The fix is to only append to _shutdownMessage when the domain is being shutdown. internal static string GenerateErrorMessage(FileAction action, String fileName = null) { string message = null; if (action == FileAction.Overwhelming) { message = "Overwhelming Change Notification in "; } else if (action == FileAction.Error) { message = "File Change Notification Error in "; } else { return null; } return (fileName != null) ? message + Path.GetDirectoryName(fileName) : message; } internal static HttpException CreateFileMonitoringException(int hr, string path) { string message; bool logEvent = false; switch (hr) { case HResults.E_FILENOTFOUND: case HResults.E_PATHNOTFOUND: message = SR.Directory_does_not_exist_for_monitoring; break; case HResults.E_ACCESSDENIED: message = SR.Access_denied_for_monitoring; logEvent = true; break; case HResults.E_INVALIDARG: message = SR.Invalid_file_name_for_monitoring; break; case HResults.ERROR_TOO_MANY_CMDS: message = SR.NetBios_command_limit_reached; logEvent = true; break; default: message = SR.Failed_to_start_monitoring; break; } if (logEvent) { // Need to raise an eventlog too. UnsafeNativeMethods.RaiseFileMonitoringEventlogEvent( SR.GetString(message, HttpRuntime.GetSafePath(path)) + "\n\r" + SR.GetString(SR.App_Virtual_Path, HttpRuntime.AppDomainAppVirtualPath), path, HttpRuntime.AppDomainAppVirtualPath, hr); } return new HttpException(SR.GetString(message, HttpRuntime.GetSafePath(path)), hr); } internal static string GetFullPath(string alias) { // Assert PathDiscovery before call to Path.GetFullPath try { new FileIOPermission(FileIOPermissionAccess.PathDiscovery, alias).Assert(); } catch { throw CreateFileMonitoringException(HResults.E_INVALIDARG, alias); } string path = Path.GetFullPath(alias); path = FileUtil.RemoveTrailingDirectoryBackSlash(path); return path; } private bool IsBeneathAppPathInternal(string fullPathName) { if (_appPathInternal != null && fullPathName.Length > _appPathInternal.Length+1 && fullPathName.IndexOf(_appPathInternal, StringComparison.OrdinalIgnoreCase) > -1 && fullPathName[_appPathInternal.Length] == Path.DirectorySeparatorChar) { return true; } return false; } private bool IsFCNDisabled { get { return _FCNMode == 1; } } internal FileChangesMonitor(FcnMode mode) { // Possible values for DWORD FCNMode: // does not exist == default behavior (create DirectoryMonitor for each subdir) // 0 or >2 == default behavior (create DirectoryMonitor for each subdir) // 1 == disable File Change Notifications (FCN) // 2 == create 1 DirectoryMonitor for AppPathInternal and watch subtrees switch (mode) { case FcnMode.NotSet: // If the mode is not set, we use the registry key's value UnsafeNativeMethods.GetDirMonConfiguration(out _FCNMode); break; case FcnMode.Disabled: _FCNMode = 1; break; case FcnMode.Single: _FCNMode = 2; break; case FcnMode.Default: default: _FCNMode = 0; break; } if (IsFCNDisabled) { return; } _aliases = Hashtable.Synchronized(new Hashtable(StringComparer.OrdinalIgnoreCase)); _dirs = new Hashtable(StringComparer.OrdinalIgnoreCase); _subDirDirMons = new Hashtable(StringComparer.OrdinalIgnoreCase); if (_FCNMode == 2 && HttpRuntime.AppDomainAppPathInternal != null) { _appPathInternal = GetFullPath(HttpRuntime.AppDomainAppPathInternal); _dirMonAppPathInternal = new DirectoryMonitor(_appPathInternal, _FCNMode); } #if DBG if ((int)Misc.GetAspNetRegValue(null /*subKey*/, "FCMRemoveTargetAssert", 0) > 0) { s_enableRemoveTargetAssert = true; } #endif } internal bool IsDirNameMonitored(string fullPath, string dirName) { // is it one of the not-so-special directories we're monitoring? if (_dirs.ContainsKey(fullPath)) { return true; } // is it one of the special directories (bin, App_Code, etc) or a subfolder? foreach (string specialDirName in s_dirsToMonitor) { if (StringUtil.StringStartsWithIgnoreCase(dirName, specialDirName)) { // a special directory? if (dirName.Length == specialDirName.Length) { return true; } // a subfolder? else if (dirName.Length > specialDirName.Length && dirName[specialDirName.Length] == Path.DirectorySeparatorChar) { return true; } } } // Dev10 Bug 663511: Deletes, moves, and renames of the App_LocalResources folder may be ignored if (dirName.IndexOf(HttpRuntime.LocalResourcesDirectoryName, StringComparison.OrdinalIgnoreCase) > -1) { return true; } // we're not monitoring it return false; } // // Find the directory monitor. If not found, maybe add it. // If the directory is not actively monitoring, ensure that // it still represents an accessible directory. // DirectoryMonitor FindDirectoryMonitor(string dir, bool addIfNotFound, bool throwOnError) { DirectoryMonitor dirMon; FileAttributesData fad = null; int hr; dirMon = (DirectoryMonitor)_dirs[dir]; if (dirMon != null) { if (!dirMon.IsMonitoring()) { hr = FileAttributesData.GetFileAttributes(dir, out fad); if (hr != HResults.S_OK || (fad.FileAttributes & FileAttributes.Directory) == 0) { dirMon = null; } } } if (dirMon != null || !addIfNotFound) { return dirMon; } lock (_dirs.SyncRoot) { // Check again, this time under synchronization. dirMon = (DirectoryMonitor)_dirs[dir]; if (dirMon != null) { if (!dirMon.IsMonitoring()) { // Fail if it's not a directory or inaccessible. hr = FileAttributesData.GetFileAttributes(dir, out fad); if (hr == HResults.S_OK && (fad.FileAttributes & FileAttributes.Directory) == 0) { // Fail if it's not a directory. hr = HResults.E_INVALIDARG; } if (hr != HResults.S_OK) { // Not accessible or a dir, so stop monitoring and remove. _dirs.Remove(dir); dirMon.StopMonitoring(); if (addIfNotFound && throwOnError) { throw FileChangesMonitor.CreateFileMonitoringException(hr, dir); } return null; } } } else if (addIfNotFound) { // Fail if it's not a directory or inaccessible. hr = FileAttributesData.GetFileAttributes(dir, out fad); if (hr == HResults.S_OK && (fad.FileAttributes & FileAttributes.Directory) == 0) { hr = HResults.E_INVALIDARG; } if (hr == HResults.S_OK) { // Add a new directory monitor. dirMon = new DirectoryMonitor(dir, false, UnsafeNativeMethods.RDCW_FILTER_FILE_AND_DIR_CHANGES, _FCNMode); _dirs.Add(dir, dirMon); } else if (throwOnError) { throw FileChangesMonitor.CreateFileMonitoringException(hr, dir); } } } return dirMon; } // Remove the aliases of a file monitor. internal void RemoveAliases(FileMonitor fileMon) { if (IsFCNDisabled) { return; } foreach (DictionaryEntry entry in fileMon.Aliases) { if (_aliases[entry.Key] == fileMon) { _aliases.Remove(entry.Key); } } } // // Request to monitor a file, which may or may not exist. // internal DateTime StartMonitoringFile(string alias, FileChangeEventHandler callback) { Debug.Trace("FileChangesMonitor", "StartMonitoringFile\n" + "\tArgs: File=" + alias + "; Callback=" + callback.Target + "(HC=" + callback.Target.GetHashCode().ToString("x", NumberFormatInfo.InvariantInfo) + ")"); FileMonitor fileMon; DirectoryMonitor dirMon; string fullPathName, dir, file; bool addAlias = false; if (alias == null) { throw CreateFileMonitoringException(HResults.E_INVALIDARG, alias); } if (IsFCNDisabled) { fullPathName = GetFullPath(alias); FindFileData ffd = null; int hr = FindFileData.FindFile(fullPathName, out ffd); if (hr == HResults.S_OK) { return ffd.FileAttributesData.UtcLastWriteTime; } else { return DateTime.MinValue; } } using (new ApplicationImpersonationContext()) { _lockDispose.AcquireReaderLock(); try{ // Don't start monitoring if disposed. if (_disposed) { return DateTime.MinValue; } fileMon = (FileMonitor)_aliases[alias]; if (fileMon != null) { // Used the cached directory monitor and file name. dirMon = fileMon.DirectoryMonitor; file = fileMon.FileNameLong; } else { addAlias = true; if (alias.Length == 0 || !UrlPath.IsAbsolutePhysicalPath(alias)) { throw CreateFileMonitoringException(HResults.E_INVALIDARG, alias); } // // Get the directory and file name, and lookup // the directory monitor. // fullPathName = GetFullPath(alias); if (IsBeneathAppPathInternal(fullPathName)) { dirMon = _dirMonAppPathInternal; file = fullPathName.Substring(_appPathInternal.Length+1); } else { dir = UrlPath.GetDirectoryOrRootName(fullPathName); file = Path.GetFileName(fullPathName); if (String.IsNullOrEmpty(file)) { // not a file throw CreateFileMonitoringException(HResults.E_INVALIDARG, alias); } dirMon = FindDirectoryMonitor(dir, true /*addIfNotFound*/, true /*throwOnError*/); } } fileMon = dirMon.StartMonitoringFileWithAssert(file, callback, alias); if (addAlias) { _aliases[alias] = fileMon; } } finally { _lockDispose.ReleaseReaderLock(); } FileAttributesData fad; fileMon.DirectoryMonitor.GetFileAttributes(file, out fad); Debug.Dump("FileChangesMonitor", this); if (fad != null) { return fad.UtcLastWriteTime; } else { return DateTime.MinValue; } } } // // Request to monitor a path, which may be file, directory, or non-existent // file. // internal DateTime StartMonitoringPath(string alias, FileChangeEventHandler callback, out FileAttributesData fad) { Debug.Trace("FileChangesMonitor", "StartMonitoringPath\n" + "\tArgs: File=" + alias + "; Callback=" + callback.Target + "(HC=" + callback.Target.GetHashCode().ToString("x", NumberFormatInfo.InvariantInfo) + ")"); FileMonitor fileMon = null; DirectoryMonitor dirMon = null; string fullPathName, dir, file = null; bool addAlias = false; fad = null; if (alias == null) { throw new HttpException(SR.GetString(SR.Invalid_file_name_for_monitoring, String.Empty)); } if (IsFCNDisabled) { fullPathName = GetFullPath(alias); FindFileData ffd = null; int hr = FindFileData.FindFile(fullPathName, out ffd); if (hr == HResults.S_OK) { fad = ffd.FileAttributesData; return ffd.FileAttributesData.UtcLastWriteTime; } else { return DateTime.MinValue; } } using (new ApplicationImpersonationContext()) { _lockDispose.AcquireReaderLock(); try{ if (_disposed) { return DateTime.MinValue; } // do/while loop once to make breaking out easy do { fileMon = (FileMonitor)_aliases[alias]; if (fileMon != null) { // Used the cached directory monitor and file name. file = fileMon.FileNameLong; fileMon = fileMon.DirectoryMonitor.StartMonitoringFileWithAssert(file, callback, alias); continue; } addAlias = true; if (alias.Length == 0 || !UrlPath.IsAbsolutePhysicalPath(alias)) { throw new HttpException(SR.GetString(SR.Invalid_file_name_for_monitoring, HttpRuntime.GetSafePath(alias))); } fullPathName = GetFullPath(alias); // see if the path is beneath HttpRuntime.AppDomainAppPathInternal if (IsBeneathAppPathInternal(fullPathName)) { dirMon = _dirMonAppPathInternal; file = fullPathName.Substring(_appPathInternal.Length+1); fileMon = dirMon.StartMonitoringFileWithAssert(file, callback, alias); continue; } // try treating the path as a directory dirMon = FindDirectoryMonitor(fullPathName, false, false); if (dirMon != null) { fileMon = dirMon.StartMonitoringFileWithAssert(null, callback, alias); continue; } // try treaing the path as a file dir = UrlPath.GetDirectoryOrRootName(fullPathName); file = Path.GetFileName(fullPathName); if (!String.IsNullOrEmpty(file)) { dirMon = FindDirectoryMonitor(dir, false, false); if (dirMon != null) { // try to add it - a file is the common case, // and we avoid hitting the disk twice try { fileMon = dirMon.StartMonitoringFileWithAssert(file, callback, alias); } catch { } if (fileMon != null) { continue; } } } // We aren't monitoring this path or its parent directory yet. // Hit the disk to determine if it's a directory or file. dirMon = FindDirectoryMonitor(fullPathName, true, false); if (dirMon != null) { // It's a directory, so monitor all changes in it file = null; } else { // It's not a directory, so treat as file if (String.IsNullOrEmpty(file)) { throw CreateFileMonitoringException(HResults.E_INVALIDARG, alias); } dirMon = FindDirectoryMonitor(dir, true, true); } fileMon = dirMon.StartMonitoringFileWithAssert(file, callback, alias); } while (false); if (!fileMon.IsDirectory) { fileMon.DirectoryMonitor.GetFileAttributes(file, out fad); } if (addAlias) { _aliases[alias] = fileMon; } } finally { _lockDispose.ReleaseReaderLock(); } Debug.Dump("FileChangesMonitor", this); if (fad != null) { return fad.UtcLastWriteTime; } else { return DateTime.MinValue; } } } // // Request to monitor the bin directory and directory renames anywhere under app // internal void StartMonitoringDirectoryRenamesAndBinDirectory(string dir, FileChangeEventHandler callback) { Debug.Trace("FileChangesMonitor", "StartMonitoringDirectoryRenamesAndBinDirectory\n" + "\tArgs: File=" + dir + "; Callback=" + callback.Target + "(HC=" + callback.Target.GetHashCode().ToString("x", NumberFormatInfo.InvariantInfo) + ")"); if (String.IsNullOrEmpty(dir)) { throw new HttpException(SR.GetString(SR.Invalid_file_name_for_monitoring, String.Empty)); } if (IsFCNDisabled) { return; } #if DBG 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."); #endif using (new ApplicationImpersonationContext()) { _lockDispose.AcquireReaderLock(); try { if (_disposed) { return; } _callbackRenameOrCriticaldirChange = callback; string dirRoot = GetFullPath(dir); // Monitor bin directory and app directory (for renames only) separately // to avoid overwhelming changes when the user writes to a subdirectory // of the app directory. _dirMonSubdirs = new DirectoryMonitor(dirRoot, true, UnsafeNativeMethods.RDCW_FILTER_DIR_RENAMES, true, _FCNMode); try { _dirMonSubdirs.StartMonitoringFileWithAssert(null, new FileChangeEventHandler(this.OnSubdirChange), dirRoot); } catch { ((IDisposable)_dirMonSubdirs).Dispose(); _dirMonSubdirs = null; throw; } _dirMonSpecialDirs = new ArrayList(); for (int i=0; i