directory_watcher 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: