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.
@@ -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