Merge pull request #439 from mono-soc-2012/garyb/iconfix
[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
179                         object obj = requests [wd];
180                         ArrayList list = null;
181                         if (obj == null) {
182                                 requests [data.Watch] = data;
183                         } else if (obj is InotifyData) {
184                                 list = new ArrayList ();
185                                 list.Add (obj);
186                                 list.Add (data);
187                                 requests [data.Watch] = list;
188                         } else {
189                                 list = (ArrayList) obj;
190                                 list.Add (data);
191                         }
192                 }
193
194                 static bool RemoveRequestData (InotifyData data)
195                 {
196                         int wd = data.Watch;
197                         object obj = requests [wd];
198                         if (obj == null)
199                                 return true;
200
201                         if (obj is InotifyData) {
202                                 if (obj == data) {
203                                         requests.Remove (wd);
204                                         return true;
205                                 }
206                                 return false;
207                         }
208
209                         ArrayList list = (ArrayList) obj;
210                         list.Remove (data);
211                         if (list.Count == 0) {
212                                 requests.Remove (wd);
213                                 return true;
214                         }
215                         return false;
216                 }
217
218                 // Attempt to match MS and linux behavior.
219                 static InotifyMask GetMaskFromFilters (NotifyFilters filters)
220                 {
221                         InotifyMask mask = InotifyMask.Create | InotifyMask.Delete | InotifyMask.DeleteSelf | InotifyMask.AddMask;
222                         if ((filters & NotifyFilters.Attributes) != 0)
223                                 mask |= InotifyMask.Attrib;
224
225                         if ((filters & NotifyFilters.Security) != 0)
226                                 mask |= InotifyMask.Attrib;
227
228                         if ((filters & NotifyFilters.Size) != 0) {
229                                 mask |= InotifyMask.Attrib;
230                                 mask |= InotifyMask.Modify;
231                         }
232
233                         if ((filters & NotifyFilters.LastAccess) != 0) {
234                                 mask |= InotifyMask.Attrib;
235                                 mask |= InotifyMask.Access;
236                                 mask |= InotifyMask.Modify;
237                         }
238
239                         if ((filters & NotifyFilters.LastWrite) != 0) {
240                                 mask |= InotifyMask.Attrib;
241                                 mask |= InotifyMask.Modify;
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                                                         if (fsw.Pattern.IsMatch (directory)) {
295                                                                 fsw.DispatchEvents (FileAction.Added, directory, ref renamed);
296                                                                 if (fsw.Waiting) {
297                                                                         fsw.Waiting = false;
298                                                                         System.Threading.Monitor.PulseAll (fsw);
299                                                                 }
300                                                         }
301                                                 }
302                                         }
303
304                                         try {
305                                                 StartMonitoringDirectory (fd, justcreated);
306                                                 AppendRequestData (fd);
307                                                 parent.children.Add(fd);
308                                         } catch {} // ignore errors and don't add directory.
309                                 }
310                         }
311
312                         if (justcreated) {
313                                 foreach (string filename in Directory.GetFiles (data.Directory)) {
314                                         lock (fsw) {
315                                                 RenamedEventArgs renamed = null;
316                                                 if (fsw.Pattern.IsMatch (filename)) {
317                                                         fsw.DispatchEvents (FileAction.Added, filename, ref renamed);
318                                                         /* If a file has been created, then it has been written to */
319                                                         fsw.DispatchEvents (FileAction.Modified, filename, ref renamed);
320
321                                                         if (fsw.Waiting) {
322                                                                 fsw.Waiting = false;
323                                                                 System.Threading.Monitor.PulseAll(fsw);
324                                                         }
325                                                 }
326                                         }
327                                 }
328                         }
329                 }
330
331                 public void StopDispatching (FileSystemWatcher fsw)
332                 {
333                         ParentInotifyData parent;
334                         lock (this) {
335                                 parent = (ParentInotifyData) watches [fsw];
336                                 if (parent == null)
337                                         return;
338
339                                 if (RemoveRequestData (parent.data)) {
340                                         StopMonitoringDirectory (parent.data);
341                                 }
342                                 watches.Remove (fsw);
343                                 if (watches.Count == 0) {
344                                         stop = true;
345                                         IntPtr fd = FD;
346                                         FD = (IntPtr) (-1);
347                                         Close (fd);
348                                 }
349
350                                 if (!parent.IncludeSubdirs)
351                                         return;
352
353                                 foreach (InotifyData idata in parent.children)
354                                 {
355                                     if (RemoveRequestData (idata)) {
356                                         StopMonitoringDirectory (idata);
357                                     }
358                                 }
359                         }
360                 }
361
362                 static void StopMonitoringDirectory (InotifyData data)
363                 {
364                         RemoveWatch (FD, data.Watch);
365                 }
366
367                 void Monitor ()
368                 {
369                         byte [] buffer = new byte [4096];
370                         int nread;
371                         while (!stop) {
372                                 nread = ReadFromFD (FD, buffer, (IntPtr) buffer.Length);
373                                 if (nread == -1)
374                                         continue;
375
376                                 lock (this) {
377                                         ProcessEvents (buffer, nread);
378
379                                 }
380                         }
381
382                         lock (this) {
383                                 thread = null;
384                                 stop = false;
385                         }
386                 }
387                 /*
388                 struct inotify_event {
389                         __s32           wd;
390                         __u32           mask;
391                         __u32           cookie;
392                         __u32           len;            // Includes any trailing null in 'name'
393                         char            name[0];
394                 };
395                 */
396
397                 static int ReadEvent (byte [] source, int off, int size, out InotifyEvent evt)
398                 {
399                         evt = new InotifyEvent ();
400                         if (size <= 0 || off > size - 16) {
401                                 return -1;
402                         }
403
404                         int len;
405                         if (BitConverter.IsLittleEndian) {
406                                 evt.WatchDescriptor = source [off] + (source [off + 1] << 8) +
407                                                         (source [off + 2] << 16) + (source [off + 3] << 24);
408                                 evt.Mask = (InotifyMask) (source [off + 4] + (source [off + 5] << 8) +
409                                                         (source [off + 6] << 16) + (source [off + 7] << 24));
410                                 // Ignore Cookie -> +4
411                                 len = source [off + 12] + (source [off + 13] << 8) +
412                                         (source [off + 14] << 16) + (source [off + 15] << 24);
413                         } else {
414                                 evt.WatchDescriptor = source [off + 3] + (source [off + 2] << 8) +
415                                                         (source [off + 1] << 16) + (source [off] << 24);
416                                 evt.Mask = (InotifyMask) (source [off + 7] + (source [off + 6] << 8) +
417                                                         (source [off + 5] << 16) + (source [off + 4] << 24));
418                                 // Ignore Cookie -> +4
419                                 len = source [off + 15] + (source [off + 14] << 8) +
420                                         (source [off + 13] << 16) + (source [off + 12] << 24);
421                         }
422
423                         if (len > 0) {
424                                 if (off > size - 16 - len)
425                                         return -1;
426                                 string name = Encoding.UTF8.GetString (source, off + 16, len);
427                                 evt.Name = name.Trim ('\0');
428                         } else {
429                                 evt.Name = null;
430                         }
431
432                         return 16 + len;
433                 }
434
435                 static IEnumerable GetEnumerator (object source)
436                 {
437                         if (source == null)
438                                 yield break;
439
440                         if (source is InotifyData)
441                                 yield return source;
442
443                         if (source is ArrayList) {
444                                 ArrayList list = (ArrayList) source;
445                                 for (int i = 0; i < list.Count; i++)
446                                         yield return list [i];
447                         }
448                 }
449
450                 /* Interesting events:
451                         * Modify
452                         * Attrib
453                         * MovedFrom
454                         * MovedTo
455                         * Create
456                         * Delete
457                         * DeleteSelf
458                 */
459                 static InotifyMask Interesting = InotifyMask.Modify | InotifyMask.Attrib | InotifyMask.MovedFrom |
460                                                         InotifyMask.MovedTo | InotifyMask.Create | InotifyMask.Delete |
461                                                         InotifyMask.DeleteSelf;
462
463                 void ProcessEvents (byte [] buffer, int length)
464                 {
465                         ArrayList newdirs = null;
466                         InotifyEvent evt;
467                         int nread = 0;
468                         RenamedEventArgs renamed = null;
469                         while (length > nread) {
470                                 int bytes_read = ReadEvent (buffer, nread, length, out evt);
471                                 if (bytes_read <= 0)
472                                         break;
473
474                                 nread += bytes_read;
475
476                                 InotifyMask mask = evt.Mask;
477                                 bool is_directory = (mask & InotifyMask.Directory) != 0;
478                                 mask = (mask & Interesting); // Clear out all the bits that we don't need
479                                 if (mask == 0)
480                                         continue;
481
482                                 foreach (InotifyData data in GetEnumerator (requests [evt.WatchDescriptor])) {
483                                         ParentInotifyData parent = (ParentInotifyData) watches[data.FSW];
484
485                                         if (data == null || parent.Enabled == false)
486                                                 continue;
487
488                                         string directory = data.Directory;
489                                         string filename = evt.Name;
490                                         if (filename == null)
491                                                 filename = directory;
492
493                                         FileSystemWatcher fsw = data.FSW;
494                                         FileAction action = 0;
495                                         if ((mask & (InotifyMask.Modify | InotifyMask.Attrib)) != 0) {
496                                                 action = FileAction.Modified;
497                                         } else if ((mask & InotifyMask.Create) != 0) {
498                                                 action = FileAction.Added;
499                                         } else if ((mask & InotifyMask.Delete) != 0) {
500                                                 action = FileAction.Removed;
501                                         } else if ((mask & InotifyMask.DeleteSelf) != 0) {
502                                                 if (data.Watch != parent.data.Watch) {
503                                                         // To avoid duplicate events handle DeleteSelf only for the top level directory.
504                                                         continue;
505                                                 }
506                                                 action = FileAction.Removed;
507                                         } else if ((mask & InotifyMask.MoveSelf) != 0) {
508                                                 //action = FileAction.Removed;
509                                                 continue; // Ignore this one
510                                         } else if ((mask & InotifyMask.MovedFrom) != 0) {
511                                                 InotifyEvent to;
512                                                 int i = ReadEvent (buffer, nread, length, out to);
513                                                 if (i == -1 || (to.Mask & InotifyMask.MovedTo) == 0 || evt.WatchDescriptor != to.WatchDescriptor) {
514                                                         action = FileAction.Removed;
515                                                 } else {
516                                                         nread += i;
517                                                         action = FileAction.RenamedNewName;
518                                                         renamed = new RenamedEventArgs (WatcherChangeTypes.Renamed, data.Directory, to.Name, evt.Name);
519                                                         if (evt.Name != data.Directory && !fsw.Pattern.IsMatch (evt.Name))
520                                                                 filename = to.Name;
521                                                 }
522                                         } else if ((mask & InotifyMask.MovedTo) != 0) {
523                                                 action = FileAction.Added;
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                                                 if (action == FileAction.RenamedNewName && is_directory) {
551                                                         string renamedOldFullPath = renamed.OldFullPath;
552                                                         string renamedFullPath = renamed.FullPath;
553                                                         int renamedOldFullPathLength = renamedOldFullPath.Length;
554                                                         
555                                                         foreach (InotifyData child in parent.children) {
556                                                                         
557                                                                 if (child.Directory.StartsWith (renamedOldFullPath
558                                                                                                 , StringComparison.Ordinal
559                                                                     )) {
560                                                                         child.Directory = renamedFullPath +
561                                                                                 child.Directory.Substring (renamedOldFullPathLength);
562                                                                 }
563                                                         }
564                                                 }
565                                         }
566
567                                         if (action == FileAction.Removed && filename == data.Directory) {
568                                                 int idx = parent.children.IndexOf (data);
569                                                 if (idx != -1) {
570                                                         parent.children.RemoveAt (idx);
571                                                         if (!fsw.Pattern.IsMatch (Path.GetFileName (filename))) {
572                                                                 continue;
573                                                         }
574                                                 }
575                                         }
576
577                                         if (filename != data.Directory && !fsw.Pattern.IsMatch (Path.GetFileName (filename))) {
578                                                 continue;
579                                         }
580
581                                         lock (fsw) {
582                                                 fsw.DispatchEvents (action, filename, ref renamed);
583                                                 if (action == FileAction.RenamedNewName)
584                                                         renamed = null;
585                                                 if (fsw.Waiting) {
586                                                         fsw.Waiting = false;
587                                                         System.Threading.Monitor.PulseAll (fsw);
588                                                 }
589                                         }
590                                 }
591                         }
592
593                         if (newdirs != null) {
594                                 foreach (InotifyData newdir in newdirs) {
595                                         try {
596                                                 StartMonitoringDirectory (newdir, true);
597                                                 AppendRequestData (newdir);
598                                                 ((ParentInotifyData) watches[newdir.FSW]).children.Add(newdir);
599                                         } catch {} // ignore the given directory
600                                 }
601                                 newdirs.Clear ();
602                         }
603                 }
604
605                 static int AddDirectoryWatch (IntPtr fd, string directory, InotifyMask mask)
606                 {
607                         mask |= InotifyMask.Directory;
608                         return AddWatch (fd, directory, mask);
609                 }
610
611                 [DllImport ("libc", EntryPoint="close")]
612                 internal extern static int Close (IntPtr fd);
613
614                 [DllImport ("libc", EntryPoint = "read")]
615                 extern static int ReadFromFD (IntPtr fd, byte [] buffer, IntPtr length);
616
617                 [MethodImplAttribute(MethodImplOptions.InternalCall)]
618                 extern static IntPtr GetInotifyInstance ();
619
620                 [MethodImplAttribute(MethodImplOptions.InternalCall)]
621                 extern static int AddWatch (IntPtr fd, string name, InotifyMask mask);
622
623                 [MethodImplAttribute(MethodImplOptions.InternalCall)]
624                 extern static IntPtr RemoveWatch (IntPtr fd, int wd);
625         }
626 }
627