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