[System.IO.KeventWatcher] More fixes and MS behavior matching for the new implementation.
authorAlexis Christoforides <alexis@thenull.net>
Fri, 5 Dec 2014 20:05:21 +0000 (15:05 -0500)
committerAlexis Christoforides <alexis@thenull.net>
Fri, 5 Dec 2014 20:09:59 +0000 (15:09 -0500)
* Fixed the FileSystemEventArgs.Name property for all events
* Fixed regression of bug #5747 (and related path normalizing issues) https://bugzilla.xamarin.com/show_bug.cgi?id=5747
* Improved error handling, set hard limit on file descriptors to 200 for now to help isolate issues
* Fixed issues with rapid event Changed+Renamed firing

mcs/class/System/System.IO/KeventWatcher.cs

index f3cc7dcc183200a81a8ca409b3ab9e49f936f7ec..09db0b238968471309aa35f762d95eadcefdfacb 100644 (file)
@@ -196,10 +196,10 @@ namespace System.IO {
 
                                startedEvent.WaitOne ();
 
-                               if (failedInit) {
+                               if (monitorExc != null) {
                                        thread.Join ();
                                        CleanUp ();
-                                       throw new IOException ("Monitor thread failed while initializing.");
+                                       throw monitorExc;
                                }
                                else 
                                        started = true;
@@ -218,6 +218,9 @@ namespace System.IO {
 
                                CleanUp ();
                                started = false;
+
+                               if (monitorExc != null)
+                                       throw monitorExc;
                        }
                }
 
@@ -237,34 +240,31 @@ namespace System.IO {
 
                void DoMonitor ()
                {
-                       Exception exc = null;
-                       failedInit = false;
+                       monitorExc = null;
 
                        try {
                                Setup ();
                        } catch (Exception e) {
-                               failedInit = true;
-                               exc = e;
+                               monitorExc = e;
                        } finally {
                                startedEvent.Set ();
                        }
 
-                       if (failedInit) {
-                               fsw.OnError (new ErrorEventArgs (exc));
+                       if (monitorExc != null) 
                                return;
-                       }
 
                        try {
                                Monitor ();
                        } catch (Exception e) {
-                               exc = e;
+                               monitorExc = e;
                        } finally {
                                if (!requestStop) { // failure
                                        CleanUp ();
                                        started = false;
+                                       if (monitorExc != null)
+                                               throw monitorExc;
                                }
-                               if (exc != null)
-                                       fsw.OnError (new ErrorEventArgs (exc));
+
                        }
                }
 
@@ -272,18 +272,24 @@ namespace System.IO {
                {       
                        var initialFds = new List<int> ();
 
+                       // fsw.FullPath may end in '/', see https://bugzilla.xamarin.com/show_bug.cgi?id=5747
+                       if (fsw.FullPath.EndsWith ("/", StringComparison.Ordinal))
+                               fullPathNoLastSlash = fsw.FullPath.Substring (0, fsw.FullPath.Length - 1);
+                       else
+                               fullPathNoLastSlash = fsw.FullPath;
+                               
                        // 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);
+                       int fd = open (fullPathNoLastSlash, O_EVTONLY, 0);
                        var resolvedFullPath = GetFilenameFromFd (fd);
                        close (fd);
 
-                       if (resolvedFullPath != fsw.FullPath)
+                       if (resolvedFullPath != fullPathNoLastSlash)
                                fixupPath = resolvedFullPath;
                        else
                                fixupPath = null;
 
-                       Scan (fsw.FullPath, false, ref initialFds);
+                       Scan (fullPathNoLastSlash, 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
@@ -332,18 +338,22 @@ namespace System.IO {
                        List<PathData> removeQueue = new List<PathData> ();
                        List<string> rescanQueue = new List<string> ();
 
+                       int retries = 0; 
+
                        while (!requestStop) {
                                var changes = CreateChangeList (ref newFds);
 
                                int numEvents = kevent (conn, changes, changes.Length, eventBuffer, eventBuffer.Length, ref timeout);
 
                                if (numEvents == -1) {
-                                       var errMsg = String.Format ("kevent() error, error code = '{0}'", Marshal.GetLastWin32Error ());
-                                       fsw.OnError (new ErrorEventArgs (new IOException (errMsg)));
-                               }
+                                       if (++retries == 3)
+                                               throw new IOException (String.Format (
+                                                       "persistent kevent() error, error code = '{0}'", Marshal.GetLastWin32Error ()));
 
-                               if (numEvents == 0)
                                        continue;
+                               }
+
+                               retries = 0;
 
                                for (var i = 0; i < numEvents; i++) {
                                        var kevt = eventBuffer [i];
@@ -354,27 +364,24 @@ namespace System.IO {
                                                fsw.OnError (new ErrorEventArgs (new IOException (errMsg)));
                                                continue;
                                        }
-
-                                       if ((kevt.fflags & FilterFlags.VNodeDelete) == FilterFlags.VNodeDelete || (kevt.fflags & FilterFlags.VNodeRevoke) == FilterFlags.VNodeRevoke)
+                                               
+                                       if ((kevt.fflags & FilterFlags.VNodeDelete) == FilterFlags.VNodeDelete || (kevt.fflags & FilterFlags.VNodeRevoke) == FilterFlags.VNodeRevoke) {
                                                removeQueue.Add (pathData);
+                                               continue;
+                                       }
 
-                                       else if ((kevt.fflags & FilterFlags.VNodeWrite) == FilterFlags.VNodeWrite) {
-                                               if (pathData.IsDirectory)
+                                       if ((kevt.fflags & FilterFlags.VNodeRename) == FilterFlags.VNodeRename) {
+                                                       UpdatePath (pathData);
+                                       } 
+
+                                       if ((kevt.fflags & FilterFlags.VNodeWrite) == FilterFlags.VNodeWrite) {
+                                               if (pathData.IsDirectory) //TODO: Check if dirs trigger Changed events on .NET
                                                        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)
+                                       }
+                                               
+                                       if ((kevt.fflags & FilterFlags.VNodeAttrib) == FilterFlags.VNodeAttrib || (kevt.fflags & FilterFlags.VNodeExtend) == FilterFlags.VNodeExtend)
                                                PostEvent (FileAction.Modified, pathData.Path);
                                }
 
@@ -396,6 +403,9 @@ namespace System.IO {
                        if (pathData != null)
                                return pathData;
 
+                       if (fdsDict.Count >= maxFds)
+                               throw new IOException ("kqueue() FileSystemWatcher has reached the maximum nunmber of files to watch."); 
+
                        var fd = open (path, O_EVTONLY, 0);
 
                        if (fd == -1) {
@@ -454,28 +464,45 @@ namespace System.IO {
                        toRemove.ForEach (Remove);
                }
 
-               void Rename (PathData pathData, string newRoot)
+               void UpdatePath (PathData pathData)
                {
+                       var newRoot = GetFilenameFromFd (pathData.Fd);
+                       if (!newRoot.StartsWith (fullPathNoLastSlash)) { // moved outside of our watched path (so stop observing it)
+                               RemoveTree (pathData);
+                               return;
+                       }
+                               
                        var toRename = new List<PathData> ();
                        var oldRoot = pathData.Path;
 
                        toRename.Add (pathData);
                                                                                                                        
-                       if (pathData.IsDirectory) {
+                       if (pathData.IsDirectory) { // anything under the directory must have their paths updated
                                var prefix = oldRoot + Path.DirectorySeparatorChar;
                                foreach (var path in pathsDict.Keys)
                                        if (path.StartsWith (prefix))
                                                toRename.Add (pathsDict [path]);
                        }
-
-                       toRename.ForEach ((pd) => { 
-                               var oldPath = pd.Path;
+               
+                       foreach (var renaming in toRename) {
+                               var oldPath = renaming.Path;
                                var newPath = newRoot + oldPath.Substring (oldRoot.Length);
-                               pd.Path = newPath;
+
+                               renaming.Path = newPath;
                                pathsDict.Remove (oldPath);
-                               pathsDict.Add (newPath, pd);
-                       });
 
+                               // destination may exist in our records from a Created event, take care of it
+                               if (pathsDict.ContainsKey (newPath)) {
+                                       var conflict = pathsDict [newPath];
+                                       if (GetFilenameFromFd (renaming.Fd) == GetFilenameFromFd (conflict.Fd))
+                                               Remove (conflict);
+                                       else
+                                               UpdatePath (conflict);
+                               }
+                                       
+                               pathsDict.Add (newPath, renaming);
+                       }
+                       
                        PostEvent (FileAction.RenamedNewName, oldRoot, newRoot);
                }
 
@@ -532,15 +559,20 @@ namespace System.IO {
                        if (action == 0)
                                return;
 
+                       // e.Name
+                       string name = path.Substring (fullPathNoLastSlash.Length + 1); 
+
                        // 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))) 
+                       if (!fsw.Pattern.IsMatch (path) && (newPath == null || !fsw.Pattern.IsMatch (newPath)))
                                return;
                                
-                       if (action == FileAction.RenamedNewName)
-                               renamed = new RenamedEventArgs (WatcherChangeTypes.Renamed, "", newPath, path);
+                       if (action == FileAction.RenamedNewName) {
+                               string newName = newPath.Substring (fullPathNoLastSlash.Length + 1);
+                               renamed = new RenamedEventArgs (WatcherChangeTypes.Renamed, fsw.Path, newName, name);
+                       }
 
                        lock (fsw) {
-                               fsw.DispatchEvents (action, path, ref renamed);
+                               fsw.DispatchEvents (action, name, ref renamed);
 
                                if (fsw.Waiting) {
                                        fsw.Waiting = false;
@@ -554,8 +586,9 @@ namespace System.IO {
                        var sb = new StringBuilder (__DARWIN_MAXPATHLEN);
 
                        if (fcntl (fd, F_GETPATH, sb) != -1) {
-                               if (fixupPath != null)
-                                       sb.Replace (fixupPath, fsw.FullPath, 0, fixupPath.Length); // see Setup()
+                               if (fixupPath != null) 
+                                       sb.Replace (fixupPath, fullPathNoLastSlash, 0, fixupPath.Length); // see Setup()
+
                                return sb.ToString ();
                        } else {
                                fsw.OnError (new ErrorEventArgs (new IOException (String.Format (
@@ -568,6 +601,7 @@ namespace System.IO {
                const int F_GETPATH = 50;
                const int __DARWIN_MAXPATHLEN = 1024;
                static readonly kevent[] emptyEventList = new System.IO.kevent[0];
+               const int maxFds = 200;
 
                FileSystemWatcher fsw;
                int conn;
@@ -575,12 +609,13 @@ namespace System.IO {
                volatile bool requestStop = false;
                AutoResetEvent startedEvent = new AutoResetEvent (false);
                bool started = false;
-               bool failedInit = false;
+               Exception monitorExc;
                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;
+               string fullPathNoLastSlash = null;
 
                [DllImport ("libc", EntryPoint="fcntl", CharSet=CharSet.Auto, SetLastError=true)]
                static extern int fcntl (int file_names_by_descriptor, int cmd, StringBuilder sb);