f3cc7dcc183200a81a8ca409b3ab9e49f936f7ec
[mono.git] / mcs / class / System / System.IO / KeventWatcher.cs
1 // 
2 // System.IO.KeventWatcher.cs: interface with osx kevent
3 //
4 // Authors:
5 //      Geoff Norton (gnorton@customerdna.com)
6 //      Cody Russell (cody@xamarin.com)
7 //      Alexis Christoforides (lexas@xamarin.com)
8 //
9 // (c) 2004 Geoff Norton
10 // Copyright 2014 Xamarin Inc
11 //
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:
19 // 
20 // The above copyright notice and this permission notice shall be
21 // included in all copies or substantial portions of the Software.
22 // 
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.
30 //
31
32 using System;
33 using System.Collections;
34 using System.Collections.Generic;
35 using System.ComponentModel;
36 using System.Runtime.CompilerServices;
37 using System.Runtime.InteropServices;
38 using System.Text;
39 using System.Threading;
40 using System.Reflection;
41
42 namespace System.IO {
43
44         [Flags]
45         enum EventFlags : ushort {
46                 Add         = 0x0001,
47                 Delete      = 0x0002,
48                 Enable      = 0x0004,
49                 Disable     = 0x0008,
50                 OneShot     = 0x0010,
51                 Clear       = 0x0020,
52                 Receipt     = 0x0040,
53                 Dispatch    = 0x0080,
54
55                 Flag0       = 0x1000,
56                 Flag1       = 0x2000,
57                 SystemFlags = unchecked (0xf000),
58                         
59                 // Return values.
60                 EOF         = 0x8000,
61                 Error       = 0x4000,
62         }
63         
64         enum EventFilter : short {
65                 Read = -1,
66                 Write = -2,
67                 Aio = -3,
68                 Vnode = -4,
69                 Proc = -5,
70                 Signal = -6,
71                 Timer = -7,
72                 MachPort = -8,
73                 FS = -9,
74                 User = -10,
75                 VM = -11
76         }
77
78         [Flags]
79         enum FilterFlags : uint {
80                 ReadPoll          = EventFlags.Flag0,
81                 ReadOutOfBand     = EventFlags.Flag1,
82                 ReadLowWaterMark  = 0x00000001,
83
84                 WriteLowWaterMark = ReadLowWaterMark,
85
86                 NoteTrigger       = 0x01000000,
87                 NoteFFNop         = 0x00000000,
88                 NoteFFAnd         = 0x40000000,
89                 NoteFFOr          = 0x80000000,
90                 NoteFFCopy        = 0xc0000000,
91                 NoteFFCtrlMask    = 0xc0000000,
92                 NoteFFlagsMask    = 0x00ffffff,
93                                   
94                 VNodeDelete       = 0x00000001,
95                 VNodeWrite        = 0x00000002,
96                 VNodeExtend       = 0x00000004,
97                 VNodeAttrib       = 0x00000008,
98                 VNodeLink         = 0x00000010,
99                 VNodeRename       = 0x00000020,
100                 VNodeRevoke       = 0x00000040,
101                 VNodeNone         = 0x00000080,
102                                   
103                 ProcExit          = 0x80000000,
104                 ProcFork          = 0x40000000,
105                 ProcExec          = 0x20000000,
106                 ProcReap          = 0x10000000,
107                 ProcSignal        = 0x08000000,
108                 ProcExitStatus    = 0x04000000,
109                 ProcResourceEnd   = 0x02000000,
110
111                 // iOS only
112                 ProcAppactive     = 0x00800000,
113                 ProcAppBackground = 0x00400000,
114                 ProcAppNonUI      = 0x00200000,
115                 ProcAppInactive   = 0x00100000,
116                 ProcAppAllStates  = 0x00f00000,
117
118                 // Masks
119                 ProcPDataMask     = 0x000fffff,
120                 ProcControlMask   = 0xfff00000,
121
122                 VMPressure        = 0x80000000,
123                 VMPressureTerminate = 0x40000000,
124                 VMPressureSuddenTerminate = 0x20000000,
125                 VMError           = 0x10000000,
126                 TimerSeconds      =    0x00000001,
127                 TimerMicroSeconds =   0x00000002,
128                 TimerNanoSeconds  =   0x00000004,
129                 TimerAbsolute     =   0x00000008,
130         }
131
132         [StructLayout(LayoutKind.Sequential)]
133         struct kevent : IDisposable {
134                 public UIntPtr ident;
135                 public EventFilter filter;
136                 public EventFlags flags;
137                 public FilterFlags fflags;
138                 public IntPtr data;
139                 public IntPtr udata;
140
141                 public void Dispose ()
142                 {
143                         if (udata != IntPtr.Zero)
144                                 Marshal.FreeHGlobal (udata);
145                 }
146
147
148         }
149
150         [StructLayout(LayoutKind.Sequential)]
151         struct timespec {
152                 public IntPtr tv_sec;
153                 public IntPtr tv_usec;
154         }
155
156         class PathData
157         {
158                 public string Path;
159                 public bool IsDirectory;
160                 public int Fd;
161         }
162
163         class KqueueMonitor : IDisposable
164         {
165                 public int Connection
166                 {
167                         get { return conn; }
168                 }
169
170                 public KqueueMonitor (FileSystemWatcher fsw)
171                 {
172                         this.fsw = fsw;
173                         this.conn = -1;
174                 }
175
176                 public void Dispose ()
177                 {
178                         CleanUp ();
179                 }
180
181                 public void Start ()
182                 {
183                         lock (stateLock) {
184                                 if (started)
185                                         return;
186
187                                 conn = kqueue ();
188
189                                 if (conn == -1)
190                                         throw new IOException (String.Format (
191                                                 "kqueue() error at init, error code = '{0}'", Marshal.GetLastWin32Error ()));
192                                         
193                                 thread = new Thread (() => DoMonitor ());
194                                 thread.IsBackground = true;
195                                 thread.Start ();
196
197                                 startedEvent.WaitOne ();
198
199                                 if (failedInit) {
200                                         thread.Join ();
201                                         CleanUp ();
202                                         throw new IOException ("Monitor thread failed while initializing.");
203                                 }
204                                 else 
205                                         started = true;
206                         }
207                 }
208
209                 public void Stop ()
210                 {
211                         lock (stateLock) {
212                                 if (!started)
213                                         return;
214                                         
215                                 requestStop = true;
216                                 thread.Join ();
217                                 requestStop = false;
218
219                                 CleanUp ();
220                                 started = false;
221                         }
222                 }
223
224                 void CleanUp ()
225                 {
226                         if (conn != -1)
227                                 close (conn);
228
229                         conn = -1;
230
231                         foreach (int fd in fdsDict.Keys)
232                                 close (fd); 
233
234                         fdsDict.Clear ();
235                         pathsDict.Clear ();
236                 }
237
238                 void DoMonitor ()
239                 {
240                         Exception exc = null;
241                         failedInit = false;
242
243                         try {
244                                 Setup ();
245                         } catch (Exception e) {
246                                 failedInit = true;
247                                 exc = e;
248                         } finally {
249                                 startedEvent.Set ();
250                         }
251
252                         if (failedInit) {
253                                 fsw.OnError (new ErrorEventArgs (exc));
254                                 return;
255                         }
256
257                         try {
258                                 Monitor ();
259                         } catch (Exception e) {
260                                 exc = e;
261                         } finally {
262                                 if (!requestStop) { // failure
263                                         CleanUp ();
264                                         started = false;
265                                 }
266                                 if (exc != null)
267                                         fsw.OnError (new ErrorEventArgs (exc));
268                         }
269                 }
270
271                 void Setup ()
272                 {       
273                         var initialFds = new List<int> ();
274
275                         // GetFilenameFromFd() returns the *realpath* which can be different than fsw.FullPath because symlinks.
276                         // If so, introduce a fixup step.
277                         int fd = open (fsw.FullPath, O_EVTONLY, 0);
278                         var resolvedFullPath = GetFilenameFromFd (fd);
279                         close (fd);
280
281                         if (resolvedFullPath != fsw.FullPath)
282                                 fixupPath = resolvedFullPath;
283                         else
284                                 fixupPath = null;
285
286                         Scan (fsw.FullPath, false, ref initialFds);
287
288                         var immediate_timeout = new timespec { tv_sec = (IntPtr)0, tv_usec = (IntPtr)0 };
289                         var eventBuffer = new kevent[0]; // we don't want to take any events from the queue at this point
290                         var changes = CreateChangeList (ref initialFds);
291
292                         int numEvents = kevent (conn, changes, changes.Length, eventBuffer, eventBuffer.Length, ref immediate_timeout);
293
294                         if (numEvents == -1) {
295                                 var errMsg = String.Format ("kevent() error at initial event registration, error code = '{0}'", Marshal.GetLastWin32Error ());
296                                 throw new IOException (errMsg);
297                         }
298                 }
299
300                 kevent[] CreateChangeList (ref List<int> FdList)
301                 {
302                         if (FdList.Count == 0)
303                                 return emptyEventList;
304
305                         var changes = new List<kevent> ();
306                         foreach (int fd in FdList) {
307                                 var change = new kevent {
308
309                                         ident = (UIntPtr)fd,
310                                         filter = EventFilter.Vnode,
311                                         flags = EventFlags.Add | EventFlags.Enable | EventFlags.Clear,
312                                         fflags = FilterFlags.VNodeDelete | FilterFlags.VNodeExtend |
313                                                 FilterFlags.VNodeRename | FilterFlags.VNodeAttrib |
314                                                 FilterFlags.VNodeLink | FilterFlags.VNodeRevoke |
315                                                 FilterFlags.VNodeWrite,
316                                         data = IntPtr.Zero,
317                                         udata = IntPtr.Zero
318                                 };
319
320                                 changes.Add (change);
321                         }
322                         FdList.Clear ();
323
324                         return changes.ToArray ();
325                 }
326
327                 void Monitor ()
328                 {
329                         var timeout = new timespec { tv_sec = (IntPtr)0, tv_usec = (IntPtr)500000000 };
330                         var eventBuffer = new kevent[32];
331                         var newFds = new List<int> ();
332                         List<PathData> removeQueue = new List<PathData> ();
333                         List<string> rescanQueue = new List<string> ();
334
335                         while (!requestStop) {
336                                 var changes = CreateChangeList (ref newFds);
337
338                                 int numEvents = kevent (conn, changes, changes.Length, eventBuffer, eventBuffer.Length, ref timeout);
339
340                                 if (numEvents == -1) {
341                                         var errMsg = String.Format ("kevent() error, error code = '{0}'", Marshal.GetLastWin32Error ());
342                                         fsw.OnError (new ErrorEventArgs (new IOException (errMsg)));
343                                 }
344
345                                 if (numEvents == 0)
346                                         continue;
347
348                                 for (var i = 0; i < numEvents; i++) {
349                                         var kevt = eventBuffer [i];
350                                         var pathData = fdsDict [(int)kevt.ident];
351
352                                         if ((kevt.flags & EventFlags.Error) == EventFlags.Error) {
353                                                 var errMsg = String.Format ("kevent() error watching path '{0}', error code = '{1}'", pathData.Path, kevt.data);
354                                                 fsw.OnError (new ErrorEventArgs (new IOException (errMsg)));
355                                                 continue;
356                                         }
357
358                                         if ((kevt.fflags & FilterFlags.VNodeDelete) == FilterFlags.VNodeDelete || (kevt.fflags & FilterFlags.VNodeRevoke) == FilterFlags.VNodeRevoke)
359                                                 removeQueue.Add (pathData);
360
361                                         else if ((kevt.fflags & FilterFlags.VNodeWrite) == FilterFlags.VNodeWrite) {
362                                                 if (pathData.IsDirectory)
363                                                         rescanQueue.Add (pathData.Path);
364                                                 else
365                                                         PostEvent (FileAction.Modified, pathData.Path);
366                                         } 
367
368                                         else if ((kevt.fflags & FilterFlags.VNodeRename) == FilterFlags.VNodeRename) {
369                                                 var newFilename = GetFilenameFromFd (pathData.Fd);
370
371                                                 if (newFilename.StartsWith (fsw.FullPath))
372                                                         Rename (pathData, newFilename);
373                                                 else //moved outside of our watched dir so stop watching
374                                                                 RemoveTree (pathData);
375                                         } 
376
377                                         else if ((kevt.fflags & FilterFlags.VNodeAttrib) == FilterFlags.VNodeAttrib || (kevt.fflags & FilterFlags.VNodeExtend) == FilterFlags.VNodeExtend)
378                                                 PostEvent (FileAction.Modified, pathData.Path);
379                                 }
380
381                                 removeQueue.ForEach (Remove);
382                                 removeQueue.Clear ();
383
384                                 rescanQueue.ForEach (path => {
385                                         Scan (path, true, ref newFds);
386                                 });
387                                 rescanQueue.Clear ();
388                         }
389                 }
390
391                 PathData Add (string path, bool postEvents, ref List<int> fds)
392                 {
393                         PathData pathData;
394                         pathsDict.TryGetValue (path, out pathData);
395
396                         if (pathData != null)
397                                 return pathData;
398
399                         var fd = open (path, O_EVTONLY, 0);
400
401                         if (fd == -1) {
402                                 fsw.OnError (new ErrorEventArgs (new IOException (String.Format (
403                                         "open() error while attempting to process path '{0}', error code = '{1}'", path, Marshal.GetLastWin32Error ()))));
404                                 return null;
405                         }
406
407                         try {
408                                 fds.Add (fd);
409
410                                 var attrs = File.GetAttributes (path);
411
412                                 pathData = new PathData {
413                                         Path = path,
414                                         Fd = fd,
415                                         IsDirectory = (attrs & FileAttributes.Directory) == FileAttributes.Directory
416                                 };
417                                 
418                                 pathsDict.Add (path, pathData);
419                                 fdsDict.Add (fd, pathData);
420
421                                 if (postEvents)
422                                         PostEvent (FileAction.Added, path);
423
424                                 return pathData;
425                         } catch (Exception e) {
426                                 close (fd);
427                                 fsw.OnError (new ErrorEventArgs (e));
428                                 return null;
429                         }
430
431                 }
432
433                 void Remove (PathData pathData)
434                 {
435                         fdsDict.Remove (pathData.Fd);
436                         pathsDict.Remove (pathData.Path);
437                         close (pathData.Fd);
438                         PostEvent (FileAction.Removed, pathData.Path);
439                 }
440
441                 void RemoveTree (PathData pathData)
442                 {
443                         var toRemove = new List<PathData> ();
444
445                         toRemove.Add (pathData);
446
447                         if (pathData.IsDirectory) {
448                                 var prefix = pathData.Path + Path.DirectorySeparatorChar;
449                                 foreach (var path in pathsDict.Keys)
450                                         if (path.StartsWith (prefix)) {
451                                                 toRemove.Add (pathsDict [path]);
452                                         }
453                         }
454                         toRemove.ForEach (Remove);
455                 }
456
457                 void Rename (PathData pathData, string newRoot)
458                 {
459                         var toRename = new List<PathData> ();
460                         var oldRoot = pathData.Path;
461
462                         toRename.Add (pathData);
463                                                                                                                         
464                         if (pathData.IsDirectory) {
465                                 var prefix = oldRoot + Path.DirectorySeparatorChar;
466                                 foreach (var path in pathsDict.Keys)
467                                         if (path.StartsWith (prefix))
468                                                 toRename.Add (pathsDict [path]);
469                         }
470
471                         toRename.ForEach ((pd) => { 
472                                 var oldPath = pd.Path;
473                                 var newPath = newRoot + oldPath.Substring (oldRoot.Length);
474                                 pd.Path = newPath;
475                                 pathsDict.Remove (oldPath);
476                                 pathsDict.Add (newPath, pd);
477                         });
478
479                         PostEvent (FileAction.RenamedNewName, oldRoot, newRoot);
480                 }
481
482                 void Scan (string path, bool postEvents, ref List<int> fds)
483                 {
484                         if (requestStop)
485                                 return;
486                                 
487                         var pathData = Add (path, postEvents, ref fds);
488
489                         if (pathData == null)
490                                 return;
491                                 
492                         if (!pathData.IsDirectory)
493                                 return;
494
495                         var dirsToProcess = new List<string> ();
496                         dirsToProcess.Add (path);
497
498                         while (dirsToProcess.Count > 0) {
499                                 var tmp = dirsToProcess [0];
500                                 dirsToProcess.RemoveAt (0);
501
502                                 var info = new DirectoryInfo (tmp);
503                                 FileSystemInfo[] fsInfos = null;
504                                 try {
505                                         fsInfos = info.GetFileSystemInfos ();
506                                                 
507                                 } catch (IOException) {
508                                         // this can happen if the directory has been deleted already.
509                                         // that's okay, just keep processing the other dirs.
510                                         fsInfos = new FileSystemInfo[0];
511                                 }
512
513                                 foreach (var fsi in fsInfos) {
514                                         if ((fsi.Attributes & FileAttributes.Directory) == FileAttributes.Directory && !fsw.IncludeSubdirectories)
515                                                 continue;
516
517                                         if ((fsi.Attributes & FileAttributes.Directory) != FileAttributes.Directory && !fsw.Pattern.IsMatch (fsi.FullName))
518                                                 continue;
519
520                                         var currentPathData = Add (fsi.FullName, postEvents, ref fds);
521
522                                         if (currentPathData != null && currentPathData.IsDirectory)
523                                                 dirsToProcess.Add (fsi.FullName);
524                                 }
525                         }
526                 }
527                         
528                 void PostEvent (FileAction action, string path, string newPath = null)
529                 {
530                         RenamedEventArgs renamed = null;
531
532                         if (action == 0)
533                                 return;
534
535                         // only post events that match filter pattern. check both old and new paths for renames
536                         if (!fsw.Pattern.IsMatch (path) && (newPath == null || !fsw.Pattern.IsMatch (newPath))) 
537                                 return;
538                                 
539                         if (action == FileAction.RenamedNewName)
540                                 renamed = new RenamedEventArgs (WatcherChangeTypes.Renamed, "", newPath, path);
541
542                         lock (fsw) {
543                                 fsw.DispatchEvents (action, path, ref renamed);
544
545                                 if (fsw.Waiting) {
546                                         fsw.Waiting = false;
547                                         System.Threading.Monitor.PulseAll (fsw);
548                                 }
549                         }
550                 }
551
552                 private string GetFilenameFromFd (int fd)
553                 {
554                         var sb = new StringBuilder (__DARWIN_MAXPATHLEN);
555
556                         if (fcntl (fd, F_GETPATH, sb) != -1) {
557                                 if (fixupPath != null)
558                                         sb.Replace (fixupPath, fsw.FullPath, 0, fixupPath.Length); // see Setup()
559                                 return sb.ToString ();
560                         } else {
561                                 fsw.OnError (new ErrorEventArgs (new IOException (String.Format (
562                                         "fcntl() error while attempting to get path for fd '{0}', error code = '{1}'", fd, Marshal.GetLastWin32Error ()))));
563                                 return String.Empty;
564                         }
565                 }
566
567                 const int O_EVTONLY = 0x8000;
568                 const int F_GETPATH = 50;
569                 const int __DARWIN_MAXPATHLEN = 1024;
570                 static readonly kevent[] emptyEventList = new System.IO.kevent[0];
571
572                 FileSystemWatcher fsw;
573                 int conn;
574                 Thread thread;
575                 volatile bool requestStop = false;
576                 AutoResetEvent startedEvent = new AutoResetEvent (false);
577                 bool started = false;
578                 bool failedInit = false;
579                 object stateLock = new object ();
580
581                 readonly Dictionary<string, PathData> pathsDict = new Dictionary<string, PathData> ();
582                 readonly Dictionary<int, PathData> fdsDict = new Dictionary<int, PathData> ();
583                 string fixupPath = null;
584
585                 [DllImport ("libc", EntryPoint="fcntl", CharSet=CharSet.Auto, SetLastError=true)]
586                 static extern int fcntl (int file_names_by_descriptor, int cmd, StringBuilder sb);
587
588                 [DllImport ("libc")]
589                 extern static int open (string path, int flags, int mode_t);
590
591                 [DllImport ("libc")]
592                 extern static int close (int fd);
593
594                 [DllImport ("libc")]
595                 extern static int kqueue ();
596
597                 [DllImport ("libc")]
598                 extern static int kevent (int kq, [In]kevent[] ev, int nchanges, [Out]kevent[] evtlist, int nevents, [In] ref timespec time);
599         }
600
601         class KeventWatcher : IFileWatcher
602         {
603                 static bool failed;
604                 static KeventWatcher instance;
605                 static Hashtable watches;  // <FileSystemWatcher, KqueueMonitor>
606
607                 private KeventWatcher ()
608                 {
609                 }
610
611                 // Locked by caller
612                 public static bool GetInstance (out IFileWatcher watcher)
613                 {
614                         if (failed == true) {
615                                 watcher = null;
616                                 return false;
617                         }
618
619                         if (instance != null) {
620                                 watcher = instance;
621                                 return true;
622                         }
623
624                         watches = Hashtable.Synchronized (new Hashtable ());
625                         var conn = kqueue();
626                         if (conn == -1) {
627                                 failed = true;
628                                 watcher = null;
629                                 return false;
630                         }
631                         close (conn);
632
633                         instance = new KeventWatcher ();
634                         watcher = instance;
635                         return true;
636                 }
637
638                 public void StartDispatching (FileSystemWatcher fsw)
639                 {
640                         KqueueMonitor monitor;
641
642                         if (watches.ContainsKey (fsw)) {
643                                 monitor = (KqueueMonitor)watches [fsw];
644                         } else {
645                                 monitor = new KqueueMonitor (fsw);
646                                 watches.Add (fsw, monitor);
647                         }
648                                 
649                         monitor.Start ();
650                 }
651
652                 public void StopDispatching (FileSystemWatcher fsw)
653                 {
654                         KqueueMonitor monitor = (KqueueMonitor)watches [fsw];
655                         if (monitor == null)
656                                 return;
657
658                         monitor.Stop ();
659                 }
660                         
661                 [DllImport ("libc")]
662                 extern static int close (int fd);
663
664                 [DllImport ("libc")]
665                 extern static int kqueue ();
666         }
667 }
668