2 // System.IO.Inotify.cs: interface with inotify
5 // Gonzalo Paniagua (gonzalo@novell.com)
6 // Anders Rune Jensen (anders@iola.dk)
8 // (c) 2006 Novell, Inc. (http://www.novell.com)
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:
19 // The above copyright notice and this permission notice shall be
20 // included in all copies or substantial portions of the Software.
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.
32 using System.Collections;
33 using System.ComponentModel;
34 using System.Runtime.CompilerServices;
35 using System.Runtime.InteropServices;
37 using System.Threading;
42 enum InotifyMask : uint {
47 CloseNoWrite = 1 << 4,
55 BaseEvents = 0x00000fff,
56 // Can be sent at any time
63 DontFollow = 0x02000000,
65 Directory = 0x40000000,
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;
75 public override string ToString ()
77 return String.Format ("[Descriptor: {0} Mask: {1} Name: {2}]", WatchDescriptor, Mask, Name);
81 class ParentInotifyData
83 public bool IncludeSubdirs;
85 public ArrayList children; // InotifyData
86 public InotifyData data;
90 public FileSystemWatcher FSW;
91 public string Directory;
95 class InotifyWatcher : IFileWatcher
98 static InotifyWatcher instance;
99 static Hashtable watches; // FSW to ParentInotifyData
100 static Hashtable requests; // FSW to InotifyData
102 static Thread thread;
105 private InotifyWatcher ()
110 public static bool GetInstance (out IFileWatcher watcher, bool gamin)
112 if (failed == true) {
117 if (instance != null) {
122 FD = GetInotifyInstance ();
123 if ((long) FD == -1) {
129 watches = Hashtable.Synchronized (new Hashtable ());
130 requests = Hashtable.Synchronized (new Hashtable ());
131 instance = new InotifyWatcher ();
136 public void StartDispatching (FileSystemWatcher fsw)
138 ParentInotifyData parent;
141 FD = GetInotifyInstance ();
143 if (thread == null) {
144 thread = new Thread (new ThreadStart (Monitor));
145 thread.IsBackground = true;
149 parent = (ParentInotifyData) watches [fsw];
152 if (parent == null) {
153 InotifyData data = new InotifyData ();
155 data.Directory = fsw.FullPath;
157 parent = new ParentInotifyData();
158 parent.IncludeSubdirs = fsw.IncludeSubdirectories;
159 parent.Enabled = true;
160 parent.children = new ArrayList();
163 watches [fsw] = parent;
166 StartMonitoringDirectory (data, false);
168 AppendRequestData (data);
171 } catch {} // ignore the directory if StartMonitoringDirectory fails.
175 static void AppendRequestData (InotifyData data)
179 object obj = requests [wd];
180 ArrayList list = null;
182 requests [data.Watch] = data;
183 } else if (obj is InotifyData) {
184 list = new ArrayList ();
187 requests [data.Watch] = list;
189 list = (ArrayList) obj;
194 static bool RemoveRequestData (InotifyData data)
197 object obj = requests [wd];
201 if (obj is InotifyData) {
203 requests.Remove (wd);
209 ArrayList list = (ArrayList) obj;
211 if (list.Count == 0) {
212 requests.Remove (wd);
218 // Attempt to match MS and linux behavior.
219 static InotifyMask GetMaskFromFilters (NotifyFilters filters)
221 InotifyMask mask = InotifyMask.Create | InotifyMask.Delete | InotifyMask.DeleteSelf | InotifyMask.AddMask;
222 if ((filters & NotifyFilters.Attributes) != 0)
223 mask |= InotifyMask.Attrib;
225 if ((filters & NotifyFilters.Security) != 0)
226 mask |= InotifyMask.Attrib;
228 if ((filters & NotifyFilters.Size) != 0) {
229 mask |= InotifyMask.Attrib;
230 mask |= InotifyMask.Modify;
233 if ((filters & NotifyFilters.LastAccess) != 0) {
234 mask |= InotifyMask.Attrib;
235 mask |= InotifyMask.Access;
236 mask |= InotifyMask.Modify;
239 if ((filters & NotifyFilters.LastWrite) != 0) {
240 mask |= InotifyMask.Attrib;
241 mask |= InotifyMask.Modify;
244 if ((filters & NotifyFilters.FileName) != 0) {
245 mask |= InotifyMask.MovedFrom;
246 mask |= InotifyMask.MovedTo;
249 if ((filters & NotifyFilters.DirectoryName) != 0) {
250 mask |= InotifyMask.MovedFrom;
251 mask |= InotifyMask.MovedTo;
257 static void StartMonitoringDirectory (InotifyData data, bool justcreated)
259 InotifyMask mask = GetMaskFromFilters (data.FSW.NotifyFilter);
260 int wd = AddDirectoryWatch (FD, data.Directory, mask);
262 int error = Marshal.GetLastWin32Error ();
263 if (error == 4) { // Too many open watches
264 string nr_watches = "(unknown)";
266 using (StreamReader reader = new StreamReader ("/proc/sys/fs/inotify/max_user_watches")) {
267 nr_watches = reader.ReadLine ();
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);
275 throw new Win32Exception (error, msg);
277 throw new Win32Exception (error);
280 FileSystemWatcher fsw = data.FSW;
283 ParentInotifyData parent = (ParentInotifyData) watches[fsw];
285 if (parent.IncludeSubdirs) {
286 foreach (string directory in Directory.GetDirectories (data.Directory)) {
287 InotifyData fd = new InotifyData ();
289 fd.Directory = directory;
293 RenamedEventArgs renamed = null;
294 if (fsw.Pattern.IsMatch (directory)) {
295 fsw.DispatchEvents (FileAction.Added, directory, ref renamed);
298 System.Threading.Monitor.PulseAll (fsw);
305 StartMonitoringDirectory (fd, justcreated);
306 AppendRequestData (fd);
307 parent.children.Add(fd);
308 } catch {} // ignore errors and don't add directory.
313 foreach (string filename in Directory.GetFiles (data.Directory)) {
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);
323 System.Threading.Monitor.PulseAll(fsw);
331 public void StopDispatching (FileSystemWatcher fsw)
333 ParentInotifyData parent;
335 parent = (ParentInotifyData) watches [fsw];
339 if (RemoveRequestData (parent.data)) {
340 StopMonitoringDirectory (parent.data);
342 watches.Remove (fsw);
343 if (watches.Count == 0) {
350 if (!parent.IncludeSubdirs)
353 foreach (InotifyData idata in parent.children)
355 if (RemoveRequestData (idata)) {
356 StopMonitoringDirectory (idata);
362 static void StopMonitoringDirectory (InotifyData data)
364 RemoveWatch (FD, data.Watch);
369 byte [] buffer = new byte [4096];
372 nread = ReadFromFD (FD, buffer, (IntPtr) buffer.Length);
377 ProcessEvents (buffer, nread);
388 struct inotify_event {
392 __u32 len; // Includes any trailing null in 'name'
397 static int ReadEvent (byte [] source, int off, int size, out InotifyEvent evt)
399 evt = new InotifyEvent ();
400 if (size <= 0 || off > size - 16) {
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);
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);
424 if (off > size - 16 - len)
426 string name = Encoding.UTF8.GetString (source, off + 16, len);
427 evt.Name = name.Trim ('\0');
435 static IEnumerable GetEnumerator (object source)
440 if (source is InotifyData)
443 if (source is ArrayList) {
444 ArrayList list = (ArrayList) source;
445 for (int i = 0; i < list.Count; i++)
446 yield return list [i];
450 /* Interesting events:
459 static InotifyMask Interesting = InotifyMask.Modify | InotifyMask.Attrib | InotifyMask.MovedFrom |
460 InotifyMask.MovedTo | InotifyMask.Create | InotifyMask.Delete |
461 InotifyMask.DeleteSelf;
463 void ProcessEvents (byte [] buffer, int length)
465 ArrayList newdirs = null;
468 RenamedEventArgs renamed = null;
469 while (length > nread) {
470 int bytes_read = ReadEvent (buffer, nread, length, out evt);
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
482 foreach (InotifyData data in GetEnumerator (requests [evt.WatchDescriptor])) {
483 ParentInotifyData parent = (ParentInotifyData) watches[data.FSW];
485 if (data == null || parent.Enabled == false)
488 string directory = data.Directory;
489 string filename = evt.Name;
490 if (filename == null)
491 filename = directory;
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.
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) {
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;
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))
522 } else if ((mask & InotifyMask.MovedTo) != 0) {
523 action = FileAction.Added;
525 if (fsw.IncludeSubdirectories) {
526 string full = fsw.FullPath;
527 string datadir = data.Directory;
528 if (datadir != full) {
529 int len = full.Length;
531 if (len > 1 && full [len - 1] == Path.DirectorySeparatorChar)
533 string reldir = datadir.Substring (full.Length + slash);
534 datadir = Path.Combine (datadir, filename);
535 filename = Path.Combine (reldir, filename);
537 datadir = Path.Combine (full, filename);
540 if (action == FileAction.Added && is_directory) {
542 newdirs = new ArrayList (2);
544 InotifyData fd = new InotifyData ();
546 fd.Directory = datadir;
550 if (action == FileAction.RenamedNewName && is_directory) {
551 string renamedOldFullPath = renamed.OldFullPath;
552 string renamedFullPath = renamed.FullPath;
553 int renamedOldFullPathLength = renamedOldFullPath.Length;
555 foreach (InotifyData child in parent.children) {
557 if (child.Directory.StartsWith (renamedOldFullPath
558 , StringComparison.Ordinal
560 child.Directory = renamedFullPath +
561 child.Directory.Substring (renamedOldFullPathLength);
567 if (action == FileAction.Removed && filename == data.Directory) {
568 int idx = parent.children.IndexOf (data);
570 parent.children.RemoveAt (idx);
571 if (!fsw.Pattern.IsMatch (Path.GetFileName (filename))) {
577 if (filename != data.Directory && !fsw.Pattern.IsMatch (Path.GetFileName (filename))) {
582 fsw.DispatchEvents (action, filename, ref renamed);
583 if (action == FileAction.RenamedNewName)
587 System.Threading.Monitor.PulseAll (fsw);
593 if (newdirs != null) {
594 foreach (InotifyData newdir in newdirs) {
596 StartMonitoringDirectory (newdir, true);
597 AppendRequestData (newdir);
598 ((ParentInotifyData) watches[newdir.FSW]).children.Add(newdir);
599 } catch {} // ignore the given directory
605 static int AddDirectoryWatch (IntPtr fd, string directory, InotifyMask mask)
607 mask |= InotifyMask.Directory;
608 return AddWatch (fd, directory, mask);
611 [DllImport ("libc", EntryPoint="close")]
612 internal extern static int Close (IntPtr fd);
614 [DllImport ("libc", EntryPoint = "read")]
615 extern static int ReadFromFD (IntPtr fd, byte [] buffer, IntPtr length);
617 [MethodImplAttribute(MethodImplOptions.InternalCall)]
618 extern static IntPtr GetInotifyInstance ();
620 [MethodImplAttribute(MethodImplOptions.InternalCall)]
621 extern static int AddWatch (IntPtr fd, string name, InotifyMask mask);
623 [MethodImplAttribute(MethodImplOptions.InternalCall)]
624 extern static IntPtr RemoveWatch (IntPtr fd, int wd);