4 // Copyright (C) 2004 Novell, Inc.
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
30 using System
.Collections
;
32 using System
.Runtime
.InteropServices
;
34 using System
.Text
.RegularExpressions
;
35 using System
.Threading
;
37 namespace Beagle
.Util
{
39 public class Inotify
{
41 public delegate void InotifyCallback (Watch watch
, string path
, string subitem
, string srcpath
, EventType type
);
43 public interface Watch
{
45 void ChangeSubscription (EventType new_mask
);
47 /////////////////////////////////////////////////////////////////////////////////////
50 public enum EventType
: uint {
51 Access
= 0x00000001, // File was accessed
52 Modify
= 0x00000002, // File was modified
53 Attrib
= 0x00000004, // File changed attributes
54 CloseWrite
= 0x00000008, // Writable file was closed
55 CloseNoWrite
= 0x00000010, // Non-writable file was close
56 Open
= 0x00000020, // File was opened
57 MovedFrom
= 0x00000040, // File was moved from X
58 MovedTo
= 0x00000080, // File was moved to Y
59 Create
= 0x00000100, // Subfile was created
60 Delete
= 0x00000200, // Subfile was deleted
61 DeleteSelf
= 0x00000400, // Self was deleted
63 Unmount
= 0x00002000, // Backing fs was unmounted
64 QueueOverflow
= 0x00004000, // Event queue overflowed
65 Ignored
= 0x00008000, // File is no longer being watched
67 IsDirectory
= 0x40000000, // Event is against a directory
68 OneShot
= 0x80000000, // Watch is one-shot
70 // For forward compatibility, define these explicitly
71 All
= (EventType
.Access
| EventType
.Modify
| EventType
.Attrib
|
72 EventType
.CloseWrite
| EventType
.CloseNoWrite
| EventType
.Open
|
73 EventType
.MovedFrom
| EventType
.MovedTo
| EventType
.Create
|
74 EventType
.Delete
| EventType
.DeleteSelf
)
77 // Events that we want internally, even if the handlers do not
78 static private EventType base_mask
= EventType
.MovedFrom
| EventType
.MovedTo
;
80 /////////////////////////////////////////////////////////////////////////////////////
82 static private Logger log
;
84 static public Logger Log
{
88 /////////////////////////////////////////////////////////////////////////////////////
90 [StructLayout (LayoutKind
.Sequential
)]
91 private struct inotify_event
{
93 public EventType mask
;
98 [DllImport ("libbeagleglue")]
99 static extern int inotify_glue_init ();
101 [DllImport ("libbeagleglue")]
102 static extern int inotify_glue_watch (int fd
, string filename
, EventType mask
);
104 [DllImport ("libbeagleglue")]
105 static extern int inotify_glue_ignore (int fd
, int wd
);
107 [DllImport ("libbeagleglue")]
108 static extern unsafe void inotify_snarf_events (int fd
,
112 [DllImport ("libbeagleglue")]
113 static extern void inotify_snarf_cancel ();
115 /////////////////////////////////////////////////////////////////////////////////////
117 static public bool Verbose
= false;
118 static private int inotify_fd
= -1;
122 log
= Logger
.Get ("Inotify");
124 if (Environment
.GetEnvironmentVariable ("BEAGLE_DISABLE_INOTIFY") != null) {
125 Logger
.Log
.Debug ("BEAGLE_DISABLE_INOTIFY is set");
129 if (Environment
.GetEnvironmentVariable ("BEAGLE_INOTIFY_VERBOSE") != null)
130 Inotify
.Verbose
= true;
133 inotify_fd
= inotify_glue_init ();
134 } catch (EntryPointNotFoundException
) {
135 Logger
.Log
.Info ("Inotify not available on system.");
139 if (inotify_fd
== -1)
140 Logger
.Log
.Warn ("Could not initialize inotify");
143 static public bool Enabled
{
144 get { return inotify_fd >= 0; }
147 /////////////////////////////////////////////////////////////////////////////////////
151 // Stubs for systems where inotify is unavailable
153 static public Watch
Subscribe (string path
, InotifyCallback callback
, EventType mask
)
158 static public void Start ()
163 static public void Stop ()
168 #else // ENABLE_INOTIFY
170 /////////////////////////////////////////////////////////////////////////////////////
171 static private ArrayList event_queue
= new ArrayList ();
173 private class QueuedEvent
{
175 public EventType Type
;
176 public string Filename
;
179 public bool Analyzed
;
180 public bool Dispatched
;
181 public DateTime HoldUntil
;
182 public QueuedEvent PairedMove
;
184 // Measured in milliseconds; 57ms is totally random
185 public const double DefaultHoldTime
= 57;
187 public QueuedEvent ()
189 // Set a default HoldUntil time
190 HoldUntil
= DateTime
.Now
.AddMilliseconds (DefaultHoldTime
);
193 public void AddMilliseconds (double x
)
195 HoldUntil
= HoldUntil
.AddMilliseconds (x
);
198 public void PairWith (QueuedEvent other
)
200 this.PairedMove
= other
;
201 other
.PairedMove
= this;
203 if (this.HoldUntil
< other
.HoldUntil
)
204 this.HoldUntil
= other
.HoldUntil
;
205 other
.HoldUntil
= this.HoldUntil
;
209 /////////////////////////////////////////////////////////////////////////////////////
211 private class WatchInternal
: Watch
{
212 private InotifyCallback callback
;
213 private EventType mask
;
214 private WatchInfo watchinfo
;
215 private bool is_subscribed
;
217 public InotifyCallback Callback
{
218 get { return callback; }
221 public EventType Mask
{
223 set { mask = value; }
226 public WatchInternal (InotifyCallback callback
, EventType mask
, WatchInfo watchinfo
)
228 this.callback
= callback
;
230 this.watchinfo
= watchinfo
;
231 this.is_subscribed
= true;
234 public void Unsubscribe ()
236 if (!this.is_subscribed
)
239 Inotify
.Unsubscribe (watchinfo
, this);
240 this.is_subscribed
= false;
243 public void ChangeSubscription (EventType mask
)
245 if (! this.is_subscribed
)
249 CreateOrModifyWatch (this.watchinfo
);
254 private class WatchInfo
{
257 public bool IsDirectory
;
258 public EventType Mask
;
260 public EventType FilterMask
;
261 public EventType FilterSeen
;
263 public ArrayList Children
;
264 public WatchInfo Parent
;
266 public ArrayList Subscribers
;
269 static Hashtable watched_by_wd
= new Hashtable ();
270 static Hashtable watched_by_path
= new Hashtable ();
271 static WatchInfo last_watched
= null;
273 private class PendingMove
{
274 public WatchInfo Watch
;
275 public string SrcName
;
276 public DateTime Time
;
279 public PendingMove (WatchInfo watched
, string srcname
, DateTime time
, uint cookie
) {
287 static public int WatchCount
{
288 get { return watched_by_wd.Count; }
291 static public bool IsWatching (string path
)
293 path
= Path
.GetFullPath (path
);
294 return watched_by_path
.Contains (path
);
297 // Filter WatchInfo items when we do the Lookup.
298 // We do the filtering here to avoid having to acquire
299 // the watched_by_wd lock yet again.
300 static private WatchInfo
Lookup (int wd
, EventType event_type
)
302 lock (watched_by_wd
) {
304 if (last_watched
!= null && last_watched
.Wd
== wd
)
305 watched
= last_watched
;
307 watched
= watched_by_wd
[wd
] as WatchInfo
;
309 last_watched
= watched
;
312 if (watched
!= null && (watched
.FilterMask
& event_type
) != 0) {
313 watched
.FilterSeen
|= event_type
;
321 // The caller has to handle all locking itself
322 static private void Forget (WatchInfo watched
)
324 if (last_watched
== watched
)
326 if (watched
.Parent
!= null)
327 watched
.Parent
.Children
.Remove (watched
);
328 watched_by_wd
.Remove (watched
.Wd
);
329 watched_by_path
.Remove (watched
.Path
);
332 static public Watch
Subscribe (string path
, InotifyCallback callback
, EventType mask
, EventType initial_filter
)
336 EventType mask_orig
= mask
;
338 if (!Path
.IsPathRooted (path
))
339 path
= Path
.GetFullPath (path
);
341 bool is_directory
= false;
342 if (Directory
.Exists (path
))
344 else if (! File
.Exists (path
))
345 throw new IOException (path
);
347 lock (watched_by_wd
) {
348 watched
= watched_by_path
[path
] as WatchInfo
;
350 if (watched
== null) {
351 // We need an entirely new WatchInfo object
352 watched
= new WatchInfo ();
354 watched
.IsDirectory
= is_directory
;
355 watched
.Subscribers
= new ArrayList ();
356 watched
.Children
= new ArrayList ();
357 DirectoryInfo dir
= new DirectoryInfo (path
);
358 if (dir
.Parent
!= null)
359 watched
.Parent
= watched_by_path
[dir
.Parent
.ToString ()] as WatchInfo
;
360 if (watched
.Parent
!= null)
361 watched
.Parent
.Children
.Add (watched
);
362 watched_by_path
[watched
.Path
] = watched
;
365 watched
.FilterMask
= initial_filter
;
366 watched
.FilterSeen
= 0;
368 watch
= new WatchInternal (callback
, mask_orig
, watched
);
369 watched
.Subscribers
.Add (watch
);
371 CreateOrModifyWatch (watched
);
372 watched_by_wd
[watched
.Wd
] = watched
;
378 static public Watch
Subscribe (string path
, InotifyCallback callback
, EventType mask
)
380 return Subscribe (path
, callback
, mask
, 0);
383 static public EventType
Filter (string path
, EventType mask
)
387 path
= Path
.GetFullPath (path
);
389 lock (watched_by_wd
) {
391 watched
= watched_by_path
[path
] as WatchInfo
;
393 seen
= watched
.FilterSeen
;
394 watched
.FilterMask
= mask
;
395 watched
.FilterSeen
= 0;
401 static private void Unsubscribe (WatchInfo watched
, WatchInternal watch
)
403 watched
.Subscribers
.Remove (watch
);
405 // Other subscribers might still be around
406 if (watched
.Subscribers
.Count
> 0) {
408 CreateOrModifyWatch (watched
);
412 int retval
= inotify_glue_ignore (inotify_fd
, watched
.Wd
);
414 string msg
= String
.Format ("Attempt to ignore {0} failed!", watched
.Path
);
415 throw new IOException (msg
);
422 // Ensure our watch exists, meets all the subscribers requirements,
423 // and isn't matching any other events that we don't care about.
424 static private void CreateOrModifyWatch (WatchInfo watched
)
426 EventType new_mask
= base_mask
;
427 foreach (WatchInternal watch
in watched
.Subscribers
)
428 new_mask
|= watch
.Mask
;
430 if (watched
.Wd
>= 0 && watched
.Mask
== new_mask
)
433 // We rely on the behaviour that watching the same inode twice won't result
434 // in the wd value changing.
435 // (no need to worry about watched_by_wd being polluted with stale watches)
438 wd
= inotify_glue_watch (inotify_fd
, watched
.Path
, new_mask
);
440 string msg
= String
.Format ("Attempt to watch {0} failed!", watched
.Path
);
441 throw new IOException (msg
);
443 if (watched
.Wd
>= 0 && watched
.Wd
!= wd
) {
444 string msg
= String
.Format ("Watch handle changed unexpectedly!", watched
.Path
);
445 throw new IOException (msg
);
449 watched
.Mask
= new_mask
;
452 /////////////////////////////////////////////////////////////////////////////////////
454 static Thread snarf_thread
= null;
455 static bool running
= false;
457 static public void Start ()
462 Logger
.Log
.Debug("Starting Inotify threads");
465 if (snarf_thread
!= null)
470 snarf_thread
= ExceptionHandlingThread
.Start (new ThreadStart (SnarfWorker
));
471 ExceptionHandlingThread
.Start (new ThreadStart (DispatchWorker
));
475 static public void Stop ()
482 Monitor
.Pulse (event_queue
);
485 inotify_snarf_cancel ();
488 static unsafe void SnarfWorker ()
490 Encoding filename_encoding
= Encoding
.UTF8
;
491 int event_size
= Marshal
.SizeOf (typeof (inotify_event
));
495 // We get much better performance if we wait a tiny bit
496 // between reads in order to let events build up.
497 // FIXME: We need to be smarter here to avoid queue overflows.
503 // Will block while waiting for events, but with a 1s timeout.
504 inotify_snarf_events (inotify_fd
,
514 ArrayList new_events
= new ArrayList ();
516 bool saw_overflow
= false;
519 // Read the low-level event struct from the buffer.
520 inotify_event raw_event
;
521 raw_event
= (inotify_event
) Marshal
.PtrToStructure (buffer
, typeof (inotify_event
));
522 buffer
= (IntPtr
) ((long) buffer
+ event_size
);
524 if ((raw_event
.mask
& EventType
.QueueOverflow
) != 0)
527 // Now we convert our low-level event struct into a nicer object.
528 QueuedEvent qe
= new QueuedEvent ();
529 qe
.Wd
= raw_event
.wd
;
530 qe
.Type
= raw_event
.mask
;
531 qe
.Cookie
= raw_event
.cookie
;
533 // Extract the filename payload (if any) from the buffer.
534 byte [] filename_bytes
= new byte[raw_event
.len
];
535 Marshal
.Copy (buffer
, filename_bytes
, 0, (int) raw_event
.len
);
536 buffer
= (IntPtr
) ((long) buffer
+ raw_event
.len
);
538 while (n_chars
< filename_bytes
.Length
&& filename_bytes
[n_chars
] != 0)
542 qe
.Filename
= filename_encoding
.GetString (filename_bytes
, 0, n_chars
);
545 nr
-= event_size
+ (int) raw_event
.len
;
549 Logger
.Log
.Warn ("Inotify queue overflow!");
552 event_queue
.AddRange (new_events
);
553 Monitor
.Pulse (event_queue
);
559 // Update the watched_by_path hash and the path stored inside the watch
560 // in response to a move event.
561 static private void MoveWatch (WatchInfo watch
, string name
)
563 lock (watched_by_wd
) {
565 watched_by_path
.Remove (watch
.Path
);
567 watched_by_path
[watch
.Path
] = watch
;
571 Console
.WriteLine ("*** inotify: Moved Watch to {0}", watch
.Path
);
574 // A directory we are watching has moved. We need to fix up its path, and the path of
575 // all of its subdirectories, their subdirectories, and so on.
576 static private void HandleMove (string srcpath
, string dstparent
, string dstname
)
578 string dstpath
= Path
.Combine (dstparent
, dstname
);
579 lock (watched_by_wd
) {
581 WatchInfo start
= watched_by_path
[srcpath
] as WatchInfo
; // not the same as src!
583 Logger
.Log
.Warn ("Lookup failed for {0}", srcpath
);
587 // Queue our starting point, then walk its subdirectories, invoking MoveWatch() on
588 // each, repeating for their subdirectories. The relationship between start, child
589 // and dstpath is fickle and important.
590 Queue queue
= new Queue();
591 queue
.Enqueue (start
);
593 WatchInfo target
= queue
.Dequeue () as WatchInfo
;
594 for (int i
= 0; i
< target
.Children
.Count
; i
++) {
595 WatchInfo child
= target
.Children
[i
] as WatchInfo
;
596 Logger
.Log
.Debug ("Moving watch on {0} from {1} to {2}", child
.Path
, srcpath
, dstpath
);
597 string name
= Path
.Combine (dstpath
, child
.Path
.Substring (srcpath
.Length
+ 1));
598 MoveWatch (child
, name
);
599 queue
.Enqueue (child
);
601 } while (queue
.Count
> 0);
603 // Ultimately, fixup the original watch, too
604 MoveWatch (start
, dstpath
);
605 if (start
.Parent
!= null)
606 start
.Parent
.Children
.Remove (start
);
607 start
.Parent
= watched_by_path
[dstparent
] as WatchInfo
;
608 if (start
.Parent
!= null)
609 start
.Parent
.Children
.Add (start
);
613 static private void SendEvent (WatchInfo watched
, string filename
, string srcpath
, EventType mask
)
615 // Does the watch care about this event?
616 if ((watched
.Mask
& mask
) == 0)
619 bool isDirectory
= false;
620 if ((mask
& EventType
.IsDirectory
) != 0)
624 Console
.WriteLine ("*** inotify: {0} {1} {2} {3} {4} {5}",
625 mask
, watched
.Wd
, watched
.Path
,
626 filename
!= "" ? filename
: "\"\"",
627 isDirectory
== true ? "(directory)" : "(file)",
628 srcpath
!= null ? "(from " + srcpath
+ ")" : "");
631 if (watched
.Subscribers
== null)
634 foreach (WatchInternal watch
in (IEnumerable
) watched
.Subscribers
.Clone ())
636 if (watch
.Callback
!= null && (watch
.Mask
& mask
) != 0)
637 watch
.Callback (watch
, watched
.Path
, filename
, srcpath
, mask
);
638 } catch (Exception e
) {
639 Logger
.Log
.Error ("Caught exception executing Inotify callbacks");
640 Logger
.Log
.Error (e
);
644 ////////////////////////////////////////////////////////////////////////////////////////////////////
646 // Dispatch-time operations on the event queue
648 static Hashtable pending_move_cookies
= new Hashtable ();
650 // Clean up the queue, removing dispatched objects.
651 // We assume that the called holds the event_queue lock.
652 static void CleanQueue_Unlocked ()
654 int first_undispatched
= 0;
655 while (first_undispatched
< event_queue
.Count
) {
656 QueuedEvent qe
= event_queue
[first_undispatched
] as QueuedEvent
;
661 pending_move_cookies
.Remove (qe
.Cookie
);
663 ++first_undispatched
;
666 if (first_undispatched
> 0)
667 event_queue
.RemoveRange (0, first_undispatched
);
671 // Apply high-level processing to the queue. Pair moves,
672 // coalesce events, etc.
673 // We assume that the caller holds the event_queue lock.
674 static void AnalyzeQueue_Unlocked ()
676 int first_unanalyzed
= event_queue
.Count
;
677 while (first_unanalyzed
> 0) {
679 QueuedEvent qe
= event_queue
[first_unanalyzed
] as QueuedEvent
;
685 if (first_unanalyzed
== event_queue
.Count
)
688 // Walk across the unanalyzed events...
689 for (int i
= first_unanalyzed
; i
< event_queue
.Count
; ++i
) {
690 QueuedEvent qe
= event_queue
[i
] as QueuedEvent
;
692 // Pair off the MovedFrom and MovedTo events.
693 if (qe
.Cookie
!= 0) {
694 if ((qe
.Type
& EventType
.MovedFrom
) != 0) {
695 pending_move_cookies
[qe
.Cookie
] = qe
;
696 // This increases the MovedFrom's HoldUntil time,
697 // giving us more time for the matching MovedTo to
699 // (512 ms is totally arbitrary)
700 qe
.AddMilliseconds (512);
701 } else if ((qe
.Type
& EventType
.MovedTo
) != 0) {
702 QueuedEvent paired_move
= pending_move_cookies
[qe
.Cookie
] as QueuedEvent
;
703 if (paired_move
!= null) {
704 paired_move
.Dispatched
= true;
705 qe
.PairedMove
= paired_move
;
714 static void DispatchWorker ()
717 QueuedEvent next_event
= null;
719 // Until we find something we want to dispatch, we will stay
720 // inside the following block of code.
724 CleanQueue_Unlocked ();
726 AnalyzeQueue_Unlocked ();
728 // Now look for an event to dispatch.
729 DateTime min_hold_until
= DateTime
.MaxValue
;
730 DateTime now
= DateTime
.Now
;
731 foreach (QueuedEvent qe
in event_queue
) {
734 if (qe
.HoldUntil
<= now
) {
738 if (qe
.HoldUntil
< min_hold_until
)
739 min_hold_until
= qe
.HoldUntil
;
742 // If we found an event, break out of this block
744 if (next_event
!= null)
747 // If we didn't find an event to dispatch, we can sleep
748 // (1) until the next hold-until time
749 // (2) until the lock pulses (which means something changed, so
750 // we need to check that we are still running, new events
751 // are on the queue, etc.)
752 // and then we go back up and try to find something to dispatch
754 if (min_hold_until
== DateTime
.MaxValue
)
755 Monitor
.Wait (event_queue
);
757 Monitor
.Wait (event_queue
, min_hold_until
- now
);
761 // If "running" gets set to false, we might get a null next_event as the above
763 if (next_event
== null)
766 // Now we have an event, so we release the event_queue lock and do
767 // the actual dispatch.
769 // Before we get any further, mark it
770 next_event
.Dispatched
= true;
773 watched
= Lookup (next_event
.Wd
, next_event
.Type
);
777 string srcpath
= null;
779 // If this event is a paired MoveTo, there is extra work to do.
780 if ((next_event
.Type
& EventType
.MovedTo
) != 0 && next_event
.PairedMove
!= null) {
781 WatchInfo paired_watched
;
782 paired_watched
= Lookup (next_event
.PairedMove
.Wd
, next_event
.PairedMove
.Type
);
784 if (paired_watched
!= null) {
785 // Set the source path accordingly.
786 srcpath
= Path
.Combine (paired_watched
.Path
, next_event
.PairedMove
.Filename
);
788 // Handle the internal rename of the directory.
789 if ((next_event
.Type
& EventType
.IsDirectory
) != 0)
790 HandleMove (srcpath
, watched
.Path
, next_event
.Filename
);
794 SendEvent (watched
, next_event
.Filename
, srcpath
, next_event
.Type
);
796 // If a directory we are watching gets ignored, we need
797 // to remove it from the watchedByFoo hashes.
798 if ((next_event
.Type
& EventType
.Ignored
) != 0) {
805 /////////////////////////////////////////////////////////////////////////////////
808 static void Main (string [] args
)
810 Queue to_watch
= new Queue ();
811 bool recursive
= false;
813 foreach (string arg
in args
) {
814 if (arg
== "-r" || arg
== "--recursive")
817 // Our hashes work without a trailing path delimiter
818 string path
= arg
.TrimEnd ('/');
819 to_watch
.Enqueue (path
);
823 while (to_watch
.Count
> 0) {
824 string path
= (string) to_watch
.Dequeue ();
826 Console
.WriteLine ("Watching {0}", path
);
827 Inotify
.Subscribe (path
, null, Inotify
.EventType
.All
);
830 foreach (string subdir
in DirectoryWalker
.GetDirectories (path
))
831 to_watch
.Enqueue (subdir
);
836 Inotify
.Verbose
= true;
838 while (Inotify
.Enabled
&& Inotify
.WatchCount
> 0)
841 if (Inotify
.WatchCount
== 0)
842 Console
.WriteLine ("Nothing being watched.");
844 // Kill the event-reading thread so that we exit
849 #endif // ENABLE_INOTIFY