directory_watcher 1.4.1 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,277 @@
1
+ #
2
+ # == Synopsis
3
+ # The Threaded module is used to perform some activity at a specified
4
+ # interval.
5
+ #
6
+ # == Details
7
+ # Sometimes it is useful for an object to have its own thread of execution
8
+ # to perform a task at a recurring interval. The Threaded module
9
+ # encapsulates this functionality so you don't have to write it yourself. It
10
+ # can be used with any object that responds to the +run+ method.
11
+ #
12
+ # The threaded object is run by calling the +start+ method. This will create
13
+ # a new thread that will invoke the +run+ method at the desired interval.
14
+ # Just before the thread is created the +before_starting+ method will be
15
+ # called (if it is defined by the threaded object). Likewise, after the
16
+ # thread is created the +after_starting+ method will be called (if it is
17
+ # defined by the threaded object).
18
+ #
19
+ # The threaded object is stopped by calling the +stop+ method. This sets an
20
+ # internal flag and then wakes up the thread. The thread gracefully exits
21
+ # after checking the flag. Like the start method, before and after methods
22
+ # are defined for stopping as well. Just before the thread is stopped the
23
+ # +before_stopping+ method will be called (if it is defined by the threaded
24
+ # object). Likewise, after the thread has died the +after_stopping+ method
25
+ # will be called (if it is defined by the threaded object).
26
+ #
27
+ # Calling the +join+ method on a threaded object will cause the calling
28
+ # thread to wait until the threaded object has stopped. An optional timeout
29
+ # parameter can be given.
30
+ #
31
+ module DirectoryWatcher::Threaded
32
+
33
+ # This method will be called by the activity thread at the desired
34
+ # interval. Implementing classes are expect to provide this
35
+ # functionality.
36
+ #
37
+ def run
38
+ raise NotImplementedError,
39
+ 'The run method must be defined by the threaded object.'
40
+ end
41
+
42
+ # Start the activity thread. If already started this method will return
43
+ # without taking any action.
44
+ #
45
+ # If the including class defines a 'before_starting' method, it will be
46
+ # called before the thread is created and run. Likewise, if the
47
+ # including class defines an 'after_starting' method, it will be called
48
+ # after the thread is created.
49
+ #
50
+ def start
51
+ return self if _activity_thread.running?
52
+
53
+ before_starting if self.respond_to?(:before_starting)
54
+ @_activity_thread.start self
55
+ after_starting if self.respond_to?(:after_starting)
56
+ self
57
+ end
58
+
59
+ # Stop the activity thread. If already stopped this method will return
60
+ # without taking any action.
61
+ #
62
+ # If the including class defines a 'before_stopping' method, it will be
63
+ # called before the thread is stopped. Likewise, if the including class
64
+ # defines an 'after_stopping' method, it will be called after the thread
65
+ # has stopped.
66
+ #
67
+ def stop
68
+ return self unless _activity_thread.running?
69
+
70
+ before_stopping if self.respond_to?(:before_stopping)
71
+ @_activity_thread.stop
72
+ self
73
+ end
74
+
75
+ # Stop the activity thread from doing work. This will not stop the activity
76
+ # thread, it will just stop it from calling the 'run' method on every
77
+ # iteration. It will also not increment the number of iterations it has run.
78
+ def pause
79
+ @_activity_thread.working = false
80
+ end
81
+
82
+ # Resume the activity thread
83
+ def resume
84
+ @_activity_thread.working = true
85
+ end
86
+
87
+ # Wait on the activity thread. If the thread is already stopped, this
88
+ # method will return without taking any action. Otherwise, this method
89
+ # does not return until the activity thread has stopped, or a specific
90
+ # number of iterations has passed since this method was called.
91
+ #
92
+ def wait( limit = nil )
93
+ return self unless _activity_thread.running?
94
+ initial_iterations = @_activity_thread.iterations
95
+ loop {
96
+ break unless @_activity_thread.running?
97
+ break if limit and @_activity_thread.iterations > ( initial_iterations + limit )
98
+ Thread.pass
99
+ }
100
+ end
101
+
102
+ # If the activity thread is running, the calling thread will suspend
103
+ # execution and run the activity thread. This method does not return until
104
+ # the activity thread is stopped or until _limit_ seconds have passed.
105
+ #
106
+ # If the activity thread is not running, this method returns immediately
107
+ # with +nil+.
108
+ #
109
+ def join( limit = nil )
110
+ _activity_thread.join(limit) ? self : nil
111
+ end
112
+
113
+ # Returns +true+ if the activity thread is running. Returns +false+
114
+ # otherwise.
115
+ #
116
+ def running?
117
+ _activity_thread.running?
118
+ end
119
+
120
+ # Returns +true+ if the activity thread has finished its maximum
121
+ # number of iterations or the thread is no longer running.
122
+ # Returns +false+ otherwise.
123
+ #
124
+ def finished_iterations?
125
+ return true unless _activity_thread.running?
126
+ @_activity_thread.finished_iterations?
127
+ end
128
+
129
+ # Returns the status of threaded object.
130
+ #
131
+ # 'sleep' : sleeping or waiting on I/O
132
+ # 'run' : executing
133
+ # 'aborting' : aborting
134
+ # false : not running or terminated normally
135
+ # nil : terminated with an exception
136
+ #
137
+ # If this method returns +nil+, then calling join on the threaded object
138
+ # will cause the exception to be raised in the calling thread.
139
+ #
140
+ def status
141
+ return false if _activity_thread.thread.nil?
142
+ @_activity_thread.thread.status
143
+ end
144
+
145
+ # Sets the number of seconds to sleep between invocations of the
146
+ # threaded object's 'run' method.
147
+ #
148
+ def interval=( value )
149
+ value = Float(value)
150
+ raise ArgumentError, "Sleep interval must be >= 0" unless value >= 0
151
+ _activity_thread.interval = value
152
+ end
153
+
154
+ # Returns the number of seconds to sleep between invocations of the
155
+ # threaded object's 'run' method.
156
+ #
157
+ def interval
158
+ _activity_thread.interval
159
+ end
160
+
161
+ # Sets the maximum number of invocations of the threaded object's
162
+ # 'run' method
163
+ #
164
+ def maximum_iterations=( value )
165
+ unless value.nil?
166
+ value = Integer(value)
167
+ raise ArgumentError, "maximum iterations must be >= 1" unless value >= 1
168
+ end
169
+
170
+ _activity_thread.maximum_iterations = value
171
+ end
172
+
173
+ # Returns the maximum number of invocations of the threaded
174
+ # object's 'run' method
175
+ #
176
+ def maximum_iterations
177
+ _activity_thread.maximum_iterations
178
+ end
179
+
180
+ # Returns the number of iterations of the threaded object's 'run' method
181
+ # completed thus far.
182
+ #
183
+ def iterations
184
+ _activity_thread.iterations
185
+ end
186
+
187
+ # Set to +true+ to continue running the threaded object even if an error
188
+ # is raised by the +run+ method. The default behavior is to stop the
189
+ # activity thread when an error is raised by the run method.
190
+ #
191
+ # A SystemExit will never be caught; it will always cause the Ruby
192
+ # interpreter to exit.
193
+ #
194
+ def continue_on_error=( value )
195
+ _activity_thread.continue_on_error = (value ? true : false)
196
+ end
197
+
198
+ # Returns +true+ if the threaded object should continue running even if an
199
+ # error is raised by the run method. The default is to return +false+. The
200
+ # threaded object will stop running when an error is raised.
201
+ #
202
+ def continue_on_error?
203
+ _activity_thread.continue_on_error
204
+ end
205
+
206
+ # :stopdoc:
207
+ def _activity_thread
208
+ @_activity_thread ||= ::DirectoryWatcher::Threaded::ThreadContainer.new(60, 0, nil, false);
209
+ end # @private
210
+
211
+ # @private
212
+ ThreadContainer = Struct.new( :interval, :iterations, :maximum_iterations, :continue_on_error, :thread, :running, :working) {
213
+ def start( threaded )
214
+
215
+ self.working = true
216
+ self.running = true
217
+ self.iterations = 0
218
+ self.thread = Thread.new { run threaded }
219
+ Thread.pass
220
+ end # @private
221
+
222
+ def stop
223
+ self.running = false
224
+ thread.wakeup
225
+ end # @private
226
+
227
+ def run( threaded )
228
+ loop do
229
+ begin
230
+ break unless running?
231
+ do_work( threaded )
232
+
233
+ sleep interval if running?
234
+ rescue SystemExit; raise
235
+ rescue Exception => err
236
+ if continue_on_error
237
+ $stderr.puts err
238
+ else
239
+ $stderr.puts err
240
+ raise err
241
+ end
242
+ end
243
+ end
244
+ ensure
245
+ if threaded.respond_to?(:after_stopping) and !self.running
246
+ threaded.after_stopping
247
+ end
248
+ self.running = false
249
+ end # @private
250
+
251
+ def join( limit = nil )
252
+ return if thread.nil?
253
+ limit ? thread.join(limit) : thread.join
254
+ end # @private
255
+
256
+ def do_work( threaded )
257
+ if working then
258
+ threaded.run
259
+
260
+ if maximum_iterations
261
+ self.iterations += 1
262
+ if finished_iterations?
263
+ self.running = false
264
+ end
265
+ end
266
+ end
267
+ end # @private
268
+
269
+ def finished_iterations?
270
+ return true if maximum_iterations and (iterations >= maximum_iterations)
271
+ return false
272
+ end # @private
273
+
274
+ alias :running? :running
275
+ }
276
+ # :startdoc:
277
+ end
@@ -0,0 +1,8 @@
1
+ class DirectoryWatcher
2
+ module Version
3
+ def version
4
+ File.read(DirectoryWatcher.path('version.txt')).strip
5
+ end
6
+ extend self
7
+ end
8
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe DirectoryWatcher do
4
+ it "has a version" do
5
+ DirectoryWatcher.version.should =~ /\d\.\d\.\d/
6
+ end
7
+ end
8
+
9
+ describe "Scanners" do
10
+ [ nil, :em, :coolio ].each do |scanner|
11
+ # [ :rev ].each do |scanner|
12
+ context "#{scanner} Scanner" do
13
+
14
+ let( :default_options ) { { :glob => "**/*", :interval => 0.05} }
15
+ let( :options ) { default_options.merge( :scanner => scanner ) }
16
+ let( :options_with_pre_load ) { options.merge( :pre_load => true ) }
17
+ let( :options_with_stable ) { options.merge( :stable => 2 ) }
18
+ let( :options_with_glob ) { options.merge( :glob => '**/*.42' ) }
19
+ let( :options_with_persist ) { options.merge( :persist => scratch_path( 'persist.yml' ) ) }
20
+
21
+ let( :directory_watcher ) { DirectoryWatcher.new( @scratch_dir, options ) }
22
+ let( :directory_watcher_with_pre_load ) { DirectoryWatcher.new( @scratch_dir, options_with_pre_load ) }
23
+ let( :directory_watcher_with_stable ) { DirectoryWatcher.new( @scratch_dir, options_with_stable ) }
24
+ let( :directory_watcher_with_glob ) { DirectoryWatcher.new( @scratch_dir, options_with_glob ) }
25
+ let( :directory_watcher_with_persist ) { DirectoryWatcher.new( @scratch_dir, options_with_persist ) }
26
+
27
+ let( :scenario ) { DirectoryWatcherSpecs::Scenario.new( directory_watcher) }
28
+ let( :scenario_with_pre_load ) { DirectoryWatcherSpecs::Scenario.new( directory_watcher_with_pre_load ) }
29
+ let( :scenario_with_stable ) { DirectoryWatcherSpecs::Scenario.new( directory_watcher_with_stable ) }
30
+ let( :scenario_with_glob ) { DirectoryWatcherSpecs::Scenario.new( directory_watcher_with_glob ) }
31
+ let( :scenario_with_persist ) { DirectoryWatcherSpecs::Scenario.new( directory_watcher_with_persist ) }
32
+
33
+ it_should_behave_like 'Scanner'
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe DirectoryWatcher::Paths do
4
+ it "has a libpath" do
5
+ DirectoryWatcher.lib_path.should == File.expand_path( "../../lib", __FILE__) + ::File::SEPARATOR
6
+ end
7
+ end
@@ -0,0 +1,236 @@
1
+ shared_examples_for "Scanner" do
2
+ context "Event Types"do
3
+ it "sends added events" do
4
+
5
+ scenario.run_and_wait_for_event_count(1) do
6
+ touch( scratch_path( 'added' ) )
7
+ end.stop
8
+
9
+ scenario.events.should be_events_like( [[ :added, 'added' ]] )
10
+ end
11
+
12
+ it "sends modified events for file size modifications" do
13
+
14
+ modified_file = scratch_path( 'modified' )
15
+ scenario.run_and_wait_for_event_count(1) do
16
+ touch( modified_file )
17
+ end.run_and_wait_for_event_count(1) do
18
+ append_to( modified_file )
19
+ end.stop
20
+
21
+ scenario.events.should be_events_like( [[ :added, 'modified'], [ :modified, 'modified']] )
22
+ end
23
+
24
+ it "sends modified events for mtime modifications" do
25
+ modified_file = scratch_path( 'modified' )
26
+
27
+ scenario.run_and_wait_for_event_count(1) do
28
+ touch( modified_file, Time.now - 5 )
29
+ end.run_and_wait_for_event_count(1) do
30
+ touch( modified_file )
31
+ end.stop
32
+
33
+ scenario.events.should be_events_like( [[ :added, 'modified'], [ :modified, 'modified']] )
34
+ end
35
+
36
+ it "sends removed events" do
37
+ removed_file = scratch_path( 'removed' )
38
+ scenario.run_and_wait_for_event_count(1) do
39
+ touch( removed_file, Time.now )
40
+ end.run_and_wait_for_event_count(1) do
41
+ File.unlink( removed_file )
42
+ end.stop
43
+
44
+ scenario.events.should be_events_like [ [:added, 'removed'], [:removed, 'removed'] ]
45
+ end
46
+
47
+ it "sends stable events" do
48
+ stable_file = scratch_path( 'stable' )
49
+ scenario_with_stable.run_and_wait_for_event_count(2) do |s|
50
+ touch( stable_file )
51
+ # do nothing wait for the stable event.
52
+ end.stop
53
+
54
+ scenario_with_stable.events.should be_events_like [ [:added, 'stable'], [:stable, 'stable'] ]
55
+ end
56
+
57
+ it "only sends stable events once" do
58
+ stable_file = scratch_path( 'stable' )
59
+ scenario_with_stable.run_and_wait_for_scan_count(5) do |s|
60
+ touch( stable_file )
61
+ # do nothing
62
+ end.stop
63
+
64
+ scenario_with_stable.events.size.should == 2
65
+ end
66
+
67
+ it "events are not sent for directory creation" do
68
+ a_dir = scratch_path( 'subdir' )
69
+
70
+ scenario.run_and_wait_for_scan_count(2) do
71
+ Dir.mkdir( a_dir )
72
+ end.stop
73
+
74
+ scenario.events.should be_empty
75
+ end
76
+
77
+ it "sends events for files in sub directories" do
78
+ a_dir = scratch_path( 'subdir' )
79
+
80
+ scenario.run_and_wait_for_event_count(1) do
81
+ Dir.mkdir( a_dir )
82
+ subfile = File.join( a_dir, 'subfile' )
83
+ touch( subfile )
84
+ end.stop
85
+
86
+ scenario.events.should be_events_like [ [:added, 'subfile'] ]
87
+ end
88
+ end
89
+
90
+ context "run_once" do
91
+ it "can be run on command via 'run_once'" do
92
+ one_shot_file = scratch_path( "run_once" )
93
+ scenario.run_once_and_wait_for_event_count(1) do
94
+ touch( one_shot_file )
95
+ end.stop
96
+ scenario.events.should be_events_like [ [:added, 'run_once'] ]
97
+ end
98
+ end
99
+
100
+ context "pre_load option " do
101
+ it "skips initial add events" do
102
+ modified_file = scratch_path( 'modified' )
103
+ touch( modified_file, Time.now - 5 )
104
+
105
+ scenario_with_pre_load.run_and_wait_for_event_count(1) do
106
+ touch( modified_file )
107
+ end.stop
108
+
109
+ scenario_with_pre_load.events.should be_events_like( [[ :modified, 'modified']] )
110
+ end
111
+ end
112
+
113
+ context "globbing" do
114
+ it "only sends events for files that match" do
115
+ non_matching = scratch_path( 'no-match' )
116
+ matching = scratch_path( 'match.42' )
117
+
118
+ scenario_with_glob.run_and_wait_for_event_count(1) do
119
+ touch( non_matching )
120
+ touch( matching, Time.now - 5 )
121
+ end.run_and_wait_for_event_count(1) do
122
+ touch( matching )
123
+ end.stop
124
+
125
+ scenario_with_glob.events.should be_events_like( [[ :added, 'match.42' ], [ :modified, 'match.42' ]] )
126
+ end
127
+ end
128
+
129
+ context "running?" do
130
+ it "is true when the watcher is running" do
131
+ directory_watcher.start
132
+ directory_watcher.running?.should be_true
133
+ directory_watcher.stop
134
+ end
135
+
136
+ it "is false when the watcher is not running" do
137
+ directory_watcher.running?.should be_false
138
+ directory_watcher.start
139
+ directory_watcher.running?.should be_true
140
+ directory_watcher.stop
141
+ directory_watcher.running?.should be_false
142
+ end
143
+ end
144
+
145
+ context "persistence" do
146
+ it "saves the current state of the system when the watcher is stopped" do
147
+ modified_file = scratch_path( 'modified' )
148
+ scenario_with_persist.run_and_wait_for_event_count(1) do
149
+ touch( modified_file, Time.now - 20 )
150
+ end.run_and_wait_for_event_count(1) do
151
+ touch( modified_file, Time.now - 10 )
152
+ end.stop
153
+
154
+ scenario_with_persist.events.should be_events_like( [[ :added, 'modified'], [ :modified, 'modified' ]] )
155
+
156
+ scenario_with_persist.reset
157
+ scenario_with_persist.resume
158
+ Thread.pass until scenario_with_persist.events.size >= 1
159
+ scenario_with_persist.pause
160
+
161
+ scenario_with_persist.run_and_wait_for_event_count(1) do
162
+ touch( modified_file )
163
+ end.stop
164
+
165
+ scenario_with_persist.events.should be_events_like( [[:added, 'persist.yml'], [ :modified, 'modified' ]] )
166
+ end
167
+ end
168
+
169
+ context "sorting" do
170
+ [:ascending, :descending].each do |ordering|
171
+ context "#{ordering}" do
172
+ context "file name" do
173
+ let( :filenames ) { ('a'..'z').sort_by {rand} }
174
+ let( :options ) { default_options.merge( :order_by => ordering ) }
175
+ before do
176
+ filenames.each do |p|
177
+ touch( scratch_path( p ))
178
+ end
179
+ end
180
+
181
+ it "#{ordering}" do
182
+ scenario.run_and_wait_for_event_count(filenames.size) do
183
+ # wait
184
+ end
185
+ final_events = filenames.sort.map { |p| [:added, p] }
186
+ final_events.reverse! if ordering == :descending
187
+ scenario.events.should be_events_like( final_events )
188
+ end
189
+ end
190
+
191
+ context "mtime" do
192
+ let( :current_time ) { Time.now }
193
+ let( :basenames ) { ('a'..'z').to_a }
194
+ let( :delta_times ) { unique_integer_list( basenames.size, 5000 ) }
195
+ let( :filenames ) { basenames.inject({}) { |h,k| h[k] = current_time - delta_times.shift; h } }
196
+ let( :options ) { default_options.merge( :sort_by => :mtime, :order_by => ordering ) }
197
+
198
+ before do
199
+ filenames.keys.sort_by{ rand }.each do |p|
200
+ touch( scratch_path(p), filenames[p] )
201
+ end
202
+ end
203
+
204
+ it "#{ordering}" do
205
+ scenario.run_and_wait_for_event_count(filenames.size) { nil }
206
+ sorted_fnames = filenames.to_a.sort_by { |v| v[1] }
207
+ final_events = sorted_fnames.map { |fn,ts| [:added, fn] }
208
+ final_events.reverse! if ordering == :descending
209
+ scenario.events.should be_events_like( final_events )
210
+ end
211
+ end
212
+
213
+ context "size" do
214
+ let( :basenames ) { ('a'..'z').to_a }
215
+ let( :file_sizes ) { unique_integer_list( basenames.size, 1000 ) }
216
+ let( :filenames ) { basenames.inject({}) { |h,k| h[k] = file_sizes.shift; h } }
217
+ let( :options ) { default_options.merge( :sort_by => :size, :order_by => ordering ) }
218
+
219
+ before do
220
+ filenames.keys.sort_by{ rand }.each do |p|
221
+ append_to( scratch_path(p), filenames[p] )
222
+ end
223
+ end
224
+
225
+ it "#{ordering}" do
226
+ scenario.run_and_wait_for_event_count(filenames.size) { nil }
227
+ sorted_fnames = filenames.to_a.sort_by { |v| v[1] }
228
+ final_events = sorted_fnames.map { |fn,ts| [:added, fn] }
229
+ final_events.reverse! if ordering == :descending
230
+ scenario.events.should be_events_like( final_events )
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end