wake 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,112 @@
|
|
1
|
+
module Wake
|
2
|
+
|
3
|
+
class Refresh < Exception; end
|
4
|
+
|
5
|
+
# The controller contains the app's core logic.
|
6
|
+
#
|
7
|
+
# ===== Examples
|
8
|
+
#
|
9
|
+
# script = Wake::Script.new(file)
|
10
|
+
# contrl = Wake::Controller.new(script)
|
11
|
+
# contrl.run
|
12
|
+
#
|
13
|
+
# Calling <tt>#run</tt> will enter the listening loop, and from then on every
|
14
|
+
# file event will trigger its corresponding action defined in <tt>script</tt>
|
15
|
+
#
|
16
|
+
# The controller also automatically adds the script's file itself to its list
|
17
|
+
# of monitored files and will detect any changes to it, providing on the fly
|
18
|
+
# updates of defined rules.
|
19
|
+
#
|
20
|
+
class Controller
|
21
|
+
|
22
|
+
def handler
|
23
|
+
@handler ||= begin
|
24
|
+
handler = Wake.handler.new
|
25
|
+
handler.add_observer self
|
26
|
+
Wake.debug "using %s handler" % handler.class.name
|
27
|
+
Script.handler = handler
|
28
|
+
handler
|
29
|
+
end
|
30
|
+
@handler
|
31
|
+
end
|
32
|
+
|
33
|
+
# Creates a controller object around given <tt>script</tt>
|
34
|
+
#
|
35
|
+
# ===== Parameters
|
36
|
+
# script<Script>:: The script object
|
37
|
+
#
|
38
|
+
def initialize(script)
|
39
|
+
@script = script
|
40
|
+
end
|
41
|
+
|
42
|
+
# Enters listening loop.
|
43
|
+
#
|
44
|
+
# Will block control flow until application is explicitly stopped/killed.
|
45
|
+
#
|
46
|
+
def run
|
47
|
+
@script.parse!
|
48
|
+
handler.listen(monitored_paths)
|
49
|
+
rescue Interrupt
|
50
|
+
end
|
51
|
+
|
52
|
+
# Callback for file events.
|
53
|
+
#
|
54
|
+
# Called while control flow in in listening loop. It will execute the
|
55
|
+
# file's corresponding action as defined in the script. If the file is the
|
56
|
+
# script itself, it will refresh its state to account for potential changes.
|
57
|
+
#
|
58
|
+
# ===== Parameters
|
59
|
+
# path<Pathname, String>:: path that triggered event
|
60
|
+
# event<Symbol>:: event type (ignored for now)
|
61
|
+
#
|
62
|
+
def update(path, event_type = nil)
|
63
|
+
path = Pathname(path).expand_path
|
64
|
+
# p path, event_type
|
65
|
+
if path == @script.path && ![ :load, :deleted, :moved ].include?(event_type)
|
66
|
+
@script.parse!
|
67
|
+
handler.refresh(monitored_paths)
|
68
|
+
else
|
69
|
+
begin
|
70
|
+
@script.call_action_for(path, event_type)
|
71
|
+
rescue Refresh => refresh
|
72
|
+
handler.refresh(monitored_paths)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# List of paths the script is monitoring.
|
78
|
+
#
|
79
|
+
# Basically this means all paths below current directoly recursivelly that
|
80
|
+
# match any of the rules' patterns, plus the script file.
|
81
|
+
#
|
82
|
+
# ===== Returns
|
83
|
+
# paths<Array[Pathname]>:: List of monitored paths
|
84
|
+
#
|
85
|
+
def monitored_paths
|
86
|
+
paths = Dir['**/*'].select do |path|
|
87
|
+
watch = false
|
88
|
+
@script.rules.reverse.each do |r|
|
89
|
+
rule_watches = r.watch(path)
|
90
|
+
if false
|
91
|
+
$stderr.print "watch ", path, " ", rule_watches, "\n"
|
92
|
+
end
|
93
|
+
next if rule_watches.nil?
|
94
|
+
watch = rule_watches
|
95
|
+
break
|
96
|
+
end
|
97
|
+
watch
|
98
|
+
end
|
99
|
+
paths.each do |path|
|
100
|
+
# $stderr.print "lookup #{path}\n"
|
101
|
+
@script.depends_on(path).each do |dependence|
|
102
|
+
# $stderr.print "add #{dependence} for #{path}\n"
|
103
|
+
paths << dependence
|
104
|
+
end
|
105
|
+
end
|
106
|
+
paths.push(@script.path).compact!
|
107
|
+
paths.uniq!
|
108
|
+
# $stderr.print "watch #{paths.map {|path| Pathname(path).expand_path }.join(' ')}\n"
|
109
|
+
paths.map {|path| Pathname(path).expand_path }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'observer'
|
2
|
+
|
3
|
+
module Wake
|
4
|
+
module EventHandler
|
5
|
+
class AbstractMethod < Exception #:nodoc:
|
6
|
+
end
|
7
|
+
|
8
|
+
# Base functionality mixin meant to be included in specific event handlers.
|
9
|
+
module Base
|
10
|
+
include Observable
|
11
|
+
|
12
|
+
# Notify that a file was modified.
|
13
|
+
#
|
14
|
+
# ===== Parameters
|
15
|
+
# path<Pathname, String>:: full path or path relative to current working directory
|
16
|
+
# event_type<Symbol>:: event type.
|
17
|
+
#--
|
18
|
+
# #changed and #notify_observers are Observable methods
|
19
|
+
def notify(path, event_type = nil)
|
20
|
+
changed(true)
|
21
|
+
notify_observers(path, event_type)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Begin watching given paths and enter listening loop. Called by the controller.
|
25
|
+
#
|
26
|
+
# Abstract method
|
27
|
+
#
|
28
|
+
# ===== Parameters
|
29
|
+
# monitored_paths<Array(Pathname)>:: list of paths the application is currently monitoring.
|
30
|
+
#
|
31
|
+
def listen(monitored_paths)
|
32
|
+
raise AbstractMethod
|
33
|
+
end
|
34
|
+
|
35
|
+
# Called by the controller when the list of paths monitored by wantchr
|
36
|
+
# has changed. It should refresh the list of paths being watched.
|
37
|
+
#
|
38
|
+
# Abstract method
|
39
|
+
#
|
40
|
+
# ===== Parameters
|
41
|
+
# monitored_paths<Array(Pathname)>:: list of paths the application is currently monitoring.
|
42
|
+
#
|
43
|
+
def refresh(monitored_paths)
|
44
|
+
raise AbstractMethod
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,232 @@
|
|
1
|
+
require "eventmachine"
|
2
|
+
|
3
|
+
require 'wake/event_handlers/unix'
|
4
|
+
require 'wake/event_handlers/base'
|
5
|
+
|
6
|
+
module Wake
|
7
|
+
module EventHandler
|
8
|
+
class EM
|
9
|
+
|
10
|
+
Wake::EventHandler::Unix.defaults << self
|
11
|
+
|
12
|
+
::EM.kqueue = true if ::EM.kqueue?
|
13
|
+
|
14
|
+
::EM.error_handler do |e|
|
15
|
+
puts "EM recevied: #{e.message}"
|
16
|
+
puts e.backtrace
|
17
|
+
exit
|
18
|
+
end
|
19
|
+
|
20
|
+
include Base
|
21
|
+
|
22
|
+
module SingleFileWatcher #:nodoc:
|
23
|
+
class << self
|
24
|
+
# Stores a reference back to handler so we can call its #nofity
|
25
|
+
# method with file event info
|
26
|
+
attr_accessor :handler
|
27
|
+
end
|
28
|
+
|
29
|
+
def init first_time, event
|
30
|
+
# p "w", path, first_time,(first_time ? :load : :created)
|
31
|
+
# $stderr.puts "#{signature}: #{pathname}"
|
32
|
+
update_reference_times
|
33
|
+
# FIX: doesn't pass events
|
34
|
+
if !event
|
35
|
+
SingleFileWatcher.handler.notify(pathname, (first_time ? :load : :created) )
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# File's path as a Pathname
|
40
|
+
def pathname
|
41
|
+
@pathname ||= Pathname(path)
|
42
|
+
end
|
43
|
+
|
44
|
+
def file_modified
|
45
|
+
# p "mod", pathname, type
|
46
|
+
SingleFileWatcher.handler.notify(pathname, type)
|
47
|
+
update_reference_times
|
48
|
+
end
|
49
|
+
|
50
|
+
def file_moved
|
51
|
+
# p "mov", pathname
|
52
|
+
SingleFileWatcher.handler.forget self, pathname
|
53
|
+
begin
|
54
|
+
# $stderr.puts "stop.fm #{signature}: #{pathname}"
|
55
|
+
stop_watching
|
56
|
+
rescue Exception => e
|
57
|
+
$stderr.puts "exception while attempting to stop_watching in file_moved: #{e}"
|
58
|
+
end
|
59
|
+
SingleFileWatcher.handler.notify(pathname, type)
|
60
|
+
end
|
61
|
+
|
62
|
+
def file_deleted
|
63
|
+
# p "del", pathname
|
64
|
+
# $stderr.puts "stop.fd #{signature}: #{pathname} #{type}"
|
65
|
+
SingleFileWatcher.handler.forget self, pathname
|
66
|
+
SingleFileWatcher.handler.notify(pathname, :deleted)
|
67
|
+
if type == :modified
|
68
|
+
# There's a race condition here ... the directory should have gotten mod'ed, but we'll get the
|
69
|
+
# delete after the directory scan, so we won't watch the new file. This isn't the cleanest way to
|
70
|
+
# handle this, but should work for now ...
|
71
|
+
SingleFileWatcher.handler.watch pathname
|
72
|
+
else
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def stop
|
77
|
+
# p "stop", pathname
|
78
|
+
begin
|
79
|
+
# $stderr.puts "stop.s #{signature}: #{pathname}"
|
80
|
+
stop_watching
|
81
|
+
rescue Exception => e
|
82
|
+
$stderr.puts "exception while attempting to stop_watching in stop: #{e}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def update_reference_times
|
89
|
+
begin
|
90
|
+
@reference_atime = pathname.atime
|
91
|
+
@reference_mtime = pathname.mtime
|
92
|
+
@reference_ctime = pathname.ctime
|
93
|
+
rescue Exception; end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Type of latest event.
|
97
|
+
#
|
98
|
+
# A single type is determined, even though more than one stat times may
|
99
|
+
# have changed on the file. The type is the first to match in the
|
100
|
+
# following hierarchy:
|
101
|
+
#
|
102
|
+
# :deleted, :modified (mtime), :accessed (atime), :changed (ctime)
|
103
|
+
#
|
104
|
+
# ===== Returns
|
105
|
+
# type<Symbol>:: latest event's type
|
106
|
+
#
|
107
|
+
def type
|
108
|
+
return :deleted if !pathname.exist?
|
109
|
+
return :modified if pathname.mtime > @reference_mtime
|
110
|
+
return :accessed if pathname.atime > @reference_atime
|
111
|
+
return :changed if pathname.ctime > @reference_ctime
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def initialize
|
116
|
+
SingleFileWatcher.handler = self
|
117
|
+
@old_paths = []
|
118
|
+
@first_time = true
|
119
|
+
@watchers = {}
|
120
|
+
@attaching = false
|
121
|
+
end
|
122
|
+
|
123
|
+
# Enters listening loop.
|
124
|
+
#
|
125
|
+
# Will block control flow until application is explicitly stopped/killed.
|
126
|
+
#
|
127
|
+
def listen(monitored_paths)
|
128
|
+
# FIX ... make more generic (handle at a higher level ...)
|
129
|
+
while true
|
130
|
+
@monitored_paths = monitored_paths
|
131
|
+
@old_paths = []
|
132
|
+
@first_time = true
|
133
|
+
@watchers = {}
|
134
|
+
::EM.run do
|
135
|
+
attach
|
136
|
+
if Wake.options.once
|
137
|
+
Wake.batches.each do |k,v|
|
138
|
+
k.deliver
|
139
|
+
end
|
140
|
+
return
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Rebuilds file bindings.
|
147
|
+
#
|
148
|
+
# will detach all current bindings, and reattach the <tt>monitored_paths</tt>
|
149
|
+
#
|
150
|
+
def refresh(monitored_paths)
|
151
|
+
@monitored_paths = monitored_paths
|
152
|
+
attach
|
153
|
+
end
|
154
|
+
|
155
|
+
def forget connection, path
|
156
|
+
if @watchers[path] != connection
|
157
|
+
$stderr.puts \
|
158
|
+
"warning: no/wrong watcher to forget for #{path}: #{@watchers[path]} vs #{connection}"
|
159
|
+
end
|
160
|
+
@watchers.delete path
|
161
|
+
raise "hell: #{path}" if !@old_paths.include? Pathname(path)
|
162
|
+
@old_paths.delete Pathname(path)
|
163
|
+
end
|
164
|
+
|
165
|
+
def watch path, event = nil
|
166
|
+
begin
|
167
|
+
# p "watch", path, @first_time
|
168
|
+
::EM.watch_file path.to_s, SingleFileWatcher do |watcher|
|
169
|
+
watcher.init @first_time, event
|
170
|
+
@watchers[path] = watcher
|
171
|
+
end
|
172
|
+
@old_paths << path
|
173
|
+
rescue Errno::ENOENT => e
|
174
|
+
$stderr.puts e
|
175
|
+
rescue Exception => e
|
176
|
+
$stderr.puts e
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def add path
|
181
|
+
# $stderr.print "new #{path}\n"
|
182
|
+
if false && !@monitored_paths.include?( path )
|
183
|
+
$stderr.print "new #{path}\n"
|
184
|
+
end
|
185
|
+
@monitored_paths << path
|
186
|
+
# $stderr.print "add #{path.inspect}\n"
|
187
|
+
attach :dependence
|
188
|
+
end
|
189
|
+
|
190
|
+
private
|
191
|
+
|
192
|
+
# Binds all <tt>monitored_paths</tt> to the listening loop.
|
193
|
+
def attach event = nil
|
194
|
+
return if @attaching
|
195
|
+
@attaching = true
|
196
|
+
new_paths = nil
|
197
|
+
remove_paths = nil
|
198
|
+
begin
|
199
|
+
@monitored_paths = @monitored_paths.uniq
|
200
|
+
new_paths = @monitored_paths - @old_paths
|
201
|
+
remove_paths = @old_paths - @monitored_paths
|
202
|
+
# p "want", @monitored_paths
|
203
|
+
# p "old", @old_paths
|
204
|
+
# p "new", new_paths
|
205
|
+
raise "hell" if @monitored_paths.length == 1
|
206
|
+
new_paths.each do |path|
|
207
|
+
if @watchers[path]
|
208
|
+
$stderr.puts "warning: replacing (ignoring) watcher for #{path}"
|
209
|
+
@watchers[path].stop
|
210
|
+
end
|
211
|
+
watch path, event
|
212
|
+
end
|
213
|
+
remove_paths.each do |path|
|
214
|
+
watcher = @watchers[path]
|
215
|
+
watcher.stop if watcher
|
216
|
+
@watchers.delete path
|
217
|
+
end
|
218
|
+
@old_paths = @monitored_paths.dup
|
219
|
+
# $stderr.print "#{new_paths} #{remove_paths}\n";
|
220
|
+
end while !new_paths.empty? and !remove_paths.empty?
|
221
|
+
@first_time = false
|
222
|
+
@attaching = false
|
223
|
+
end
|
224
|
+
|
225
|
+
# Unbinds all paths currently attached to listening loop.
|
226
|
+
def detach
|
227
|
+
@loop.watchers.each {|watcher| watcher.detach }
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Wake
|
2
|
+
module EventHandler
|
3
|
+
class Portable
|
4
|
+
include Base
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@reference_mtime = @reference_atime = @reference_ctime = Time.now
|
8
|
+
end
|
9
|
+
|
10
|
+
# Enters listening loop.
|
11
|
+
#
|
12
|
+
# Will block control flow until application is explicitly stopped/killed.
|
13
|
+
#
|
14
|
+
def listen(monitored_paths)
|
15
|
+
@monitored_paths = monitored_paths
|
16
|
+
loop { trigger; sleep(1) }
|
17
|
+
end
|
18
|
+
|
19
|
+
# See if an event occured, and if so notify observers.
|
20
|
+
def trigger #:nodoc:
|
21
|
+
path, type = detect_event
|
22
|
+
notify(path, type) unless path.nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
# Update list of monitored paths.
|
26
|
+
def refresh(monitored_paths)
|
27
|
+
@monitored_paths = monitored_paths
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Verify mtimes of monitored files.
|
33
|
+
#
|
34
|
+
# If the latest mtime is more recent than the reference mtime, return
|
35
|
+
# that file's path.
|
36
|
+
#
|
37
|
+
# ===== Returns
|
38
|
+
# path and type of event if event occured, nil otherwise
|
39
|
+
#
|
40
|
+
#--
|
41
|
+
# OPTIMIZE, REFACTOR
|
42
|
+
def detect_event
|
43
|
+
@monitored_paths.each do |path|
|
44
|
+
return [path, :deleted] unless path.exist?
|
45
|
+
end
|
46
|
+
|
47
|
+
mtime_path = @monitored_paths.max {|a,b| a.mtime <=> b.mtime }
|
48
|
+
atime_path = @monitored_paths.max {|a,b| a.atime <=> b.atime }
|
49
|
+
ctime_path = @monitored_paths.max {|a,b| a.ctime <=> b.ctime }
|
50
|
+
|
51
|
+
if mtime_path.mtime > @reference_mtime then @reference_mtime = mtime_path.mtime; [mtime_path, :modified]
|
52
|
+
elsif atime_path.atime > @reference_atime then @reference_atime = atime_path.atime; [atime_path, :accessed]
|
53
|
+
elsif ctime_path.ctime > @reference_ctime then @reference_ctime = ctime_path.ctime; [ctime_path, :changed ]
|
54
|
+
else; nil; end
|
55
|
+
rescue Errno::ENOENT => e
|
56
|
+
retry
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|