2005-08-16 Gabor Kelemen <kelemeng@gnome.hu>
[beagle.git] / Util / Inotify.cs
bloba22835719d99b7a589f34d0d157d23e03f65e8f8
1 //
2 // Inotify.cs
3 //
4 // Copyright (C) 2004 Novell, Inc.
5 //
7 //
8 // Permission is hereby granted, free of charge, to any person obtaining a
9 // copy of this software and associated documentation files (the "Software"),
10 // to deal in the Software without restriction, including without limitation
11 // the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 // and/or sell copies of the Software, and to permit persons to whom the
13 // Software is furnished to do so, subject to the following conditions:
15 // The above copyright notice and this permission notice shall be included in
16 // all copies or substantial portions of the Software.
18 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 // DEALINGS IN THE SOFTWARE.
27 // WARNING: This is not portable to Win32
29 using System;
30 using System.Collections;
31 using System.IO;
32 using System.Runtime.InteropServices;
33 using System.Text;
34 using System.Text.RegularExpressions;
35 using System.Threading;
37 using Mono.Posix;
39 namespace Beagle.Util {
41 public class Inotify {
43 public delegate void InotifyCallback (Watch watch, string path, string subitem, string srcpath, EventType type);
45 /////////////////////////////////////////////////////////////////////////////////////
47 [Flags]
48 public enum EventType : uint {
49 Access = 0x00000001, // File was accessed
50 Modify = 0x00000002, // File was modified
51 Attrib = 0x00000004, // File changed attributes
52 CloseWrite = 0x00000008, // Writable file was closed
53 CloseNoWrite = 0x00000010, // Non-writable file was close
54 Open = 0x00000020, // File was opened
55 MovedFrom = 0x00000040, // File was moved from X
56 MovedTo = 0x00000080, // File was moved to Y
57 Create = 0x00000100, // Subfile was created
58 Delete = 0x00000200, // Subfile was deleted
59 DeleteSelf = 0x00000400, // Self was deleted
61 Unmount = 0x00002000, // Backing fs was unmounted
62 QueueOverflow = 0x00004000, // Event queue overflowed
63 Ignored = 0x00008000, // File is no longer being watched
65 IsDirectory = 0x40000000, // Event is against a directory
66 OneShot = 0x80000000, // Watch is one-shot
68 // For forward compatibility, define these explicitly
69 All = (EventType.Access | EventType.Modify | EventType.Attrib |
70 EventType.CloseWrite | EventType.CloseNoWrite | EventType.Open |
71 EventType.MovedFrom | EventType.MovedTo | EventType.Create |
72 EventType.Delete | EventType.DeleteSelf)
75 // Events that we want internally, even if the handlers do not
76 static private EventType base_mask = EventType.MovedFrom | EventType.MovedTo;
78 /////////////////////////////////////////////////////////////////////////////////////
80 static private Logger log;
82 static public Logger Log {
83 get { return log; }
86 /////////////////////////////////////////////////////////////////////////////////////
88 [StructLayout (LayoutKind.Sequential)]
89 private struct inotify_event {
90 public int wd;
91 public EventType mask;
92 public uint cookie;
93 public uint len;
96 [DllImport ("libinotifyglue")]
97 static extern int inotify_glue_init ();
99 [DllImport ("libinotifyglue")]
100 static extern int inotify_glue_watch (int fd, string filename, EventType mask);
102 [DllImport ("libinotifyglue")]
103 static extern int inotify_glue_ignore (int fd, int wd);
105 [DllImport ("libinotifyglue")]
106 static extern unsafe void inotify_snarf_events (int fd,
107 int timeout_seconds,
108 out int nr,
109 out IntPtr buffer);
111 /////////////////////////////////////////////////////////////////////////////////////
113 private class QueuedEvent {
114 public int Wd;
115 public EventType Type;
116 public string Filename;
117 public uint Cookie;
119 public bool Analyzed;
120 public bool Dispatched;
121 public DateTime HoldUntil;
122 public QueuedEvent PairedMove;
124 // Measured in milliseconds; 57ms is totally random
125 public const double DefaultHoldTime = 57;
127 public QueuedEvent ()
129 // Set a default HoldUntil time
130 HoldUntil = DateTime.Now.AddMilliseconds (DefaultHoldTime);
133 public void AddMilliseconds (double x)
135 HoldUntil = HoldUntil.AddMilliseconds (x);
138 public void PairWith (QueuedEvent other)
140 this.PairedMove = other;
141 other.PairedMove = this;
143 if (this.HoldUntil < other.HoldUntil)
144 this.HoldUntil = other.HoldUntil;
145 other.HoldUntil = this.HoldUntil;
149 /////////////////////////////////////////////////////////////////////////////////////
151 static private int inotify_fd = -1;
152 static private ArrayList event_queue = new ArrayList ();
154 static Inotify ()
156 log = Logger.Get ("Inotify");
158 if (Environment.GetEnvironmentVariable ("BEAGLE_DISABLE_INOTIFY") != null) {
159 Logger.Log.Debug ("BEAGLE_DISABLE_INOTIFY is set");
160 return;
163 if (Environment.GetEnvironmentVariable ("BEAGLE_INOTIFY_VERBOSE") != null)
164 Inotify.Verbose = true;
166 inotify_fd = inotify_glue_init ();
167 if (inotify_fd == -1)
168 Logger.Log.Warn ("Could not initialize inotify");
171 static public bool Enabled {
172 get { return inotify_fd >= 0; }
175 /////////////////////////////////////////////////////////////////////////////////////
177 public interface Watch {
178 void Unsubscribe ();
179 void ChangeSubscription (EventType new_mask);
182 private class WatchInternal : Watch {
183 private InotifyCallback callback;
184 private EventType mask;
185 private WatchInfo watchinfo;
186 private bool is_subscribed;
188 public InotifyCallback Callback {
189 get { return callback; }
192 public EventType Mask {
193 get { return mask; }
194 set { mask = value; }
197 public WatchInternal (InotifyCallback callback, EventType mask, WatchInfo watchinfo)
199 this.callback = callback;
200 this.mask = mask;
201 this.watchinfo = watchinfo;
202 this.is_subscribed = true;
205 public void Unsubscribe ()
207 if (!this.is_subscribed)
208 return;
210 Inotify.Unsubscribe (watchinfo, this);
211 this.is_subscribed = false;
214 public void ChangeSubscription (EventType mask)
216 if (! this.is_subscribed)
217 return;
219 this.mask = mask;
220 CreateOrModifyWatch (this.watchinfo);
225 private class WatchInfo {
226 public int Wd = -1;
227 public string Path;
228 public bool IsDirectory;
229 public EventType Mask;
231 public EventType FilterMask;
232 public EventType FilterSeen;
234 public ArrayList Children;
235 public WatchInfo Parent;
237 public ArrayList Subscribers;
240 static Hashtable watched_by_wd = new Hashtable ();
241 static Hashtable watched_by_path = new Hashtable ();
242 static WatchInfo last_watched = null;
244 private class PendingMove {
245 public WatchInfo Watch;
246 public string SrcName;
247 public DateTime Time;
248 public uint Cookie;
250 public PendingMove (WatchInfo watched, string srcname, DateTime time, uint cookie) {
251 Watch = watched;
252 SrcName = srcname;
253 Time = time;
254 Cookie = cookie;
258 static public int WatchCount {
259 get { return watched_by_wd.Count; }
262 static public bool IsWatching (string path)
264 path = Path.GetFullPath (path);
265 return watched_by_path.Contains (path);
268 // Filter WatchInfo items when we do the Lookup.
269 // We do the filtering here to avoid having to acquire
270 // the watched_by_wd lock yet again.
271 static private WatchInfo Lookup (int wd, EventType event_type)
273 lock (watched_by_wd) {
274 WatchInfo watched;
275 if (last_watched != null && last_watched.Wd == wd)
276 watched = last_watched;
277 else {
278 watched = watched_by_wd [wd] as WatchInfo;
279 if (watched != null)
280 last_watched = watched;
283 if (watched != null && (watched.FilterMask & event_type) != 0) {
284 watched.FilterSeen |= event_type;
285 watched = null;
288 return watched;
292 // The caller has to handle all locking itself
293 static private void Forget (WatchInfo watched)
295 if (last_watched == watched)
296 last_watched = null;
297 if (watched.Parent != null)
298 watched.Parent.Children.Remove (watched);
299 watched_by_wd.Remove (watched.Wd);
300 watched_by_path.Remove (watched.Path);
303 static public Watch Subscribe (string path, InotifyCallback callback, EventType mask, EventType initial_filter)
305 WatchInternal watch;
306 WatchInfo watched;
307 EventType mask_orig = mask;
309 if (!Path.IsPathRooted (path))
310 path = Path.GetFullPath (path);
312 bool is_directory = false;
313 if (Directory.Exists (path))
314 is_directory = true;
315 else if (! File.Exists (path))
316 throw new IOException (path);
318 lock (watched_by_wd) {
319 watched = watched_by_path [path] as WatchInfo;
321 if (watched == null) {
322 // We need an entirely new WatchInfo object
323 watched = new WatchInfo ();
324 watched.Path = path;
325 watched.IsDirectory = is_directory;
326 watched.Subscribers = new ArrayList ();
327 watched.Children = new ArrayList ();
328 DirectoryInfo dir = new DirectoryInfo (path);
329 watched.Parent = watched_by_path [dir.Parent.ToString ()] as WatchInfo;
330 if (watched.Parent != null)
331 watched.Parent.Children.Add (watched);
332 watched_by_path [watched.Path] = watched;
335 watched.FilterMask = initial_filter;
336 watched.FilterSeen = 0;
338 watch = new WatchInternal (callback, mask_orig, watched);
339 watched.Subscribers.Add (watch);
341 CreateOrModifyWatch (watched);
342 watched_by_wd [watched.Wd] = watched;
345 return watch;
348 static public Watch Subscribe (string path, InotifyCallback callback, EventType mask)
350 return Subscribe (path, callback, mask, 0);
353 static public EventType Filter (string path, EventType mask)
355 EventType seen = 0;
357 path = Path.GetFullPath (path);
359 lock (watched_by_wd) {
360 WatchInfo watched;
361 watched = watched_by_path [path] as WatchInfo;
363 seen = watched.FilterSeen;
364 watched.FilterMask = mask;
365 watched.FilterSeen = 0;
368 return seen;
371 static private void Unsubscribe (WatchInfo watched, WatchInternal watch)
373 watched.Subscribers.Remove (watch);
375 // Other subscribers might still be around
376 if (watched.Subscribers.Count > 0) {
377 // Minimize it
378 CreateOrModifyWatch (watched);
379 return;
382 int retval = inotify_glue_ignore (inotify_fd, watched.Wd);
383 if (retval < 0) {
384 string msg = String.Format ("Attempt to ignore {0} failed!", watched.Path);
385 throw new IOException (msg);
388 Forget (watched);
389 return;
392 // Ensure our watch exists, meets all the subscribers requirements,
393 // and isn't matching any other events that we don't care about.
394 static private void CreateOrModifyWatch (WatchInfo watched)
396 EventType new_mask = base_mask;
397 foreach (WatchInternal watch in watched.Subscribers)
398 new_mask |= watch.Mask;
400 if (watched.Wd >= 0 && watched.Mask == new_mask)
401 return;
403 // We rely on the behaviour that watching the same inode twice won't result
404 // in the wd value changing.
405 // (no need to worry about watched_by_wd being polluted with stale watches)
407 int wd = -1;
408 wd = inotify_glue_watch (inotify_fd, watched.Path, new_mask);
409 if (wd < 0) {
410 string msg = String.Format ("Attempt to watch {0} failed!", watched.Path);
411 throw new IOException (msg);
413 if (watched.Wd >= 0 && watched.Wd != wd) {
414 string msg = String.Format ("Watch handle changed unexpectedly!", watched.Path);
415 throw new IOException (msg);
418 watched.Wd = wd;
419 watched.Mask = new_mask;
422 /////////////////////////////////////////////////////////////////////////////////////
424 static Thread snarf_thread = null;
425 static Thread dispatch_thread = null;
426 static bool running = false;
428 static public void Start ()
430 if (! Enabled)
431 return;
433 Logger.Log.Debug("Starting Inotify threads");
435 lock (event_queue) {
436 if (snarf_thread != null)
437 return;
439 running = true;
441 snarf_thread = new Thread (new ThreadStart (SnarfWorker));
442 snarf_thread.Start ();
444 dispatch_thread = new Thread (new ThreadStart (DispatchWorker));
445 dispatch_thread.Start ();
449 static public void Stop ()
451 if (! Enabled)
452 return;
454 lock (event_queue) {
455 running = false;
456 Monitor.Pulse (event_queue);
460 static unsafe void SnarfWorker ()
462 Encoding filename_encoding = Encoding.UTF8;
463 int event_size = Marshal.SizeOf (typeof (inotify_event));
465 while (running) {
467 // We get much better performance if we wait a tiny bit
468 // between reads in order to let events build up.
469 // FIXME: We need to be smarter here to avoid queue overflows.
470 Thread.Sleep (15);
472 IntPtr buffer;
473 int nr;
475 // Will block while waiting for events, but with a 1s timeout.
476 inotify_snarf_events (inotify_fd,
478 out nr,
479 out buffer);
481 if (!running)
482 break;
484 if (nr == 0)
485 continue;
487 ArrayList new_events = new ArrayList ();
489 bool saw_overflow = false;
490 while (nr > 0) {
492 // Read the low-level event struct from the buffer.
493 inotify_event raw_event;
494 raw_event = (inotify_event) Marshal.PtrToStructure (buffer, typeof (inotify_event));
495 buffer = (IntPtr) ((long) buffer + event_size);
497 if ((raw_event.mask & EventType.QueueOverflow) != 0)
498 saw_overflow = true;
500 // Now we convert our low-level event struct into a nicer object.
501 QueuedEvent qe = new QueuedEvent ();
502 qe.Wd = raw_event.wd;
503 qe.Type = raw_event.mask;
504 qe.Cookie = raw_event.cookie;
506 // Extract the filename payload (if any) from the buffer.
507 byte [] filename_bytes = new byte[raw_event.len];
508 Marshal.Copy (buffer, filename_bytes, 0, (int) raw_event.len);
509 buffer = (IntPtr) ((long) buffer + raw_event.len);
510 int n_chars = 0;
511 while (n_chars < filename_bytes.Length && filename_bytes [n_chars] != 0)
512 ++n_chars;
513 qe.Filename = "";
514 if (n_chars > 0)
515 qe.Filename = filename_encoding.GetString (filename_bytes, 0, n_chars);
517 new_events.Add (qe);
518 nr -= event_size + (int) raw_event.len;
521 if (saw_overflow)
522 Logger.Log.Warn ("Inotify queue overflow!");
524 lock (event_queue) {
525 event_queue.AddRange (new_events);
526 Monitor.Pulse (event_queue);
532 static public bool Verbose = false;
534 // Update the watched_by_path hash and the path stored inside the watch
535 // in response to a move event.
536 static private void MoveWatch (WatchInfo watch, string name)
538 watched_by_path.Remove (watch.Path);
539 watch.Path = name;
540 watched_by_path [watch.Path] = watch;
542 if (Verbose)
543 Console.WriteLine ("*** inotify: Moved Watch to {0}", watch.Path);
546 // A directory we are watching has moved. We need to fix up its path, and the path of
547 // all of its subdirectories, their subdirectories, and so on.
548 static private void HandleMove (string srcpath, string dstpath)
550 WatchInfo start = watched_by_path [srcpath] as WatchInfo; // not the same as src!
551 if (start == null) {
552 Console.WriteLine ("Lookup failed for {0}", srcpath);
553 return;
556 // Queue our starting point, then walk its subdirectories, invoking MoveWatch() on
557 // each, repeating for their subdirectories. The relationship between start, child
558 // and dstpath is fickle and important.
559 Queue queue = new Queue();
560 queue.Enqueue (start);
561 do {
562 WatchInfo target = queue.Dequeue () as WatchInfo;
563 for (int i = 0; i < target.Children.Count; i++) {
564 WatchInfo child = target.Children[i] as WatchInfo;
565 string name = Path.Combine (dstpath, child.Path.Substring (start.Path.Length + 1));
566 MoveWatch (child, name);
567 queue.Enqueue (child);
569 } while (queue.Count > 0);
571 // Ultimately, fixup the original watch, too
572 MoveWatch (start, dstpath);
575 static private void SendEvent (WatchInfo watched, string filename, string srcpath, EventType mask)
577 // Does the watch care about this event?
578 if ((watched.Mask & mask) == 0)
579 return;
581 bool isDirectory = false;
582 if ((mask & EventType.IsDirectory) != 0)
583 isDirectory = true;
585 if (Verbose) {
586 Console.WriteLine ("*** inotify: {0} {1} {2} {3} {4} {5}",
587 mask, watched.Wd, watched.Path,
588 filename != "" ? filename : "\"\"",
589 isDirectory == true ? "(directory)" : "(file)",
590 srcpath != null ? "(from " + srcpath + ")" : "");
593 if (watched.Subscribers == null)
594 return;
596 foreach (WatchInternal watch in watched.Subscribers)
597 try {
598 if (watch.Callback != null && (watch.Mask & mask) != 0)
599 watch.Callback (watch, watched.Path, filename, srcpath, mask);
600 } catch (Exception e) {
601 Logger.Log.Error ("Caught exception executing Inotify callbacks");
602 Logger.Log.Error (e);
606 ////////////////////////////////////////////////////////////////////////////////////////////////////
608 // Dispatch-time operations on the event queue
610 static Hashtable pending_move_cookies = new Hashtable ();
612 // Clean up the queue, removing dispatched objects.
613 // We assume that the called holds the event_queue lock.
614 static void CleanQueue_Unlocked ()
616 int first_undispatched = 0;
617 while (first_undispatched < event_queue.Count) {
618 QueuedEvent qe = event_queue [first_undispatched] as QueuedEvent;
619 if (! qe.Dispatched)
620 break;
622 if (qe.Cookie != 0)
623 pending_move_cookies.Remove (qe.Cookie);
625 ++first_undispatched;
628 if (first_undispatched > 0)
629 event_queue.RemoveRange (0, first_undispatched);
633 // Apply high-level processing to the queue. Pair moves,
634 // coalesce events, etc.
635 // We assume that the caller holds the event_queue lock.
636 static void AnalyzeQueue_Unlocked ()
638 int first_unanalyzed = event_queue.Count;
639 while (first_unanalyzed > 0) {
640 --first_unanalyzed;
641 QueuedEvent qe = event_queue [first_unanalyzed] as QueuedEvent;
642 if (qe.Analyzed) {
643 ++first_unanalyzed;
644 break;
647 if (first_unanalyzed == event_queue.Count)
648 return;
650 // Walk across the unanalyzed events...
651 for (int i = first_unanalyzed; i < event_queue.Count; ++i) {
652 QueuedEvent qe = event_queue [i] as QueuedEvent;
654 // Pair off the MovedFrom and MovedTo events.
655 if (qe.Cookie != 0) {
656 if ((qe.Type & EventType.MovedFrom) != 0) {
657 pending_move_cookies [qe.Cookie] = qe;
658 // This increases the MovedFrom's HoldUntil time,
659 // giving us more time for the matching MovedTo to
660 // show up.
661 // (512 ms is totally arbitrary)
662 qe.AddMilliseconds (512);
663 } else if ((qe.Type & EventType.MovedTo) != 0) {
664 QueuedEvent paired_move = pending_move_cookies [qe.Cookie] as QueuedEvent;
665 if (paired_move != null) {
666 paired_move.Dispatched = true;
667 qe.PairedMove = paired_move;
672 qe.Analyzed = true;
676 static void DispatchWorker ()
678 while (running) {
679 QueuedEvent next_event = null;
681 // Until we find something we want to dispatch, we will stay
682 // inside the following block of code.
683 lock (event_queue) {
685 while (running) {
686 CleanQueue_Unlocked ();
688 AnalyzeQueue_Unlocked ();
690 // Now look for an event to dispatch.
691 DateTime min_hold_until = DateTime.MaxValue;
692 DateTime now = DateTime.Now;
693 foreach (QueuedEvent qe in event_queue) {
694 if (qe.Dispatched)
695 continue;
696 if (qe.HoldUntil <= now) {
697 next_event = qe;
698 break;
700 if (qe.HoldUntil < min_hold_until)
701 min_hold_until = qe.HoldUntil;
704 // If we found an event, break out of this block
705 // and dispatch it.
706 if (next_event != null)
707 break;
709 // If we didn't find an event to dispatch, we can sleep
710 // (1) until the next hold-until time
711 // (2) until the lock pulses (which means something changed, so
712 // we need to check that we are still running, new events
713 // are on the queue, etc.)
714 // and then we go back up and try to find something to dispatch
715 // all over again.
716 if (min_hold_until == DateTime.MaxValue)
717 Monitor.Wait (event_queue);
718 else
719 Monitor.Wait (event_queue, min_hold_until - now);
723 // If "running" gets set to false, we might get a null next_event as the above
724 // loop terminates
725 if (next_event == null)
726 return;
728 // Now we have an event, so we release the event_queue lock and do
729 // the actual dispatch.
731 // Before we get any further, mark it
732 next_event.Dispatched = true;
734 WatchInfo watched;
735 watched = Lookup (next_event.Wd, next_event.Type);
736 if (watched == null)
737 continue;
739 string srcpath = null;
741 // If this event is a paired MoveTo, there is extra work to do.
742 if ((next_event.Type & EventType.MovedTo) != 0 && next_event.PairedMove != null) {
743 WatchInfo paired_watched;
744 paired_watched = Lookup (next_event.PairedMove.Wd, next_event.PairedMove.Type);
746 if (paired_watched != null) {
747 // Set the source path accordingly.
748 srcpath = Path.Combine (paired_watched.Path, next_event.PairedMove.Filename);
750 // Handle the internal rename of the directory.
751 string dstpath = Path.Combine (watched.Path, next_event.Filename);
753 //if (Directory.Exists (dstpath))
754 HandleMove (srcpath, dstpath);
758 SendEvent (watched, next_event.Filename, srcpath, next_event.Type);
760 // If a directory we are watching gets ignored, we need
761 // to remove it from the watchedByFoo hashes.
762 if ((next_event.Type & EventType.Ignored) != 0) {
763 lock (watched_by_wd)
764 Forget (watched);
769 /////////////////////////////////////////////////////////////////////////////////
771 #if INOTIFY_TEST
772 static void Main (string [] args)
774 Queue to_watch = new Queue ();
775 bool recursive = false;
777 foreach (string arg in args) {
778 if (arg == "-r" || arg == "--recursive")
779 recursive = true;
780 else {
781 // Our hashes work without a trailing path delimiter
782 string path = arg.TrimEnd ('/');
783 to_watch.Enqueue (path);
787 while (to_watch.Count > 0) {
788 string path = (string) to_watch.Dequeue ();
790 Console.WriteLine ("Watching {0}", path);
791 Inotify.Subscribe (path, null, Inotify.EventType.All);
793 if (recursive) {
794 foreach (string subdir in DirectoryWalker.GetDirectories (path))
795 to_watch.Enqueue (subdir);
799 Inotify.Start ();
800 Inotify.Verbose = true;
802 while (Inotify.Enabled && Inotify.WatchCount > 0)
803 Thread.Sleep (1000);
805 if (Inotify.WatchCount == 0)
806 Console.WriteLine ("Nothing being watched.");
808 // Kill the event-reading thread so that we exit
809 Inotify.Stop ();
811 #endif