sinotify 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,334 @@
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 [:create, :modify, :delete]
36
+ # Use :all_events to trace everything (although this may be more than you bargained for).
37
+ #
38
+ # :logger =>
39
+ # Where to log errors to. Default is Logger.new(STDOUT).
40
+ #
41
+ # :announcement_throttle =>
42
+ # How many events can be announced at a time before the queue goes back to sleep for a cycle.
43
+ # (ie. Cosell's 'announcements_per_cycle')
44
+ #
45
+ # :announcements_sleep_time =>
46
+ # How long the queue should sleep for before announcing the next batch of queued up
47
+ # Sinotify::Events (ie. Cosell's 'sleep_time')
48
+ #
49
+ def initialize(file_or_dir_name, opts = {})
50
+
51
+ initialize_cosell! # init the announcements framework
52
+
53
+ raise "Could not find #{file_or_dir_name}" unless File.exist?(file_or_dir_name)
54
+ self.file_or_dir_name = file_or_dir_name
55
+
56
+ # by default, recurse if directory?. If opts[:recurse] was true and passed in,
57
+ # make sure the watch is on a directory
58
+ self.recurse = opts[:recurse].nil?? self.on_directory? : opts[:recurse]
59
+ raise "Cannot recurse, #{file_or_dir_name} is not a directory" if self.recurse? && !self.on_directory?
60
+
61
+ # how many directories at a time to register.
62
+ self.recurse_throttle = opts[:recurse_throttle] || 10
63
+
64
+ self.etypes = Array( opts[:etypes] || [:create, :modify, :delete] )
65
+ validate_etypes!
66
+
67
+ self.prim_notifier = Sinotify::PrimNotifier.new
68
+
69
+ # setup async announcements queue (part of the Cosell mixin)
70
+ @logger = opts[:logger] || Logger.new(STDOUT)
71
+ sleep_time = opts[:announcements_sleep_time] || 0.05
72
+ announcement_throttle = opts[:announcement_throttle] || 50
73
+ self.queue_announcements!(:sleep_time => sleep_time,
74
+ :logger => @logger,
75
+ :announcements_per_cycle => announcement_throttle)
76
+
77
+ self.closed = false
78
+
79
+ # initialize a few variables just to shut up the ruby warnings
80
+ # Apparently the lazy init idiom using ||= is no longer approved of. Shame that.
81
+ @spy_logger = nil
82
+ @spy_logger_level = nil
83
+ @watch_thread = nil
84
+ end
85
+
86
+ # Sugar.
87
+ #
88
+ # Equivalent of calling cosell's
89
+ #
90
+ # self.when_announcing(Sinotify::Event) do |event|
91
+ # do_something_with_event(event)
92
+ # end
93
+ #
94
+ # becomes
95
+ #
96
+ # self.on_event { |event| do_something_with_event(event) }
97
+ #
98
+ # Since this class only announces one kind of event, it made sense to
99
+ # provide a more terse version of that statement.
100
+ def on_event &block
101
+ self.when_announcing(Sinotify::Event, &block)
102
+ end
103
+
104
+ # whether this watch is on a directory
105
+ def on_directory?
106
+ File.directory?(self.file_or_dir_name)
107
+ end
108
+
109
+ # Start watching for inotify file system events.
110
+ def watch!
111
+ raise "Cannot reopen an inotifier. Create a new one instead" if self.closed?
112
+ self.add_all_directories_in_background
113
+ self.start_prim_event_loop_thread
114
+ return self
115
+ end
116
+
117
+ # Close this notifier. Notifiers cannot be reopened after close!.
118
+ def close!
119
+ @closed = true
120
+ self.remove_all_watches
121
+ self.kill_queue! # cosell
122
+ end
123
+
124
+ # Log a message every time a prim_event comes in (will be logged even if it is considered 'noise'),
125
+ # and log a message whenever an event is announced. Overrides Cosell's spy! method (and uses cosell's
126
+ # spy! to log announced events).
127
+ #
128
+ # Options:
129
+ # :logger => The log to log to. Default is a logger on STDOUT
130
+ # :level => The log level to log with. Default is :info
131
+ # :spy_on_prim_events => Spy on PrimEvents (raw inotify events) too
132
+ #
133
+ def spy!(opts = {})
134
+ self.spy_on_prim_events = opts[:spy_on_prim_events].eql?(true)
135
+ self.spy_logger = opts[:logger] || Logger.new(STDOUT)
136
+ self.spy_logger_level = opts[:level] || :info
137
+ opts[:on] = Sinotify::Event
138
+ opts[:preface_with] = "Sinotify::Notifier Event Spy"
139
+ super(opts)
140
+ end
141
+
142
+ # Return a list of files/directories currently being watched. Will only contain one entry unless
143
+ # this notifier was setup on a directory with the option :recurse => true.
144
+ def all_directories_being_watched
145
+ self.watches.values.collect{|w| w.path }.sort
146
+ end
147
+
148
+ def watches
149
+ @watches ||= {}
150
+ end
151
+
152
+ # Whether this notifier watches all the files in all of the subdirectories
153
+ # of the directory being watched.
154
+ def recurse?
155
+ self.recurse
156
+ end
157
+
158
+ def to_s
159
+ "Sinotify::Notifier[#{self.file_or_dir_name}, :watches => #{self.watches.size}]"
160
+ end
161
+
162
+ protected
163
+
164
+ #:stopdoc:
165
+
166
+ attr_accessor :spy_on_prim_events, :spy_logger, :spy_logger_level
167
+
168
+ def validate_etypes!
169
+ bad = self.etypes.detect{|etype| PrimEvent.mask_from_etype(etype).nil? }
170
+ raise "Unrecognized etype '#{bad}'. Please see valid list in docs for Sinotify::Event" if bad
171
+ end
172
+
173
+ # some events we don't want to report (certain events are generated just from creating watches)
174
+ def event_is_noise? prim_event, watch
175
+
176
+ etypes_strings = prim_event.etypes.map{|e|e.to_s}.sort
177
+
178
+ # the simple act of creating a watch causes these to fire"
179
+ return true if ["close_nowrite", "isdir"].eql?(etypes_strings)
180
+ return true if ["isdir", "open"].eql?(etypes_strings)
181
+ return true if ["ignored"].eql?(etypes_strings)
182
+
183
+ # If the event is on a subdir of the directory specified in watch, don't send it because
184
+ # there should be another even (on the subdir itself) that comes through, and this one
185
+ # will be redundant.
186
+ return true if ["delete", "isdir"].eql?(etypes_strings)
187
+
188
+ return false
189
+ end
190
+
191
+ # Open up a background thread that adds all the watches on @file_or_dir_name and,
192
+ # if @recurse is true, all of its subdirs.
193
+ def add_all_directories_in_background
194
+ @child_dir_thread = Thread.new do
195
+ begin
196
+ self.add_watches!
197
+ rescue Exception => x
198
+ log "Exception: #{x}, trace: \n\t#{x.backtrace.join("\n\t")}", :error
199
+ end
200
+ end
201
+ end
202
+
203
+ def add_watches!(fn = self.file_or_dir_name, throttle = 0)
204
+
205
+ return if closed?
206
+ if throttle.eql?(self.recurse_throttle)
207
+ sleep 0.1
208
+ throttle = 0
209
+ end
210
+ throttle += 1
211
+
212
+ self.add_watch(fn)
213
+
214
+ if recurse?
215
+ Dir[File.join(fn, '/**')].each do |child_fn|
216
+ next if child_fn.eql?(fn)
217
+ self.add_watches!(child_fn, throttle) if File.directory?(child_fn)
218
+ end
219
+ end
220
+
221
+ end
222
+
223
+ def add_watch(fn)
224
+ watch_descriptor = self.prim_notifier.add_watch(fn, self.raw_mask)
225
+ # puts "ADDED WATCH: #{watch_descriptor} for #{fn}"
226
+ remove_watch(watch_descriptor) # remove the existing, if it exists
227
+ watch = Watch.new(:path => fn, :watch_descriptor => watch_descriptor)
228
+ self.watches[watch_descriptor.to_s] = watch
229
+ end
230
+
231
+ # Remove the watch associated with the watch_descriptor passed in
232
+ def remove_watch(watch_descriptor, prim_remove = false)
233
+ if watches[watch_descriptor.to_s]
234
+ #logger.debug "REMOVING WATCH: #{watch_descriptor}"
235
+ self.watches.delete(watch_descriptor.to_s)
236
+
237
+ # the prim_notifier will complain if we remove a watch on a deleted file,
238
+ # since the watch will have automatically been removed already. Be default we
239
+ # don't care, but if caller is sure there are some prim watches to clean
240
+ # up, then they can pass 'true' for prim_remove. Another way to handle
241
+ # this would be to just default to true and fail silently, but trying this
242
+ # more conservative approach for now.
243
+ self.prim_notifier.rm_watch(watch_descriptor.to_i) if prim_remove
244
+ end
245
+ end
246
+
247
+ def remove_all_watches
248
+ logger.debug "REMOVING ALL WATHCES"
249
+ self.watches.keys.each{|watch_descriptor| self.remove_watch(watch_descriptor, true) }
250
+ @watches = nil
251
+ end
252
+
253
+ def log(msg, level = :debug)
254
+ puts(msg) unless [:debug, :info].include?(level)
255
+ self.logger.send(level, msg) if self.logger
256
+ end
257
+
258
+ # Listen for linux inotify events, and as they come in
259
+ # 1. adapt them into Sinotify::Event objects
260
+ # 2. 'announce' them using Cosell.
261
+ # By default, Cosell is setup to Queue the announcements in a bg thread.
262
+ #
263
+ # The references to two different logs in this method may be a bit confusing. The @spy_logger
264
+ # exclusively logs (spys on) events and announcements. The "log" method instead uses the @logger
265
+ # and logs errors and exceptions. The @logger is defined when creating this object (using the :logger
266
+ # option), and the @spy_logger is defined in the :spy! method.
267
+ #
268
+ def start_prim_event_loop_thread
269
+
270
+ raise "Already watching!" unless @watch_thread.nil?
271
+
272
+ @watch_thread = Thread.new do
273
+ begin
274
+ self.prim_notifier.each_event do |prim_event|
275
+ watch = self.watches[prim_event.watch_descriptor.to_s]
276
+ if event_is_noise?(prim_event, watch)
277
+ self.spy_logger.debug("Sinotify::Notifier Spy: Skipping noise[#{prim_event.inspect}]") if self.spy_on_prim_events
278
+ else
279
+ spy_on_prim_event(prim_event)
280
+ if watch.nil?
281
+ self.log "Could not determine watch from descriptor #{prim_event.watch_descriptor}, something is wrong. Event: #{prim_event.inspect}", :warn
282
+ else
283
+ event = Sinotify::Event.from_prim_event_and_watch(prim_event, watch)
284
+ self.announce event
285
+ if event.has_etype?(:create) && event.directory?
286
+ Thread.new do
287
+ # have to thread this because the :create event comes in _before_ the directory exists,
288
+ # and inotify will not permit a watch on a file unless it exists
289
+ sleep 0.1
290
+ self.add_watch(event.path)
291
+ end
292
+ end
293
+ # puts "REMOVING: #{event.inspect}, WATCH: #{self.watches[event.watch_descriptor.to_s]}" if event.has_etype?(:delete) && event.directory?
294
+ self.remove_watch(event.watch_descriptor) if event.has_etype?(:delete) && event.directory?
295
+ break if closed?
296
+ end
297
+ end
298
+ end
299
+ rescue Exception => x
300
+ log "Exception: #{x}, trace: \n\t#{x.backtrace.join("\n\t")}", :error
301
+ end
302
+
303
+ log "Exiting prim event loop thread for #{self}"
304
+ end
305
+
306
+ end
307
+
308
+ def raw_mask
309
+ if @raw_mask.nil?
310
+ (self.etypes << :delete_self) if self.etypes.include?(:delete)
311
+ @raw_mask = self.etypes.inject(0){|raw, etype| raw | PrimEvent.mask_from_etype(etype) }
312
+ end
313
+ @raw_mask
314
+ end
315
+
316
+ def spy_on_prim_event(prim_event)
317
+ if self.spy_on_prim_events
318
+ msg = "Sinotify::Notifier Prim Event Spy: #{prim_event.inspect}"
319
+ self.spy_logger.send(@spy_logger_level, msg)
320
+ end
321
+ end
322
+
323
+ # ruby gives warnings in verbose mode if you use attr_accessor to set these next few:
324
+ def prim_notifier; @prim_notifier; end
325
+ def prim_notifier= x; @prim_notifier = x; end
326
+ def watch_descriptor; @watch_descriptor; end
327
+ def watch_descriptor= x; @watch_descriptor = x; end
328
+ def closed?; @closed.eql?(true); end
329
+ def closed= x; @closed = x; end
330
+
331
+ #:startdoc:
332
+ end
333
+ end
334
+
@@ -0,0 +1,114 @@
1
+ module Sinotify
2
+
3
+ #
4
+ # Sinotify::PrimEvent is a ruby wrapper for an inotify event
5
+ # Use the Sinotify::PrimNotifier to register to listen for these Events.
6
+ #
7
+ # Most users of Sinotify will not want to listen for prim events, instead opting
8
+ # to use a Sinotify::Notifier to listen for Sinotify::Events. See docs for those classes.
9
+ #
10
+ # Methods :name, :mask, and :wd defined in sinotify.c
11
+ #
12
+ # For convenience, inotify masks are represented in the PrimEvent as an 'etype',
13
+ # which is just a ruby symbol corresponding to the mask. For instance, a mask
14
+ # represented as Sinotify::MODIFY has an etype of :modify. You can still get
15
+ # the mask if you want the 'raw' int mask value. In other words:
16
+ # <pre>
17
+ # $ irb
18
+ # >> require 'sinotify'
19
+ # => true
20
+ # >> Sinotify::MODIFY
21
+ # => 2
22
+ # >> Sinotify::Event.etype_from_mask(Sinotify::MODIFY)
23
+ # => :modify
24
+ # >> Sinotify::Event.mask_from_etype(:modify)
25
+ # => 2
26
+ # </pre>
27
+ #
28
+ # See docs for Sinotify::Event class for full list of supported event symbol types and
29
+ # their symbols.
30
+ #
31
+ class PrimEvent
32
+
33
+ # map the constants defined in the 'c' lib to ruby symbols
34
+ @@mask_to_etype_map = {
35
+ Sinotify::PrimEvent::CREATE => :create,
36
+ Sinotify::PrimEvent::MOVE => :move,
37
+ Sinotify::PrimEvent::ACCESS => :access,
38
+ Sinotify::PrimEvent::MODIFY => :modify,
39
+ Sinotify::PrimEvent::ATTRIB => :attrib,
40
+ Sinotify::PrimEvent::CLOSE_WRITE => :close_write,
41
+ Sinotify::PrimEvent::CLOSE_NOWRITE => :close_nowrite,
42
+ Sinotify::PrimEvent::OPEN => :open,
43
+ Sinotify::PrimEvent::MOVED_FROM => :moved_from,
44
+ Sinotify::PrimEvent::MOVED_TO => :moved_to,
45
+ Sinotify::PrimEvent::DELETE => :delete,
46
+ Sinotify::PrimEvent::DELETE_SELF => :delete_self,
47
+ Sinotify::PrimEvent::MOVE_SELF => :move_self,
48
+ Sinotify::PrimEvent::UNMOUNT => :unmount,
49
+ Sinotify::PrimEvent::Q_OVERFLOW => :q_overflow,
50
+ Sinotify::PrimEvent::IGNORED => :ignored,
51
+ Sinotify::PrimEvent::CLOSE => :close,
52
+ Sinotify::PrimEvent::MASK_ADD => :mask_add,
53
+ Sinotify::PrimEvent::ISDIR => :isdir,
54
+ Sinotify::PrimEvent::ONLYDIR => :onlydir,
55
+ Sinotify::PrimEvent::DONT_FOLLOW => :dont_follow,
56
+ Sinotify::PrimEvent::ONESHOT => :oneshot,
57
+ Sinotify::PrimEvent::ALL_EVENTS => :all_events,
58
+ }
59
+
60
+ @@etype_to_mask_map = {}
61
+ @@mask_to_etype_map.each{|k,v| @@etype_to_mask_map[v] = k}
62
+
63
+ def self.etype_from_mask(mask)
64
+ @@mask_to_etype_map[mask]
65
+ end
66
+
67
+ def self.mask_from_etype(etype)
68
+ @@etype_to_mask_map[etype]
69
+ end
70
+
71
+ def self.all_etypes
72
+ @@mask_to_etype_map.values.sort{|e1,e2| e1.to_s <=> e2.to_s}
73
+ end
74
+
75
+ def name
76
+ @name ||= self.prim_name
77
+ end
78
+
79
+ def wd
80
+ @wd ||= self.prim_wd
81
+ end
82
+
83
+ def mask
84
+ @mask ||= self.prim_mask
85
+ end
86
+
87
+ # When first creating a watch, inotify sends a bunch of events that have masks
88
+ # don't seem to match up w/ any of the masks defined in inotify.h. Pass on those.
89
+ def recognized?
90
+ return !self.etypes.empty?
91
+ end
92
+
93
+ # Return whether this event has etype specified
94
+ def has_etype?(etype)
95
+ mask_for_etype = self.class.mask_from_etype(etype)
96
+ return (self.mask & mask_for_etype).eql?(mask_for_etype)
97
+ end
98
+
99
+ def etypes
100
+ @etypes ||= self.class.all_etypes.select{|et| self.has_etype?(et) }
101
+ end
102
+
103
+ def watch_descriptor
104
+ self.wd
105
+ end
106
+
107
+ def inspect
108
+ "<#{self.class} :name => '#{self.name}', :etypes => #{self.etypes.inspect}, :mask => #{self.mask.to_s(16)}, :watch_descriptor => #{self.watch_descriptor}>"
109
+ end
110
+
111
+ end
112
+
113
+ end
114
+
@@ -0,0 +1,21 @@
1
+ module Sinotify
2
+ #
3
+ # Just a little struct to describe a single inotifier watch
4
+ # Note that the is_dir needs to be saved because we won't be
5
+ # able to deduce that later if it was a deleted object.
6
+ #
7
+ class Watch
8
+ attr_accessor :is_dir, :path, :watch_descriptor
9
+ def initialize(args={})
10
+ args.each{|k,v| self.send("#{k}=",v)}
11
+ @timestamp ||= Time.now
12
+ @is_dir = File.directory?(path)
13
+ end
14
+ def directory?
15
+ self.is_dir.eql?(true)
16
+ end
17
+ def to_s
18
+ "Sinotify::Watch[:is_dir => #{is_dir}, :path => #{path}, :watch_descriptor => #{watch_descriptor}]"
19
+ end
20
+ end
21
+ end