2006-10-04 Gonzalo Paniagua Javier <gonzalo@ximian.com>
[mono.git] / mcs / class / System / System.IO / InotifyWatcher.cs
1 // 
2 // System.IO.Inotify.cs: interface with inotify
3 //
4 // Authors:
5 //      Gonzalo Paniagua (gonzalo@novell.com)
6 //
7 // (c) 2006 Novell, Inc. (http://www.novell.com)
8
9 //
10 // Permission is hereby granted, free of charge, to any person obtaining
11 // a copy of this software and associated documentation files (the
12 // "Software"), to deal in the Software without restriction, including
13 // without limitation the rights to use, copy, modify, merge, publish,
14 // distribute, sublicense, and/or sell copies of the Software, and to
15 // permit persons to whom the Software is furnished to do so, subject to
16 // the following conditions:
17 // 
18 // The above copyright notice and this permission notice shall be
19 // included in all copies or substantial portions of the Software.
20 // 
21 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
25 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
26 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
27 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28 //
29
30 using System;
31 using System.Collections;
32 using System.ComponentModel;
33 using System.Runtime.CompilerServices;
34 using System.Runtime.InteropServices;
35 using System.Text;
36 using System.Threading;
37
38 namespace System.IO {
39
40         [Flags]
41         enum InotifyMask : uint {
42                 Access = 1 << 0,
43                 Modify = 1 << 1,
44                 Attrib = 1 << 2,
45                 CloseWrite = 1 << 3,
46                 CloseNoWrite = 1 << 4,
47                 Open = 1 << 5,
48                 MovedFrom = 1 << 6,
49                 MovedTo = 1 << 7,
50                 Create = 1 << 8,
51                 Delete = 1 << 9,
52                 DeleteSelf = 1 << 10,
53                 MoveSelf = 1 << 11,
54                 BaseEvents = 0x00000fff,
55                 // Can be sent at any time
56                 Umount = 0x0002000,
57                 Overflow = 0x0004000,
58                 Ignored = 0x0008000,
59
60                 // Special flags.
61                 OnlyDir = 0x01000000,
62                 DontFollow = 0x02000000,
63                 AddMask = 0x20000000,
64                 Directory = 0x40000000,
65                 OneShot = 0x80000000
66         }
67
68         struct InotifyEvent { // Our internal representation for the data returned by the kernel
69                 public static readonly InotifyEvent Default = new InotifyEvent ();
70                 public int WatchDescriptor;
71                 public InotifyMask Mask;
72                 public string Name;
73
74                 public override string ToString ()
75                 {
76                         return String.Format ("[Descriptor: {0} Mask: {1} Name: {2}]", WatchDescriptor, Mask, Name);
77                 }
78         }
79
80         class InotifyData {
81                 public FileSystemWatcher FSW;
82                 public string Directory;
83                 public string FileMask;
84                 public bool IncludeSubdirs;
85                 public bool Enabled;
86                 public int Watch;
87                 public Hashtable SubDirs;
88         }
89
90         class InotifyWatcher : IFileWatcher
91         {
92                 static bool failed;
93                 static InotifyWatcher instance;
94                 static Hashtable watches; // FSW to InotifyData
95                 static Hashtable requests; // FSW to InotifyData
96                 static IntPtr FD;
97                 static Thread thread;
98                 static bool stop;
99                 
100                 private InotifyWatcher ()
101                 {
102                 }
103                 
104                 // Locked by caller
105                 public static bool GetInstance (out IFileWatcher watcher, bool gamin)
106                 {
107                         if (failed == true) {
108                                 watcher = null;
109                                 return false;
110                         }
111
112                         if (instance != null) {
113                                 watcher = instance;
114                                 return true;
115                         }
116
117                         FD = GetInotifyInstance ();
118                         if ((long) FD == -1) {
119                                 failed = true;
120                                 watcher = null;
121                                 return false;
122                         }
123
124                         watches = Hashtable.Synchronized (new Hashtable ());
125                         requests = Hashtable.Synchronized (new Hashtable ());
126                         instance = new InotifyWatcher ();
127                         watcher = instance;
128                         return true;
129                 }
130                 
131                 public void StartDispatching (FileSystemWatcher fsw)
132                 {
133                         InotifyData data;
134                         lock (this) {
135                                 if ((long) FD == -1)
136                                         FD = GetInotifyInstance ();
137
138                                 if (thread == null) {
139                                         thread = new Thread (new ThreadStart (Monitor));
140                                         thread.IsBackground = true;
141                                         thread.Start ();
142                                 }
143
144                                 data = (InotifyData) watches [fsw];
145                         }
146
147                         if (data == null) {
148                                 data = new InotifyData ();
149                                 data.FSW = fsw;
150                                 data.Directory = fsw.FullPath;
151                                 data.FileMask = fsw.MangledFilter;
152                                 data.IncludeSubdirs = fsw.IncludeSubdirectories;
153                                 if (data.IncludeSubdirs)
154                                         data.SubDirs = new Hashtable ();
155
156                                 data.Enabled = true;
157                                 try {
158                                         StartMonitoringDirectory (data, false);
159                                         lock (this) {
160                                                 watches [fsw] = data;
161                                                 AppendRequestData (data);
162                                                 stop = false;
163                                         }
164                                 } catch {} // ignore the directory if StartMonitoringDirectory fails.
165                         }
166                 }
167                 
168                 static void AppendRequestData (InotifyData data)
169                 {
170                         int wd = data.Watch;
171                         object obj = requests [wd];
172                         ArrayList list = null;
173                         if (obj == null) {
174                                 requests [data.Watch] = data;
175                         } else if (obj is InotifyData) {
176                                 list = new ArrayList ();
177                                 list.Add (obj);
178                                 list.Add (data);
179                                 requests [data.Watch] = list;
180                         } else {
181                                 list = (ArrayList) obj;
182                                 list.Add (data);
183                         }
184                 }
185
186                 static bool RemoveRequestData (InotifyData data)
187                 {
188                         int wd = data.Watch;
189                         object obj = requests [wd];
190                         if (obj == null)
191                                 return true;
192
193                         if (obj is InotifyData) {
194                                 if (obj == data) {
195                                         requests.Remove (wd);
196                                         return true;
197                                 }
198                                 return false;
199                         }
200
201                         ArrayList list = (ArrayList) obj;
202                         list.Remove (data);
203                         if (list.Count == 0) {
204                                 requests.Remove (wd);
205                                 return true;
206                         }
207                         return false;
208                 }
209
210                 // Attempt to match MS and linux behavior.
211                 static InotifyMask GetMaskFromFilters (NotifyFilters filters)
212                 {
213                         InotifyMask mask = InotifyMask.Create | InotifyMask.Delete | InotifyMask.DeleteSelf | InotifyMask.AddMask;
214                         if ((filters & NotifyFilters.Attributes) != 0)
215                                 mask |= InotifyMask.Attrib;
216
217                         if ((filters & NotifyFilters.Security) != 0)
218                                 mask |= InotifyMask.Attrib;
219
220                         if ((filters & NotifyFilters.Size) != 0) {
221                                 mask |= InotifyMask.Attrib;
222                                 mask |= InotifyMask.Modify;
223                         }
224
225                         if ((filters & NotifyFilters.LastAccess) != 0) {
226                                 mask |= InotifyMask.Attrib;
227                                 mask |= InotifyMask.Access;
228                                 mask |= InotifyMask.Modify;
229                                 mask |= InotifyMask.CloseWrite;
230                         }
231
232                         if ((filters & NotifyFilters.LastWrite) != 0) {
233                                 mask |= InotifyMask.Attrib;
234                                 mask |= InotifyMask.CloseWrite;
235                         }
236
237                         if ((filters & NotifyFilters.FileName) != 0) {
238                                 mask |= InotifyMask.MovedFrom;
239                                 mask |= InotifyMask.MovedTo;
240                         }
241
242                         if ((filters & NotifyFilters.DirectoryName) != 0) {
243                                 mask |= InotifyMask.MovedFrom;
244                                 mask |= InotifyMask.MovedTo;
245                         }
246
247                         return mask;
248                 }
249
250                 static void StartMonitoringDirectory (InotifyData data, bool justcreated)
251                 {
252                         InotifyMask mask = GetMaskFromFilters (data.FSW.NotifyFilter);
253                         int wd = AddDirectoryWatch (FD, data.Directory, mask);
254                         if (wd == -1) {
255                                 int error = Marshal.GetLastWin32Error ();
256                                 if (error == 4) { // Too many open watches
257                                         string watches = "(unknown)";
258                                         try {
259                                                 using (StreamReader reader = new StreamReader ("/proc/sys/fs/inotify/max_user_watches")) {
260                                                         watches = reader.ReadLine ();
261                                                 }
262                                         } catch {}
263
264                                         string msg = String.Format ("The per-user inotify watches limit of {0} has been reached. " +
265                                                                 "If you're experiencing problems with your application, increase that limit " +
266                                                                 "in /proc/sys/fs/inotify/max_user_watches.", watches);
267                                         
268                                         throw new Win32Exception (error, msg);
269                                 }
270                                 throw new Win32Exception (error);
271                         }
272
273                         FileSystemWatcher fsw = data.FSW;
274                         data.Watch = wd;
275
276                         if (data.IncludeSubdirs) {
277                                 foreach (string directory in Directory.GetDirectories (data.Directory)) {
278                                         InotifyData fd = new InotifyData ();
279                                         fd.FSW = fsw;
280                                         fd.Directory = directory;
281                                         fd.FileMask = data.FSW.MangledFilter;
282                                         fd.IncludeSubdirs = true;
283                                         fd.SubDirs = new Hashtable ();
284                                         fd.Enabled = true;
285
286                                         if (justcreated) {
287                                                 lock (fsw) {
288                                                         RenamedEventArgs renamed = null;
289                                                         fsw.DispatchEvents (FileAction.Added, directory, ref renamed);
290                                                         if (fsw.Waiting) {
291                                                                 fsw.Waiting = false;
292                                                                 System.Threading.Monitor.PulseAll (fsw);
293                                                         }
294                                                 }
295                                         }
296
297                                         try {
298                                                 StartMonitoringDirectory (fd, justcreated);
299                                                 fd.SubDirs [directory] = fd;
300                                                 AppendRequestData (fd);
301                                         } catch {} // ignore errors and don't add directory.
302                                 }
303                         }
304
305                         if (justcreated) {
306                                 foreach (string filename in Directory.GetFiles (data.Directory)) {
307                                         lock (fsw) {
308                                                 RenamedEventArgs renamed = null;
309
310                                                 fsw.DispatchEvents (FileAction.Added, filename, ref renamed);
311                                                 /* If a file has been created, then it has been written to */
312                                                 fsw.DispatchEvents (FileAction.Modified, filename, ref renamed);
313
314                                                 if (fsw.Waiting) {
315                                                         fsw.Waiting = false;
316                                                         System.Threading.Monitor.PulseAll(fsw);
317                                                 }
318                                         }
319                                 }
320                         }
321                 }
322
323                 public void StopDispatching (FileSystemWatcher fsw)
324                 {
325                         InotifyData data;
326                         lock (this) {
327                                 data = (InotifyData) watches [fsw];
328                                 if (data == null)
329                                         return;
330
331                                 if (RemoveRequestData (data)) {
332                                         StopMonitoringDirectory (data);
333                                 }
334                                 watches.Remove (fsw);
335                                 if (watches.Count == 0) {
336                                         stop = true;
337                                         IntPtr fd = FD;
338                                         FD = (IntPtr) (-1);
339                                         Close (fd);
340                                 }
341
342                                 if (!data.IncludeSubdirs)
343                                         return;
344
345                                 foreach (InotifyData idata in data.SubDirs.Values) {
346                                         if (RemoveRequestData (idata)) {
347                                                 StopMonitoringDirectory (idata);
348                                         }
349                                 }
350                         }
351                 }
352
353                 static void StopMonitoringDirectory (InotifyData data)
354                 {
355                         RemoveWatch (FD, data.Watch);
356                 }
357
358                 void Monitor ()
359                 {
360                         byte [] buffer = new byte [4096];
361                         int nread;
362                         while (!stop) {
363                                 nread = ReadFromFD (FD, buffer, (IntPtr) buffer.Length);
364                                 if (nread == -1)
365                                         continue;
366
367                                 lock (this) {
368                                         ProcessEvents (buffer, nread);
369
370                                 }
371                         }
372
373                         lock (this) {
374                                 thread = null;
375                                 stop = false;
376                         }
377                 }
378                 /*
379                 struct inotify_event {
380                         __s32           wd;
381                         __u32           mask;
382                         __u32           cookie;
383                         __u32           len;            // Includes any trailing null in 'name'
384                         char            name[0];
385                 };
386                 */
387
388                 static int ReadEvent (byte [] source, int off, int size, out InotifyEvent evt)
389                 {
390                         evt = new InotifyEvent ();
391                         if (size <= 0 || off > size - 16) {
392                                 return -1;
393                         }
394
395                         int len;
396                         if (BitConverter.IsLittleEndian) {
397                                 evt.WatchDescriptor = source [off] + (source [off + 1] << 8) +
398                                                         (source [off + 2] << 16) + (source [off + 3] << 24);
399                                 evt.Mask = (InotifyMask) (source [off + 4] + (source [off + 5] << 8) +
400                                                         (source [off + 6] << 16) + (source [off + 7] << 24));
401                                 // Ignore Cookie -> +4
402                                 len = source [off + 12] + (source [off + 13] << 8) +
403                                         (source [off + 14] << 16) + (source [off + 15] << 24);
404                         } else {
405                                 evt.WatchDescriptor = source [off + 3] + (source [off + 2] << 8) +
406                                                         (source [off + 1] << 16) + (source [off] << 24);
407                                 evt.Mask = (InotifyMask) (source [off + 7] + (source [off + 6] << 8) +
408                                                         (source [off + 5] << 16) + (source [off + 4] << 24));
409                                 // Ignore Cookie -> +4
410                                 len = source [off + 15] + (source [off + 14] << 8) +
411                                         (source [off + 13] << 16) + (source [off + 12] << 24);
412                         }
413
414                         if (len > 0) {
415                                 if (off > size - 16 - len)
416                                         return -1;
417                                 string name = Encoding.UTF8.GetString (source, off + 16, len);
418                                 evt.Name = name.Trim ('\0');
419                         } else {
420                                 evt.Name = null;
421                         }
422
423                         return 16 + len;
424                 }
425
426                 static IEnumerable GetEnumerator (object source)
427                 {
428                         if (source == null)
429                                 yield break;
430
431                         if (source is InotifyData)
432                                 yield return source;
433
434                         if (source is ArrayList) {
435                                 ArrayList list = (ArrayList) source;
436                                 for (int i = 0; i < list.Count; i++)
437                                         yield return list [i];
438                         }
439                 }
440
441                 /* Interesting events:
442                         * Modify
443                         * Attrib
444                         * MovedFrom
445                         * MovedTo
446                         * Create
447                         * Delete
448                         * DeleteSelf
449                         * CloseWrite
450                 */
451                 static InotifyMask Interesting = InotifyMask.Modify | InotifyMask.Attrib | InotifyMask.MovedFrom |
452                                                         InotifyMask.MovedTo | InotifyMask.Create | InotifyMask.Delete |
453                                                         InotifyMask.DeleteSelf | InotifyMask.CloseWrite;
454
455                 void ProcessEvents (byte [] buffer, int length)
456                 {
457                         ArrayList newdirs = null;
458                         InotifyEvent evt;
459                         int nread = 0;
460                         bool new_name_needed = false;
461                         RenamedEventArgs renamed = null;
462                         while (length > nread) {
463                                 int bytes_read = ReadEvent (buffer, nread, length, out evt);
464                                 if (bytes_read <= 0)
465                                         break;
466
467                                 nread += bytes_read;
468
469                                 InotifyMask mask = evt.Mask;
470                                 bool is_directory = (mask & InotifyMask.Directory) != 0;
471                                 mask = (mask & Interesting); // Clear out all the bits that we don't need
472                                 if (mask == 0)
473                                         continue;
474
475                                 foreach (InotifyData data in GetEnumerator (requests [evt.WatchDescriptor])) {
476                                         if (data == null || data.Enabled == false)
477                                                 continue;
478
479                                         string directory = data.Directory;
480                                         string filename = evt.Name;
481                                         if (filename == null)
482                                                 filename = directory;
483
484                                         FileSystemWatcher fsw = data.FSW;
485                                         FileAction action = 0;
486                                         if ((mask & (InotifyMask.Modify | InotifyMask.CloseWrite | InotifyMask.Attrib)) != 0) {
487                                                 action = FileAction.Modified;
488                                         } else if ((mask & InotifyMask.Create) != 0) {
489                                                 action = FileAction.Added;
490                                         } else if ((mask & InotifyMask.Delete) != 0) {
491                                                 action = FileAction.Removed;
492                                         } else if ((mask & InotifyMask.DeleteSelf) != 0) {
493                                                 action = FileAction.Removed;
494                                         } else if ((mask & InotifyMask.MoveSelf) != 0) {
495                                                 //action = FileAction.Removed;
496                                                 continue; // Ignore this one
497                                         } else if ((mask & InotifyMask.MovedFrom) != 0) {
498                                                 InotifyEvent to;
499                                                 int i = ReadEvent (buffer, nread, length, out to);
500                                                 if (i == -1 || (to.Mask & InotifyMask.MovedTo) == 0) {
501                                                         action = FileAction.Removed;
502                                                 } else {
503                                                         nread += i;
504                                                         action = FileAction.RenamedNewName;
505                                                         if (evt.Name == data.Directory || fsw.Pattern.IsMatch (evt.Name)) {
506                                                                 renamed = new RenamedEventArgs (WatcherChangeTypes.Renamed, data.Directory, to.Name, evt.Name);
507                                                         } else {
508                                                                 renamed = new RenamedEventArgs (WatcherChangeTypes.Renamed, data.Directory, evt.Name, to.Name);
509                                                                 filename = to.Name;
510                                                         }
511                                                 }
512                                         } else if ((mask & InotifyMask.MovedTo) != 0) {
513                                                 action = (new_name_needed) ? FileAction.RenamedNewName : FileAction.Added;
514                                                 new_name_needed = false;
515                                         }
516
517                                         if (fsw.IncludeSubdirectories) {
518                                                 string full = fsw.FullPath;
519                                                 string datadir = data.Directory;
520                                                 if (datadir != full) {
521                                                         int len = full.Length;
522                                                         int slash = 1;
523                                                         if (len > 1 && full [len - 1] == Path.DirectorySeparatorChar)
524                                                                 slash = 0;
525                                                         string reldir = datadir.Substring (full.Length + slash);
526                                                         datadir = Path.Combine (datadir, filename);
527                                                         filename = Path.Combine (reldir, filename);
528                                                 } else {
529                                                         datadir = Path.Combine (full, filename);
530                                                 }
531
532                                                 if (action == FileAction.Added && is_directory) {
533                                                         if (newdirs == null)
534                                                                 newdirs = new ArrayList (4);
535
536                                                         InotifyData fd = new InotifyData ();
537                                                         fd.FSW = fsw;
538                                                         fd.Directory = datadir;
539                                                         fd.FileMask = fsw.MangledFilter;
540                                                         fd.IncludeSubdirs = true;
541                                                         fd.SubDirs = new Hashtable ();
542                                                         fd.Enabled = true;
543                                                         newdirs.Add (fd);
544                                                         newdirs.Add (data);
545                                                 }
546                                         }
547
548                                         if (filename != data.Directory && !fsw.Pattern.IsMatch (filename)) {
549                                                 continue;
550                                         }
551
552                                         lock (fsw) {
553                                                 fsw.DispatchEvents (action, filename, ref renamed);
554                                                 if (action == FileAction.RenamedNewName)
555                                                         renamed = null;
556                                                 if (fsw.Waiting) {
557                                                         fsw.Waiting = false;
558                                                         System.Threading.Monitor.PulseAll (fsw);
559                                                 }
560                                         }
561                                 }
562                         }
563
564                         if (newdirs != null) {
565                                 int count = newdirs.Count;
566                                 for (int n = 0; n < count; n += 2) {
567                                         InotifyData newdir = (InotifyData) newdirs [n];
568                                         InotifyData parent = (InotifyData) newdirs [n + 1];
569                                         try {
570                                                 StartMonitoringDirectory (newdir, true);
571                                                 AppendRequestData (newdir);
572                                                 lock (parent) {
573                                                         parent.SubDirs [newdir.Directory] = newdir;
574                                                 }
575                                         } catch {} // ignore the given directory
576                                 }
577                                 newdirs.Clear ();
578                         }
579                 }
580
581                 static int AddDirectoryWatch (IntPtr fd, string directory, InotifyMask mask)
582                 {
583                         mask |= InotifyMask.Directory;
584                         return AddWatch (fd, directory, mask);
585                 }
586
587                 [DllImport ("libc", EntryPoint="close")]
588                 internal extern static int Close (IntPtr fd);
589
590                 [DllImport ("libc", EntryPoint = "read")]
591                 extern static int ReadFromFD (IntPtr fd, byte [] buffer, IntPtr length);
592
593                 [MethodImplAttribute(MethodImplOptions.InternalCall)]
594                 extern static IntPtr GetInotifyInstance ();
595
596                 [MethodImplAttribute(MethodImplOptions.InternalCall)]
597                 extern static int AddWatch (IntPtr fd, string name, InotifyMask mask);
598
599                 [MethodImplAttribute(MethodImplOptions.InternalCall)]
600                 extern static IntPtr RemoveWatch (IntPtr fd, int wd);
601         }
602 }
603