New test.
[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                                 StartMonitoringDirectory (data, false);
158                                 lock (this) {
159                                         watches [fsw] = data;
160                                         AppendRequestData (data);
161                                         stop = false;
162                                 }
163                         }
164                 }
165                 
166                 static void AppendRequestData (InotifyData data)
167                 {
168                         int wd = data.Watch;
169                         object obj = requests [wd];
170                         ArrayList list = null;
171                         if (obj == null) {
172                                 requests [data.Watch] = data;
173                         } else if (obj is InotifyData) {
174                                 list = new ArrayList ();
175                                 list.Add (obj);
176                                 list.Add (data);
177                                 requests [data.Watch] = list;
178                         } else {
179                                 list = (ArrayList) obj;
180                                 list.Add (data);
181                         }
182                 }
183
184                 static bool RemoveRequestData (InotifyData data)
185                 {
186                         int wd = data.Watch;
187                         object obj = requests [wd];
188                         if (obj == null)
189                                 return true;
190
191                         if (obj is InotifyData) {
192                                 if (obj == data) {
193                                         requests.Remove (wd);
194                                         return true;
195                                 }
196                                 return false;
197                         }
198
199                         ArrayList list = (ArrayList) obj;
200                         list.Remove (data);
201                         if (list.Count == 0) {
202                                 requests.Remove (wd);
203                                 return true;
204                         }
205                         return false;
206                 }
207
208                 // Attempt to match MS and linux behavior.
209                 static InotifyMask GetMaskFromFilters (NotifyFilters filters)
210                 {
211                         InotifyMask mask = InotifyMask.Create | InotifyMask.Delete | InotifyMask.DeleteSelf | InotifyMask.AddMask;
212                         if ((filters & NotifyFilters.Attributes) != 0)
213                                 mask |= InotifyMask.Attrib;
214
215                         if ((filters & NotifyFilters.Security) != 0)
216                                 mask |= InotifyMask.Attrib;
217
218                         if ((filters & NotifyFilters.Size) != 0) {
219                                 mask |= InotifyMask.Attrib;
220                                 mask |= InotifyMask.Modify;
221                         }
222
223                         if ((filters & NotifyFilters.LastAccess) != 0) {
224                                 mask |= InotifyMask.Attrib;
225                                 mask |= InotifyMask.Access;
226                                 mask |= InotifyMask.Modify;
227                                 mask |= InotifyMask.CloseWrite;
228                         }
229
230                         if ((filters & NotifyFilters.LastWrite) != 0) {
231                                 mask |= InotifyMask.Attrib;
232                                 mask |= InotifyMask.CloseWrite;
233                         }
234
235                         if ((filters & NotifyFilters.FileName) != 0) {
236                                 mask |= InotifyMask.MovedFrom;
237                                 mask |= InotifyMask.MovedTo;
238                         }
239
240                         if ((filters & NotifyFilters.DirectoryName) != 0) {
241                                 mask |= InotifyMask.MovedFrom;
242                                 mask |= InotifyMask.MovedTo;
243                         }
244
245                         return mask;
246                 }
247
248                 static void StartMonitoringDirectory (InotifyData data, bool justcreated)
249                 {
250                         InotifyMask mask = GetMaskFromFilters (data.FSW.NotifyFilter);
251                         int wd = AddDirectoryWatch (FD, data.Directory, mask);
252                         if (wd == -1) {
253                                 int error = Marshal.GetLastWin32Error ();
254                                 if (error == 4) { // Too many open watches
255                                         string watches = "(unknown)";
256                                         try {
257                                                 using (StreamReader reader = new StreamReader ("/proc/sys/fs/inotify/max_user_watches")) {
258                                                         watches = reader.ReadLine ();
259                                                 }
260                                         } catch {}
261
262                                         string msg = String.Format ("The per-user inotify watches limit of {0} has been reached. " +
263                                                                 "If you're experiencing problems with your application, increase that limit " +
264                                                                 "in /proc/sys/fs/inotify/max_user_watches.", watches);
265                                         
266                                         throw new Win32Exception (error, msg);
267                                 }
268                                 throw new Win32Exception (error);
269                         }
270
271                         FileSystemWatcher fsw = data.FSW;
272                         data.Watch = wd;
273
274                         if (data.IncludeSubdirs) {
275                                 foreach (string directory in Directory.GetDirectories (data.Directory)) {
276                                         InotifyData fd = new InotifyData ();
277                                         fd.FSW = fsw;
278                                         fd.Directory = directory;
279                                         fd.FileMask = data.FSW.MangledFilter;
280                                         fd.IncludeSubdirs = true;
281                                         fd.SubDirs = new Hashtable ();
282                                         fd.Enabled = true;
283
284                                         if (justcreated) {
285                                                 lock (fsw) {
286                                                         RenamedEventArgs renamed = null;
287                                                         fsw.DispatchEvents (FileAction.Added, directory, ref renamed);
288                                                         if (fsw.Waiting) {
289                                                                 fsw.Waiting = false;
290                                                                 System.Threading.Monitor.PulseAll (fsw);
291                                                         }
292                                                 }
293                                         }
294
295                                         StartMonitoringDirectory (fd, justcreated);
296                                         fd.SubDirs [directory] = fd;
297                                         AppendRequestData (fd);
298                                 }
299                         }
300
301                         if (justcreated) {
302                                 foreach (string filename in Directory.GetFiles (data.Directory)) {
303                                         lock (fsw) {
304                                                 RenamedEventArgs renamed = null;
305
306                                                 fsw.DispatchEvents (FileAction.Added, filename, ref renamed);
307                                                 /* If a file has been created, then it has been written to */
308                                                 fsw.DispatchEvents (FileAction.Modified, filename, ref renamed);
309
310                                                 if (fsw.Waiting) {
311                                                         fsw.Waiting = false;
312                                                         System.Threading.Monitor.PulseAll(fsw);
313                                                 }
314                                         }
315                                 }
316                         }
317                 }
318
319                 public void StopDispatching (FileSystemWatcher fsw)
320                 {
321                         InotifyData data;
322                         lock (this) {
323                                 data = (InotifyData) watches [fsw];
324                                 if (data == null)
325                                         return;
326
327                                 if (RemoveRequestData (data)) {
328                                         StopMonitoringDirectory (data);
329                                 }
330                                 watches.Remove (fsw);
331                                 if (watches.Count == 0) {
332                                         stop = true;
333                                         IntPtr fd = FD;
334                                         FD = (IntPtr) (-1);
335                                         Close (fd);
336                                 }
337
338                                 if (!data.IncludeSubdirs)
339                                         return;
340
341                                 foreach (InotifyData idata in data.SubDirs.Values) {
342                                         if (RemoveRequestData (idata)) {
343                                                 StopMonitoringDirectory (idata);
344                                         }
345                                 }
346                         }
347                 }
348
349                 static void StopMonitoringDirectory (InotifyData data)
350                 {
351                         RemoveWatch (FD, data.Watch);
352                 }
353
354                 void Monitor ()
355                 {
356                         byte [] buffer = new byte [4096];
357                         int nread;
358                         while (!stop) {
359                                 nread = ReadFromFD (FD, buffer, (IntPtr) buffer.Length);
360                                 if (nread == -1)
361                                         continue;
362
363                                 lock (this) {
364                                         ProcessEvents (buffer, nread);
365
366                                 }
367                         }
368
369                         lock (this) {
370                                 thread = null;
371                                 stop = false;
372                         }
373                 }
374                 /*
375                 struct inotify_event {
376                         __s32           wd;
377                         __u32           mask;
378                         __u32           cookie;
379                         __u32           len;            // Includes any trailing null in 'name'
380                         char            name[0];
381                 };
382                 */
383
384                 static int ReadEvent (byte [] source, int off, int size, out InotifyEvent evt)
385                 {
386                         evt = new InotifyEvent ();
387                         if (size <= 0 || off > size - 16) {
388                                 return -1;
389                         }
390
391                         int len;
392                         if (BitConverter.IsLittleEndian) {
393                                 evt.WatchDescriptor = source [off] + (source [off + 1] << 8) +
394                                                         (source [off + 2] << 16) + (source [off + 3] << 24);
395                                 evt.Mask = (InotifyMask) (source [off + 4] + (source [off + 5] << 8) +
396                                                         (source [off + 6] << 16) + (source [off + 7] << 24));
397                                 // Ignore Cookie -> +4
398                                 len = source [off + 12] + (source [off + 13] << 8) +
399                                         (source [off + 14] << 16) + (source [off + 15] << 24);
400                         } else {
401                                 evt.WatchDescriptor = source [off + 3] + (source [off + 2] << 8) +
402                                                         (source [off + 1] << 16) + (source [off] << 24);
403                                 evt.Mask = (InotifyMask) (source [off + 7] + (source [off + 6] << 8) +
404                                                         (source [off + 5] << 16) + (source [off + 4] << 24));
405                                 // Ignore Cookie -> +4
406                                 len = source [off + 15] + (source [off + 14] << 8) +
407                                         (source [off + 13] << 16) + (source [off + 12] << 24);
408                         }
409
410                         if (len > 0) {
411                                 if (off > size - 16 - len)
412                                         return -1;
413                                 string name = Encoding.UTF8.GetString (source, off + 16, len);
414                                 evt.Name = name.Trim ('\0');
415                         } else {
416                                 evt.Name = null;
417                         }
418
419                         return 16 + len;
420                 }
421
422                 static IEnumerable GetEnumerator (object source)
423                 {
424                         if (source == null)
425                                 yield break;
426
427                         if (source is InotifyData)
428                                 yield return source;
429
430                         if (source is ArrayList) {
431                                 ArrayList list = (ArrayList) source;
432                                 for (int i = 0; i < list.Count; i++)
433                                         yield return list [i];
434                         }
435                 }
436
437                 /* Interesting events:
438                         * Modify
439                         * Attrib
440                         * MovedFrom
441                         * MovedTo
442                         * Create
443                         * Delete
444                         * DeleteSelf
445                         * CloseWrite
446                 */
447                 static InotifyMask Interesting = InotifyMask.Modify | InotifyMask.Attrib | InotifyMask.MovedFrom |
448                                                         InotifyMask.MovedTo | InotifyMask.Create | InotifyMask.Delete |
449                                                         InotifyMask.DeleteSelf | InotifyMask.CloseWrite;
450
451                 void ProcessEvents (byte [] buffer, int length)
452                 {
453                         ArrayList newdirs = null;
454                         InotifyEvent evt;
455                         int nread = 0;
456                         bool new_name_needed = false;
457                         RenamedEventArgs renamed = null;
458                         while (length > nread) {
459                                 int bytes_read = ReadEvent (buffer, nread, length, out evt);
460                                 if (bytes_read <= 0)
461                                         break;
462
463                                 nread += bytes_read;
464
465                                 InotifyMask mask = evt.Mask;
466                                 bool is_directory = (mask & InotifyMask.Directory) != 0;
467                                 mask = (mask & Interesting); // Clear out all the bits that we don't need
468                                 if (mask == 0)
469                                         continue;
470
471                                 foreach (InotifyData data in GetEnumerator (requests [evt.WatchDescriptor])) {
472                                         if (data == null || data.Enabled == false)
473                                                 continue;
474
475                                         string directory = data.Directory;
476                                         string filename = evt.Name;
477                                         if (filename == null)
478                                                 filename = directory;
479
480                                         FileSystemWatcher fsw = data.FSW;
481                                         FileAction action = 0;
482                                         if ((mask & (InotifyMask.Modify | InotifyMask.CloseWrite | InotifyMask.Attrib)) != 0) {
483                                                 action = FileAction.Modified;
484                                         } else if ((mask & InotifyMask.Create) != 0) {
485                                                 action = FileAction.Added;
486                                         } else if ((mask & InotifyMask.Delete) != 0) {
487                                                 action = FileAction.Removed;
488                                         } else if ((mask & InotifyMask.DeleteSelf) != 0) {
489                                                 action = FileAction.Removed;
490                                         } else if ((mask & InotifyMask.MoveSelf) != 0) {
491                                                 //action = FileAction.Removed;
492                                                 continue; // Ignore this one
493                                         } else if ((mask & InotifyMask.MovedFrom) != 0) {
494                                                 InotifyEvent to;
495                                                 int i = ReadEvent (buffer, nread, length, out to);
496                                                 if (i == -1 || (to.Mask & InotifyMask.MovedTo) == 0) {
497                                                         action = FileAction.Removed;
498                                                 } else {
499                                                         nread += i;
500                                                         action = FileAction.RenamedNewName;
501                                                         if (evt.Name == data.Directory || fsw.Pattern.IsMatch (evt.Name)) {
502                                                                 renamed = new RenamedEventArgs (WatcherChangeTypes.Renamed, data.Directory, to.Name, evt.Name);
503                                                         } else {
504                                                                 renamed = new RenamedEventArgs (WatcherChangeTypes.Renamed, data.Directory, evt.Name, to.Name);
505                                                                 filename = to.Name;
506                                                         }
507                                                 }
508                                         } else if ((mask & InotifyMask.MovedTo) != 0) {
509                                                 action = (new_name_needed) ? FileAction.RenamedNewName : FileAction.Added;
510                                                 new_name_needed = false;
511                                         }
512
513                                         if (fsw.IncludeSubdirectories) {
514                                                 string full = fsw.FullPath;
515                                                 string datadir = data.Directory;
516                                                 if (datadir != full) {
517                                                         int len = full.Length;
518                                                         int slash = 1;
519                                                         if (len > 1 && full [len - 1] == Path.DirectorySeparatorChar)
520                                                                 slash = 0;
521                                                         string reldir = datadir.Substring (full.Length + slash);
522                                                         datadir = Path.Combine (datadir, filename);
523                                                         filename = Path.Combine (reldir, filename);
524                                                 } else {
525                                                         datadir = Path.Combine (full, filename);
526                                                 }
527
528                                                 if (action == FileAction.Added && is_directory) {
529                                                         if (newdirs == null)
530                                                                 newdirs = new ArrayList (4);
531
532                                                         InotifyData fd = new InotifyData ();
533                                                         fd.FSW = fsw;
534                                                         fd.Directory = datadir;
535                                                         fd.FileMask = fsw.MangledFilter;
536                                                         fd.IncludeSubdirs = true;
537                                                         fd.SubDirs = new Hashtable ();
538                                                         fd.Enabled = true;
539                                                         newdirs.Add (fd);
540                                                         newdirs.Add (data);
541                                                 }
542                                         }
543
544                                         if (filename != data.Directory && !fsw.Pattern.IsMatch (filename)) {
545                                                 continue;
546                                         }
547
548                                         lock (fsw) {
549                                                 fsw.DispatchEvents (action, filename, ref renamed);
550                                                 if (action == FileAction.RenamedNewName)
551                                                         renamed = null;
552                                                 if (fsw.Waiting) {
553                                                         fsw.Waiting = false;
554                                                         System.Threading.Monitor.PulseAll (fsw);
555                                                 }
556                                         }
557                                 }
558                         }
559
560                         if (newdirs != null) {
561                                 int count = newdirs.Count;
562                                 for (int n = 0; n < count; n += 2) {
563                                         InotifyData newdir = (InotifyData) newdirs [n];
564                                         InotifyData parent = (InotifyData) newdirs [n + 1];
565                                         StartMonitoringDirectory (newdir, true);
566                                         AppendRequestData (newdir);
567                                         lock (parent) {
568                                                 parent.SubDirs [newdir.Directory] = newdir;
569                                         }
570                                 }
571                                 newdirs.Clear ();
572                         }
573                 }
574
575                 static int AddDirectoryWatch (IntPtr fd, string directory, InotifyMask mask)
576                 {
577                         mask |= InotifyMask.Directory;
578                         return AddWatch (fd, directory, mask);
579                 }
580
581                 [DllImport ("libc", EntryPoint="close")]
582                 internal extern static int Close (IntPtr fd);
583
584                 [DllImport ("libc", EntryPoint = "read")]
585                 extern static int ReadFromFD (IntPtr fd, byte [] buffer, IntPtr length);
586
587                 [MethodImplAttribute(MethodImplOptions.InternalCall)]
588                 extern static IntPtr GetInotifyInstance ();
589
590                 [MethodImplAttribute(MethodImplOptions.InternalCall)]
591                 extern static int AddWatch (IntPtr fd, string name, InotifyMask mask);
592
593                 [MethodImplAttribute(MethodImplOptions.InternalCall)]
594                 extern static IntPtr RemoveWatch (IntPtr fd, int wd);
595         }
596 }
597