diff options
author | Duncan Mak <duncan.mak@xamarin.com> | 2015-02-02 22:18:31 +0300 |
---|---|---|
committer | Duncan Mak <duncan.mak@xamarin.com> | 2015-02-02 22:18:31 +0300 |
commit | de2f33f7b2d390e297e00b2ae33502ef37f79115 (patch) | |
tree | 63d6a7a768d1a1520b8a802914425b23868e78b8 | |
parent | 7569e1a18d6ed45c9b65a74213d7e8ecc81f7b3d (diff) |
Revert "Revert "[System.IO] Phase 2 of revamping KeventWatcher (FileSystemWatcher implementation for OS X/kqueue). Dozens of bugs and reliability issues fixed.""mono-3.12.0.76
This reverts commit d778fb3631576e42dfa35c53951ea3334f9f2651.
-rw-r--r-- | mcs/class/System/System.IO/FileSystemWatcher.cs | 69 | ||||
-rw-r--r-- | mcs/class/System/System.IO/KeventWatcher.cs | 466 | ||||
-rw-r--r-- | mcs/class/System/System.IO/SearchPattern.cs | 23 |
3 files changed, 370 insertions, 188 deletions
diff --git a/mcs/class/System/System.IO/FileSystemWatcher.cs b/mcs/class/System/System.IO/FileSystemWatcher.cs index 125f07805f9..0d293e53188 100644 --- a/mcs/class/System/System.IO/FileSystemWatcher.cs +++ b/mcs/class/System/System.IO/FileSystemWatcher.cs @@ -183,7 +183,10 @@ namespace System.IO { internal SearchPattern2 Pattern { get { if (pattern == null) { - pattern = new SearchPattern2 (MangledFilter); + if (watcher.GetType () == typeof (KeventWatcher)) + pattern = new SearchPattern2 (MangledFilter, true); //assume we want to ignore case (OS X) + else + pattern = new SearchPattern2 (MangledFilter); } return pattern; } @@ -372,52 +375,60 @@ namespace System.IO { ErrorEvent, RenameEvent } - private void RaiseEvent (Delegate ev, EventArgs arg, EventType evtype) - { - if (ev == null) - return; - - if (synchronizingObject == null) { - switch (evtype) { - case EventType.RenameEvent: - ((RenamedEventHandler)ev).BeginInvoke (this, (RenamedEventArgs) arg, null, null); - break; - case EventType.ErrorEvent: - ((ErrorEventHandler)ev).BeginInvoke (this, (ErrorEventArgs) arg, null, null); - break; - case EventType.FileSystemEvent: - ((FileSystemEventHandler)ev).BeginInvoke (this, (FileSystemEventArgs) arg, null, null); - break; - } - return; - } - synchronizingObject.BeginInvoke (ev, new object [] {this, arg}); - } - protected void OnChanged (FileSystemEventArgs e) { - RaiseEvent (Changed, e, EventType.FileSystemEvent); + if (Changed == null) + return; + + if (synchronizingObject == null) + Changed (this, e); + else + synchronizingObject.BeginInvoke (Changed, new object[] { this, e }); } protected void OnCreated (FileSystemEventArgs e) { - RaiseEvent (Created, e, EventType.FileSystemEvent); + if (Created == null) + return; + + if (synchronizingObject == null) + Created (this, e); + else + synchronizingObject.BeginInvoke (Created, new object[] { this, e }); } protected void OnDeleted (FileSystemEventArgs e) { - RaiseEvent (Deleted, e, EventType.FileSystemEvent); + if (Deleted == null) + return; + + if (synchronizingObject == null) + Deleted (this, e); + else + synchronizingObject.BeginInvoke (Deleted, new object[] { this, e }); } - protected void OnError (ErrorEventArgs e) + internal void OnError (ErrorEventArgs e) { - RaiseEvent (Error, e, EventType.ErrorEvent); + if (Error == null) + return; + + if (synchronizingObject == null) + Error (this, e); + else + synchronizingObject.BeginInvoke (Error, new object[] { this, e }); } protected void OnRenamed (RenamedEventArgs e) { - RaiseEvent (Renamed, e, EventType.RenameEvent); + if (Renamed == null) + return; + + if (synchronizingObject == null) + Renamed (this, e); + else + synchronizingObject.BeginInvoke (Renamed, new object[] { this, e }); } public WaitForChangedResult WaitForChanged (WatcherChangeTypes changeType) diff --git a/mcs/class/System/System.IO/KeventWatcher.cs b/mcs/class/System/System.IO/KeventWatcher.cs index 2b96de34404..f3cc7dcc183 100644 --- a/mcs/class/System/System.IO/KeventWatcher.cs +++ b/mcs/class/System/System.IO/KeventWatcher.cs @@ -4,6 +4,7 @@ // Authors: // Geoff Norton (gnorton@customerdna.com) // Cody Russell (cody@xamarin.com) +// Alexis Christoforides (lexas@xamarin.com) // // (c) 2004 Geoff Norton // Copyright 2014 Xamarin Inc @@ -36,6 +37,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using System.Threading; +using System.Reflection; namespace System.IO { @@ -73,6 +75,7 @@ namespace System.IO { VM = -11 } + [Flags] enum FilterFlags : uint { ReadPoll = EventFlags.Flag0, ReadOutOfBand = EventFlags.Flag1, @@ -128,7 +131,7 @@ namespace System.IO { [StructLayout(LayoutKind.Sequential)] struct kevent : IDisposable { - public int ident; + public UIntPtr ident; public EventFilter filter; public EventFlags flags; public FilterFlags fflags; @@ -140,17 +143,21 @@ namespace System.IO { if (udata != IntPtr.Zero) Marshal.FreeHGlobal (udata); } + + } + [StructLayout(LayoutKind.Sequential)] struct timespec { - public int tv_sec; - public int tv_usec; + public IntPtr tv_sec; + public IntPtr tv_usec; } class PathData { public string Path; public bool IsDirectory; + public int Fd; } class KqueueMonitor : IDisposable @@ -168,211 +175,367 @@ namespace System.IO { public void Dispose () { - Stop (); + CleanUp (); } public void Start () { - conn = kqueue (); + lock (stateLock) { + if (started) + return; - if (thread == null) { - thread = new Thread (new ThreadStart (Monitor)); + conn = kqueue (); + + if (conn == -1) + throw new IOException (String.Format ( + "kqueue() error at init, error code = '{0}'", Marshal.GetLastWin32Error ())); + + thread = new Thread (() => DoMonitor ()); thread.IsBackground = true; thread.Start (); - } - var pathData = Add (fsw.FullPath); + startedEvent.WaitOne (); - Scan (pathData); + if (failedInit) { + thread.Join (); + CleanUp (); + throw new IOException ("Monitor thread failed while initializing."); + } + else + started = true; + } } public void Stop () { - stop = true; - - if (thread != null) - thread.Interrupt (); + lock (stateLock) { + if (!started) + return; + + requestStop = true; + thread.Join (); + requestStop = false; + + CleanUp (); + started = false; + } + } + void CleanUp () + { if (conn != -1) close (conn); conn = -1; + + foreach (int fd in fdsDict.Keys) + close (fd); + + fdsDict.Clear (); + pathsDict.Clear (); } - private PathData FindPath (string path) + void DoMonitor () { - foreach (KeyValuePair<PathData, int> kv in paths) { - if (kv.Key.Path == path) - return kv.Key; + Exception exc = null; + failedInit = false; + + try { + Setup (); + } catch (Exception e) { + failedInit = true; + exc = e; + } finally { + startedEvent.Set (); + } + + if (failedInit) { + fsw.OnError (new ErrorEventArgs (exc)); + return; } - return null; + try { + Monitor (); + } catch (Exception e) { + exc = e; + } finally { + if (!requestStop) { // failure + CleanUp (); + started = false; + } + if (exc != null) + fsw.OnError (new ErrorEventArgs (exc)); + } } - private PathData FindPath (int fd) + void Setup () + { + var initialFds = new List<int> (); + + // GetFilenameFromFd() returns the *realpath* which can be different than fsw.FullPath because symlinks. + // If so, introduce a fixup step. + int fd = open (fsw.FullPath, O_EVTONLY, 0); + var resolvedFullPath = GetFilenameFromFd (fd); + close (fd); + + if (resolvedFullPath != fsw.FullPath) + fixupPath = resolvedFullPath; + else + fixupPath = null; + + Scan (fsw.FullPath, false, ref initialFds); + + var immediate_timeout = new timespec { tv_sec = (IntPtr)0, tv_usec = (IntPtr)0 }; + var eventBuffer = new kevent[0]; // we don't want to take any events from the queue at this point + var changes = CreateChangeList (ref initialFds); + + int numEvents = kevent (conn, changes, changes.Length, eventBuffer, eventBuffer.Length, ref immediate_timeout); + + if (numEvents == -1) { + var errMsg = String.Format ("kevent() error at initial event registration, error code = '{0}'", Marshal.GetLastWin32Error ()); + throw new IOException (errMsg); + } + } + + kevent[] CreateChangeList (ref List<int> FdList) { - foreach (KeyValuePair<PathData, int> kv in paths) { - if (kv.Value == fd) - return kv.Key; + if (FdList.Count == 0) + return emptyEventList; + + var changes = new List<kevent> (); + foreach (int fd in FdList) { + var change = new kevent { + + ident = (UIntPtr)fd, + filter = EventFilter.Vnode, + flags = EventFlags.Add | EventFlags.Enable | EventFlags.Clear, + fflags = FilterFlags.VNodeDelete | FilterFlags.VNodeExtend | + FilterFlags.VNodeRename | FilterFlags.VNodeAttrib | + FilterFlags.VNodeLink | FilterFlags.VNodeRevoke | + FilterFlags.VNodeWrite, + data = IntPtr.Zero, + udata = IntPtr.Zero + }; + + changes.Add (change); } + FdList.Clear (); - return null; + return changes.ToArray (); } - private void Monitor () + void Monitor () { - bool firstRun = true; + var timeout = new timespec { tv_sec = (IntPtr)0, tv_usec = (IntPtr)500000000 }; + var eventBuffer = new kevent[32]; + var newFds = new List<int> (); + List<PathData> removeQueue = new List<PathData> (); + List<string> rescanQueue = new List<string> (); - while (!stop) { - removeQueue.ForEach (Remove); + while (!requestStop) { + var changes = CreateChangeList (ref newFds); - var changes = new List<kevent> (); - var outEvents = new List<kevent> (); + int numEvents = kevent (conn, changes, changes.Length, eventBuffer, eventBuffer.Length, ref timeout); - rescanQueue.ForEach (fd => { - var path = FindPath (fd); - Scan (path, !firstRun); - }); - rescanQueue.Clear (); - - foreach (KeyValuePair<PathData, int> kv in paths) { - var change = new kevent { - ident = kv.Value, - filter = EventFilter.Vnode, - flags = EventFlags.Add | EventFlags.Enable | EventFlags.Clear, - fflags = FilterFlags.VNodeDelete | FilterFlags.VNodeExtend | - FilterFlags.VNodeRename | FilterFlags.VNodeAttrib | - FilterFlags.VNodeLink | FilterFlags.VNodeRevoke | - FilterFlags.VNodeWrite, - data = IntPtr.Zero, - udata = IntPtr.Zero - }; - - changes.Add (change); - outEvents.Add (new kevent ()); + if (numEvents == -1) { + var errMsg = String.Format ("kevent() error, error code = '{0}'", Marshal.GetLastWin32Error ()); + fsw.OnError (new ErrorEventArgs (new IOException (errMsg))); } - if (changes.Count > 0) { - var outArray = outEvents.ToArray (); - var changesArray = changes.ToArray (); - int numEvents = kevent (conn, changesArray, changesArray.Length, outArray, outArray.Length, IntPtr.Zero); - - for (var i = 0; i < numEvents; i++) { - var kevt = outArray [i]; - var pathData = FindPath (kevt.ident); - - if ((kevt.fflags & FilterFlags.VNodeDelete) != 0) { - removeQueue.Add (kevt.ident); - PostEvent (FileAction.Removed, pathData.Path); - } else if (((kevt.fflags & FilterFlags.VNodeRename) != 0) || ((kevt.fflags & FilterFlags.VNodeRevoke) != 0) || ((kevt.fflags & FilterFlags.VNodeWrite) != 0)) { - if (pathData.IsDirectory && Directory.Exists (pathData.Path)) - rescanQueue.Add (kevt.ident); - - if ((kevt.fflags & FilterFlags.VNodeRename) != 0) { - var fd = paths [pathData]; - var newFilename = GetFilenameFromFd (fd); - var oldFilename = pathData.Path; - - Remove (pathData); - PostEvent (FileAction.RenamedNewName, oldFilename, newFilename); - - Add (newFilename, false); - } - } else if ((kevt.fflags & FilterFlags.VNodeAttrib) != 0) { - PostEvent (FileAction.Modified, pathData.Path); - } + if (numEvents == 0) + continue; + + for (var i = 0; i < numEvents; i++) { + var kevt = eventBuffer [i]; + var pathData = fdsDict [(int)kevt.ident]; + + if ((kevt.flags & EventFlags.Error) == EventFlags.Error) { + var errMsg = String.Format ("kevent() error watching path '{0}', error code = '{1}'", pathData.Path, kevt.data); + fsw.OnError (new ErrorEventArgs (new IOException (errMsg))); + continue; } - } else { - Thread.Sleep (500); + + if ((kevt.fflags & FilterFlags.VNodeDelete) == FilterFlags.VNodeDelete || (kevt.fflags & FilterFlags.VNodeRevoke) == FilterFlags.VNodeRevoke) + removeQueue.Add (pathData); + + else if ((kevt.fflags & FilterFlags.VNodeWrite) == FilterFlags.VNodeWrite) { + if (pathData.IsDirectory) + rescanQueue.Add (pathData.Path); + else + PostEvent (FileAction.Modified, pathData.Path); + } + + else if ((kevt.fflags & FilterFlags.VNodeRename) == FilterFlags.VNodeRename) { + var newFilename = GetFilenameFromFd (pathData.Fd); + + if (newFilename.StartsWith (fsw.FullPath)) + Rename (pathData, newFilename); + else //moved outside of our watched dir so stop watching + RemoveTree (pathData); + } + + else if ((kevt.fflags & FilterFlags.VNodeAttrib) == FilterFlags.VNodeAttrib || (kevt.fflags & FilterFlags.VNodeExtend) == FilterFlags.VNodeExtend) + PostEvent (FileAction.Modified, pathData.Path); } - firstRun = false; + removeQueue.ForEach (Remove); + removeQueue.Clear (); + + rescanQueue.ForEach (path => { + Scan (path, true, ref newFds); + }); + rescanQueue.Clear (); } } - private PathData Add (string path, bool postEvents = false) + PathData Add (string path, bool postEvents, ref List<int> fds) { + PathData pathData; + pathsDict.TryGetValue (path, out pathData); + + if (pathData != null) + return pathData; + var fd = open (path, O_EVTONLY, 0); - if (fd == -1) + if (fd == -1) { + fsw.OnError (new ErrorEventArgs (new IOException (String.Format ( + "open() error while attempting to process path '{0}', error code = '{1}'", path, Marshal.GetLastWin32Error ())))); return null; + } - var attrs = File.GetAttributes (path); - bool isDir = false; - if ((attrs & FileAttributes.Directory) == FileAttributes.Directory) - isDir = true; + try { + fds.Add (fd); - var pathData = new PathData { - Path = path, - IsDirectory = isDir - }; + var attrs = File.GetAttributes (path); - if (FindPath (path) == null) { - paths.Add (pathData, fd); + pathData = new PathData { + Path = path, + Fd = fd, + IsDirectory = (attrs & FileAttributes.Directory) == FileAttributes.Directory + }; + + pathsDict.Add (path, pathData); + fdsDict.Add (fd, pathData); if (postEvents) PostEvent (FileAction.Added, path); + + return pathData; + } catch (Exception e) { + close (fd); + fsw.OnError (new ErrorEventArgs (e)); + return null; } - return pathData; } - private void Remove (int fd) + void Remove (PathData pathData) { - var path = FindPath (fd); - paths.Remove (path); - removeQueue.Remove (fd); - - close (fd); + fdsDict.Remove (pathData.Fd); + pathsDict.Remove (pathData.Path); + close (pathData.Fd); + PostEvent (FileAction.Removed, pathData.Path); } - private void Remove (PathData pathData) + void RemoveTree (PathData pathData) { - var fd = paths [pathData]; - paths.Remove (pathData); - removeQueue.Remove (fd); + var toRemove = new List<PathData> (); - close (fd); + toRemove.Add (pathData); + + if (pathData.IsDirectory) { + var prefix = pathData.Path + Path.DirectorySeparatorChar; + foreach (var path in pathsDict.Keys) + if (path.StartsWith (prefix)) { + toRemove.Add (pathsDict [path]); + } + } + toRemove.ForEach (Remove); } - private void Scan (PathData pathData, bool postEvents = false) + void Rename (PathData pathData, string newRoot) { - var path = pathData.Path; + var toRename = new List<PathData> (); + var oldRoot = pathData.Path; + + toRename.Add (pathData); + + if (pathData.IsDirectory) { + var prefix = oldRoot + Path.DirectorySeparatorChar; + foreach (var path in pathsDict.Keys) + if (path.StartsWith (prefix)) + toRename.Add (pathsDict [path]); + } - Add (path, postEvents); + toRename.ForEach ((pd) => { + var oldPath = pd.Path; + var newPath = newRoot + oldPath.Substring (oldRoot.Length); + pd.Path = newPath; + pathsDict.Remove (oldPath); + pathsDict.Add (newPath, pd); + }); - if (!fsw.IncludeSubdirectories) + PostEvent (FileAction.RenamedNewName, oldRoot, newRoot); + } + + void Scan (string path, bool postEvents, ref List<int> fds) + { + if (requestStop) + return; + + var pathData = Add (path, postEvents, ref fds); + + if (pathData == null) + return; + + if (!pathData.IsDirectory) return; - var attrs = File.GetAttributes (path); - if ((attrs & FileAttributes.Directory) == FileAttributes.Directory) { - var dirsToProcess = new List<string> (); - dirsToProcess.Add (path); + var dirsToProcess = new List<string> (); + dirsToProcess.Add (path); + + while (dirsToProcess.Count > 0) { + var tmp = dirsToProcess [0]; + dirsToProcess.RemoveAt (0); + + var info = new DirectoryInfo (tmp); + FileSystemInfo[] fsInfos = null; + try { + fsInfos = info.GetFileSystemInfos (); + + } catch (IOException) { + // this can happen if the directory has been deleted already. + // that's okay, just keep processing the other dirs. + fsInfos = new FileSystemInfo[0]; + } - while (dirsToProcess.Count > 0) { - var tmp = dirsToProcess [0]; - dirsToProcess.RemoveAt (0); + foreach (var fsi in fsInfos) { + if ((fsi.Attributes & FileAttributes.Directory) == FileAttributes.Directory && !fsw.IncludeSubdirectories) + continue; - var info = new DirectoryInfo (tmp); - foreach (var fsi in info.GetFileSystemInfos ()) { - if (Add (fsi.FullName, postEvents) == null) - continue; + if ((fsi.Attributes & FileAttributes.Directory) != FileAttributes.Directory && !fsw.Pattern.IsMatch (fsi.FullName)) + continue; - var childAttrs = File.GetAttributes (fsi.FullName); - if ((childAttrs & FileAttributes.Directory) == FileAttributes.Directory) - dirsToProcess.Add (fsi.FullName); - } + var currentPathData = Add (fsi.FullName, postEvents, ref fds); + + if (currentPathData != null && currentPathData.IsDirectory) + dirsToProcess.Add (fsi.FullName); } } } - - private void PostEvent (FileAction action, string path, string newPath = null) + + void PostEvent (FileAction action, string path, string newPath = null) { RenamedEventArgs renamed = null; if (action == 0) return; + // only post events that match filter pattern. check both old and new paths for renames + if (!fsw.Pattern.IsMatch (path) && (newPath == null || !fsw.Pattern.IsMatch (newPath))) + return; + if (action == FileAction.RenamedNewName) renamed = new RenamedEventArgs (WatcherChangeTypes.Renamed, "", newPath, path); @@ -388,23 +551,36 @@ namespace System.IO { private string GetFilenameFromFd (int fd) { - var sb = new StringBuilder (1024); + var sb = new StringBuilder (__DARWIN_MAXPATHLEN); - if (fcntl (fd, F_GETPATH, sb) != -1) + if (fcntl (fd, F_GETPATH, sb) != -1) { + if (fixupPath != null) + sb.Replace (fixupPath, fsw.FullPath, 0, fixupPath.Length); // see Setup() return sb.ToString (); - else + } else { + fsw.OnError (new ErrorEventArgs (new IOException (String.Format ( + "fcntl() error while attempting to get path for fd '{0}', error code = '{1}'", fd, Marshal.GetLastWin32Error ())))); return String.Empty; + } } - private const int O_EVTONLY = 0x8000; - private const int F_GETPATH = 50; - private FileSystemWatcher fsw; - private int conn; - private Thread thread; - private bool stop; - private readonly List<int> removeQueue = new List<int> (); - private readonly List<int> rescanQueue = new List<int> (); - private readonly Dictionary<PathData, int> paths = new Dictionary<PathData, int> (); + const int O_EVTONLY = 0x8000; + const int F_GETPATH = 50; + const int __DARWIN_MAXPATHLEN = 1024; + static readonly kevent[] emptyEventList = new System.IO.kevent[0]; + + FileSystemWatcher fsw; + int conn; + Thread thread; + volatile bool requestStop = false; + AutoResetEvent startedEvent = new AutoResetEvent (false); + bool started = false; + bool failedInit = false; + object stateLock = new object (); + + readonly Dictionary<string, PathData> pathsDict = new Dictionary<string, PathData> (); + readonly Dictionary<int, PathData> fdsDict = new Dictionary<int, PathData> (); + string fixupPath = null; [DllImport ("libc", EntryPoint="fcntl", CharSet=CharSet.Auto, SetLastError=true)] static extern int fcntl (int file_names_by_descriptor, int cmd, StringBuilder sb); @@ -419,7 +595,7 @@ namespace System.IO { extern static int kqueue (); [DllImport ("libc")] - extern static int kevent(int kq, [In]kevent[] ev, int nchanges, [Out]kevent[] evtlist, int nevents, IntPtr time); + extern static int kevent (int kq, [In]kevent[] ev, int nchanges, [Out]kevent[] evtlist, int nevents, [In] ref timespec time); } class KeventWatcher : IFileWatcher @@ -467,10 +643,9 @@ namespace System.IO { monitor = (KqueueMonitor)watches [fsw]; } else { monitor = new KqueueMonitor (fsw); + watches.Add (fsw, monitor); } - - watches.Add (fsw, monitor); - + monitor.Start (); } @@ -482,8 +657,7 @@ namespace System.IO { monitor.Stop (); } - - + [DllImport ("libc")] extern static int close (int fd); diff --git a/mcs/class/System/System.IO/SearchPattern.cs b/mcs/class/System/System.IO/SearchPattern.cs index 0fbab4e5e7e..f302dffaaa0 100644 --- a/mcs/class/System/System.IO/SearchPattern.cs +++ b/mcs/class/System/System.IO/SearchPattern.cs @@ -47,7 +47,7 @@ namespace System.IO { Compile (pattern); } - // OSX has a retarded case-insensitive yet case-aware filesystem + // OSX has a case-insensitive yet case-aware filesystem // so we need a overload in here for the Kqueue watcher public bool IsMatch (string text, bool ignorecase) { @@ -55,20 +55,17 @@ namespace System.IO { bool match = String.Compare (pattern, text, ignorecase) == 0; if (match) return true; - - // This is a special case for FSW. It needs to match e.g. subdir/file.txt - // when the pattern is "file.txt" - int idx = text.LastIndexOf ('/'); - if (idx == -1) - return false; - idx++; - if (idx == text.Length) - return false; - - return (String.Compare (pattern, text.Substring (idx), ignorecase) == 0); } + + // This is a special case for FSW. It needs to match e.g. subdir/file.txt + // when the pattern is "file.txt" + var fileName = Path.GetFileName (text); + + if (!hasWildcard) + return (String.Compare (pattern, fileName, ignorecase) == 0); + - return Match (ops, text, 0); + return Match (ops, fileName, 0); } public bool IsMatch (string text) |