wake 0.1.0

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