Reimplemented much of the kqueue-based file watcher so that watching subdirectories...
[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 //
8 // (c) 2004 Geoff Norton
9 // Copyright 2014 Xamarin Inc
10 //
11 // Permission is hereby granted, free of charge, to any person obtaining
12 // a copy of this software and associated documentation files (the
13 // "Software"), to deal in the Software without restriction, including
14 // without limitation the rights to use, copy, modify, merge, publish,
15 // distribute, sublicense, and/or sell copies of the Software, and to
16 // permit persons to whom the Software is furnished to do so, subject to
17 // the following conditions:
18 // 
19 // The above copyright notice and this permission notice shall be
20 // included in all copies or substantial portions of the Software.
21 // 
22 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
26 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
27 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
28 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29 //
30
31 using System;
32 using System.IO;
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
41 namespace System.IO {
42         [Flags]
43         enum EventFlags : ushort {
44                 Add         = 0x0001,
45                 Delete      = 0x0002,
46                 Enable      = 0x0004,
47                 Disable     = 0x0008,
48                 OneShot     = 0x0010,
49                 Clear       = 0x0020,
50                 Receipt     = 0x0040,
51                 Dispatch    = 0x0080,
52
53                 Flag0       = 0x1000,
54                 Flag1       = 0x2000,
55                 SystemFlags = unchecked (0xf000),
56
57                 // Return values.
58                 EOF         = 0x8000,
59                 Error       = 0x4000,
60         }
61         
62         enum EventFilter : short {
63                 Read     = -1,
64                 Write    = -2,
65                 Aio      = -3,
66                 Vnode    = -4,
67                 Proc     = -5,
68                 Signal   = -6,
69                 Timer    = -7,
70                 MachPort = -8,
71                 FS       = -9,
72                 User     = -10,
73                 VM       = -11
74         }
75
76         enum FilterFlags : uint {
77                 ReadPoll          = EventFlags.Flag0,
78                 ReadOutOfBand     = EventFlags.Flag1,
79                 ReadLowWaterMark  = 0x00000001,
80
81                 WriteLowWaterMark = ReadLowWaterMark,
82
83                 NoteTrigger       = 0x01000000,
84                 NoteFFNop         = 0x00000000,
85                 NoteFFAnd         = 0x40000000,
86                 NoteFFOr          = 0x80000000,
87                 NoteFFCopy        = 0xc0000000,
88                 NoteFFCtrlMask    = 0xc0000000,
89                 NoteFFlagsMask    = 0x00ffffff,
90
91                 VNodeDelete       = 0x00000001,
92                 VNodeWrite        = 0x00000002,
93                 VNodeExtend       = 0x00000004,
94                 VNodeAttrib       = 0x00000008,
95                 VNodeLink         = 0x00000010,
96                 VNodeRename       = 0x00000020,
97                 VNodeRevoke       = 0x00000040,
98                 VNodeNone         = 0x00000080,
99
100                 ProcExit          = 0x80000000,
101                 ProcFork          = 0x40000000,
102                 ProcExec          = 0x20000000,
103                 ProcReap          = 0x10000000,
104                 ProcSignal        = 0x08000000,
105                 ProcExitStatus    = 0x04000000,
106                 ProcResourceEnd   = 0x02000000,
107
108                 // iOS only
109                 ProcAppactive     = 0x00800000,
110                 ProcAppBackground = 0x00400000,
111                 ProcAppNonUI      = 0x00200000,
112                 ProcAppInactive   = 0x00100000,
113                 ProcAppAllStates  = 0x00f00000,
114
115                 // Masks
116                 ProcPDataMask     = 0x000fffff,
117                 ProcControlMask   = 0xfff00000,
118
119                 VMPressure                = 0x80000000,
120                 VMPressureTerminate       = 0x40000000,
121                 VMPressureSuddenTerminate = 0x20000000,
122                 VMError                   = 0x10000000,
123                 TimerSeconds              = 0x00000001,
124                 TimerMicroSeconds         = 0x00000002,
125                 TimerNanoSeconds          = 0x00000004,
126                 TimerAbsolute             = 0x00000008,
127         }
128
129         [StructLayout(LayoutKind.Sequential)]
130         struct kevent : IDisposable {
131                 public int ident;
132                 public EventFilter filter;
133                 public EventFlags flags;
134                 public FilterFlags fflags;
135                 public IntPtr data;
136                 public IntPtr udata;
137
138                 public void Dispose ()
139                 {
140                         if (udata != IntPtr.Zero)
141                                 Marshal.FreeHGlobal (udata);
142                 }
143         }
144
145         struct timespec {
146                 public int tv_sec;
147                 public int tv_usec;
148         }
149
150         class KeventFileData {
151                 public FileSystemInfo fsi;
152                 public DateTime LastAccessTime;
153                 public DateTime LastWriteTime;
154
155                 public KeventFileData(FileSystemInfo fsi, DateTime LastAccessTime, DateTime LastWriteTime) {
156                         this.fsi = fsi;
157                         this.LastAccessTime = LastAccessTime;
158                         this.LastWriteTime = LastWriteTime;
159                 }
160         }
161
162         class KeventData {
163                 public FileSystemWatcher FSW;
164                 public string Path;
165                 public string FileMask;
166                 public bool IncludeSubdirs;
167                 public bool Enabled;
168                 public Hashtable DirEntries;
169                 public kevent ev;
170                 public int fd;
171                 public bool IsDirectory;
172         }
173
174         class KeventWatcher : IFileWatcher
175         {
176                 static bool failed;
177                 static KeventWatcher instance;
178                 static Hashtable watches;  // <FileSystemWatcher, KeventData>
179                 static Thread thread;
180                 static int conn;
181                 static bool stop;
182
183                 readonly Dictionary<string, KeventData> filenamesDict = new Dictionary<string, KeventData> ();
184                 readonly Dictionary<int, KeventData> fdsDict = new Dictionary<int, KeventData> ();
185                 readonly List<int> removeQueue = new List<int> ();
186                 readonly List<int> rescanQueue = new List<int> ();
187                 
188                 private KeventWatcher ()
189                 {
190                 }
191                 
192                 // Locked by caller
193                 public static bool GetInstance (out IFileWatcher watcher)
194                 {
195                         if (failed == true) {
196                                 watcher = null;
197                                 return false;
198                         }
199
200                         if (instance != null) {
201                                 watcher = instance;
202                                 return true;
203                         }
204
205                         watches = Hashtable.Synchronized (new Hashtable ());
206                         conn = kqueue();
207                         if (conn == -1) {
208                                 failed = true;
209                                 watcher = null;
210                                 return false;
211                         }
212
213                         instance = new KeventWatcher ();
214                         watcher = instance;
215                         return true;
216                 }
217                 
218                 public void StartDispatching (FileSystemWatcher fsw)
219                 {
220                         KeventData data;
221
222                         lock (this) {
223                                 if (thread == null) {
224                                         thread = new Thread (new ThreadStart (Monitor));
225                                         thread.IsBackground = true;
226                                         thread.Start ();
227                                 }
228
229                                 data = (KeventData) watches [fsw];
230                         }
231
232                         if (data == null) {
233                                 data = new KeventData ();
234                                 data.FSW = fsw;
235                                 data.Path = fsw.FullPath;
236                                 data.FileMask = fsw.MangledFilter;
237                                 data.IncludeSubdirs = fsw.IncludeSubdirectories;
238
239                                 data.Enabled = true;
240                                 lock (this) {
241                                         Scan (data);
242                                         watches [fsw] = data;
243                                         stop = false;
244                                 }
245                         }
246                 }
247
248                 bool Add (KeventData data, bool postEvents = false)
249                 {
250                         var path = data.Path;
251
252                         if (filenamesDict.ContainsKey (path) || fdsDict.ContainsKey (data.fd) ) {
253                                 return false;
254                         }
255
256                         var fd = open (path, 0x8000 /* O_EVTONLY */, 0);
257
258                         if (fd != -1) {
259                                 data.fd = fd;
260                                 filenamesDict.Add (path, data);
261                                 fdsDict.Add (fd, data);
262
263                                 var attrs = File.GetAttributes (data.Path);
264                                 data.IsDirectory = ((attrs & FileAttributes.Directory) == FileAttributes.Directory);
265
266                                 if (postEvents)
267                                         PostEvent (path, data.FSW, FileAction.Added, path);
268
269                                 return true;
270                         } else {
271                                 return false;
272                         }
273                 }
274
275                 void Remove (int fd)
276                 {
277                         if (!fdsDict.ContainsKey (fd))
278                                 return;
279
280                         var data = fdsDict [fd];
281                         fdsDict.Remove (fd);
282                         filenamesDict.Remove (data.Path);
283                         removeQueue.Remove (fd);
284
285                         close (fd);
286                 }
287
288                 void Remove (string path)
289                 {
290                         var data = filenamesDict [path];
291
292                         filenamesDict.Remove (path);
293                         fdsDict.Remove (data.fd);
294                         close (data.fd);
295                 }
296
297                 bool Scan (KeventData data, bool postEvents = false)
298                 {
299                         var path = data.Path;
300
301                         Add (data);
302                         if (!data.IncludeSubdirs) {
303                                 return true;
304                         }
305
306                         if (data.IsDirectory && !Directory.Exists (path))
307                                 return false;
308
309                         var attrs = File.GetAttributes (path);
310                         if ((attrs & FileAttributes.Directory) == FileAttributes.Directory) {
311                                 var dirs_to_process = new List<string> ();
312                                 dirs_to_process.Add (path);
313
314                                 while (dirs_to_process.Count > 0) {
315                                         var tmp_path = dirs_to_process [0];
316                                         dirs_to_process.RemoveAt (0);
317                                         var dirinfo = new DirectoryInfo (tmp_path);
318                                         foreach (var fsi in dirinfo.GetFileSystemInfos ()) {
319                                                 var newdata = new KeventData {
320                                                         Path = fsi.FullName,
321                                                         FileMask = data.FileMask,
322                                                         FSW = data.FSW,
323                                                         IncludeSubdirs = data.IncludeSubdirs
324                                                 };
325
326                                                 if (!Add (newdata, postEvents))
327                                                         continue;
328
329                                                 var childAttrs = File.GetAttributes (fsi.FullName);
330                                                 if ((childAttrs & FileAttributes.Directory) == FileAttributes.Directory)
331                                                         dirs_to_process.Add (fsi.FullName);
332                                         }
333                                 }
334                         }
335
336                         return true;
337                 }
338
339                 public void StopDispatching (FileSystemWatcher fsw)
340                 {
341                         KeventData data;
342                         lock (this) {
343                                 data = (KeventData) watches [fsw];
344                                 if (data == null)
345                                         return;
346
347                                 StopMonitoringDirectory (data);
348                                 watches.Remove (fsw);
349                                 if (watches.Count == 0)
350                                         stop = true;
351
352                                 if (!data.IncludeSubdirs)
353                                         return;
354
355                         }
356                 }
357
358                 static void StopMonitoringDirectory (KeventData data)
359                 {
360                         close(data.ev.ident);
361                 }
362
363                 void Monitor ()
364                 {
365                         bool firstRun = true;
366
367                         while (!stop) {
368                                 removeQueue.ForEach (Remove);
369
370                                 rescanQueue.ForEach (
371                                         fd => {
372                                                 var data = fdsDict[fd];
373                                                 Scan (data, !firstRun);
374
375                                                 rescanQueue.Remove (fd);
376                                         }
377                                 );
378
379                                 foreach (KeventData data in watches.Values) {
380                                         Scan (data);
381                                 }
382
383                                 var changes = new List<kevent> ();
384                                 var outEvents = new List<kevent> ();
385
386                                 foreach (KeyValuePair<int, KeventData> kv in fdsDict) {
387                                         var change = new kevent {
388                                                 ident = kv.Key,
389                                                 filter = EventFilter.Vnode,
390                                                 flags = EventFlags.Add | EventFlags.Enable | EventFlags.Clear,
391                                                 fflags = FilterFlags.VNodeDelete | FilterFlags.VNodeExtend | FilterFlags.VNodeRename | FilterFlags.VNodeAttrib | FilterFlags.VNodeLink | FilterFlags.VNodeRevoke | FilterFlags.VNodeWrite,
392                                                 data = IntPtr.Zero,
393                                                 udata = IntPtr.Zero
394                                         };
395
396                                         changes.Add (change);
397                                         outEvents.Add (new kevent());
398                                 }
399
400                                 if (changes.Count > 0) {
401                                         int numEvents = 0;
402                                         var out_array = outEvents.ToArray ();
403
404                                         lock (this) {
405                                                 kevent[] changes_array = changes.ToArray ();
406                                                 numEvents = kevent (conn, changes_array, changes_array.Length, out_array, out_array.Length, IntPtr.Zero);
407                                         }
408
409                                         for (var i = 0; i < numEvents; i++) {
410                                                 var kevt = out_array [i];
411                                                 if ((kevt.flags & EventFlags.Error) == EventFlags.Error)
412                                                         throw new Exception ("kevent error");
413
414                                                 if ((kevt.fflags & FilterFlags.VNodeDelete) != 0) {
415                                                         removeQueue.Add (kevt.ident);
416                                                         var data = fdsDict [kevt.ident];
417                                                         PostEvent (data.Path, data.FSW, FileAction.Removed, data.Path);
418                                                 } else if (((kevt.fflags & FilterFlags.VNodeRename) != 0) || ((kevt.fflags & FilterFlags.VNodeRevoke) != 0) || ((kevt.fflags & FilterFlags.VNodeWrite) != 0)) {
419                                                         var data = fdsDict [kevt.ident];
420                                                         if (data.IsDirectory && Directory.Exists (data.Path))
421                                                                 rescanQueue.Add (kevt.ident);
422
423                                                         if ((kevt.fflags & FilterFlags.VNodeRename) != 0) {
424                                                                 var newFilename = GetFilenameFromFd (data.fd);
425                                                                 Remove (data.fd);
426                                                                 PostEvent (data.Path, data.FSW, FileAction.RenamedNewName, data.Path, newFilename);
427
428                                                                 var newEvent = new KeventData {
429                                                                         Path = newFilename,
430                                                                         FileMask = data.FileMask,
431                                                                         FSW = data.FSW,
432                                                                         IncludeSubdirs = data.IncludeSubdirs
433                                                                 };
434
435                                                                 Add (newEvent, false);
436                                                         }
437                                                 } else if ((kevt.fflags & FilterFlags.VNodeAttrib) != 0) {
438                                                         var data = fdsDict[kevt.ident];
439                                                         PostEvent (data.Path, data.FSW, FileAction.Modified, data.Path);
440                                                 }
441                                         }
442                                 } else {
443                                         Thread.Sleep (500);
444                                 }
445
446                                 firstRun = false;
447                         }
448
449                         lock (this) {
450                                 thread = null;
451                                 stop = false;
452                         }
453                 }
454
455                 private void PostEvent (string filename, FileSystemWatcher fsw, FileAction fa, string fullname, string newname = null)
456                 {
457                         RenamedEventArgs renamed = null;
458
459                         if (fa == 0)
460                                 return;
461
462                         if (fa == FileAction.RenamedNewName)
463                                 renamed = new RenamedEventArgs (WatcherChangeTypes.Renamed, "", newname, fullname);
464
465                         lock (fsw) {
466                                 fsw.DispatchEvents (fa, filename, ref renamed);
467                                 if (fsw.Waiting) {
468                                         fsw.Waiting = false;
469                                         System.Threading.Monitor.PulseAll (fsw);
470                                 }
471                         }
472                 }
473
474                 private string GetFilenameFromFd (int fd)
475                 {
476                         var sb = new StringBuilder (1024);
477
478                         if (fcntl (fd, 50 /* F_GETPATH */, sb) != -1) {
479                                 return sb.ToString ();
480                         } else {
481                                 return String.Empty;
482                         }
483                 }
484
485                 [DllImport("libc", EntryPoint="fcntl", CharSet=CharSet.Auto, SetLastError=true)]
486                 public static extern int fcntl (int file_names_by_descriptor, int cmd, StringBuilder sb);
487
488                 [DllImport ("libc")]
489                 extern static int open(string path, int flags, int mode_t);
490                 
491                 [DllImport ("libc")]
492                 extern static int close(int fd);
493
494                 [DllImport ("libc")]
495                 extern static int kqueue();
496
497                 [DllImport ("libc")]
498                 extern static int kevent(int kq, [In]kevent[] ev, int nchanges, [Out]kevent[] evtlist, int nevents, IntPtr time);
499         }
500 }