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,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