joshaven-filemonitor 0.0.3
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.
- data/CHANGELOG +25 -0
- data/Manifest.txt +11 -0
- data/README.markdown +90 -0
- data/lib/FileMonitor.rb +288 -0
- data/lib/FileMonitor/store.rb +45 -0
- data/spec/FileMonitor_spec.rb +180 -0
- data/spec/MonitoredItem_spec.rb +42 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- data/tasks/rdoc.rake +9 -0
- data/tasks/rspec.rake +21 -0
- metadata +75 -0
data/CHANGELOG
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
=== 0.0.3 2009-09-24
|
2
|
+
* Major Enhancements:
|
3
|
+
* Accept regexp filter when adding files: fm.add(path, regexp=/.*/, &block)
|
4
|
+
* Minor changes
|
5
|
+
* Change this file from History.txt to CHANGELOG
|
6
|
+
* Remove unused options hash from the process method
|
7
|
+
* Remove script/generate script/console, script/destroy & Rakefile from gem
|
8
|
+
* Fixup filemonitor.gemspec to work for github gem creation
|
9
|
+
|
10
|
+
=== 0.0.2 2009-09-24
|
11
|
+
|
12
|
+
* Major Enhancements:
|
13
|
+
* Various API Changes!
|
14
|
+
* RSpec Testing! Oh yah!
|
15
|
+
* RDocs! Oh yah!
|
16
|
+
* Reduced I/O load: Updated method of saving old file state. Changes are not
|
17
|
+
detectable if changed within 1 second of prior digest. If this is a problem
|
18
|
+
for anyone, I can make a md5 digest of the file optional.
|
19
|
+
* Handle duplicates file entries by overwriting the old with the new
|
20
|
+
* Add start / stop & halt methods for handling spawns
|
21
|
+
|
22
|
+
=== 0.0.1 2009-09-15
|
23
|
+
|
24
|
+
* 1 major enhancement:
|
25
|
+
* Initial release
|
data/Manifest.txt
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# FileMonitor
|
2
|
+
Calls a Proc when a watched file is changed.
|
3
|
+
|
4
|
+
* Documentation: http://filemonitor.rubyforge.org/rdoc
|
5
|
+
* Code Repository: http://github.com/joshaven/FileMonitor
|
6
|
+
* Joshaven Potter: yourtech@gmail.com
|
7
|
+
|
8
|
+
## Notice
|
9
|
+
This project has only been tested on posix compatible systems, sorry for you windows users. If you get this
|
10
|
+
to run under windows, let me know and I'll release a windows compatible version.
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
gem install filemonitor # you may need to run: sudo gem install filemonitor
|
14
|
+
|
15
|
+
## Examples
|
16
|
+
In the following examples, the block passed will be executed when the file changes. These examples only
|
17
|
+
echo strings but I am sure you'll find something more useful to do in the callback proc.
|
18
|
+
|
19
|
+
require 'filemonitor'
|
20
|
+
|
21
|
+
# Example 1: Generate a FileMonitor subprocess with one command:
|
22
|
+
fm = FileMonitor.when_modified(Dir.pwd, "/path/to/other/file.rb") do |watched_item|
|
23
|
+
puts "Change detected in #{watched_item.path} "
|
24
|
+
end
|
25
|
+
|
26
|
+
# thats it... if you want to stop the subprocess, issue: fm.stop
|
27
|
+
|
28
|
+
# Example 2: Generate a manually run FileMonitor:
|
29
|
+
file_spy = FileMonitor.new {|f| puts "Default proc called because #{f.path} changed."}
|
30
|
+
file_spy.add(Dir.pwd) do |f|
|
31
|
+
puts "Monitored Item's proc called because #{f.path} changed."
|
32
|
+
end
|
33
|
+
|
34
|
+
file_spy.process # This will look for changes one time... call this again when you want to find changes.
|
35
|
+
# if you want to launch a background process then do file_spy.spawn or its alias: file_spy.start
|
36
|
+
|
37
|
+
### Context Examples
|
38
|
+
Both the FileMonitor.new method and a FileMonitor instance fm.add method can accept a block with 0 to 2 arities.
|
39
|
+
See Also the api docs: <http://filemonitor.rubyforge.org/rdoc>
|
40
|
+
|
41
|
+
This block is working in the context in which it was created:
|
42
|
+
|
43
|
+
changes = false
|
44
|
+
fm.add(Dir.pwd) {changes = true} # The 'changes' variable will be set to true when a change is detected
|
45
|
+
fm.process
|
46
|
+
respond_to_file_changes if changes
|
47
|
+
|
48
|
+
This block is working in the context created and is aware of the watched file instance 'f'. The watched file will
|
49
|
+
respond to :path, :callback, & :digest and is an instance of: MonitoredItems::Store
|
50
|
+
|
51
|
+
changed_ruby_files = []
|
52
|
+
fm.add(Dir.pwd, /\.rb$/) do |f| # Adds all .rb files recursively starting with the working directory
|
53
|
+
changed_ruby_files << f.path # The changed_ruby_files will contain the path of the changed files.
|
54
|
+
end
|
55
|
+
fm.process
|
56
|
+
handel_changes(changed_ruby_files) # Do somehting with the changed files
|
57
|
+
changed_ruby_files = [] # Cleanup changes so the array doesn't keep growing
|
58
|
+
|
59
|
+
|
60
|
+
This block is working in the context created and is aware of the watched_file instance 'f' as well as the
|
61
|
+
file_monitor instance 'fm'. The fm object can be acted on within the block the same way it is outside the block,
|
62
|
+
an example use of this may be to add all files in a folder when a file is changed...
|
63
|
+
|
64
|
+
fm.add(Dir.pwd) do |f, fm|
|
65
|
+
fm.add(APP_ROOT) if f.path == (APP_ROOT + Manifest.txt) # Start watching any new files when added to to the manifest.txt file.
|
66
|
+
end
|
67
|
+
|
68
|
+
## License
|
69
|
+
(The MIT License)
|
70
|
+
|
71
|
+
Copyright (c) 2009 Joshaven Potter
|
72
|
+
|
73
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
74
|
+
a copy of this software and associated documentation files (the
|
75
|
+
'Software'), to deal in the Software without restriction, including
|
76
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
77
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
78
|
+
permit persons to whom the Software is furnished to do so, subject to
|
79
|
+
the following conditions:
|
80
|
+
|
81
|
+
The above copyright notice and this permission notice shall be
|
82
|
+
included in all copies or substantial portions of the Software.
|
83
|
+
|
84
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
85
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
86
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
87
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
88
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
89
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
90
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/lib/FileMonitor.rb
ADDED
@@ -0,0 +1,288 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/FileMonitor/store'
|
2
|
+
require 'find' # needed for the :files_recursive method
|
3
|
+
# Purpose:
|
4
|
+
# Watches the file system for changes
|
5
|
+
#
|
6
|
+
# Usage:
|
7
|
+
#
|
8
|
+
# require 'filemonitor'
|
9
|
+
#
|
10
|
+
# # Create a FileMonitor instance and assign the callback to the entire object.
|
11
|
+
# # In the following example, "watched_item" is a MonitoredItems::Store, see the new
|
12
|
+
# # method for a better example of working in your block.
|
13
|
+
# file_spy = FileMonitor.new do |watched_item| ... end
|
14
|
+
#
|
15
|
+
# # Any files in the working directory (Dir.pwd) and its sub-directories will
|
16
|
+
# # be watched for changes. If a change is found the callback assigned above
|
17
|
+
# # will be enacted.
|
18
|
+
# file_spy << Dir.pwd
|
19
|
+
# file_spy << "/path/to/other/file.rb"
|
20
|
+
#
|
21
|
+
# # launch an independent process to do the monitoring:
|
22
|
+
# file_spy.spawn
|
23
|
+
#
|
24
|
+
# # Alternatively you can do all of the above in one line:
|
25
|
+
# FileMonitor.when_modified(Dir.pwd, "/path/to/other/file.rb") do |watched_item| ... end
|
26
|
+
class FileMonitor
|
27
|
+
VERSION = '0.0.3'
|
28
|
+
attr_accessor :callback, :pid, :watched
|
29
|
+
# The new method may be called with an optional callback which must be a block
|
30
|
+
# either do...end or {...}.
|
31
|
+
# The block may consiste of upto arguments ie {|watched_item, monitored|}, in which case, the
|
32
|
+
# watched_item is an instance of FileMonitor::Store and monitored is the FileMonitor instances self.
|
33
|
+
#
|
34
|
+
# The first argument of the block, 'watched_item' in this case, will respond to: (:path, :modified & :callback).
|
35
|
+
# The second argument of the block, 'monitored' in this case, will respond any FileMonitor method.
|
36
|
+
#
|
37
|
+
# Example:
|
38
|
+
# FileMonitor.new do |watched_item|
|
39
|
+
# puts "I am watched this file: #{watched_item.path}"
|
40
|
+
# puts "When I find a change I will call watched_item.callback"
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# FileMonitor.new do |watched_item, monitored|
|
44
|
+
#
|
45
|
+
# # add files from a file that is a list of files to watch... Note: There
|
46
|
+
# IO.readlines(watched_item.path).each {|file| monitored << file } if watched_item.path == '/path/to/file/watchme.list'
|
47
|
+
#
|
48
|
+
# # clear watchme.list so we won't add all watch files every time the file changes
|
49
|
+
# open(watched_item) { |f| puts '' }
|
50
|
+
# end
|
51
|
+
def initialize(&callback)
|
52
|
+
@watched = []
|
53
|
+
@callback = callback unless callback.nil?
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns a spawned FileMonitor instance. The independent process automatically calls the given
|
57
|
+
# callback when changes are found.
|
58
|
+
#
|
59
|
+
# Example:
|
60
|
+
# fm = FileMonitor.when_modified(Dir.pwd, "/path/to/other/file.rb") {|watched_item, file_monitor| ... }
|
61
|
+
# fm.pid # => 23994
|
62
|
+
# fm.callback.nil? # => false
|
63
|
+
# fm.watched.size # => 28
|
64
|
+
def self.when_modified(*paths, &callback)
|
65
|
+
fm = FileMonitor.new &callback
|
66
|
+
paths.each {|path| fm << path}
|
67
|
+
fm.spawn
|
68
|
+
return fm
|
69
|
+
end
|
70
|
+
|
71
|
+
# The add method accepts a directory path or file path and optional callback. If a directory path is given all files in that
|
72
|
+
# path are recursively added. If a callback is given then that proc will be called when a change is detected on that file or
|
73
|
+
# group of files. If no proc is given via the add method then the object callback is called. If a regexp is given as the
|
74
|
+
# second argument only files matching the regexp will be monitored.
|
75
|
+
#
|
76
|
+
# Example:
|
77
|
+
# fm = FileMonitor.new do |path|
|
78
|
+
# puts "Detected a change on #{path}"
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# # The following will run the default callback when changes are found in the /tmp folder:
|
82
|
+
# fm.add '/tmp'
|
83
|
+
#
|
84
|
+
# # The following will run the given callback on any files ending in 'txt' in the /home folder when changed:
|
85
|
+
# fm.add('/home', /txt$/) do |path|
|
86
|
+
# puts "A users file has changed: #{path}"
|
87
|
+
# end
|
88
|
+
def add(path, regexp_file_filter=/.*/, &callback)
|
89
|
+
if File.file?(path) && regexp_file_filter === File.split(path).last
|
90
|
+
index = index_of(path) || @watched.size
|
91
|
+
@watched[index] = MonitoredItems::Store.new({:path=>File.expand_path(path), :callback=>callback, :digest=>digest(path)})
|
92
|
+
return true
|
93
|
+
elsif File.directory? path
|
94
|
+
files_recursive(path).each {|f| add(f, regexp_file_filter, &callback) }
|
95
|
+
return true
|
96
|
+
end
|
97
|
+
false
|
98
|
+
end
|
99
|
+
|
100
|
+
# The '<<' method works the same way as the 'add' method but does not support a callback.
|
101
|
+
#
|
102
|
+
# Example:
|
103
|
+
# fm = FileMonitor.new do |path|
|
104
|
+
# puts "Detected a change on #{path}"
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
# # The following will run the default callback when changes are found in the /tmp folder:
|
108
|
+
# fm << '/tmp'
|
109
|
+
def <<(path)
|
110
|
+
add path, regexp_file_filter
|
111
|
+
end
|
112
|
+
|
113
|
+
# Itterates watched files and runs callbacks when changes are detected. This is the semi-automatic way to run the FileMonitor.
|
114
|
+
#
|
115
|
+
# Example:
|
116
|
+
# changed_files = []
|
117
|
+
# fm = FileMonitor.new() {|watched_item| changed_files = watched_item.path}
|
118
|
+
# fm << '/tmp'
|
119
|
+
# fm.process # this will look for changes in any watched items only once... call this when you want to look for changes.
|
120
|
+
def process
|
121
|
+
@watched.each do |i|
|
122
|
+
key = digest(i.path)
|
123
|
+
# i.digest = key if i.digest.nil? # skip first change detection, its always unknown on first run
|
124
|
+
|
125
|
+
unless i.digest == key
|
126
|
+
respond_to_change(i, key)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Runs an endless loop watching for changes. It will sleep for the given interval between looking for changes. This method
|
132
|
+
# is intended to be run in a subprocess or threaded environment. The spawn method calls this method and takes care of
|
133
|
+
# the forking and pid management for you.
|
134
|
+
#
|
135
|
+
# Example:
|
136
|
+
# fm = FileMonitor.new
|
137
|
+
# fm << '/tmp'
|
138
|
+
# fm.monitor
|
139
|
+
# puts "will not get here unless a signal is sent to the process which interrupts the loop."
|
140
|
+
def monitor(interval = 1)
|
141
|
+
trap("INT") do
|
142
|
+
puts " FileMonitor was interrupted by Control-C... exiting gracefully"
|
143
|
+
# exit
|
144
|
+
@shutdown = true
|
145
|
+
end
|
146
|
+
|
147
|
+
trap("USR1") do
|
148
|
+
puts " FileMonitor was asked nicely to stop."
|
149
|
+
@shutdown = true
|
150
|
+
pid = nil
|
151
|
+
end
|
152
|
+
|
153
|
+
trap("USR2") do
|
154
|
+
puts " FileMonitor was halted."
|
155
|
+
pid = nil
|
156
|
+
exit
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
while true
|
161
|
+
exit if @shutdown
|
162
|
+
process
|
163
|
+
sleep interval unless @shutdown
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Returns index of watched item or false if non existant.
|
168
|
+
#
|
169
|
+
# Example:
|
170
|
+
# fm = FileMonitor.new
|
171
|
+
# fm << '/tmp/first.txt'
|
172
|
+
# fm << '/tmp/second.txt'
|
173
|
+
# fm.index_of '/tmp/first.txt' # => 0
|
174
|
+
# fm.index_of '/tmp/first.txt' # => 1
|
175
|
+
# fm.index_of '/tmp/woops.txt' # => false
|
176
|
+
def index_of(path)
|
177
|
+
watched.each_with_index {|watched,i| return i if watched.path == path}
|
178
|
+
false
|
179
|
+
end
|
180
|
+
|
181
|
+
# Spauns a child process that is looking for changes at every given interval.
|
182
|
+
# The interval is in seconds and defaults to 1 second.
|
183
|
+
#
|
184
|
+
# Example:
|
185
|
+
# fm = FileMonitor.new {|watched_item| puts 'do something when file is changed'}
|
186
|
+
# fm << @app_root + '/lib'
|
187
|
+
# fm.spawn # and now its doing its job...
|
188
|
+
def spawn(interval = 1)
|
189
|
+
if pid.nil?
|
190
|
+
@pid = fork {monitor interval}
|
191
|
+
Process.detach(pid)
|
192
|
+
|
193
|
+
Kernel.at_exit do
|
194
|
+
# sends the kill command unless the pid is not found on the system
|
195
|
+
Process.kill('HUP', pid) if process_running?
|
196
|
+
pid = nil
|
197
|
+
end
|
198
|
+
end
|
199
|
+
pid
|
200
|
+
end
|
201
|
+
alias_method :start, :spawn
|
202
|
+
# Stops a spawned FileMonitor instance. The FileMonitor will finish the the currnet iteration and exit gracefully. See Also: Halt
|
203
|
+
#
|
204
|
+
# Example:
|
205
|
+
# fm = FileMonitor.new {|watched_item| puts 'do something when file is changed'}
|
206
|
+
# fm.spawn # and now its doing its job...
|
207
|
+
# fm.stop
|
208
|
+
def stop()
|
209
|
+
# Send user defined signal USR1 to process. This is trapped in spauned processes and tells the process to Ctrl+C
|
210
|
+
# The user defined signial is sent as a safty percausion because the process id is not tracked through a pid file
|
211
|
+
# nor compared with the running command. The FileMonitor spaun will respond to the USR1 signal by exiting properly.*
|
212
|
+
if Fixnum === pid
|
213
|
+
Process.kill('USR1', pid)
|
214
|
+
Process.wait pid
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# Halts a spawned FileMonitor Instance. The current iteration will be halted in its tracks. See Also: stop
|
219
|
+
#
|
220
|
+
# Example:
|
221
|
+
# fm = FileMonitor.new {|watched_item| puts 'do something when file is changed'}
|
222
|
+
# fm.spawn # and now its doing its job...
|
223
|
+
# fm.stop
|
224
|
+
def halt()
|
225
|
+
if Fixnum === pid
|
226
|
+
Process.kill('USR2', pid)
|
227
|
+
Process.wait pid
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
private
|
232
|
+
# Returns true or false
|
233
|
+
def process_running?
|
234
|
+
pid ? !`ps -p #{pid}|grep ^[0-9]`.split().empty? : false
|
235
|
+
end
|
236
|
+
|
237
|
+
# Call callback and update digest with given key.
|
238
|
+
def respond_to_change(item, key)
|
239
|
+
if Proc === item.callback # Use watched instance callback if possible.
|
240
|
+
call item.callback, item, self
|
241
|
+
elsif Proc === self.callback # Use object level callback if possible.
|
242
|
+
call self.callback, item, self
|
243
|
+
end
|
244
|
+
item.digest(key)
|
245
|
+
end
|
246
|
+
|
247
|
+
# Calls a proc with no more then the needed arguments. This avoids an error when
|
248
|
+
# calling a proc with two arguments when the proc only handels one argument.
|
249
|
+
def call(proc, *args)
|
250
|
+
if proc.arity > 0
|
251
|
+
proc.call(*args[0..(proc.arity-1)])
|
252
|
+
else
|
253
|
+
proc.call
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
# Returns new digest if changed or false if unchanged.
|
258
|
+
def changed?(item)
|
259
|
+
d = digest(item.path) # get current digets
|
260
|
+
d == item.digest ? false : d # return new digest or false if unchanged
|
261
|
+
end
|
262
|
+
|
263
|
+
# Returns a string representation of the file state.
|
264
|
+
def digest(file)
|
265
|
+
begin
|
266
|
+
File.mtime(file).to_f
|
267
|
+
rescue Errno::ENOENT
|
268
|
+
nil
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
# Returns an array of all files in dirname recursively. Accepts an optional file name regexp filter.
|
273
|
+
# The following will find files ending in ".rb" recursively beginning in the working dir:
|
274
|
+
# files_recursive Dir.pwd, /\.rb$/
|
275
|
+
def files_recursive(dirname, file_name_regexp=/.*/)
|
276
|
+
paths = []
|
277
|
+
|
278
|
+
Find.find(dirname) do |path|
|
279
|
+
if FileTest.directory?(path)
|
280
|
+
Find.prune if File.basename(path)[0] == ?. # Don't look any further into directies beginning with a dot.
|
281
|
+
else
|
282
|
+
paths << path if file_name_regexp === path # Amend the return array if the file found matches the regexp
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
return paths
|
287
|
+
end
|
288
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module MonitoredItems #:nodoc:
|
2
|
+
class Nothing #:nodoc:
|
3
|
+
end
|
4
|
+
|
5
|
+
|
6
|
+
# A Store object is much like a hash but instead of getting an setting keys you get and set instance variables.
|
7
|
+
# The following examples are using methods used by the FileMonitor object, however the store object is not limited to these methods.
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
#
|
11
|
+
# s = MonitoredItems::Store.new
|
12
|
+
# s.path = '/tmp' # => "/tmp"
|
13
|
+
# s.callback Proc.new {'Hello World'} # => #<Proc:0x0000000100317508@(irb):18>
|
14
|
+
# s.callback.call # => "Hello World"
|
15
|
+
# s.path # => "/tmp"
|
16
|
+
#
|
17
|
+
# # OR send a hash when initializing the store
|
18
|
+
# i = MonitoredItems::Store.new({:path => '/tmp'})
|
19
|
+
# i.path #=> "/tmp"
|
20
|
+
class Store
|
21
|
+
# Supports initialization with a hash of methods & values. It makes no difference if
|
22
|
+
# the keys of the hash are strings or symbols, but they are case sensitive.
|
23
|
+
def initialize(hsh = {})
|
24
|
+
hsh.map {|k,v| self.send(k, v)} if Hash === hsh
|
25
|
+
end
|
26
|
+
|
27
|
+
# Gets or sets instance variables based upon the methods called.
|
28
|
+
def method_missing(mth, arg=Nothing)
|
29
|
+
# append the @ symbol and remove the equal symbol (if exists):
|
30
|
+
mth = "@#{mth}".chomp('=').to_sym
|
31
|
+
# get or set instnace variable
|
32
|
+
arg == Nothing ? self.instance_variable_get(mth) : self.instance_variable_set(mth, arg)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Return Hash representation of Store object
|
36
|
+
def to_h
|
37
|
+
Hash[*self.instance_variables.collect {|m| [m.slice(1..-1).to_sym, self.instance_variable_get(m)] }.flatten]
|
38
|
+
end
|
39
|
+
|
40
|
+
# Return inspection of hash representation of Store object
|
41
|
+
def to_s
|
42
|
+
self.to_h.inspect
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
# Time to add your specs!
|
4
|
+
# http://rspec.info/
|
5
|
+
describe "FileMonitor" do
|
6
|
+
before :all do
|
7
|
+
@app_root = File.expand_path(File.dirname(__FILE__)+"/..")
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should be able to instantize' do
|
11
|
+
fm = FileMonitor.new
|
12
|
+
FileMonitor.should === fm
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should support adding files' do
|
16
|
+
fm = FileMonitor.new
|
17
|
+
fm.watched.size.should == 0
|
18
|
+
(fm << @app_root + '/lib/FileMonitor.rb').should be_true # Should add single file using the '<<' method
|
19
|
+
fm.watched.size.should == 1
|
20
|
+
(fm.add @app_root + '/lib/FileMonitor/store.rb').should be_true # Should add single file using the 'add' method
|
21
|
+
fm.watched.size.should == 2
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should add files recursively when given a directory as a path' do
|
25
|
+
fm = FileMonitor.new
|
26
|
+
fm.watched.size.should == 0
|
27
|
+
fm.add(@app_root + '/lib').should be_true # Should add single file
|
28
|
+
fm.watched.size.should > 1
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should respond to index_of' do
|
32
|
+
fm = FileMonitor.new
|
33
|
+
fm.add(@app_root + '/lib').should be_true # Should add single file
|
34
|
+
fm.watched.size.should > 1
|
35
|
+
fm.index_of(@app_root + '/lib/FileMonitor.rb').should < fm.index_of(@app_root + '/lib/FileMonitor/store.rb')
|
36
|
+
fm.index_of(@app_root + '/lib/not_here.txt').should be_false
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should support a FileMonitor object level callback' do
|
40
|
+
fm = FileMonitor.new {true}
|
41
|
+
fm.callback.arity.should == -1
|
42
|
+
|
43
|
+
fm = FileMonitor.new {|watched_item| true}
|
44
|
+
fm.callback.arity.should == 1
|
45
|
+
|
46
|
+
fm = FileMonitor.new {|watched_item, file_monitor| true}
|
47
|
+
fm.callback.arity.should == 2
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should support adding files with individual callbacks' do
|
51
|
+
proc1 = Proc.new {return "hello from proc 1"}
|
52
|
+
proc2 = Proc.new {return "hello from proc 2"}
|
53
|
+
fm = FileMonitor.new &proc1
|
54
|
+
(fm << @app_root + '/lib/FileMonitor.rb').should be_true # Should add single file
|
55
|
+
(fm.add @app_root + '/lib/FileMonitor/store.rb', &proc2).should be_true # Should add single file
|
56
|
+
fm.watched.first.callback.should be_nil
|
57
|
+
fm.watched.last.callback.should == proc2
|
58
|
+
fm.watched.last.callback.should_not == proc1
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'should overwrite existing file watches with successive additions' do
|
62
|
+
fm = FileMonitor.new
|
63
|
+
fm.watched.size.should == 0
|
64
|
+
(fm << @app_root + '/lib/FileMonitor.rb').should be_true # Should add single file
|
65
|
+
(fm << @app_root + '/lib/FileMonitor.rb').should be_true # Should add single file
|
66
|
+
fm.watched.size.should == 1
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should spawn processes' do
|
70
|
+
fm = FileMonitor.new
|
71
|
+
fm << @app_root + '/lib'
|
72
|
+
pid = fm.spawn
|
73
|
+
pid.should_not == Process.pid
|
74
|
+
`ps -p #{fm.spawn} -o 'pid ppid'|grep ^[0-9]`.split().should == [pid.to_s, Process.pid.to_s]
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should stop spawn' do
|
78
|
+
fm = FileMonitor.new
|
79
|
+
fm << @app_root + '/lib'
|
80
|
+
pid = fm.spawn
|
81
|
+
`ps -p #{fm.spawn} -o 'pid ppid'|grep ^[0-9]`.split().should == [pid.to_s, Process.pid.to_s]
|
82
|
+
t=Time.now
|
83
|
+
fm.stop
|
84
|
+
puts "Time to stop: #{Time.now-t}"
|
85
|
+
`ps -p #{fm.spawn} -o 'pid ppid'|grep ^[0-9]`.split().should be_empty
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should halt spawn' do
|
89
|
+
fm = FileMonitor.new
|
90
|
+
fm << @app_root + '/lib'
|
91
|
+
pid = fm.spawn
|
92
|
+
`ps -p #{fm.spawn} -o 'pid ppid'|grep ^[0-9]`.split().should == [pid.to_s, Process.pid.to_s]
|
93
|
+
t=Time.now
|
94
|
+
fm.halt
|
95
|
+
puts "Time to halt: #{Time.now-t}"
|
96
|
+
`ps -p #{fm.spawn} -o 'pid ppid'|grep ^[0-9]`.split().should be_empty
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'should setup & spawn using the when_modified class method' do
|
100
|
+
fm = FileMonitor.when_modified(Dir.pwd, "/path/to/other/file.rb") {|watched_item| true}
|
101
|
+
fm.callback.arity.should == 1
|
102
|
+
fm.pid.should > 1
|
103
|
+
fm.pid.should == fm.spawn
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'should run callback on change' do
|
107
|
+
changed_files = nil
|
108
|
+
filename = @app_root + '/spec/temp.txt'
|
109
|
+
File.open(filename, 'w') {|f| f.write('hello') }
|
110
|
+
|
111
|
+
fm = FileMonitor.new() {|watched_item| changed_files = watched_item.path}
|
112
|
+
fm << filename
|
113
|
+
|
114
|
+
original = fm.watched.first.digest
|
115
|
+
sleep 1
|
116
|
+
File.open(filename, 'w') {|f| f.write('hello world') }
|
117
|
+
fm.process
|
118
|
+
fm.watched.first.digest.should_not == original
|
119
|
+
File.delete filename
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'should use item callback if possible, otherwise object callback' do
|
123
|
+
@global_callback = []
|
124
|
+
@files_callback = []
|
125
|
+
file1 = @app_root + '/spec/temp1.txt'
|
126
|
+
file2 = @app_root + '/spec/temp2.txt'
|
127
|
+
file3 = @app_root + '/spec/temp3.txt'
|
128
|
+
|
129
|
+
[file1,file2,file3].each {|file| File.open(file, 'w') {|f| f.write('hello') }}
|
130
|
+
|
131
|
+
fm = FileMonitor.new() {|watched_item| @global_callback << watched_item.path }
|
132
|
+
fm << file1
|
133
|
+
fm.add(file2) {|watched_item| @files_callback << watched_item.path }
|
134
|
+
fm << file3
|
135
|
+
|
136
|
+
sleep 1
|
137
|
+
[file1,file2,file3].each {|file| File.open(file, 'w') {|f| f.write('Hello World') }}
|
138
|
+
fm.process
|
139
|
+
|
140
|
+
@global_callback.should == [file1,file3]
|
141
|
+
@files_callback.should == [file2]
|
142
|
+
[file1,file2,file3].each {|file| File.delete(file)}
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'should have access to local, FileMonitor & Store contexts' do
|
146
|
+
filename = @app_root + '/spec/temp.txt'
|
147
|
+
File.open(filename, 'w') {|f| f.write('hello') }
|
148
|
+
|
149
|
+
fm = FileMonitor.new() do |watched_item, file_monitor|
|
150
|
+
MonitoredItems::Store.should === watched_item
|
151
|
+
Spec::Example::ExampleGroup::Subclass_1.should === self
|
152
|
+
FileMonitor.should === file_monitor
|
153
|
+
end
|
154
|
+
fm << filename
|
155
|
+
sleep 1
|
156
|
+
File.open(filename, 'w') {|f| f.write('hello world') }
|
157
|
+
fm.process
|
158
|
+
|
159
|
+
File.delete filename
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'should handel missing files without throwing errors' do
|
163
|
+
filename = @app_root + '/spec/temp.txt'
|
164
|
+
File.open(filename, 'w') {|f| f.write('hello') }
|
165
|
+
fm = FileMonitor.new() {true}
|
166
|
+
fm << filename
|
167
|
+
fm.process
|
168
|
+
File.delete(filename)
|
169
|
+
fm.process
|
170
|
+
fm.watched.first.digest.should be_nil
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'should be able to use a filter when specifying files' do
|
174
|
+
fm = FileMonitor.new() {|watched_item| changed_files = watched_item.path}
|
175
|
+
fm.add(@app_root, /\.rb$/) {|f| puts "Change found in: #{f.path}"}
|
176
|
+
fm.watched.each do |i|
|
177
|
+
File.split(i.path).last.should =~ /\.rb$/
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
# Time to add your specs!
|
4
|
+
# http://rspec.info/
|
5
|
+
describe "FileMonitor::Store" do
|
6
|
+
before :all do
|
7
|
+
@item = MonitoredItems::Store.new
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should store data and be accessable via mehtod() calls" do
|
11
|
+
pth = Dir.pwd
|
12
|
+
@item.file(pth).should == pth
|
13
|
+
@item.file.should == pth
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should store data and be accessable via mehtod()= calls" do
|
17
|
+
pth = Dir.pwd
|
18
|
+
(@item.file = pth).should == pth
|
19
|
+
@item.file.should == pth
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should store keys as methods & values as data when initialized a hash' do
|
23
|
+
i = MonitoredItems::Store.new({'string' => true, :symbol => true, :path => Dir.pwd})
|
24
|
+
i.string.should be_true
|
25
|
+
i.symbol.should be_true
|
26
|
+
i.path.should == Dir.pwd
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should know how to to_h' do
|
30
|
+
@item = MonitoredItems::Store.new
|
31
|
+
@item.test true
|
32
|
+
@item.hello = 'world'
|
33
|
+
@item.to_h.should == {:test => true, :hello => 'world'}
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should know how to to_s' do
|
37
|
+
@item = MonitoredItems::Store.new
|
38
|
+
@item.test true
|
39
|
+
@item.hello = 'world'
|
40
|
+
@item.to_s.should == '{:test=>true, :hello=>"world"}'
|
41
|
+
end
|
42
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/spec/spec_helper.rb
ADDED
data/tasks/rdoc.rake
ADDED
data/tasks/rspec.rake
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
begin
|
2
|
+
require 'spec'
|
3
|
+
rescue LoadError
|
4
|
+
require 'rubygems' unless ENV['NO_RUBYGEMS']
|
5
|
+
require 'spec'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'spec/rake/spectask'
|
9
|
+
rescue LoadError
|
10
|
+
puts <<-EOS
|
11
|
+
To use rspec for testing you must install rspec gem:
|
12
|
+
gem install rspec
|
13
|
+
EOS
|
14
|
+
exit(0)
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Run the specs under spec/models"
|
18
|
+
Spec::Rake::SpecTask.new do |t|
|
19
|
+
t.spec_opts = ['--options', "spec/spec.opts"]
|
20
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: joshaven-filemonitor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joshaven Potter
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-09-24 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: hoe
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.3.3
|
24
|
+
version:
|
25
|
+
description: ""
|
26
|
+
email:
|
27
|
+
- yourtech@gmail.com
|
28
|
+
executables: []
|
29
|
+
|
30
|
+
extensions: []
|
31
|
+
|
32
|
+
extra_rdoc_files:
|
33
|
+
- Manifest.txt
|
34
|
+
files:
|
35
|
+
- CHANGELOG
|
36
|
+
- Manifest.txt
|
37
|
+
- README.markdown
|
38
|
+
- lib/FileMonitor.rb
|
39
|
+
- lib/FileMonitor/store.rb
|
40
|
+
- spec/FileMonitor_spec.rb
|
41
|
+
- spec/MonitoredItem_spec.rb
|
42
|
+
- spec/spec.opts
|
43
|
+
- spec/spec_helper.rb
|
44
|
+
- tasks/rspec.rake
|
45
|
+
- tasks/rdoc.rake
|
46
|
+
has_rdoc: false
|
47
|
+
homepage: http://filemonitor.rubyforge.org/
|
48
|
+
licenses:
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options:
|
51
|
+
- --main
|
52
|
+
- README.markdown
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
version:
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: "0"
|
66
|
+
version:
|
67
|
+
requirements: []
|
68
|
+
|
69
|
+
rubyforge_project: filemonitor
|
70
|
+
rubygems_version: 1.3.5
|
71
|
+
signing_key:
|
72
|
+
specification_version: 3
|
73
|
+
summary: Calls a Proc when a watched file is changed.
|
74
|
+
test_files: []
|
75
|
+
|