directory_watcher 0.1.0

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.
Files changed (2) hide show
  1. data/lib/directory_watcher.rb +510 -0
  2. metadata +54 -0
@@ -0,0 +1,510 @@
1
+ #
2
+ # = directory_watcher.rb
3
+ #
4
+ # See DirectoryWatcher for detailed documentation and usage.
5
+ #
6
+
7
+ require 'observer'
8
+
9
+ #
10
+ # == Synopsis
11
+ #
12
+ # A class for watching files within a directory and generating events when
13
+ # those files change.
14
+ #
15
+ # == Details
16
+ #
17
+ # A directory watcher is an +Observable+ object that sends events to
18
+ # registered observers when file changes are detected within the directory
19
+ # being watched.
20
+ #
21
+ # The directory watcher operates by scanning the directory at some interval
22
+ # and creating a list of the files it finds. File events are detected by
23
+ # comparing the current file list with the file list from the previous scan
24
+ # interval. Three types of events are supported -- *added*, *modified*, and
25
+ # *removed*.
26
+ #
27
+ # An added event is generated when the file appears in the current file
28
+ # list but not in the previous scan interval file list. A removed event is
29
+ # generated when the file appears in the previous scan interval file list
30
+ # but not in the current file list. A modified event is generated when the
31
+ # file appears in the current and the previous interval file list, but the
32
+ # file modification time or the file size differs between the two lists.
33
+ #
34
+ # The file events are collected into an array, and all registered observers
35
+ # receive all file events for each scan interval. It is up to the individual
36
+ # observers to filter the events they are interested in.
37
+ #
38
+ # === File Selection
39
+ #
40
+ # The directory watcher uses glob patterns to select the files to scan. The
41
+ # default glob pattern will select all regular files in the directory of
42
+ # interest '*'.
43
+ #
44
+ # Here are a few useful glob examples:
45
+ #
46
+ # '*' => all files in the current directory
47
+ # '**/*' => all files in all subdirectories
48
+ # '**/*.rb' => all ruby files
49
+ # 'ext/**/*.{h,c}' => all C source code files
50
+ #
51
+ # *Note*: file events will never be generated for directories. Only regular
52
+ # files are included in the file scan.
53
+ #
54
+ # === Stable Files
55
+ #
56
+ # A fourth file event is supported but not enabled by default -- the
57
+ # *stable* event. This event is generated after a file has been added or
58
+ # modified and then remains unchanged for a certain number of scan
59
+ # intervals.
60
+ #
61
+ # To enable the generation of this event the +stable+ count must be
62
+ # configured. This is the number of scan intervals a file must remain
63
+ # unchanged (based modification time and file size) before it is considered
64
+ # stable.
65
+ #
66
+ # To disable this event the +stable+ count should be set to +nil+.
67
+ #
68
+ # == Usage
69
+ #
70
+ # Learn by Doing -- here are a few different ways to configure and use a
71
+ # directory watcher.
72
+ #
73
+ # === Basic
74
+ #
75
+ # This basic recipe will watch all files in the current directory and
76
+ # generate the three default events. We'll register an observer that simply
77
+ # prints the events to standard out.
78
+ #
79
+ # require 'directory_watcher'
80
+ #
81
+ # dw = DirectoryWatcher.new '.'
82
+ # dw.add_observer {|*args| args.each {|event| puts event}}
83
+ #
84
+ # dw.start
85
+ # gets # when the user hits "enter" the script will terminate
86
+ # dw.stop
87
+ #
88
+ # === Suppress Initial "added" Events
89
+ #
90
+ # This little twist will suppress the initial "added" events that are
91
+ # generated the first time the directory is scanned. This is done by
92
+ # pre-loading the watcher with files -- i.e. telling the watcher to scan for
93
+ # files before actually starting the scan loop.
94
+ #
95
+ # require 'directory_watcher'
96
+ #
97
+ # dw = DirectoryWatcher.new '.', :pre_load => true
98
+ # dw.glob = '**/*.rb'
99
+ # dw.add_observer {|*args| args.each {|event| puts event}}
100
+ #
101
+ # dw.start
102
+ # gets # when the user hits "enter" the script will terminate
103
+ # dw.stop
104
+ #
105
+ # There is one catch with this recipe. The glob pattern must be specified
106
+ # before the pre-load takes place. The glob pattern can be given as an
107
+ # option to the constructor:
108
+ #
109
+ # dw = DirectoryWatcher.new '.', :glob => '**/*.rb', :pre_load => true
110
+ #
111
+ # The other option is to use the reset method:
112
+ #
113
+ # dw = DirectoryWatcher.new '.'
114
+ # dw.glob = '**/*.rb'
115
+ # dw.reset true # the +true+ flag causes the watcher to pre-load
116
+ # # the files
117
+ #
118
+ # === Generate "stable" Events
119
+ #
120
+ # In order to generate stable events, the stable count must be specified. In
121
+ # this example the interval is set to 5.0 seconds and the stable count is
122
+ # set to 2. Stable events will only be generated for files after they have
123
+ # remain unchanged for 10 seconds (5.0 * 2).
124
+ #
125
+ # require 'directory_watcher'
126
+ #
127
+ # dw = DirectoryWatcher.new '.', :glob => '**/*.rb'
128
+ # dw.interval = 5.0
129
+ # dw.stable = 2
130
+ # dw.add_observer {|*args| args.each {|event| puts event}}
131
+ #
132
+ # dw.start
133
+ # gets # when the user hits "enter" the script will terminate
134
+ # dw.stop
135
+ #
136
+ # == Contact
137
+ #
138
+ # A lot of discussion happens about Ruby in general on the ruby-talk
139
+ # mailing list (http://www.ruby-lang.org/en/ml.html), and you can ask
140
+ # any questions you might have there. I monitor the list, as do many
141
+ # other helpful Rubyists, and you're sure to get a quick answer. Of
142
+ # course, you're also welcome to email me (Tim Pease) directly at the
143
+ # at tim.pease@gmail.com, and I'll do my best to help you out.
144
+ #
145
+ # (the above paragraph was blatantly stolen from Nathaniel Talbott's
146
+ # Test::Unit documentation)
147
+ #
148
+ # == Author
149
+ #
150
+ # Tim Pease
151
+ #
152
+ class DirectoryWatcher
153
+ include Observable
154
+
155
+ #
156
+ # An +Event+ structure contains the _type_ of the event and the file _path_
157
+ # to which the event pertains. The type can be one of the following:
158
+ #
159
+ # :added => file has been added to the directory
160
+ # :modified => file has been modified (either mtime or size or both
161
+ # have changed)
162
+ # :removed => file has been removed from the directory
163
+ # :stable => file has stabilized since being added or modified
164
+ #
165
+ Event = Struct.new :type, :path
166
+
167
+ FileInfo = Struct.new :mtime, :size, :stable # :nodoc:
168
+
169
+ #
170
+ # call-seq:
171
+ # DirectoryWatcher.new( directory, options )
172
+ #
173
+ # Create a new +DirectoryWatcher+ that will generate events when file
174
+ # changes are detected in the given _directory_. If the _directory_ does
175
+ # not exist, it will be created. The following options can be passed to
176
+ # this method:
177
+ #
178
+ # :glob => '*' file glob pattern to restrict scanning
179
+ # :interval => 30.0 the directory scan interval (in seconds)
180
+ # :stable => nil the number of intervals a file must remain
181
+ # unchanged for it to be considered "stable"
182
+ # :pre_load => false setting this option to true will pre-load the
183
+ # file list effectively skipping the initial
184
+ # round of file added events that would normally
185
+ # be generated (glob pattern must also be
186
+ # specified otherwise odd things will happen)
187
+ #
188
+ # The default glob pattern will scan all files in the configured directory.
189
+ # Setting the :stable option to +nil+ will prevent stable events from being
190
+ # generated.
191
+ #
192
+ def initialize( directory, opts = {} )
193
+ @dir = directory
194
+
195
+ if Kernel.test(?e, @dir)
196
+ unless Kernel.test(?d, @dir)
197
+ raise ArgumentError, "'#{dir}' is not a directory"
198
+ end
199
+ else
200
+ Dir.create @dir
201
+ end
202
+
203
+ self.glob = opts[:glob] || '*'
204
+ self.interval = opts[:interval] || 30
205
+ self.stable = opts[:stable] || nil
206
+
207
+ @files = (opts[:pre_load] ? scan_files : Hash.new)
208
+ @events = []
209
+ end
210
+
211
+ #
212
+ # call-seq:
213
+ # add_observer( observer )
214
+ # add_observer {|*args| block}
215
+ #
216
+ # Adds the given _observer_ as an observer on this directory watcher. The
217
+ # _observer_ will now receive file events when they are generated.
218
+ #
219
+ # Optionally, a block can be passed as the observer. The block will be
220
+ # executed with the file events passed as the arguments. A reference to the
221
+ # underlying +Proc+ object will be returned for use with the
222
+ # +delete_observer+ method.
223
+ #
224
+ def add_observer( observer = nil, &block )
225
+ unless block.nil?
226
+ observer = block.to_proc
227
+ class << observer
228
+ alias_method :update, :call
229
+ end
230
+ end
231
+ super observer
232
+ end
233
+
234
+ #
235
+ # call-seq:
236
+ # glob = '*'
237
+ # glob = ['lib/**/*.rb', 'test/**/*.rb']
238
+ #
239
+ # Sets the glob pattern that will be used when scanning the directory for
240
+ # files. A single glob pattern can be given or an array of glob patterns.
241
+ #
242
+ def glob=( val )
243
+ @glob = case val
244
+ when String: [File.join(@dir, val)]
245
+ when Array: val.flatten.map! {|g| File.join(@dir, g)}
246
+ else
247
+ raise(ArgumentError,
248
+ 'expecting a glob pattern or an array of glob patterns')
249
+ end
250
+ @glob.uniq!
251
+ val
252
+ end
253
+ attr_reader :glob
254
+
255
+ #
256
+ # call-seq:
257
+ # interval = 30.0
258
+ #
259
+ # Sets the directory scan interval. The directory will be scanned every
260
+ # _interval_ seconds for changes to files matching the glob pattern.
261
+ # Raises +ArgumentError+ if the interval is zero or negative.
262
+ #
263
+ def interval=( val )
264
+ val = Float(val)
265
+ raise ArgumentError, "interval must be greater than zero" if val <= 0
266
+ @interval = Float(val)
267
+ end
268
+ attr_reader :interval
269
+
270
+ #
271
+ # call-seq:
272
+ # stable = 2
273
+ #
274
+ # Sets the number of intervals a file must remain unchanged before it is
275
+ # considered "stable". When this condition is met, a stable event is
276
+ # generated for the file. If stable is set to +nil+ then stable events
277
+ # will not be generated.
278
+ #
279
+ # A stable event will be generated once for a file. Another stable event
280
+ # will only be generated after the file has been modified and then remains
281
+ # unchanged for _stable_ intervals.
282
+ #
283
+ # Example:
284
+ #
285
+ # dw = DirectoryWatcher.new( '/tmp', :glob => 'swap.*' )
286
+ # dw.interval = 15.0
287
+ # dw.stable = 4
288
+ #
289
+ # In this example, a directory watcher is configured to look for swap files
290
+ # in the /tmp directory. Stable events will be generated every 4 scan
291
+ # intervals iff a swap remains unchanged for that time. In this case the
292
+ # time is 60 seconds (15.0 * 4).
293
+ #
294
+ def stable=( val )
295
+ if val.nil?
296
+ @stable = nil
297
+ return
298
+ end
299
+
300
+ val = Integer(val)
301
+ raise ArgumentError, "stable must be greater than zero" if val <= 0
302
+ @stable = val
303
+ end
304
+ attr_reader :stable
305
+
306
+ #
307
+ # call-seq:
308
+ # running?
309
+ #
310
+ # Returns +true+ if the directory watcher is currently running. Returns
311
+ # +false+ if this is not the case.
312
+ #
313
+ def running?
314
+ !@thread.nil?
315
+ end
316
+
317
+ #
318
+ # call-seq:
319
+ # start
320
+ #
321
+ # Start the directory watcher scanning thread. If the directory watcher is
322
+ # already running, this method will return without taking any action.
323
+ #
324
+ def start
325
+ return if running?
326
+
327
+ @stop = false
328
+ @thread = Thread.new(self) {|dw| dw.send :run}
329
+ self
330
+ end
331
+
332
+ #
333
+ # call-seq:
334
+ # stop
335
+ #
336
+ # Stop the directory watcher scanning thread. If the directory watcher is
337
+ # already stopped, this method will return without taking any action.
338
+ #
339
+ def stop
340
+ return unless running?
341
+
342
+ @stop = true
343
+ @thread.wakeup if @thread.status == 'sleep'
344
+ @thread.join
345
+ @thread = nil
346
+ self
347
+ end
348
+
349
+ #
350
+ # call-seq:
351
+ # reset( pre_load = false )
352
+ #
353
+ # Reset the directory watcher state by clearing the stored file list. If
354
+ # the directory watcher is running, it will be stopped, the file list
355
+ # cleared, and then restarted. Passing +true+ to this method will cause
356
+ # the file list to be pre-loaded after it has been cleared effectively
357
+ # skipping the initial round of file added events that would normally be
358
+ # generated.
359
+ #
360
+ def reset( pre_load = false )
361
+ was_running = running?
362
+
363
+ stop if was_running
364
+ @files = (pre_load ? scan_files : Hash.new)
365
+ start if was_running
366
+ end
367
+
368
+ private
369
+ #
370
+ # call-seq:
371
+ # scan_files
372
+ #
373
+ # Using the configured glob pattern, scan the directory for all files and
374
+ # return a hash with the filenames as keys and +FileInfo+ objects as the
375
+ # values. The +FileInfo+ objects contain the mtime and size of the file.
376
+ #
377
+ def scan_files
378
+ files = {}
379
+ @glob.each do |glob|
380
+ Dir.glob(glob).each do |fn|
381
+ begin
382
+ if Kernel.test(?f, fn)
383
+ files[fn] = FileInfo.new(File.mtime(fn), File.size(fn))
384
+ end
385
+ rescue SystemCallError; end
386
+ end
387
+ end
388
+ files
389
+ end
390
+
391
+ #
392
+ # call-seq:
393
+ # run
394
+ #
395
+ # Calling this method will enter the directory watcher's run loop. The
396
+ # calling thread will not return until the +stop+ method is called.
397
+ #
398
+ # The run loop is responsible for scanning the directory for file changes,
399
+ # and then dispatching events to registered listeners.
400
+ #
401
+ def run
402
+ until @stop
403
+ start = Time.now.to_f
404
+
405
+ files = scan_files
406
+ keys = [files.keys, @files.keys]
407
+ common = keys.first & keys.last
408
+
409
+ find_added(files, *keys)
410
+ find_modified(files, common)
411
+ find_removed(*keys)
412
+
413
+ notify_observers
414
+ @files = files
415
+
416
+ nap_time = @interval - (Time.now.to_f - start)
417
+ sleep nap_time if nap_time > 0
418
+ end
419
+ end
420
+
421
+ #
422
+ # call-seq:
423
+ # find_added( files, cur, prev )
424
+ #
425
+ # Taking the list of current files, _cur_, and the list of files found
426
+ # previously, _prev_, figure out which files have been added and generate
427
+ # a new file added event for each. The events are returned as an array
428
+ # from this method. If no files have been added, the returned array is
429
+ # empty.
430
+ #
431
+ def find_added( files, cur, prev )
432
+ added = cur - prev
433
+ added.each do |fn|
434
+ files[fn].stable = @stable
435
+ @events << Event.new(:added, fn)
436
+ end
437
+ self
438
+ end
439
+
440
+ #
441
+ # call-seq:
442
+ # find_removed( cur, prev )
443
+ #
444
+ # Taking the list of current files, _cur_, and the list of files found
445
+ # previously, _prev_, figure out which files have been removed and
446
+ # generate a new file removed event for each. The events are returned as
447
+ # an array from this method. If no files have been removed, the returned
448
+ # array is empty.
449
+ #
450
+ def find_removed( cur, prev )
451
+ removed = prev - cur
452
+ removed.each {|fn| @events << Event.new(:removed, fn)}
453
+ self
454
+ end
455
+
456
+ #
457
+ # call-seq:
458
+ # find_modified( files, common )
459
+ #
460
+ # Taking the list of _common_ files (those that exist in the current file
461
+ # list and in the previous file list) determine if any have been modified.
462
+ # Generate a new file modified event for each modified file. Also, by
463
+ # looking at the stable count in the _files_ hash, figure out if any files
464
+ # have become stable since being added or modified. Generate a new stable
465
+ # event for each stabilized file. The events are returned as an array from
466
+ # this method. If there are no events, the array is empty.
467
+ #
468
+ def find_modified( files, common )
469
+ common.each do |key|
470
+ cur, prev = files[key], @files[key]
471
+
472
+ # if the modification time or the file size differs from the last
473
+ # time it was seen, then create a :modified event
474
+ if cur.mtime != prev.mtime or cur.size != prev.size
475
+ @events << Event.new(:modified, key)
476
+ cur.stable = @stable
477
+
478
+ # otherwise, if the count is not nil see if we need to create a
479
+ # :stable event
480
+ elsif !prev.stable.nil?
481
+ cur.stable = prev.stable - 1
482
+ if cur.stable == 0
483
+ @events << Event.new(:stable, key)
484
+ cur.stable = nil
485
+ end
486
+ end
487
+ end
488
+ self
489
+ end
490
+
491
+ #
492
+ # call-seq:
493
+ # notify_observers
494
+ #
495
+ # If there are queued files events, then invoke the update method of each
496
+ # registered observer in turn passing the list of file events to each.
497
+ # The file events array is cleared at the end of this method call.
498
+ #
499
+ def notify_observers
500
+ unless @events.empty? or !@observer_peers
501
+ @observer_peers.dup.each do |observer|
502
+ begin; observer.update(*@events); rescue Exception; end
503
+ end
504
+ end
505
+ @events.clear
506
+ end
507
+
508
+ end # class DirectoryWatcher
509
+
510
+ # EOF
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.0
3
+ specification_version: 1
4
+ name: directory_watcher
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.0
7
+ date: 2006-11-10 00:00:00 -07:00
8
+ summary: A class for watching files within a directory and generating events when those files change
9
+ require_paths:
10
+ - lib
11
+ email: tim.pease@gmail.com
12
+ homepage:
13
+ rubyforge_project: codeforpeople.com
14
+ description: The directory watcher operates by scanning a directory at some interval and generating a list of files based on a user supplied glob pattern. As the file list changes from one interval to the next, events are generated and dispatched to registered observers. Three types of events are supported -- added, modified, and removed.
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Tim Pease
31
+ files:
32
+ - lib/directory_watcher.rb
33
+ test_files: []
34
+
35
+ rdoc_options: []
36
+
37
+ extra_rdoc_files: []
38
+
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ requirements: []
44
+
45
+ dependencies:
46
+ - !ruby/object:Gem::Dependency
47
+ name: hoe
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Version::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 1.1.3
54
+ version: