smparkes-watchr 0.5.7

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