wake 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.
- data/.gitignore +6 -0
- data/LICENSE +44 -0
- data/Manifest +30 -0
- data/README.rdoc +105 -0
- data/Rakefile +51 -0
- data/bin/wake +88 -0
- data/docs.wk +26 -0
- data/gem.wk +32 -0
- data/lib/wake.rb +109 -0
- data/lib/wake/controller.rb +112 -0
- data/lib/wake/event_handlers/base.rb +48 -0
- data/lib/wake/event_handlers/em.rb +232 -0
- data/lib/wake/event_handlers/portable.rb +60 -0
- data/lib/wake/event_handlers/rev.rb +104 -0
- data/lib/wake/event_handlers/unix.rb +25 -0
- data/lib/wake/script.rb +349 -0
- data/manifest.wk +70 -0
- data/specs.wk +38 -0
- data/test/README +11 -0
- data/test/event_handlers/test_base.rb +24 -0
- data/test/event_handlers/test_em.rb +162 -0
- data/test/event_handlers/test_portable.rb +142 -0
- data/test/event_handlers/test_rev.rb +162 -0
- data/test/test_controller.rb +130 -0
- data/test/test_helper.rb +60 -0
- data/test/test_script.rb +124 -0
- data/test/test_wake.rb +60 -0
- data/wake.gemspec +61 -0
- metadata +139 -0
@@ -0,0 +1,104 @@
|
|
1
|
+
require "rev"
|
2
|
+
|
3
|
+
require 'wake/event_handlers/unix'
|
4
|
+
|
5
|
+
module Wake
|
6
|
+
module EventHandler
|
7
|
+
class Rev
|
8
|
+
|
9
|
+
Wake::EventHandler::Unix.defaults << self
|
10
|
+
|
11
|
+
include Base
|
12
|
+
|
13
|
+
# Used by Rev. Wraps a monitored path, and Rev::Loop will call its
|
14
|
+
# callback on file events.
|
15
|
+
class SingleFileWatcher < ::Rev::StatWatcher #:nodoc:
|
16
|
+
class << self
|
17
|
+
# Stores a reference back to handler so we can call its #nofity
|
18
|
+
# method with file event info
|
19
|
+
attr_accessor :handler
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(path)
|
23
|
+
super
|
24
|
+
update_reference_times
|
25
|
+
end
|
26
|
+
|
27
|
+
# File's path as a Pathname
|
28
|
+
def pathname
|
29
|
+
@pathname ||= Pathname(@path)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Callback. Called on file change event
|
33
|
+
# Delegates to Controller#update, passing in path and event type
|
34
|
+
def on_change
|
35
|
+
self.class.handler.notify(path, type)
|
36
|
+
update_reference_times unless type == :deleted
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def update_reference_times
|
42
|
+
@reference_atime = pathname.atime
|
43
|
+
@reference_mtime = pathname.mtime
|
44
|
+
@reference_ctime = pathname.ctime
|
45
|
+
end
|
46
|
+
|
47
|
+
# Type of latest event.
|
48
|
+
#
|
49
|
+
# A single type is determined, even though more than one stat times may
|
50
|
+
# have changed on the file. The type is the first to match in the
|
51
|
+
# following hierarchy:
|
52
|
+
#
|
53
|
+
# :deleted, :modified (mtime), :accessed (atime), :changed (ctime)
|
54
|
+
#
|
55
|
+
# ===== Returns
|
56
|
+
# type<Symbol>:: latest event's type
|
57
|
+
#
|
58
|
+
def type
|
59
|
+
return :deleted if !pathname.exist?
|
60
|
+
return :modified if pathname.mtime > @reference_mtime
|
61
|
+
return :accessed if pathname.atime > @reference_atime
|
62
|
+
return :changed if pathname.ctime > @reference_ctime
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def initialize
|
67
|
+
SingleFileWatcher.handler = self
|
68
|
+
@loop = ::Rev::Loop.default
|
69
|
+
end
|
70
|
+
|
71
|
+
# Enters listening loop.
|
72
|
+
#
|
73
|
+
# Will block control flow until application is explicitly stopped/killed.
|
74
|
+
#
|
75
|
+
def listen(monitored_paths)
|
76
|
+
@monitored_paths = monitored_paths
|
77
|
+
attach
|
78
|
+
@loop.run
|
79
|
+
end
|
80
|
+
|
81
|
+
# Rebuilds file bindings.
|
82
|
+
#
|
83
|
+
# will detach all current bindings, and reattach the <tt>monitored_paths</tt>
|
84
|
+
#
|
85
|
+
def refresh(monitored_paths)
|
86
|
+
@monitored_paths = monitored_paths
|
87
|
+
detach
|
88
|
+
attach
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
# Binds all <tt>monitored_paths</tt> to the listening loop.
|
94
|
+
def attach
|
95
|
+
@monitored_paths.each {|path| SingleFileWatcher.new(path.to_s).attach(@loop) }
|
96
|
+
end
|
97
|
+
|
98
|
+
# Unbinds all paths currently attached to listening loop.
|
99
|
+
def detach
|
100
|
+
@loop.watchers.each {|watcher| watcher.detach }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Wake
|
2
|
+
module EventHandler
|
3
|
+
module Unix
|
4
|
+
|
5
|
+
@defaults = []
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_reader :defaults
|
9
|
+
|
10
|
+
def default
|
11
|
+
defaults.empty? &&
|
12
|
+
begin; require( 'wake/event_handlers/rev' );
|
13
|
+
rescue LoadError => e; end
|
14
|
+
defaults.empty? &&
|
15
|
+
begin require( 'wake/event_handlers/portable' );
|
16
|
+
defaults << Wake::EventHandler::Portable;
|
17
|
+
end
|
18
|
+
defaults[0]
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/wake/script.rb
ADDED
@@ -0,0 +1,349 @@
|
|
1
|
+
module Wake
|
2
|
+
|
3
|
+
class << self
|
4
|
+
def batches
|
5
|
+
@batches ||= {}
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
# A script object wraps a script file, and is used by a controller.
|
10
|
+
#
|
11
|
+
# ===== Examples
|
12
|
+
#
|
13
|
+
# path = Pathname.new('specs.wk')
|
14
|
+
# script = Wake::Script.new(path)
|
15
|
+
#
|
16
|
+
class Script
|
17
|
+
|
18
|
+
DEFAULT_EVENT_TYPE = :modified
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
class Batch
|
23
|
+
def initialize rule
|
24
|
+
@timer = nil
|
25
|
+
@rule = rule
|
26
|
+
@events = []
|
27
|
+
end
|
28
|
+
|
29
|
+
def call data, event, path
|
30
|
+
# $stderr.print "batch add #{data} #{event} #{path}\n"
|
31
|
+
if @timer
|
32
|
+
@timer.cancel
|
33
|
+
end
|
34
|
+
@timer = EM::Timer.new(0.001) do
|
35
|
+
deliver
|
36
|
+
Wake.batches.delete self
|
37
|
+
end
|
38
|
+
Wake.batches[self] = self
|
39
|
+
# p data, event, path
|
40
|
+
@events << [ data.to_a, event, path ]
|
41
|
+
@events.uniq!
|
42
|
+
# p @events
|
43
|
+
end
|
44
|
+
|
45
|
+
def deliver
|
46
|
+
events = @events
|
47
|
+
@timer = nil
|
48
|
+
@events = []
|
49
|
+
@rule.action.call [events]
|
50
|
+
events.each do |event|
|
51
|
+
Script.learn event[2]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Convenience type. Provides clearer and simpler access to rule properties.
|
57
|
+
#
|
58
|
+
# ===== Examples
|
59
|
+
#
|
60
|
+
# rule = script.watch('lib/.*\.rb') { 'ohaie' }
|
61
|
+
# rule.pattern #=> 'lib/.*\.rb'
|
62
|
+
# rule.action.call #=> 'ohaie'
|
63
|
+
#
|
64
|
+
Rule = Struct.new(:pattern, :event_types, :predicate, :options, :action, :batch)
|
65
|
+
|
66
|
+
class Rule
|
67
|
+
|
68
|
+
def call data, event, path
|
69
|
+
# $stderr.print "call #{data} #{event} #{path}\n"
|
70
|
+
if options[:batch]
|
71
|
+
self.batch ||= Batch.new self
|
72
|
+
batch.call data, event, path
|
73
|
+
else
|
74
|
+
res = nil
|
75
|
+
if action.arity == 1
|
76
|
+
res = action.call data
|
77
|
+
elsif action.arity == 2
|
78
|
+
res = action.call data, event
|
79
|
+
else
|
80
|
+
res = action.call data, event, path
|
81
|
+
end
|
82
|
+
Script.learn path
|
83
|
+
res
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def watch path
|
88
|
+
watch = nil
|
89
|
+
pattern = self.pattern
|
90
|
+
( pattern.class == String ) and ( pattern = Regexp.new pattern )
|
91
|
+
md = pattern.match(path)
|
92
|
+
if md
|
93
|
+
watch = self.predicate.nil? || self.predicate.call(md)
|
94
|
+
end
|
95
|
+
return watch
|
96
|
+
end
|
97
|
+
|
98
|
+
def match path
|
99
|
+
# $stderr.print("match #{path}\n")
|
100
|
+
pattern = self.pattern
|
101
|
+
( pattern.class == String ) and ( pattern = Regexp.new pattern )
|
102
|
+
# p path, pattern, pattern.match(path)
|
103
|
+
( md = pattern.match(path) ) &&
|
104
|
+
( self.predicate == nil || self.predicate.call(md) )
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
# TODO eval context
|
110
|
+
class API #:nodoc:
|
111
|
+
end
|
112
|
+
|
113
|
+
# Creates a script object for <tt>path</tt>.
|
114
|
+
#
|
115
|
+
# Does not parse the script. The controller knows when to parse the script.
|
116
|
+
#
|
117
|
+
# ===== Parameters
|
118
|
+
# path<Pathname>:: the path to the script
|
119
|
+
#
|
120
|
+
def initialize(path)
|
121
|
+
self.class.script = self
|
122
|
+
@path = path
|
123
|
+
@rules = []
|
124
|
+
@default_action = lambda {}
|
125
|
+
end
|
126
|
+
|
127
|
+
# Main script API method. Builds a new rule, binding a pattern to an action.
|
128
|
+
#
|
129
|
+
# Whenever a file is saved that matches a rule's <tt>pattern</tt>, its
|
130
|
+
# corresponding <tt>action</tt> is triggered.
|
131
|
+
#
|
132
|
+
# Patterns can be either a Regexp or a string. Because they always
|
133
|
+
# represent paths however, it's simpler to use strings. But remember to use
|
134
|
+
# single quotes (not double quotes), otherwise escape sequences will be
|
135
|
+
# parsed (for example "foo/bar\.rb" #=> "foo/bar.rb", notice "\." becomes
|
136
|
+
# "."), and won't be interpreted as the regexp you expect.
|
137
|
+
#
|
138
|
+
# Also note that patterns will be matched against relative paths (relative
|
139
|
+
# from current working directory).
|
140
|
+
#
|
141
|
+
# Actions, the blocks passed to <tt>watch</tt>, receive a MatchData object
|
142
|
+
# as argument. It will be populated with the whole matched string (md[0])
|
143
|
+
# as well as individual backreferences (md[1..n]). See MatchData#[]
|
144
|
+
# documentation for more details.
|
145
|
+
#
|
146
|
+
# ===== Examples
|
147
|
+
#
|
148
|
+
# # in script file
|
149
|
+
# watch( 'test/test_.*\.rb' ) {|md| system("ruby #{md[0]}") }
|
150
|
+
# watch( 'lib/(.*)\.rb' ) {|md| system("ruby test/test_#{md[1]}.rb") }
|
151
|
+
#
|
152
|
+
# With these two rules, wake will run any test file whenever it is itself
|
153
|
+
# changed (first rule), and will also run a corresponding test file
|
154
|
+
# whenever a lib file is changed (second rule).
|
155
|
+
#
|
156
|
+
# ===== Parameters
|
157
|
+
# pattern<~#match>:: pattern to match targetted paths
|
158
|
+
# event_types<Symbol|Array<Symbol>>::
|
159
|
+
# Rule will only match events of one of these type. Accepted types are :accessed,
|
160
|
+
# :modified, :changed, :delete and nil (any), where the first three
|
161
|
+
# correspond to atime, mtime and ctime respectively. Defaults to
|
162
|
+
# :modified.
|
163
|
+
# action<Block>:: action to trigger
|
164
|
+
#
|
165
|
+
# ===== Returns
|
166
|
+
# rule<Rule>:: rule created by the method
|
167
|
+
#
|
168
|
+
def watch(pattern, event_type = DEFAULT_EVENT_TYPE, predicate = nil, options = {}, &action)
|
169
|
+
event_types = Array(event_type)
|
170
|
+
@rules << Rule.new(pattern, event_types, predicate, options, action || @default_action)
|
171
|
+
@rules.last
|
172
|
+
end
|
173
|
+
|
174
|
+
# Convenience method. Define a default action to be triggered when a rule
|
175
|
+
# has none specified.
|
176
|
+
#
|
177
|
+
# ===== Examples
|
178
|
+
#
|
179
|
+
# # in script file
|
180
|
+
#
|
181
|
+
# default_action { system('rake --silent rdoc') }
|
182
|
+
#
|
183
|
+
# watch( 'lib/.*\.rb' )
|
184
|
+
# watch( 'README.rdoc' )
|
185
|
+
# watch( 'TODO.txt' )
|
186
|
+
# watch( 'LICENSE' )
|
187
|
+
#
|
188
|
+
# # equivalent to:
|
189
|
+
#
|
190
|
+
# watch( 'lib/.*\.rb' ) { system('rake --silent rdoc') }
|
191
|
+
# watch( 'README.rdoc' ) { system('rake --silent rdoc') }
|
192
|
+
# watch( 'TODO.txt' ) { system('rake --silent rdoc') }
|
193
|
+
# watch( 'LICENSE' ) { system('rake --silent rdoc') }
|
194
|
+
#
|
195
|
+
def default_action(&action)
|
196
|
+
@default_action = action
|
197
|
+
end
|
198
|
+
|
199
|
+
# Eval content of script file.
|
200
|
+
#--
|
201
|
+
# TODO fix script file not found error
|
202
|
+
def parse!
|
203
|
+
Wake.debug('loading script file %s' % @path.to_s.inspect)
|
204
|
+
|
205
|
+
reset
|
206
|
+
|
207
|
+
# Some editors do delete/rename. Even when they don't some events come very fast ...
|
208
|
+
# and editor could do a trunc/write. If you look after the trunc, before the write, well,
|
209
|
+
# things aren't pretty.
|
210
|
+
|
211
|
+
# Should probably use a watchdog timer that gets reset on every change and then only fire actions
|
212
|
+
# after the watchdog timer fires without get reset ..
|
213
|
+
|
214
|
+
v = nil
|
215
|
+
(1..10).each do
|
216
|
+
old_v = v
|
217
|
+
v = @path.read
|
218
|
+
break if v != "" && v == old_v
|
219
|
+
sleep(0.3)
|
220
|
+
end
|
221
|
+
|
222
|
+
instance_eval(@path.read)
|
223
|
+
|
224
|
+
rescue Errno::ENOENT
|
225
|
+
# TODO figure out why this is happening. still can't reproduce
|
226
|
+
Wake.debug('script file "not found". wth')
|
227
|
+
sleep(0.3) #enough?
|
228
|
+
instance_eval(@path.read)
|
229
|
+
end
|
230
|
+
|
231
|
+
class << self
|
232
|
+
attr_accessor :script, :handler
|
233
|
+
def learn path
|
234
|
+
script.depends_on(path).each do |p|
|
235
|
+
# $stderr.print "#{path} depends on #{p}\n"
|
236
|
+
handler.add Pathname(p)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def depends_on path
|
242
|
+
[]
|
243
|
+
end
|
244
|
+
|
245
|
+
def depended_on_by path
|
246
|
+
[]
|
247
|
+
end
|
248
|
+
|
249
|
+
# Find an action corresponding to a path and event type. The returned
|
250
|
+
# action is actually a wrapper around the rule's action, with the
|
251
|
+
# match_data prepopulated.
|
252
|
+
#
|
253
|
+
# ===== Params
|
254
|
+
# path<Pathnane,String>:: Find action that correspond to this path.
|
255
|
+
# event_type<Symbol>:: Find action only if rule's event if of this type.
|
256
|
+
#
|
257
|
+
# ===== Examples
|
258
|
+
#
|
259
|
+
# script.watch( 'test/test_.*\.rb' ) {|md| "ruby #{md[0]}" }
|
260
|
+
# script.action_for('test/test_wake.rb').call #=> "ruby test/test_wake.rb"
|
261
|
+
#
|
262
|
+
def call_action_for(path, event_type = DEFAULT_EVENT_TYPE)
|
263
|
+
# $stderr.print "caf #{path} #{event_type}\n";
|
264
|
+
pathname = path
|
265
|
+
path = rel_path(path).to_s
|
266
|
+
# $stderr.print "dob #{path} #{depended_on_by(path).join(' ')}\n"
|
267
|
+
string = nil
|
268
|
+
begin
|
269
|
+
string = Pathname(pathname).realpath.to_s
|
270
|
+
rescue Errno::ENOENT; end
|
271
|
+
string && depended_on_by(string).each do |dependence|
|
272
|
+
# $stderr.print "for caf #{Pathname(pathname).realpath.to_s}\n";
|
273
|
+
call_action_for(dependence, event_type)
|
274
|
+
end
|
275
|
+
rules_for(path).each do |rule|
|
276
|
+
# begin
|
277
|
+
types = rule.event_types
|
278
|
+
!types.empty? or types = [ nil ]
|
279
|
+
types.each do |rule_event_type|
|
280
|
+
# $stderr.print "#{rule.inspect} #{rule_event_type.inspect} #{event_type.inspect} #{path} #{rule_event_type == event_type}\n"
|
281
|
+
if ( rule_event_type.nil? && ( event_type != :load ) ) || ( rule_event_type == event_type )
|
282
|
+
data = path.match(rule.pattern)
|
283
|
+
# $stderr.print "data #{data}\n"
|
284
|
+
return rule.call(data, event_type, pathname)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
# rescue Exception => e; $stderr.print "oops #{e}\n"; raise; end
|
288
|
+
end
|
289
|
+
# $stderr.print "no path for #{path}\n"
|
290
|
+
nil
|
291
|
+
end
|
292
|
+
|
293
|
+
# Collection of all patterns defined in script.
|
294
|
+
#
|
295
|
+
# ===== Returns
|
296
|
+
# patterns<String, Regexp>:: all patterns
|
297
|
+
#
|
298
|
+
def patterns
|
299
|
+
#@rules.every.pattern
|
300
|
+
@rules.map {|r| r.pattern }
|
301
|
+
end
|
302
|
+
|
303
|
+
def rules
|
304
|
+
@rules
|
305
|
+
end
|
306
|
+
|
307
|
+
# Path to the script file
|
308
|
+
#
|
309
|
+
# ===== Returns
|
310
|
+
# path<Pathname>:: path to script file
|
311
|
+
#
|
312
|
+
def path
|
313
|
+
Pathname(@path.respond_to?(:to_path) ? @path.to_path : @path.to_s).expand_path
|
314
|
+
end
|
315
|
+
|
316
|
+
private
|
317
|
+
|
318
|
+
# Rules corresponding to a given path, in reversed order of precedence
|
319
|
+
# (latest one is most inportant).
|
320
|
+
#
|
321
|
+
# ===== Parameters
|
322
|
+
# path<Pathname, String>:: path to look up rule for
|
323
|
+
#
|
324
|
+
# ===== Returns
|
325
|
+
# rules<Array(Rule)>:: rules corresponding to <tt>path</tt>
|
326
|
+
#
|
327
|
+
def rules_for(path)
|
328
|
+
@rules.reverse.select do |rule| path.match(rule.pattern) end
|
329
|
+
end
|
330
|
+
|
331
|
+
# Make a path relative to current working directory.
|
332
|
+
#
|
333
|
+
# ===== Parameters
|
334
|
+
# path<Pathname, String>:: absolute or relative path
|
335
|
+
#
|
336
|
+
# ===== Returns
|
337
|
+
# path<Pathname>:: relative path, from current working directory.
|
338
|
+
#
|
339
|
+
def rel_path(path)
|
340
|
+
Pathname(path).expand_path.relative_path_from(Pathname(Dir.pwd))
|
341
|
+
end
|
342
|
+
|
343
|
+
# Reset script state
|
344
|
+
def reset
|
345
|
+
@default_action = lambda {}
|
346
|
+
@rules.clear
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|