wake 0.1.0

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