2 // System.IO.KeventWatcher.cs: interface with osx kevent
5 // Geoff Norton (gnorton@customerdna.com)
6 // Cody Russell (cody@xamarin.com)
7 // Alexis Christoforides (lexas@xamarin.com)
9 // (c) 2004 Geoff Norton
10 // Copyright 2014 Xamarin Inc
12 // Permission is hereby granted, free of charge, to any person obtaining
13 // a copy of this software and associated documentation files (the
14 // "Software"), to deal in the Software without restriction, including
15 // without limitation the rights to use, copy, modify, merge, publish,
16 // distribute, sublicense, and/or sell copies of the Software, and to
17 // permit persons to whom the Software is furnished to do so, subject to
18 // the following conditions:
20 // The above copyright notice and this permission notice shall be
21 // included in all copies or substantial portions of the Software.
23 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
33 using System.Collections;
34 using System.Collections.Generic;
35 using System.ComponentModel;
36 using System.Runtime.CompilerServices;
37 using System.Runtime.InteropServices;
39 using System.Threading;
40 using System.Reflection;
45 enum EventFlags : ushort {
57 SystemFlags = unchecked (0xf000),
64 enum EventFilter : short {
79 enum FilterFlags : uint {
80 ReadPoll = EventFlags.Flag0,
81 ReadOutOfBand = EventFlags.Flag1,
82 ReadLowWaterMark = 0x00000001,
84 WriteLowWaterMark = ReadLowWaterMark,
86 NoteTrigger = 0x01000000,
87 NoteFFNop = 0x00000000,
88 NoteFFAnd = 0x40000000,
89 NoteFFOr = 0x80000000,
90 NoteFFCopy = 0xc0000000,
91 NoteFFCtrlMask = 0xc0000000,
92 NoteFFlagsMask = 0x00ffffff,
94 VNodeDelete = 0x00000001,
95 VNodeWrite = 0x00000002,
96 VNodeExtend = 0x00000004,
97 VNodeAttrib = 0x00000008,
98 VNodeLink = 0x00000010,
99 VNodeRename = 0x00000020,
100 VNodeRevoke = 0x00000040,
101 VNodeNone = 0x00000080,
103 ProcExit = 0x80000000,
104 ProcFork = 0x40000000,
105 ProcExec = 0x20000000,
106 ProcReap = 0x10000000,
107 ProcSignal = 0x08000000,
108 ProcExitStatus = 0x04000000,
109 ProcResourceEnd = 0x02000000,
112 ProcAppactive = 0x00800000,
113 ProcAppBackground = 0x00400000,
114 ProcAppNonUI = 0x00200000,
115 ProcAppInactive = 0x00100000,
116 ProcAppAllStates = 0x00f00000,
119 ProcPDataMask = 0x000fffff,
120 ProcControlMask = 0xfff00000,
122 VMPressure = 0x80000000,
123 VMPressureTerminate = 0x40000000,
124 VMPressureSuddenTerminate = 0x20000000,
125 VMError = 0x10000000,
126 TimerSeconds = 0x00000001,
127 TimerMicroSeconds = 0x00000002,
128 TimerNanoSeconds = 0x00000004,
129 TimerAbsolute = 0x00000008,
132 [StructLayout(LayoutKind.Sequential)]
133 struct kevent : IDisposable {
134 public UIntPtr ident;
135 public EventFilter filter;
136 public EventFlags flags;
137 public FilterFlags fflags;
141 public void Dispose ()
143 if (udata != IntPtr.Zero)
144 Marshal.FreeHGlobal (udata);
150 [StructLayout(LayoutKind.Sequential)]
152 public IntPtr tv_sec;
153 public IntPtr tv_nsec;
159 public bool IsDirectory;
163 class KqueueMonitor : IDisposable
165 static bool initialized;
167 public int Connection
172 public KqueueMonitor (FileSystemWatcher fsw)
179 var maxenv = Environment.GetEnvironmentVariable ("MONO_DARWIN_WATCHER_MAXFDS");
180 if (maxenv != null && Int32.TryParse (maxenv, out t))
185 public void Dispose ()
199 throw new IOException (String.Format (
200 "kqueue() error at init, error code = '{0}'", Marshal.GetLastWin32Error ()));
202 thread = new Thread (() => DoMonitor ());
203 thread.IsBackground = true;
206 startedEvent.WaitOne ();
228 // This will break the wait in Monitor ()
235 if (!thread.Join (2000))
254 foreach (int fd in fdsDict.Keys)
265 } catch (Exception e) {
272 fsw.DispatchErrorEvents (new ErrorEventArgs (exc));
278 } catch (Exception e) {
282 if (!requestStop) { // failure
285 fsw.EnableRaisingEvents = false;
288 fsw.DispatchErrorEvents (new ErrorEventArgs (exc));
295 var initialFds = new List<int> ();
297 // fsw.FullPath may end in '/', see https://bugzilla.xamarin.com/show_bug.cgi?id=5747
298 if (fsw.FullPath != "/" && fsw.FullPath.EndsWith ("/", StringComparison.Ordinal))
299 fullPathNoLastSlash = fsw.FullPath.Substring (0, fsw.FullPath.Length - 1);
301 fullPathNoLastSlash = fsw.FullPath;
303 // realpath() returns the *realpath* which can be different than fsw.FullPath because symlinks.
304 // If so, introduce a fixup step.
305 var sb = new StringBuilder (__DARWIN_MAXPATHLEN);
306 if (realpath(fsw.FullPath, sb) == IntPtr.Zero) {
307 var errMsg = String.Format ("realpath({0}) failed, error code = '{1}'", fsw.FullPath, Marshal.GetLastWin32Error ());
308 throw new IOException (errMsg);
310 var resolvedFullPath = sb.ToString();
312 if (resolvedFullPath != fullPathNoLastSlash)
313 fixupPath = resolvedFullPath;
317 Scan (fullPathNoLastSlash, false, ref initialFds);
319 var immediate_timeout = new timespec { tv_sec = (IntPtr)0, tv_nsec = (IntPtr)0 };
320 var eventBuffer = new kevent[0]; // we don't want to take any events from the queue at this point
321 var changes = CreateChangeList (ref initialFds);
326 numEvents = kevent (conn, changes, changes.Length, eventBuffer, eventBuffer.Length, ref immediate_timeout);
327 if (numEvents == -1) {
328 errno = Marshal.GetLastWin32Error ();
330 } while (numEvents == -1 && errno == EINTR);
332 if (numEvents == -1) {
333 var errMsg = String.Format ("kevent() error at initial event registration, error code = '{0}'", errno);
334 throw new IOException (errMsg);
338 kevent[] CreateChangeList (ref List<int> FdList)
340 if (FdList.Count == 0)
341 return emptyEventList;
343 var changes = new List<kevent> ();
344 foreach (int fd in FdList) {
345 var change = new kevent {
348 filter = EventFilter.Vnode,
349 flags = EventFlags.Add | EventFlags.Enable | EventFlags.Clear,
350 fflags = FilterFlags.VNodeDelete | FilterFlags.VNodeExtend |
351 FilterFlags.VNodeRename | FilterFlags.VNodeAttrib |
352 FilterFlags.VNodeLink | FilterFlags.VNodeRevoke |
353 FilterFlags.VNodeWrite,
358 changes.Add (change);
362 return changes.ToArray ();
367 var eventBuffer = new kevent[32];
368 var newFds = new List<int> ();
369 List<PathData> removeQueue = new List<PathData> ();
370 List<string> rescanQueue = new List<string> ();
374 while (!requestStop) {
375 var changes = CreateChangeList (ref newFds);
377 // We are calling an icall, so have to marshal manually
379 int ksize = Marshal.SizeOf<kevent> ();
380 var changesNative = Marshal.AllocHGlobal (ksize * changes.Length);
381 for (int i = 0; i < changes.Length; ++i)
382 Marshal.StructureToPtr (changes [i], changesNative + (i * ksize), false);
383 var eventBufferNative = Marshal.AllocHGlobal (ksize * eventBuffer.Length);
385 int numEvents = kevent_notimeout (ref conn, changesNative, changes.Length, eventBufferNative, eventBuffer.Length);
388 Marshal.FreeHGlobal (changesNative);
389 for (int i = 0; i < numEvents; ++i)
390 eventBuffer [i] = Marshal.PtrToStructure<kevent> (eventBufferNative + (i * ksize));
391 Marshal.FreeHGlobal (eventBufferNative);
393 if (numEvents == -1) {
394 // Stop () signals us to stop by closing the connection
397 int errno = Marshal.GetLastWin32Error ();
398 if (errno != EINTR && ++retries == 3)
399 throw new IOException (String.Format (
400 "persistent kevent() error, error code = '{0}'", errno));
406 for (var i = 0; i < numEvents; i++) {
407 var kevt = eventBuffer [i];
409 if (!fdsDict.ContainsKey ((int)kevt.ident))
410 // The event is for a file that was removed
413 var pathData = fdsDict [(int)kevt.ident];
415 if ((kevt.flags & EventFlags.Error) == EventFlags.Error) {
416 var errMsg = String.Format ("kevent() error watching path '{0}', error code = '{1}'", pathData.Path, kevt.data);
417 fsw.DispatchErrorEvents (new ErrorEventArgs (new IOException (errMsg)));
421 if ((kevt.fflags & FilterFlags.VNodeDelete) == FilterFlags.VNodeDelete || (kevt.fflags & FilterFlags.VNodeRevoke) == FilterFlags.VNodeRevoke) {
422 if (pathData.Path == fullPathNoLastSlash)
423 // The root path is deleted; exit silently
426 removeQueue.Add (pathData);
430 if ((kevt.fflags & FilterFlags.VNodeRename) == FilterFlags.VNodeRename) {
431 UpdatePath (pathData);
434 if ((kevt.fflags & FilterFlags.VNodeWrite) == FilterFlags.VNodeWrite) {
435 if (pathData.IsDirectory) //TODO: Check if dirs trigger Changed events on .NET
436 rescanQueue.Add (pathData.Path);
438 PostEvent (FileAction.Modified, pathData.Path);
441 if ((kevt.fflags & FilterFlags.VNodeAttrib) == FilterFlags.VNodeAttrib || (kevt.fflags & FilterFlags.VNodeExtend) == FilterFlags.VNodeExtend)
442 PostEvent (FileAction.Modified, pathData.Path);
445 removeQueue.ForEach (Remove);
446 removeQueue.Clear ();
448 rescanQueue.ForEach (path => {
449 Scan (path, true, ref newFds);
451 rescanQueue.Clear ();
455 PathData Add (string path, bool postEvents, ref List<int> fds)
458 pathsDict.TryGetValue (path, out pathData);
460 if (pathData != null)
463 if (fdsDict.Count >= maxFds)
464 throw new IOException ("kqueue() FileSystemWatcher has reached the maximum number of files to watch.");
466 var fd = open (path, O_EVTONLY, 0);
469 fsw.DispatchErrorEvents (new ErrorEventArgs (new IOException (String.Format (
470 "open() error while attempting to process path '{0}', error code = '{1}'", path, Marshal.GetLastWin32Error ()))));
477 var attrs = File.GetAttributes (path);
479 pathData = new PathData {
482 IsDirectory = (attrs & FileAttributes.Directory) == FileAttributes.Directory
485 pathsDict.Add (path, pathData);
486 fdsDict.Add (fd, pathData);
489 PostEvent (FileAction.Added, path);
492 } catch (Exception e) {
494 fsw.DispatchErrorEvents (new ErrorEventArgs (e));
500 void Remove (PathData pathData)
502 fdsDict.Remove (pathData.Fd);
503 pathsDict.Remove (pathData.Path);
505 PostEvent (FileAction.Removed, pathData.Path);
508 void RemoveTree (PathData pathData)
510 var toRemove = new List<PathData> ();
512 toRemove.Add (pathData);
514 if (pathData.IsDirectory) {
515 var prefix = pathData.Path + Path.DirectorySeparatorChar;
516 foreach (var path in pathsDict.Keys)
517 if (path.StartsWith (prefix)) {
518 toRemove.Add (pathsDict [path]);
521 toRemove.ForEach (Remove);
524 void UpdatePath (PathData pathData)
526 var newRoot = GetFilenameFromFd (pathData.Fd);
527 if (!newRoot.StartsWith (fullPathNoLastSlash)) { // moved outside of our watched path (so stop observing it)
528 RemoveTree (pathData);
532 var toRename = new List<PathData> ();
533 var oldRoot = pathData.Path;
535 toRename.Add (pathData);
537 if (pathData.IsDirectory) { // anything under the directory must have their paths updated
538 var prefix = oldRoot + Path.DirectorySeparatorChar;
539 foreach (var path in pathsDict.Keys)
540 if (path.StartsWith (prefix))
541 toRename.Add (pathsDict [path]);
544 foreach (var renaming in toRename) {
545 var oldPath = renaming.Path;
546 var newPath = newRoot + oldPath.Substring (oldRoot.Length);
548 renaming.Path = newPath;
549 pathsDict.Remove (oldPath);
551 // destination may exist in our records from a Created event, take care of it
552 if (pathsDict.ContainsKey (newPath)) {
553 var conflict = pathsDict [newPath];
554 if (GetFilenameFromFd (renaming.Fd) == GetFilenameFromFd (conflict.Fd))
557 UpdatePath (conflict);
560 pathsDict.Add (newPath, renaming);
563 PostEvent (FileAction.RenamedNewName, oldRoot, newRoot);
566 void Scan (string path, bool postEvents, ref List<int> fds)
571 var pathData = Add (path, postEvents, ref fds);
573 if (pathData == null)
576 if (!pathData.IsDirectory)
579 var dirsToProcess = new List<string> ();
580 dirsToProcess.Add (path);
582 while (dirsToProcess.Count > 0) {
583 var tmp = dirsToProcess [0];
584 dirsToProcess.RemoveAt (0);
586 var info = new DirectoryInfo (tmp);
587 FileSystemInfo[] fsInfos = null;
589 fsInfos = info.GetFileSystemInfos ();
591 } catch (IOException) {
592 // this can happen if the directory has been deleted already.
593 // that's okay, just keep processing the other dirs.
594 fsInfos = new FileSystemInfo[0];
597 foreach (var fsi in fsInfos) {
598 if ((fsi.Attributes & FileAttributes.Directory) == FileAttributes.Directory && !fsw.IncludeSubdirectories)
601 if ((fsi.Attributes & FileAttributes.Directory) != FileAttributes.Directory && !fsw.Pattern.IsMatch (fsi.FullName))
604 var currentPathData = Add (fsi.FullName, postEvents, ref fds);
606 if (currentPathData != null && currentPathData.IsDirectory)
607 dirsToProcess.Add (fsi.FullName);
612 void PostEvent (FileAction action, string path, string newPath = null)
614 RenamedEventArgs renamed = null;
616 if (requestStop || action == 0)
620 string name = (path.Length > fullPathNoLastSlash.Length) ? path.Substring (fullPathNoLastSlash.Length + 1) : String.Empty;
622 // only post events that match filter pattern. check both old and new paths for renames
623 if (!fsw.Pattern.IsMatch (path) && (newPath == null || !fsw.Pattern.IsMatch (newPath)))
626 if (action == FileAction.RenamedNewName) {
627 string newName = (newPath.Length > fullPathNoLastSlash.Length) ? newPath.Substring (fullPathNoLastSlash.Length + 1) : String.Empty;
628 renamed = new RenamedEventArgs (WatcherChangeTypes.Renamed, fsw.Path, newName, name);
631 fsw.DispatchEvents (action, name, ref renamed);
636 System.Threading.Monitor.PulseAll (fsw);
641 private string GetFilenameFromFd (int fd)
643 var sb = new StringBuilder (__DARWIN_MAXPATHLEN);
645 if (fcntl (fd, F_GETPATH, sb) != -1) {
646 if (fixupPath != null)
647 sb.Replace (fixupPath, fullPathNoLastSlash, 0, fixupPath.Length); // see Setup()
649 return sb.ToString ();
651 fsw.DispatchErrorEvents (new ErrorEventArgs (new IOException (String.Format (
652 "fcntl() error while attempting to get path for fd '{0}', error code = '{1}'", fd, Marshal.GetLastWin32Error ()))));
657 const int O_EVTONLY = 0x8000;
658 const int F_GETPATH = 50;
659 const int __DARWIN_MAXPATHLEN = 1024;
661 static readonly kevent[] emptyEventList = new System.IO.kevent[0];
662 int maxFds = Int32.MaxValue;
664 FileSystemWatcher fsw;
667 volatile bool requestStop = false;
668 AutoResetEvent startedEvent = new AutoResetEvent (false);
669 bool started = false;
670 bool inDispatch = false;
671 Exception exc = null;
672 object stateLock = new object ();
673 object connLock = new object ();
675 readonly Dictionary<string, PathData> pathsDict = new Dictionary<string, PathData> ();
676 readonly Dictionary<int, PathData> fdsDict = new Dictionary<int, PathData> ();
677 string fixupPath = null;
678 string fullPathNoLastSlash = null;
680 [DllImport ("libc", CharSet=CharSet.Auto, SetLastError=true)]
681 static extern int fcntl (int file_names_by_descriptor, int cmd, StringBuilder sb);
683 [DllImport ("libc", CharSet=CharSet.Auto, SetLastError=true)]
684 static extern IntPtr realpath (string pathname, StringBuilder sb);
686 [DllImport ("libc", SetLastError=true)]
687 extern static int open (string path, int flags, int mode_t);
690 extern static int close (int fd);
692 [DllImport ("libc", SetLastError=true)]
693 extern static int kqueue ();
695 [DllImport ("libc", SetLastError=true)]
696 extern static int kevent (int kq, [In]kevent[] ev, int nchanges, [Out]kevent[] evtlist, int nevents, [In] ref timespec time);
698 [MethodImplAttribute(MethodImplOptions.InternalCall)]
699 extern static int kevent_notimeout (ref int kq, IntPtr ev, int nchanges, IntPtr evtlist, int nevents);
702 class KeventWatcher : IFileWatcher
705 static KeventWatcher instance;
706 static Hashtable watches; // <FileSystemWatcher, KqueueMonitor>
708 private KeventWatcher ()
713 public static bool GetInstance (out IFileWatcher watcher)
715 if (failed == true) {
720 if (instance != null) {
725 watches = Hashtable.Synchronized (new Hashtable ());
734 instance = new KeventWatcher ();
739 public void StartDispatching (FileSystemWatcher fsw)
741 KqueueMonitor monitor;
743 if (watches.ContainsKey (fsw)) {
744 monitor = (KqueueMonitor)watches [fsw];
746 monitor = new KqueueMonitor (fsw);
747 watches.Add (fsw, monitor);
753 public void StopDispatching (FileSystemWatcher fsw)
755 KqueueMonitor monitor = (KqueueMonitor)watches [fsw];
763 extern static int close (int fd);
766 extern static int kqueue ();