smparkes-watchr 0.5.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,93 @@
1
+ module Watchr
2
+
3
+ class Refresh < Exception; end
4
+
5
+ # The controller contains the app's core logic.
6
+ #
7
+ # ===== Examples
8
+ #
9
+ # script = Watchr::Script.new(file)
10
+ # contrl = Watchr::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 = Watchr.handler.new
25
+ handler.add_observer self
26
+ Watchr.debug "using %s handler" % handler.class.name
27
+ handler
28
+ end
29
+ @handler
30
+ end
31
+
32
+ # Creates a controller object around given <tt>script</tt>
33
+ #
34
+ # ===== Parameters
35
+ # script<Script>:: The script object
36
+ #
37
+ def initialize(script)
38
+ @script = script
39
+ end
40
+
41
+ # Enters listening loop.
42
+ #
43
+ # Will block control flow until application is explicitly stopped/killed.
44
+ #
45
+ def run
46
+ @script.parse!
47
+ handler.listen(monitored_paths)
48
+ rescue Interrupt
49
+ end
50
+
51
+ # Callback for file events.
52
+ #
53
+ # Called while control flow in in listening loop. It will execute the
54
+ # file's corresponding action as defined in the script. If the file is the
55
+ # script itself, it will refresh its state to account for potential changes.
56
+ #
57
+ # ===== Parameters
58
+ # path<Pathname, String>:: path that triggered event
59
+ # event<Symbol>:: event type (ignored for now)
60
+ #
61
+ def update(path, event_type = nil)
62
+ path = Pathname(path).expand_path
63
+ # p path, event_type
64
+ if path == @script.path && ![ :load, :deleted, :moved ].include?(event_type)
65
+ @script.parse!
66
+ handler.refresh(monitored_paths)
67
+ else
68
+ begin
69
+ @script.call_action_for(path, event_type)
70
+ rescue Refresh => refresh
71
+ handler.refresh(monitored_paths)
72
+ end
73
+ end
74
+ end
75
+
76
+ # List of paths the script is monitoring.
77
+ #
78
+ # Basically this means all paths below current directoly recursivelly that
79
+ # match any of the rules' patterns, plus the script file.
80
+ #
81
+ # ===== Returns
82
+ # paths<Array[Pathname]>:: List of monitored paths
83
+ #
84
+ def monitored_paths
85
+ paths = Dir['**/*'].select do |path|
86
+ @script.rules.any? {|r| r.match(path) }
87
+ end
88
+ paths.push(@script.path).compact!
89
+ paths.map {|path| Pathname(path).expand_path }
90
+ end
91
+ end
92
+ end
93
+
@@ -0,0 +1,48 @@
1
+ require 'observer'
2
+
3
+ module Watchr
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,147 @@
1
+ require "eventmachine"
2
+
3
+ require 'watchr/event_handlers/unix'
4
+
5
+ module Watchr
6
+ module EventHandler
7
+ class EM
8
+
9
+ Watchr::EventHandler::Unix.defaults << self
10
+
11
+ ::EM.kqueue = true if ::EM.kqueue?
12
+
13
+ ::EM.error_handler do |e|
14
+ puts "EM recevied: #{e.message}"
15
+ puts e.backtrace
16
+ exit
17
+ end
18
+
19
+ include Base
20
+
21
+ module SingleFileWatcher #:nodoc:
22
+ class << self
23
+ # Stores a reference back to handler so we can call its #nofity
24
+ # method with file event info
25
+ attr_accessor :handler
26
+ end
27
+
28
+ def init first_time
29
+ # p "w", path, first_time,(first_time ? :load : :created)
30
+ update_reference_times
31
+ SingleFileWatcher.handler.notify(path, (first_time ? :load : :created) )
32
+ end
33
+
34
+ # File's path as a Pathname
35
+ def pathname
36
+ @pathname ||= Pathname(path)
37
+ end
38
+
39
+ def file_modified
40
+ SingleFileWatcher.handler.notify(path, type)
41
+ end
42
+
43
+ def file_moved
44
+ stop_watching
45
+ SingleFileWatcher.handler.notify(path, type)
46
+ end
47
+
48
+ def file_deleted
49
+ stop_watching
50
+ SingleFileWatcher.handler.notify(path, type)
51
+ end
52
+
53
+ # Callback. Called on file change event
54
+ # Delegates to Controller#update, passing in path and event type
55
+ def on_change
56
+ self.class.handler.notify(path, type)
57
+ update_reference_times unless type == :deleted
58
+ end
59
+
60
+ private
61
+
62
+ def update_reference_times
63
+ @reference_atime = pathname.atime
64
+ @reference_mtime = pathname.mtime
65
+ @reference_ctime = pathname.ctime
66
+ end
67
+
68
+ # Type of latest event.
69
+ #
70
+ # A single type is determined, even though more than one stat times may
71
+ # have changed on the file. The type is the first to match in the
72
+ # following hierarchy:
73
+ #
74
+ # :deleted, :modified (mtime), :accessed (atime), :changed (ctime)
75
+ #
76
+ # ===== Returns
77
+ # type<Symbol>:: latest event's type
78
+ #
79
+ def type
80
+ return :deleted if !pathname.exist?
81
+ return :modified if pathname.mtime > @reference_mtime
82
+ return :accessed if pathname.atime > @reference_atime
83
+ return :changed if pathname.ctime > @reference_ctime
84
+ end
85
+ end
86
+
87
+ def initialize
88
+ SingleFileWatcher.handler = self
89
+ @old_paths = []
90
+ @first_time = true
91
+ @watchers = {}
92
+ end
93
+
94
+ # Enters listening loop.
95
+ #
96
+ # Will block control flow until application is explicitly stopped/killed.
97
+ #
98
+ def listen(monitored_paths)
99
+ @monitored_paths = monitored_paths
100
+ ::EM.run do
101
+ attach
102
+ end
103
+ end
104
+
105
+ # Rebuilds file bindings.
106
+ #
107
+ # will detach all current bindings, and reattach the <tt>monitored_paths</tt>
108
+ #
109
+ def refresh(monitored_paths)
110
+ @monitored_paths = monitored_paths
111
+ attach
112
+ end
113
+
114
+ private
115
+
116
+ # Binds all <tt>monitored_paths</tt> to the listening loop.
117
+ def attach
118
+ @monitored_paths = @monitored_paths.uniq
119
+ new_paths = @monitored_paths - @old_paths
120
+ remove_paths = @old_paths - @monitored_paths
121
+ # p "want", @monitored_paths
122
+ # p "old", @old_paths
123
+ # p "new", new_paths
124
+ raise "hell" if @monitored_paths.length == 1
125
+ new_paths.each do |path|
126
+ ::EM.watch_file path.to_s, SingleFileWatcher do |watcher|
127
+ watcher.init @first_time
128
+ raise "hell" if @watchers[path]
129
+ @watchers[path] = watcher
130
+ end
131
+ end
132
+ remove_paths.each do |path|
133
+ watcher = @watchers[path]
134
+ raise "hell" if !watcher
135
+ watcher.stop
136
+ end
137
+ @old_paths = @monitored_paths
138
+ @first_time = false
139
+ end
140
+
141
+ # Unbinds all paths currently attached to listening loop.
142
+ def detach
143
+ @loop.watchers.each {|watcher| watcher.detach }
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,60 @@
1
+ module Watchr
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
@@ -0,0 +1,104 @@
1
+ require "rev"
2
+
3
+ require 'watchr/event_handlers/unix'
4
+
5
+ module Watchr
6
+ module EventHandler
7
+ class Rev
8
+
9
+ Watchr::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