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 OnlyDir
= 0x01000000, // Only watch a path if it is a directory
68 DoNotFollow
= 0x02000000, // Do not follow symbolic links
69 MaskAdd
= 0x20000000, // Add, do not replace, mask on the inode if it exists
70 IsDirectory
= 0x40000000, // Event is against a directory
71 OneShot
= 0x80000000, // Watch is one-shot
73 // For forward compatibility, define these explicitly
74 All
= (EventType
.Access
| EventType
.Modify
| EventType
.Attrib
|
75 EventType
.CloseWrite
| EventType
.CloseNoWrite
| EventType
.Open
|
76 EventType
.MovedFrom
| EventType
.MovedTo
| EventType
.Create
|
77 EventType
.Delete
| EventType
.DeleteSelf
)
80 // Events that we want internally, even if the handlers do not
81 private static EventType base_mask
= EventType
.MovedFrom
| EventType
.MovedTo
;
83 /////////////////////////////////////////////////////////////////////////////////////
85 private static Logger log
;
87 public static Logger Log
{
91 /////////////////////////////////////////////////////////////////////////////////////
93 [StructLayout (LayoutKind
.Sequential
)]
94 private struct inotify_event
{
96 public EventType mask
;
101 [DllImport ("libbeagleglue")]
102 static extern int inotify_glue_init ();
104 [DllImport ("libbeagleglue")]
105 static extern int inotify_glue_watch (int fd
, string filename
, EventType mask
);
107 [DllImport ("libbeagleglue")]
108 static extern int inotify_glue_ignore (int fd
, int wd
);
110 [DllImport ("libbeagleglue")]
111 static extern unsafe void inotify_snarf_events (int fd
,
115 [DllImport ("libbeagleglue")]
116 static extern void inotify_snarf_cancel ();
118 /////////////////////////////////////////////////////////////////////////////////////
120 public static bool Verbose
= false;
121 private static int inotify_fd
= -1;
125 log
= Logger
.Get ("Inotify");
127 if (Environment
.GetEnvironmentVariable ("BEAGLE_DISABLE_INOTIFY") != null) {
128 Logger
.Log
.Debug ("BEAGLE_DISABLE_INOTIFY is set");
132 if (Environment
.GetEnvironmentVariable ("BEAGLE_INOTIFY_VERBOSE") != null)
133 Inotify
.Verbose
= true;
136 inotify_fd
= inotify_glue_init ();
137 } catch (EntryPointNotFoundException
) {
138 Logger
.Log
.Info ("Inotify not available on system.");
142 if (inotify_fd
== -1)
143 Logger
.Log
.Warn ("Could not initialize inotify");
146 public static bool Enabled
{
147 get { return inotify_fd >= 0; }
150 /////////////////////////////////////////////////////////////////////////////////////
154 // Stubs for systems where inotify is unavailable
156 public static Watch
Subscribe (string path
, InotifyCallback callback
, EventType mask
)
161 public static void Start ()
166 public static void Stop ()
171 #else // ENABLE_INOTIFY
173 /////////////////////////////////////////////////////////////////////////////////////
174 private static ArrayList event_queue
= new ArrayList ();
176 private class QueuedEvent
{
178 public EventType Type
;
179 public string Filename
;
182 public bool Analyzed
;
183 public bool Dispatched
;
184 public DateTime HoldUntil
;
185 public QueuedEvent PairedMove
;
187 // Measured in milliseconds; 57ms is totally random
188 public const double DefaultHoldTime
= 57;
190 public QueuedEvent ()
192 // Set a default HoldUntil time
193 HoldUntil
= DateTime
.Now
.AddMilliseconds (DefaultHoldTime
);
196 public void AddMilliseconds (double x
)
198 HoldUntil
= HoldUntil
.AddMilliseconds (x
);
201 public void PairWith (QueuedEvent other
)
203 this.PairedMove
= other
;
204 other
.PairedMove
= this;
206 if (this.HoldUntil
< other
.HoldUntil
)
207 this.HoldUntil
= other
.HoldUntil
;
208 other
.HoldUntil
= this.HoldUntil
;
212 /////////////////////////////////////////////////////////////////////////////////////
214 private class WatchInternal
: Watch
{
215 private InotifyCallback callback
;
216 private EventType mask
;
217 private WatchInfo watchinfo
;
218 private bool is_subscribed
;
220 public InotifyCallback Callback
{
221 get { return callback; }
224 public EventType Mask
{
226 set { mask = value; }
229 public WatchInternal (InotifyCallback callback
, EventType mask
, WatchInfo watchinfo
)
231 this.callback
= callback
;
233 this.watchinfo
= watchinfo
;
234 this.is_subscribed
= true;
237 public void Unsubscribe ()
239 if (!this.is_subscribed
)
242 Inotify
.Unsubscribe (watchinfo
, this);
243 this.is_subscribed
= false;
246 public void ChangeSubscription (EventType mask
)
248 if (! this.is_subscribed
)
252 CreateOrModifyWatch (this.watchinfo
);
257 private class WatchInfo
{
260 public bool IsDirectory
;
261 public EventType Mask
;
263 public EventType FilterMask
;
264 public EventType FilterSeen
;
266 public ArrayList Children
;
267 public WatchInfo Parent
;
269 public ArrayList Subscribers
;
272 private static Hashtable watched_by_wd
= new Hashtable ();
273 private static Hashtable watched_by_path
= new Hashtable ();
274 private static WatchInfo last_watched
= null;
276 private class PendingMove
{
277 public WatchInfo Watch
;
278 public string SrcName
;
279 public DateTime Time
;
282 public PendingMove (WatchInfo watched
, string srcname
, DateTime time
, uint cookie
) {
290 public static int WatchCount
{
291 get { return watched_by_wd.Count; }
294 public static bool IsWatching (string path
)
296 path
= Path
.GetFullPath (path
);
297 return watched_by_path
.Contains (path
);
300 // Filter WatchInfo items when we do the Lookup.
301 // We do the filtering here to avoid having to acquire
302 // the watched_by_wd lock yet again.
303 private static WatchInfo
Lookup (int wd
, EventType event_type
)
305 lock (watched_by_wd
) {
307 if (last_watched
!= null && last_watched
.Wd
== wd
)
308 watched
= last_watched
;
310 watched
= watched_by_wd
[wd
] as WatchInfo
;
312 last_watched
= watched
;
315 if (watched
!= null && (watched
.FilterMask
& event_type
) != 0) {
316 watched
.FilterSeen
|= event_type
;
324 // The caller has to handle all locking itself
325 private static void Forget (WatchInfo watched
)
327 if (last_watched
== watched
)
329 if (watched
.Parent
!= null)
330 watched
.Parent
.Children
.Remove (watched
);
331 watched_by_wd
.Remove (watched
.Wd
);
332 watched_by_path
.Remove (watched
.Path
);
335 public static Watch
Subscribe (string path
, InotifyCallback callback
, EventType mask
, EventType initial_filter
)
339 EventType mask_orig
= mask
;
341 if (!Path
.IsPathRooted (path
))
342 path
= Path
.GetFullPath (path
);
344 bool is_directory
= false;
345 if (Directory
.Exists (path
))
347 else if (! File
.Exists (path
))
348 throw new IOException (path
);
350 lock (watched_by_wd
) {
351 watched
= watched_by_path
[path
] as WatchInfo
;
353 if (watched
== null) {
354 // We need an entirely new WatchInfo object
355 watched
= new WatchInfo ();
357 watched
.IsDirectory
= is_directory
;
358 watched
.Subscribers
= new ArrayList ();
359 watched
.Children
= new ArrayList ();
360 DirectoryInfo dir
= new DirectoryInfo (path
);
361 if (dir
.Parent
!= null)
362 watched
.Parent
= watched_by_path
[dir
.Parent
.ToString ()] as WatchInfo
;
363 if (watched
.Parent
!= null)
364 watched
.Parent
.Children
.Add (watched
);
365 watched_by_path
[watched
.Path
] = watched
;
368 watched
.FilterMask
= initial_filter
;
369 watched
.FilterSeen
= 0;
371 watch
= new WatchInternal (callback
, mask_orig
, watched
);
372 watched
.Subscribers
.Add (watch
);
374 CreateOrModifyWatch (watched
);
375 watched_by_wd
[watched
.Wd
] = watched
;
381 public static Watch
Subscribe (string path
, InotifyCallback callback
, EventType mask
)
383 return Subscribe (path
, callback
, mask
, 0);
386 public static EventType
Filter (string path
, EventType mask
)
390 path
= Path
.GetFullPath (path
);
392 lock (watched_by_wd
) {
394 watched
= watched_by_path
[path
] as WatchInfo
;
396 seen
= watched
.FilterSeen
;
397 watched
.FilterMask
= mask
;
398 watched
.FilterSeen
= 0;
404 private static void Unsubscribe (WatchInfo watched
, WatchInternal watch
)
406 watched
.Subscribers
.Remove (watch
);
408 // Other subscribers might still be around
409 if (watched
.Subscribers
.Count
> 0) {
411 CreateOrModifyWatch (watched
);
415 int retval
= inotify_glue_ignore (inotify_fd
, watched
.Wd
);
417 string msg
= String
.Format ("Attempt to ignore {0} failed!", watched
.Path
);
418 throw new IOException (msg
);
425 // Ensure our watch exists, meets all the subscribers requirements,
426 // and isn't matching any other events that we don't care about.
427 private static void CreateOrModifyWatch (WatchInfo watched
)
429 EventType new_mask
= base_mask
;
430 foreach (WatchInternal watch
in watched
.Subscribers
)
431 new_mask
|= watch
.Mask
;
433 if (watched
.Wd
>= 0 && watched
.Mask
== new_mask
)
436 // We rely on the behaviour that watching the same inode twice won't result
437 // in the wd value changing.
438 // (no need to worry about watched_by_wd being polluted with stale watches)
441 wd
= inotify_glue_watch (inotify_fd
, watched
.Path
, new_mask
);
443 string msg
= String
.Format ("Attempt to watch {0} failed!", watched
.Path
);
444 throw new IOException (msg
);
446 if (watched
.Wd
>= 0 && watched
.Wd
!= wd
) {
447 string msg
= String
.Format ("Watch handle changed unexpectedly!", watched
.Path
);
448 throw new IOException (msg
);
452 watched
.Mask
= new_mask
;
455 /////////////////////////////////////////////////////////////////////////////////////
457 private static Thread snarf_thread
= null;
458 private static bool running
= false;
459 private static bool shutdown_requested
= false;
461 public static void ShutdownRequested () {
463 shutdown_requested
= true;
467 public static void Start ()
472 Logger
.Log
.Debug("Starting Inotify threads");
475 if (shutdown_requested
|| snarf_thread
!= null)
480 snarf_thread
= ExceptionHandlingThread
.Start (new ThreadStart (SnarfWorker
));
481 ExceptionHandlingThread
.Start (new ThreadStart (DispatchWorker
));
485 public static void Stop ()
490 Log
.Debug ("Stopping inotify threads");
493 shutdown_requested
= true;
499 Monitor
.Pulse (event_queue
);
502 inotify_snarf_cancel ();
505 private static unsafe void SnarfWorker ()
507 Encoding filename_encoding
= Encoding
.UTF8
;
508 int event_size
= Marshal
.SizeOf (typeof (inotify_event
));
512 // We get much better performance if we wait a tiny bit
513 // between reads in order to let events build up.
514 // FIXME: We need to be smarter here to avoid queue overflows.
520 // Will block while waiting for events, but with a 1s timeout.
521 inotify_snarf_events (inotify_fd
,
531 ArrayList new_events
= new ArrayList ();
533 bool saw_overflow
= false;
536 // Read the low-level event struct from the buffer.
537 inotify_event raw_event
;
538 raw_event
= (inotify_event
) Marshal
.PtrToStructure (buffer
, typeof (inotify_event
));
539 buffer
= (IntPtr
) ((long) buffer
+ event_size
);
541 if ((raw_event
.mask
& EventType
.QueueOverflow
) != 0)
544 // Now we convert our low-level event struct into a nicer object.
545 QueuedEvent qe
= new QueuedEvent ();
546 qe
.Wd
= raw_event
.wd
;
547 qe
.Type
= raw_event
.mask
;
548 qe
.Cookie
= raw_event
.cookie
;
550 // Extract the filename payload (if any) from the buffer.
551 byte [] filename_bytes
= new byte[raw_event
.len
];
552 Marshal
.Copy (buffer
, filename_bytes
, 0, (int) raw_event
.len
);
553 buffer
= (IntPtr
) ((long) buffer
+ raw_event
.len
);
555 while (n_chars
< filename_bytes
.Length
&& filename_bytes
[n_chars
] != 0)
559 qe
.Filename
= filename_encoding
.GetString (filename_bytes
, 0, n_chars
);
562 nr
-= event_size
+ (int) raw_event
.len
;
566 Logger
.Log
.Warn ("Inotify queue overflow!");
569 event_queue
.AddRange (new_events
);
570 Monitor
.Pulse (event_queue
);
576 // Update the watched_by_path hash and the path stored inside the watch
577 // in response to a move event.
578 private static void MoveWatch (WatchInfo watch
, string name
)
580 lock (watched_by_wd
) {
582 watched_by_path
.Remove (watch
.Path
);
584 watched_by_path
[watch
.Path
] = watch
;
588 Console
.WriteLine ("*** inotify: Moved Watch to {0}", watch
.Path
);
591 // A directory we are watching has moved. We need to fix up its path, and the path of
592 // all of its subdirectories, their subdirectories, and so on.
593 private static void HandleMove (string srcpath
, string dstparent
, string dstname
)
595 string dstpath
= Path
.Combine (dstparent
, dstname
);
596 lock (watched_by_wd
) {
598 WatchInfo start
= watched_by_path
[srcpath
] as WatchInfo
; // not the same as src!
600 Logger
.Log
.Warn ("Lookup failed for {0}", srcpath
);
604 // Queue our starting point, then walk its subdirectories, invoking MoveWatch() on
605 // each, repeating for their subdirectories. The relationship between start, child
606 // and dstpath is fickle and important.
607 Queue queue
= new Queue();
608 queue
.Enqueue (start
);
610 WatchInfo target
= queue
.Dequeue () as WatchInfo
;
611 for (int i
= 0; i
< target
.Children
.Count
; i
++) {
612 WatchInfo child
= target
.Children
[i
] as WatchInfo
;
613 Logger
.Log
.Debug ("Moving watch on {0} from {1} to {2}", child
.Path
, srcpath
, dstpath
);
614 string name
= Path
.Combine (dstpath
, child
.Path
.Substring (srcpath
.Length
+ 1));
615 MoveWatch (child
, name
);
616 queue
.Enqueue (child
);
618 } while (queue
.Count
> 0);
620 // Ultimately, fixup the original watch, too
621 MoveWatch (start
, dstpath
);
622 if (start
.Parent
!= null)
623 start
.Parent
.Children
.Remove (start
);
624 start
.Parent
= watched_by_path
[dstparent
] as WatchInfo
;
625 if (start
.Parent
!= null)
626 start
.Parent
.Children
.Add (start
);
630 private static void SendEvent (WatchInfo watched
, string filename
, string srcpath
, EventType mask
)
632 // Does the watch care about this event?
633 if ((watched
.Mask
& mask
) == 0)
636 bool isDirectory
= false;
637 if ((mask
& EventType
.IsDirectory
) != 0)
641 Console
.WriteLine ("*** inotify: {0} {1} {2} {3} {4} {5}",
642 mask
, watched
.Wd
, watched
.Path
,
643 filename
!= "" ? filename
: "\"\"",
644 isDirectory
== true ? "(directory)" : "(file)",
645 srcpath
!= null ? "(from " + srcpath
+ ")" : "");
648 if (watched
.Subscribers
== null)
651 foreach (WatchInternal watch
in (IEnumerable
) watched
.Subscribers
.Clone ())
653 if (watch
.Callback
!= null && (watch
.Mask
& mask
) != 0)
654 watch
.Callback (watch
, watched
.Path
, filename
, srcpath
, mask
);
655 } catch (Exception e
) {
656 Logger
.Log
.Error (e
, "Caught exception executing Inotify callbacks");
660 ////////////////////////////////////////////////////////////////////////////////////////////////////
662 // Dispatch-time operations on the event queue
664 private static Hashtable pending_move_cookies
= new Hashtable ();
666 // Clean up the queue, removing dispatched objects.
667 // We assume that the called holds the event_queue lock.
668 private static void CleanQueue_Unlocked ()
670 int first_undispatched
= 0;
671 while (first_undispatched
< event_queue
.Count
) {
672 QueuedEvent qe
= event_queue
[first_undispatched
] as QueuedEvent
;
677 pending_move_cookies
.Remove (qe
.Cookie
);
679 ++first_undispatched
;
682 if (first_undispatched
> 0)
683 event_queue
.RemoveRange (0, first_undispatched
);
687 // Apply high-level processing to the queue. Pair moves,
688 // coalesce events, etc.
689 // We assume that the caller holds the event_queue lock.
690 private static void AnalyzeQueue_Unlocked ()
692 int first_unanalyzed
= event_queue
.Count
;
693 while (first_unanalyzed
> 0) {
695 QueuedEvent qe
= event_queue
[first_unanalyzed
] as QueuedEvent
;
701 if (first_unanalyzed
== event_queue
.Count
)
704 // Walk across the unanalyzed events...
705 for (int i
= first_unanalyzed
; i
< event_queue
.Count
; ++i
) {
706 QueuedEvent qe
= event_queue
[i
] as QueuedEvent
;
708 // Pair off the MovedFrom and MovedTo events.
709 if (qe
.Cookie
!= 0) {
710 if ((qe
.Type
& EventType
.MovedFrom
) != 0) {
711 pending_move_cookies
[qe
.Cookie
] = qe
;
712 // This increases the MovedFrom's HoldUntil time,
713 // giving us more time for the matching MovedTo to
715 // (512 ms is totally arbitrary)
716 qe
.AddMilliseconds (512);
717 } else if ((qe
.Type
& EventType
.MovedTo
) != 0) {
718 QueuedEvent paired_move
= pending_move_cookies
[qe
.Cookie
] as QueuedEvent
;
719 if (paired_move
!= null) {
720 paired_move
.Dispatched
= true;
721 qe
.PairedMove
= paired_move
;
730 private static void DispatchWorker ()
733 QueuedEvent next_event
= null;
735 // Until we find something we want to dispatch, we will stay
736 // inside the following block of code.
740 CleanQueue_Unlocked ();
742 AnalyzeQueue_Unlocked ();
744 // Now look for an event to dispatch.
745 DateTime min_hold_until
= DateTime
.MaxValue
;
746 DateTime now
= DateTime
.Now
;
747 foreach (QueuedEvent qe
in event_queue
) {
750 if (qe
.HoldUntil
<= now
) {
754 if (qe
.HoldUntil
< min_hold_until
)
755 min_hold_until
= qe
.HoldUntil
;
758 // If we found an event, break out of this block
760 if (next_event
!= null)
763 // If we didn't find an event to dispatch, we can sleep
764 // (1) until the next hold-until time
765 // (2) until the lock pulses (which means something changed, so
766 // we need to check that we are still running, new events
767 // are on the queue, etc.)
768 // and then we go back up and try to find something to dispatch
770 if (min_hold_until
== DateTime
.MaxValue
)
771 Monitor
.Wait (event_queue
);
773 Monitor
.Wait (event_queue
, min_hold_until
- now
);
777 // If "running" gets set to false, we might get a null next_event as the above
779 if (next_event
== null)
782 // Now we have an event, so we release the event_queue lock and do
783 // the actual dispatch.
785 // Before we get any further, mark it
786 next_event
.Dispatched
= true;
789 watched
= Lookup (next_event
.Wd
, next_event
.Type
);
793 string srcpath
= null;
795 // If this event is a paired MoveTo, there is extra work to do.
796 if ((next_event
.Type
& EventType
.MovedTo
) != 0 && next_event
.PairedMove
!= null) {
797 WatchInfo paired_watched
;
798 paired_watched
= Lookup (next_event
.PairedMove
.Wd
, next_event
.PairedMove
.Type
);
800 if (paired_watched
!= null) {
801 // Set the source path accordingly.
802 srcpath
= Path
.Combine (paired_watched
.Path
, next_event
.PairedMove
.Filename
);
804 // Handle the internal rename of the directory.
805 if ((next_event
.Type
& EventType
.IsDirectory
) != 0)
806 HandleMove (srcpath
, watched
.Path
, next_event
.Filename
);
810 SendEvent (watched
, next_event
.Filename
, srcpath
, next_event
.Type
);
812 // If a directory we are watching gets ignored, we need
813 // to remove it from the watchedByFoo hashes.
814 if ((next_event
.Type
& EventType
.Ignored
) != 0) {
821 /////////////////////////////////////////////////////////////////////////////////
824 private static void Main (string [] args
)
826 Queue to_watch
= new Queue ();
827 bool recursive
= false;
829 foreach (string arg
in args
) {
830 if (arg
== "-r" || arg
== "--recursive")
833 // Our hashes work without a trailing path delimiter
834 string path
= arg
.TrimEnd ('/');
835 to_watch
.Enqueue (path
);
839 while (to_watch
.Count
> 0) {
840 string path
= (string) to_watch
.Dequeue ();
842 Console
.WriteLine ("Watching {0}", path
);
843 Inotify
.Subscribe (path
, null, Inotify
.EventType
.All
);
846 foreach (string subdir
in DirectoryWalker
.GetDirectories (path
))
847 to_watch
.Enqueue (subdir
);
852 Inotify
.Verbose
= true;
854 while (Inotify
.Enabled
&& Inotify
.WatchCount
> 0)
857 if (Inotify
.WatchCount
== 0)
858 Console
.WriteLine ("Nothing being watched.");
860 // Kill the event-reading thread so that we exit
865 #endif // ENABLE_INOTIFY