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
;
39 namespace Beagle
.Util
{
41 public class Inotify
{
43 public delegate void InotifyCallback (Watch watch
, string path
, string subitem
, string srcpath
, EventType type
);
45 /////////////////////////////////////////////////////////////////////////////////////
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
{
86 /////////////////////////////////////////////////////////////////////////////////////
88 [StructLayout (LayoutKind
.Sequential
)]
89 private struct inotify_event
{
91 public EventType mask
;
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
,
111 /////////////////////////////////////////////////////////////////////////////////////
113 private class QueuedEvent
{
115 public EventType Type
;
116 public string Filename
;
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 ();
156 log
= Logger
.Get ("Inotify");
158 if (Environment
.GetEnvironmentVariable ("BEAGLE_DISABLE_INOTIFY") != null) {
159 Logger
.Log
.Debug ("BEAGLE_DISABLE_INOTIFY is set");
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
{
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
{
194 set { mask = value; }
197 public WatchInternal (InotifyCallback callback
, EventType mask
, WatchInfo watchinfo
)
199 this.callback
= callback
;
201 this.watchinfo
= watchinfo
;
202 this.is_subscribed
= true;
205 public void Unsubscribe ()
207 if (!this.is_subscribed
)
210 Inotify
.Unsubscribe (watchinfo
, this);
211 this.is_subscribed
= false;
214 public void ChangeSubscription (EventType mask
)
216 if (! this.is_subscribed
)
220 CreateOrModifyWatch (this.watchinfo
);
225 private class WatchInfo
{
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
;
250 public PendingMove (WatchInfo watched
, string srcname
, DateTime time
, uint 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
) {
275 if (last_watched
!= null && last_watched
.Wd
== wd
)
276 watched
= last_watched
;
278 watched
= watched_by_wd
[wd
] as WatchInfo
;
280 last_watched
= watched
;
283 if (watched
!= null && (watched
.FilterMask
& event_type
) != 0) {
284 watched
.FilterSeen
|= event_type
;
292 // The caller has to handle all locking itself
293 static private void Forget (WatchInfo watched
)
295 if (last_watched
== watched
)
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
)
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
))
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 ();
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
;
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
)
357 path
= Path
.GetFullPath (path
);
359 lock (watched_by_wd
) {
361 watched
= watched_by_path
[path
] as WatchInfo
;
363 seen
= watched
.FilterSeen
;
364 watched
.FilterMask
= mask
;
365 watched
.FilterSeen
= 0;
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) {
378 CreateOrModifyWatch (watched
);
382 int retval
= inotify_glue_ignore (inotify_fd
, watched
.Wd
);
384 string msg
= String
.Format ("Attempt to ignore {0} failed!", watched
.Path
);
385 throw new IOException (msg
);
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
)
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)
408 wd
= inotify_glue_watch (inotify_fd
, watched
.Path
, new_mask
);
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
);
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 ()
433 Logger
.Log
.Debug("Starting Inotify threads");
436 if (snarf_thread
!= null)
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 ()
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
));
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.
475 // Will block while waiting for events, but with a 1s timeout.
476 inotify_snarf_events (inotify_fd
,
487 ArrayList new_events
= new ArrayList ();
489 bool saw_overflow
= false;
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)
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
);
511 while (n_chars
< filename_bytes
.Length
&& filename_bytes
[n_chars
] != 0)
515 qe
.Filename
= filename_encoding
.GetString (filename_bytes
, 0, n_chars
);
518 nr
-= event_size
+ (int) raw_event
.len
;
522 Logger
.Log
.Warn ("Inotify queue overflow!");
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
);
540 watched_by_path
[watch
.Path
] = watch
;
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!
552 Console
.WriteLine ("Lookup failed for {0}", srcpath
);
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
);
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)
581 bool isDirectory
= false;
582 if ((mask
& EventType
.IsDirectory
) != 0)
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)
596 foreach (WatchInternal watch
in watched
.Subscribers
)
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
;
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) {
641 QueuedEvent qe
= event_queue
[first_unanalyzed
] as QueuedEvent
;
647 if (first_unanalyzed
== event_queue
.Count
)
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
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
;
676 static void DispatchWorker ()
679 QueuedEvent next_event
= null;
681 // Until we find something we want to dispatch, we will stay
682 // inside the following block of code.
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
) {
696 if (qe
.HoldUntil
<= now
) {
700 if (qe
.HoldUntil
< min_hold_until
)
701 min_hold_until
= qe
.HoldUntil
;
704 // If we found an event, break out of this block
706 if (next_event
!= null)
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
716 if (min_hold_until
== DateTime
.MaxValue
)
717 Monitor
.Wait (event_queue
);
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
725 if (next_event
== null)
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;
735 watched
= Lookup (next_event
.Wd
, next_event
.Type
);
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) {
769 /////////////////////////////////////////////////////////////////////////////////
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")
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
);
794 foreach (string subdir
in DirectoryWalker
.GetDirectories (path
))
795 to_watch
.Enqueue (subdir
);
800 Inotify
.Verbose
= true;
802 while (Inotify
.Enabled
&& Inotify
.WatchCount
> 0)
805 if (Inotify
.WatchCount
== 0)
806 Console
.WriteLine ("Nothing being watched.");
808 // Kill the event-reading thread so that we exit