swerling-sinotify 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,205 @@
1
+ #include <ruby.h>
2
+
3
+ // includes that are different for ruby 1.8 vs. ruby 1.9
4
+ #ifdef RUBY_19
5
+ #warning "INCLUDING RUBY19 HEADERS (not really a warning, just for info)"
6
+ #include <ruby/io.h>
7
+ #else
8
+ #warning "INCLUDING RUBY18 HEADERS (not really a warning, just for info) "
9
+ #include <rubyio.h>
10
+
11
+ #endif
12
+
13
+ #ifdef HAVE_VERSION_H
14
+ #include <version.h>
15
+ #endif
16
+
17
+ #ifdef HAVE_LINUX_INOTIFY_H
18
+ #include <asm/unistd.h>
19
+ #include <linux/inotify.h>
20
+ #else
21
+ #include "inotify.h"
22
+ #include "inotify-syscalls.h"
23
+ #endif
24
+
25
+ #include <sys/syscall.h>
26
+ #include <unistd.h>
27
+
28
+ static inline int inotify_init (void)
29
+ {
30
+ return syscall (__NR_inotify_init);
31
+ }
32
+
33
+ static inline int inotify_add_watch (int fd, const char *name, __u32 mask)
34
+ {
35
+ return syscall (__NR_inotify_add_watch, fd, name, mask);
36
+ }
37
+
38
+ static inline int inotify_rm_watch (int fd, __u32 wd)
39
+ {
40
+ return syscall (__NR_inotify_rm_watch, fd, wd);
41
+ }
42
+
43
+ VALUE rb_cSinotify;
44
+ VALUE rb_cNotifier;
45
+ VALUE rb_cSinotifyEvent;
46
+
47
+ int event_check (int fd) {
48
+ struct timeval;
49
+ int r;
50
+
51
+ fd_set rfds;
52
+ FD_ZERO(&rfds);
53
+ FD_SET(fd, &rfds);
54
+
55
+ // Not using timout anymore
56
+ r = rb_thread_select (fd+1, &rfds, NULL, NULL, NULL);
57
+ return r;
58
+ }
59
+
60
+ static VALUE rb_inotify_event_new(struct inotify_event *event) {
61
+ VALUE retval;
62
+ retval = Data_Wrap_Struct(rb_cSinotifyEvent, NULL, free, event);
63
+ rb_obj_call_init(retval, 0, NULL);
64
+ return retval;
65
+ }
66
+
67
+ static VALUE rb_inotify_new(VALUE klass) {
68
+ int *fd;
69
+ VALUE retval;
70
+ fd = malloc(sizeof(int));
71
+ *fd = inotify_init();
72
+ if(*fd < 0) rb_sys_fail("inotify_init()");
73
+ retval = Data_Wrap_Struct(klass, NULL, free, fd);
74
+ rb_obj_call_init(retval, 0, NULL);
75
+ return retval;
76
+ }
77
+
78
+ static VALUE rb_inotify_add_watch(VALUE self, VALUE filename, VALUE mask) {
79
+ int *fd, wd;
80
+ Data_Get_Struct(self, int, fd);
81
+ wd = inotify_add_watch(*fd, RSTRING_PTR(filename), NUM2INT(mask));
82
+ if(wd < 0) {
83
+ rb_sys_fail(RSTRING_PTR(filename));
84
+ }
85
+ return INT2NUM(wd);
86
+ }
87
+
88
+ static VALUE rb_inotify_rm_watch(VALUE self, VALUE wdnum) {
89
+ int *fd;
90
+ Data_Get_Struct(self, int, fd);
91
+ if(inotify_rm_watch(*fd, NUM2INT(wdnum)) < 0) {
92
+ rb_sys_fail("removing watch");
93
+ }
94
+ return Qtrue;
95
+ }
96
+
97
+ static VALUE rb_inotify_each_event(VALUE self) {
98
+ int *fd, r;
99
+ struct inotify_event *event, *pevent;
100
+ char buffer[16384];
101
+ size_t buffer_n, event_size;
102
+
103
+ Data_Get_Struct(self, int, fd);
104
+ while(1) {
105
+ r = event_check(*fd);
106
+ if(r == 0) {
107
+ continue;
108
+ }
109
+ if((r = read(*fd, buffer, 16384)) < 0) {
110
+ rb_sys_fail("reading event");
111
+ }
112
+ buffer_n = 0;
113
+ while (buffer_n < r) {
114
+ pevent = (struct inotify_event *)&buffer[buffer_n];
115
+ event_size = sizeof(struct inotify_event) + pevent->len;
116
+ event = malloc(event_size);
117
+ memmove(event, pevent, event_size);
118
+ buffer_n += event_size;
119
+ rb_yield(rb_inotify_event_new(event));
120
+ }
121
+ }
122
+ return Qnil;
123
+ }
124
+
125
+ static VALUE rb_inotify_close(VALUE self) {
126
+ int *fd;
127
+ Data_Get_Struct(self, int, fd);
128
+ if(close(*fd) != 0) {
129
+ rb_sys_fail("closing inotify");
130
+ }
131
+ return Qnil;
132
+ }
133
+
134
+ static VALUE rb_inotify_event_name(VALUE self) {
135
+ struct inotify_event *event;
136
+ Data_Get_Struct(self, struct inotify_event, event);
137
+ if(event->len) {
138
+ return rb_str_new2(event->name);
139
+ } else {
140
+ return Qnil;
141
+ }
142
+ }
143
+
144
+ static VALUE rb_inotify_event_wd(VALUE self) {
145
+ struct inotify_event *event;
146
+ Data_Get_Struct(self, struct inotify_event, event);
147
+ return INT2NUM(event->wd);
148
+ }
149
+
150
+
151
+ static VALUE rb_inotify_event_mask(VALUE self) {
152
+ struct inotify_event *event;
153
+ Data_Get_Struct(self, struct inotify_event, event);
154
+ return LONG2NUM(event->mask);
155
+ }
156
+
157
+ void Init_sinotify () {
158
+ rb_cSinotify = rb_define_module("Sinotify");
159
+
160
+ //
161
+ // The Sinotify::PrimNotifier class
162
+ //
163
+ rb_cNotifier = rb_define_class_under(rb_cSinotify, "PrimNotifier", rb_cObject);
164
+
165
+ rb_define_singleton_method(rb_cNotifier, "new", rb_inotify_new, 0);
166
+ rb_define_method(rb_cNotifier, "add_watch", rb_inotify_add_watch, 2);
167
+ rb_define_method(rb_cNotifier, "rm_watch", rb_inotify_rm_watch, 1);
168
+ rb_define_method(rb_cNotifier, "each_event", rb_inotify_each_event, 0);
169
+ rb_define_method(rb_cNotifier, "close", rb_inotify_close, 0);
170
+
171
+ //
172
+ // The Sinotify::PrimEvent class
173
+ //
174
+ rb_cSinotifyEvent = rb_define_class_under(rb_cSinotify, "PrimEvent", rb_cObject);
175
+ rb_define_method(rb_cSinotifyEvent, "prim_name", rb_inotify_event_name, 0);
176
+ rb_define_method(rb_cSinotifyEvent, "prim_wd", rb_inotify_event_wd, 0);
177
+ rb_define_method(rb_cSinotifyEvent, "prim_mask", rb_inotify_event_mask, 0);
178
+
179
+ // following inotify masks taken from inotify lib, see 'man inotify', section
180
+ rb_const_set(rb_cSinotifyEvent, rb_intern("ACCESS"), INT2NUM(IN_ACCESS));
181
+ rb_const_set(rb_cSinotifyEvent, rb_intern("MODIFY"), INT2NUM(IN_MODIFY));
182
+ rb_const_set(rb_cSinotifyEvent, rb_intern("ATTRIB"), INT2NUM(IN_ATTRIB));
183
+ rb_const_set(rb_cSinotifyEvent, rb_intern("CLOSE_WRITE"), INT2NUM(IN_CLOSE_WRITE));
184
+ rb_const_set(rb_cSinotifyEvent, rb_intern("CLOSE_NOWRITE"), INT2NUM(IN_CLOSE_NOWRITE));
185
+ rb_const_set(rb_cSinotifyEvent, rb_intern("OPEN"), INT2NUM(IN_OPEN));
186
+ rb_const_set(rb_cSinotifyEvent, rb_intern("MOVED_FROM"), INT2NUM(IN_MOVED_FROM));
187
+ rb_const_set(rb_cSinotifyEvent, rb_intern("MOVED_TO"), INT2NUM(IN_MOVED_TO));
188
+ rb_const_set(rb_cSinotifyEvent, rb_intern("CREATE"), INT2NUM(IN_CREATE));
189
+ rb_const_set(rb_cSinotifyEvent, rb_intern("DELETE"), INT2NUM(IN_DELETE));
190
+ rb_const_set(rb_cSinotifyEvent, rb_intern("DELETE_SELF"), INT2NUM(IN_DELETE_SELF));
191
+ rb_const_set(rb_cSinotifyEvent, rb_intern("MOVE_SELF"), INT2NUM(IN_MOVE_SELF));
192
+ rb_const_set(rb_cSinotifyEvent, rb_intern("UNMOUNT"), INT2NUM(IN_UNMOUNT));
193
+ rb_const_set(rb_cSinotifyEvent, rb_intern("Q_OVERFLOW"), INT2NUM(IN_Q_OVERFLOW));
194
+ rb_const_set(rb_cSinotifyEvent, rb_intern("IGNORED"), INT2NUM(IN_IGNORED));
195
+ rb_const_set(rb_cSinotifyEvent, rb_intern("CLOSE"), INT2NUM(IN_CLOSE));
196
+ rb_const_set(rb_cSinotifyEvent, rb_intern("MOVE"), INT2NUM(IN_MOVE));
197
+ rb_const_set(rb_cSinotifyEvent, rb_intern("ONLYDIR"), INT2NUM(IN_ONLYDIR));
198
+ rb_const_set(rb_cSinotifyEvent, rb_intern("DONT_FOLLOW"), INT2NUM(IN_DONT_FOLLOW));
199
+ rb_const_set(rb_cSinotifyEvent, rb_intern("MASK_ADD"), INT2NUM(IN_MASK_ADD));
200
+ rb_const_set(rb_cSinotifyEvent, rb_intern("ISDIR"), INT2NUM(IN_ISDIR));
201
+ rb_const_set(rb_cSinotifyEvent, rb_intern("ONESHOT"), INT2NUM(IN_ONESHOT));
202
+ rb_const_set(rb_cSinotifyEvent, rb_intern("ALL_EVENTS"), INT2NUM(IN_ALL_EVENTS));
203
+
204
+
205
+ }
data/lib/sinotify.rb ADDED
@@ -0,0 +1,18 @@
1
+ ext_lib = File.join(File.dirname(__FILE__), '../ext/sinotify.so')
2
+ unless File.exist?(ext_lib)
3
+ raise "Could not find ext/sinotify.so. \n" \
4
+ + "Please build the sinotify.so extention first (cd [sinotify gem]/ext && ruby extconf.rb && make)"
5
+ end
6
+
7
+ # load base info and c lib
8
+ require ext_lib
9
+ require File.join(File.dirname(__FILE__), 'sinotify_info')
10
+
11
+ # load external dependencies
12
+ require 'rubygems'
13
+ require 'cosell'
14
+ require 'logger'
15
+
16
+ # load application
17
+ Sinotify.require_all_libs_relative_to(__FILE__)
18
+
@@ -0,0 +1,185 @@
1
+ module Sinotify
2
+
3
+ #
4
+ # Sinotify events are triggered as they come in to the Notifier ('announced'
5
+ # in the parlance of the Cosell announcement framework that sinotify uses).
6
+ # Each event has the 'path' of the file or dir that was effected, the
7
+ # timestamp of the event (generated in ruby, not at the primitive level), and
8
+ # whether the event was on a file or a directory. Also available is the event
9
+ # type, called the 'etype,' which can be :modify, :create, :delete, etc. The
10
+ # list of event types is below.
11
+ #
12
+ # A Sinotify::Event does not perfectly model a linux inotify event. See
13
+ # Sinotify::PrimEvent for that.
14
+ #
15
+ # This event class deviates from Sinotify::PrimEvent in one significant regard. Sinotify does not
16
+ # pass events about children of a given directory, it only passes events about the directory
17
+ # (or file) itself. That is _not_ to say you can't setup a recursive watch in the Notifier class,
18
+ # just that _the event itself_ only pertains the the inode/file/directory being altered, not to
19
+ # its children.
20
+ #
21
+ # This is perhaps best illustrated by an example. Let's say you
22
+ #
23
+ # 1. Create a directory called '/tmp/test'
24
+ # 2. Create a file in '/tmp/test' called '/tmp/test/blah'
25
+ # 3. You put a watch on the directory '/tmp/test'
26
+ # 4. You then do a 'rm -rf /tmp/test'
27
+ # (thus deleting both the file /tmp/test/blah AND the directory /tmp/test)
28
+ #
29
+ # In linux inotify, you would get two events in this scenario, _both_ on the
30
+ # watch for the /tmp/test directory. One of the events would be a ':delete' event
31
+ # (that is, the mask of the event would be equal to
32
+ # Sinotify::PrimEvent::DELETE, or the 'etype' of the PrimEvent would equal
33
+ # ':delete'), and the 'name' slot in the event would be 'blah.' This is your
34
+ # cue that the event _really_ happened on a child of the thing being watched
35
+ # ('/tmp/test'), not to the directory itself. Since you deleted both the file
36
+ # and the directory with your 'rm -rf' command, another event would come in
37
+ # of the etype :delete_self for the directory, and 'is_dir' would be in the
38
+ # mask (ie. the mask would be Sinotify::PrimEvent::DELETE & Sinotify::PrimEvent::IS_DIR).
39
+ #
40
+ # Sinotify events would be a bit different in the example above.
41
+ # You would still get 2 events, but both would be :delete events,
42
+ # one where the 'path' is '/tmp/test', and the other where the 'path'
43
+ # is '/tmp/test/blah'. In the case of the event for '/tmp/test', the call
44
+ # to 'directory?' would return true.
45
+ #
46
+ # If you want to work with an event notifier that works more like the low level linux inotify
47
+ # (receiving both :delete with name slot filled in and another event w/ :delete_self),
48
+ # you will have to work directly with PrimNotifier and PrimEvent (along with their irritating
49
+ # linux inotify-style blocking synchronous event loop)
50
+ #
51
+ # Here is the list of possible events adapted from the definitions
52
+ # in [linux_src]/include/linux/inotify.h:
53
+ #
54
+ # File related:
55
+ # :access # File was accessed
56
+ # :modify # file modified
57
+ # :attrib # meta data changed
58
+ # :close_write # writable file was closed
59
+ # :close_nowrite # unwritable file was closed
60
+ # :open # file was opened
61
+ # :moved_from # file moved from X
62
+ # :moved_to # file moved to Y
63
+ # :create # file created
64
+ # :delete # file deleted
65
+ # :delete_self # self was deleted
66
+ # :move_self # self was moved
67
+ #
68
+ # File related helpers:
69
+ #
70
+ # :close # (close_write | close_nowrite)
71
+ # :move # (moved_from | moved_to)
72
+ #
73
+ # Misc events
74
+ #
75
+ # :unmount # backing fs was unmounted
76
+ # :q_overflow # event queue overflowed
77
+ # :ignored # file was ignored
78
+ # :mask_add # added to mask of already existing event
79
+ # :isdir # event occurred against dir
80
+ # :oneshot # only send event once
81
+ #
82
+ class Event
83
+
84
+ attr_accessor :prim_event, :path, :timestamp, :is_dir
85
+
86
+ # a few attr declarations just so they show up in rdoc
87
+
88
+ # Given a prim_event, and the Watch associated with the event's watch descriptor,
89
+ # return a Sinotify::Event.
90
+ def self.from_prim_event_and_watch(prim_event, watch)
91
+ path = watch.path # path for the watch associated w/ this even
92
+ is_dir = watch.directory? # original watch was on a dir or a file?
93
+
94
+ # This gets a little odd. The prim_event's 'name' field
95
+ # will be nil if the change was to a directory itself, or if
96
+ # the watch was on a file to begin with. However,
97
+ # when a watch is on a dir, but the event occurs on a file in that dir
98
+ # inotify sets the 'name' field to the file. :isdir will be in the etypes
99
+ # if that file happens to be a subdir. Sinotify events do not
100
+ # play this game, only sending events for the thing that was altered
101
+ # in the first place. So right here is where we deduce if the
102
+ # event was _really_ on a file or a dir.
103
+ unless prim_event.name.nil?
104
+ path = File.join(path, prim_event.name)
105
+ is_dir = prim_event.etypes.include?(:isdir)
106
+ end
107
+
108
+ # is_dir must be passed along, since it may no longer exist (and thus cant be deduced later)
109
+ # inotify prim_events to not retain enough information to make it possible to deduce the
110
+ # original fullpath and whether it was a file or directory, so this info must be passed around.
111
+ return Sinotify::Event.new(:prim_event => prim_event,
112
+ :path => path,
113
+ :timestamp => Time.now, # any way to get this from prim event?
114
+ :is_dir => is_dir)
115
+ end
116
+
117
+ def initialize(args={})
118
+ args.each{|k,v| self.send("#{k}=",v)}
119
+ @timestamp ||= Time.now
120
+
121
+ # initialize a few variables just to shut up the ruby warnings
122
+ @etypes = nil
123
+ end
124
+
125
+ # The Sinotify::PrimEvent associated with this event (a straight
126
+ # wrapper around the linux inotify event)
127
+ def prim_event; @prim_event; end
128
+
129
+ # The full path of the file or directory on which the event happened
130
+ def path; @path; end
131
+
132
+ # when the event happened
133
+ def timestamp; @timestamp; end
134
+
135
+
136
+ def to_s; self.inspect_or_to_s(false); end
137
+ def inspect; self.inspect_or_to_s(true); end
138
+
139
+ # The etypes associated with this event (eg. :create, :modify, :delete, etc)
140
+ def etypes
141
+ # The etype/mask functions delegated to prim_event, EXCEPT: when :delete_self is in
142
+ # the list, and path is a directory, change it to 'delete'. If you want
143
+ # the etypes in the original prim_event, ask for event.prim_event.etypes
144
+ if @etypes.nil?
145
+ @etypes = self.prim_event.etypes
146
+
147
+ # change :delete_self into :delete
148
+ if self.directory? and @etypes.include?(:delete_self)
149
+ @etypes.delete(:delete_self)
150
+ @etypes << :delete
151
+ end
152
+
153
+ # add :close if :close_write or :close_nowrite are there, but :close is not
154
+ if @etypes.include?(:close_write) || @etypes.include?(:close_nowrite)
155
+ (@etypes << :close) unless @etypes.include?(:close)
156
+ end
157
+
158
+ end
159
+ @etypes
160
+ end
161
+
162
+ def directory?
163
+ self.is_dir.eql?(true)
164
+ end
165
+
166
+ def has_etype? etype
167
+ self.etypes.include?(etype)
168
+ end
169
+
170
+ def watch_descriptor
171
+ self.prim_event.watch_descriptor
172
+ end
173
+
174
+ protected
175
+
176
+ # :stopdoc:
177
+ def inspect_or_to_s(show_prim_event = false)
178
+ prim_event = (show_prim_event)? ", :prim_event => #{self.prim_event.inspect}" : ''
179
+ "<#{self.class} :path => '#{self.path}', dir? => #{self.directory?}, :etypes => #{self.etypes.inspect rescue 'could not determine'}#{prim_event}>"
180
+ end
181
+
182
+ # :startdoc:
183
+ end
184
+
185
+ end
@@ -0,0 +1,308 @@
1
+ module Sinotify
2
+
3
+ #
4
+ # Watch a directory or file for events like create, modify, delete, etc.
5
+ # (See Sinotify::Event for full list).
6
+ #
7
+ # See the synopsis section in the README.txt for example usage.
8
+ #
9
+ #
10
+ class Notifier
11
+ include Cosell
12
+
13
+ attr_accessor :file_or_dir_name, :etypes, :recurse, :recurse_throttle, :logger
14
+
15
+ # Required Args
16
+ #
17
+ # file_or_dir_name: the file/directory to watch
18
+ #
19
+ # Options:
20
+ # :recurse => (true|false)
21
+ # whether to automatically create watches on sub directories
22
+ # default: true if file_or_dir_name is a directory, else false
23
+ # raises if true and file_or_dir_name is not a directory
24
+ #
25
+ # :recurse_throttle =>
26
+ # When recursing, a background thread drills down into all the child directories
27
+ # creating notifiers on them. The recurse_throttle tells the notifier how far
28
+ # to recurse before sleeping for 0.1 seconds, so that drilling down does not hog
29
+ # the system on large directorie hierarchies.
30
+ # default is 10
31
+ #
32
+ # :etypes =>
33
+ # which inotify file system event types to listen for (eg :create, :delete, etc)
34
+ # See docs for Sinotify::Event for list of event types.
35
+ # default is :all_types
36
+ #
37
+ # :logger =>
38
+ # Where to log errors to. Default is Logger.new(STDOUT).
39
+ #
40
+ # :announcement_throttle =>
41
+ # How many events can be announced at a time before the queue goes back to sleep for a cycle.
42
+ # (ie. Cosell's 'announcements_per_cycle')
43
+ #
44
+ # :announcements_sleep_time =>
45
+ # How long the queue should sleep for before announcing the next batch of queued up
46
+ # Sinotify::Events (ie. Cosell's 'sleep_time')
47
+ #
48
+ def initialize(file_or_dir_name, opts = {})
49
+
50
+ initialize_cosell! # init the announcements framework
51
+
52
+ raise "Could not find #{file_or_dir_name}" unless File.exist?(file_or_dir_name)
53
+ self.file_or_dir_name = file_or_dir_name
54
+
55
+ # by default, recurse if directory?. If opts[:recurse] was true and passed in,
56
+ # make sure the watch is on a directory
57
+ self.recurse = opts[:recurse].nil?? self.on_directory? : opts[:recurse]
58
+ raise "Cannot recurse, #{file_or_dir_name} is not a directory" if self.recurse? && !self.on_directory?
59
+
60
+ # how many directories at a time to register.
61
+ self.recurse_throttle = opts[:recurse_throttle] || 10
62
+
63
+ self.etypes = Array(opts[:etypes] || :all_events)
64
+ validate_etypes!
65
+
66
+ self.prim_notifier = Sinotify::PrimNotifier.new
67
+
68
+ # setup async announcements queue (part of the Cosell mixin)
69
+ @logger = opts[:logger] || Logger.new(STDOUT)
70
+ sleep_time = opts[:announcements_sleep_time] || 0.05
71
+ announcement_throttle = opts[:announcement_throttle] || 50
72
+ self.queue_announcements!(:sleep_time => sleep_time,
73
+ :logger => opts[:logger],
74
+ :announcements_per_cycle => announcement_throttle)
75
+
76
+ self.closed = false
77
+
78
+ # initialize a few variables just to shut up the ruby warnings
79
+ # Apparently the lazy init idiom using ||= is no longer approved of. Shame that.
80
+ @spy_logger = nil
81
+ @spy_logger_level = nil
82
+ @watch_thread = nil
83
+ end
84
+
85
+ # whether this watch is on a directory
86
+ def on_directory?
87
+ File.directory?(self.file_or_dir_name)
88
+ end
89
+
90
+ # Start watching for inotify file system events.
91
+ def watch!
92
+ raise "Cannot reopen an inotifier. Create a new one instead" if self.closed?
93
+ self.add_all_directories_in_background
94
+ self.start_prim_event_loop_thread
95
+ return self
96
+ end
97
+
98
+ # Close this notifier. Notifiers cannot be reopened after close!.
99
+ def close!
100
+ @closed = true
101
+ self.remove_all_watches
102
+ end
103
+
104
+ # Log a message every time a prim_event comes in (will be logged even if it is considered 'noise'),
105
+ # and log a message whenever an event is announced. Overrides Cosell's spy! method (and uses cosell's
106
+ # spy! to log announced events).
107
+ #
108
+ # Options:
109
+ # :logger => The log to log to. Default is a logger on STDOUT
110
+ # :level => The log level to log with. Default is :info
111
+ #
112
+ def spy!(opts = {})
113
+ @spy_logger = opts[:logger] || Logger.new(STDOUT)
114
+ @spy_logger_level = opts[:level] || :info
115
+ opts[:on] = Sinotify::Event
116
+ opts[:preface_with] = "Sinotify::Notifier Event Spy"
117
+ super(opts)
118
+ end
119
+
120
+ # Return a list of files/directories currently being watched. Will only contain one entry unless
121
+ # this notifier was setup on a directory with the option :recurse => true.
122
+ def all_directories_being_watched
123
+ self.watches.values.collect{|w| w.path }.sort
124
+ end
125
+
126
+ def watches
127
+ @watches ||= {}
128
+ end
129
+
130
+ # Whether this notifier watches all the files in all of the subdirectories
131
+ # of the directory being watched.
132
+ def recurse?
133
+ self.recurse
134
+ end
135
+
136
+ def to_s
137
+ "Sinotify::Notifier[#{self.file_or_dir_name}, :watches => #{self.watches.size}]"
138
+ end
139
+
140
+ protected
141
+
142
+ #:stopdoc:
143
+
144
+ def validate_etypes!
145
+ bad = self.etypes.detect{|etype| PrimEvent.mask_from_etype(etype).nil? }
146
+ raise "Unrecognized etype '#{bad}'. Please see valid list in docs for Sinotify::Event" if bad
147
+ end
148
+
149
+ # some events we don't want to report (certain events are generated just from creating watches)
150
+ def event_is_noise? prim_event, watch
151
+
152
+ etypes_strings = prim_event.etypes.map{|e|e.to_s}.sort
153
+
154
+ # the simple act of creating a watch causes these to fire"
155
+ return true if ["close_nowrite", "isdir"].eql?(etypes_strings)
156
+ return true if ["isdir", "open"].eql?(etypes_strings)
157
+ return true if ["ignored"].eql?(etypes_strings)
158
+
159
+ # If the event is on a subdir of the directory specified in watch, don't send it because
160
+ # there should be another even (on the subdir itself) that comes through, and this one
161
+ # will be redundant.
162
+ return true if ["delete", "isdir"].eql?(etypes_strings)
163
+
164
+ return false
165
+ end
166
+
167
+ # Open up a background thread that adds all the watches on @file_or_dir_name and,
168
+ # if @recurse is true, all of its subdirs.
169
+ def add_all_directories_in_background
170
+ @child_dir_thread = Thread.new do
171
+ begin
172
+ self.add_watches!
173
+ rescue Exception => x
174
+ log "Exception: #{x}, trace: \n\t#{x.backtrace.join("\n\t")}", :error
175
+ end
176
+ end
177
+ end
178
+
179
+ def add_watches!(fn = self.file_or_dir_name, throttle = 0)
180
+
181
+ return if closed?
182
+ if throttle.eql?(self.recurse_throttle)
183
+ sleep 0.1
184
+ throttle = 0
185
+ end
186
+ throttle += 1
187
+
188
+ self.add_watch(fn)
189
+
190
+ if recurse?
191
+ Dir[File.join(fn, '/**')].each do |child_fn|
192
+ next if child_fn.eql?(fn)
193
+ self.add_watches!(child_fn, throttle) if File.directory?(child_fn)
194
+ end
195
+ end
196
+
197
+ end
198
+
199
+ def add_watch(fn)
200
+ watch_descriptor = self.prim_notifier.add_watch(fn, self.raw_mask)
201
+ # puts "ADDED WATCH: #{watch_descriptor} for #{fn}"
202
+ remove_watch(watch_descriptor) # remove the existing, if it exists
203
+ watch = Watch.new(:path => fn, :watch_descriptor => watch_descriptor)
204
+ self.watches[watch_descriptor.to_s] = watch
205
+ end
206
+
207
+ # Remove the watch associated with the watch_descriptor passed in
208
+ def remove_watch(watch_descriptor, prim_remove = false)
209
+ if watches[watch_descriptor.to_s]
210
+ # puts "REMOVING: #{watch_descriptor}"
211
+ self.watches.delete(watch_descriptor.to_s)
212
+
213
+ # the prim_notifier will complain if we remove a watch on a deleted file,
214
+ # since the watch will have automatically been removed already. Be default we
215
+ # don't care, but if caller is sure there are some prim watches to clean
216
+ # up, then they can pass 'true' for prim_remove. Another way to handle
217
+ # this would be to just default to true and fail silently, but trying this
218
+ # more conservative approach for now.
219
+ self.prim_notifier.rm_watch(watch_descriptor.to_i) if prim_remove
220
+ end
221
+ end
222
+
223
+ def remove_all_watches
224
+ self.watches.keys.each{|watch_descriptor| self.remove_watch(watch_descriptor, true) }
225
+ end
226
+
227
+ def log(msg, level = :debug)
228
+ puts(msg) unless [:debug, :info].include?(level)
229
+ self.logger.send(level, msg) if self.logger
230
+ end
231
+
232
+ def spy_on_event(prim_event)
233
+ if @spy_logger
234
+ msg = "Sinotify::Notifier Prim Event Spy: #{prim_event.inspect}"
235
+ @spy_logger.send(@spy_logger_level, msg)
236
+ end
237
+ end
238
+
239
+ # Listen for linux inotify events, and as they come in
240
+ # 1. adapt them into Sinotify::Event objects
241
+ # 2. 'announce' them using Cosell.
242
+ # By default, Cosell is setup to Queue the announcements in a bg thread.
243
+ #
244
+ # The references to two different logs in this method may be a bit confusing. The @spy_logger
245
+ # exclusively logs (spys on) events and announcements. The "log" method instead uses the @logger
246
+ # and logs errors and exceptions. The @logger is defined when creating this object (using the :logger
247
+ # option), and the @spy_logger is defined in the :spy! method.
248
+ #
249
+ def start_prim_event_loop_thread
250
+
251
+ raise "Already watching!" unless @watch_thread.nil?
252
+
253
+ @watch_thread = Thread.new do
254
+ begin
255
+ self.prim_notifier.each_event do |prim_event|
256
+ watch = self.watches[prim_event.watch_descriptor.to_s]
257
+ if event_is_noise?(prim_event, watch)
258
+ @spy_logger.debug("Sinotify::Notifier Spy: Skipping noise[#{prim_event.inspect}]") if @spy_logger
259
+ else
260
+ spy_on_event(prim_event)
261
+ if watch.nil?
262
+ self.log "Could not determine watch from descriptor #{prim_event.watch_descriptor}, something is wrong. Event: #{prim_event.inspect}", :warn
263
+ else
264
+ event = Sinotify::Event.from_prim_event_and_watch(prim_event, watch)
265
+ self.announce event
266
+ if event.has_etype?(:create) && event.directory?
267
+ Thread.new do
268
+ # have to thread this because the :create event comes in _before_ the directory exists,
269
+ # and inotify will not permit a watch on a file unless it exists
270
+ sleep 0.1
271
+ self.add_watch(event.path)
272
+ end
273
+ end
274
+ # puts "REMOVING: #{event.inspect}, WATCH: #{self.watches[event.watch_descriptor.to_s]}" if event.has_etype?(:delete) && event.directory?
275
+ self.remove_watch(event.watch_descriptor) if event.has_etype?(:delete) && event.directory?
276
+ break if closed?
277
+ end
278
+ end
279
+ end
280
+ rescue Exception => x
281
+ log "Exception: #{x}, trace: \n\t#{x.backtrace.join("\n\t")}", :error
282
+ end
283
+
284
+ log "Exiting prim event loop thread for #{self}"
285
+ end
286
+
287
+ end
288
+
289
+ def raw_mask
290
+ if @raw_mask.nil?
291
+ (self.etypes << :delete_self) if self.etypes.include?(:delete)
292
+ @raw_mask = self.etypes.inject(0){|raw, etype| raw | PrimEvent.mask_from_etype(etype) }
293
+ end
294
+ @raw_mask
295
+ end
296
+
297
+ # ruby gives warnings in verbose mode if you use attr_accessor to set these next few:
298
+ def prim_notifier; @prim_notifier; end
299
+ def prim_notifier= x; @prim_notifier = x; end
300
+ def watch_descriptor; @watch_descriptor; end
301
+ def watch_descriptor= x; @watch_descriptor = x; end
302
+ def closed?; @closed.eql?(true); end
303
+ def closed= x; @closed = x; end
304
+
305
+ #:startdoc:
306
+ end
307
+ end
308
+