observr 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,87 @@
1
+ module Observr
2
+
3
+ # The controller contains the app's core logic.
4
+ #
5
+ # @example
6
+ #
7
+ # script = Observr::Script.new(file)
8
+ # contrl = Observr::Controller.new(script, Observr.handler.new)
9
+ # contrl.run
10
+ #
11
+ # # Calling `run` will enter the listening loop, and from then on every
12
+ # # file event will trigger its corresponding action defined in `script`
13
+ #
14
+ # # The controller also automatically adds the script's file to its list of
15
+ # # monitored files and will detect any changes to it, providing on the fly
16
+ # # updates of defined rules.
17
+ #
18
+ class Controller
19
+
20
+ # Create a controller object around given `script`
21
+ #
22
+ # @param [Script] script
23
+ # The script object
24
+ #
25
+ # @param [EventHandler::Base] handler
26
+ # The filesystem event handler
27
+ #
28
+ # @see Observr::Script
29
+ # @see Observr.handler
30
+ #
31
+ def initialize(script, handler)
32
+ @script, @handler = script, handler
33
+ @handler.add_observer(self)
34
+
35
+ Observr.debug "using %s handler" % handler.class.name
36
+ end
37
+
38
+ # Enter listening loop. Will block control flow until application is
39
+ # explicitly stopped/killed.
40
+ def run
41
+ @script.parse!
42
+ @handler.listen(monitored_paths)
43
+ rescue Interrupt
44
+ end
45
+
46
+ # Callback for file events
47
+ #
48
+ # Called while control flow is in listening loop. It will execute the
49
+ # file's corresponding action as defined in the script. If the file is the
50
+ # script itself, it will refresh its state to account for potential changes.
51
+ #
52
+ # @param [Pathname, String] path
53
+ # path that triggered the event
54
+ #
55
+ # @param [Symbol] event
56
+ # event type
57
+ #
58
+ def update(path, event_type = nil)
59
+ path = Pathname(path).expand_path
60
+
61
+ Observr.debug("received #{event_type.inspect} event for #{path.relative_path_from(Pathname(Dir.pwd))}")
62
+ if path == @script.path && event_type != :accessed
63
+ @script.parse!
64
+ @handler.refresh(monitored_paths)
65
+ else
66
+ @script.action_for(path, event_type).call
67
+ end
68
+ end
69
+
70
+ # List of paths the script is monitoring.
71
+ #
72
+ # Basically this means all paths below current directoly recursivelly that
73
+ # match any of the rules' patterns, plus the script file.
74
+ #
75
+ # @return [Array<Pathname>]
76
+ # list of all monitored paths
77
+ #
78
+ def monitored_paths
79
+ paths = Dir['**/*'].select do |path|
80
+ @script.patterns.any? {|p| path.match(p) }
81
+ end
82
+ paths.push(@script.path).compact!
83
+ paths.map {|path| Pathname(path).expand_path }
84
+ end
85
+ end
86
+ end
87
+
@@ -0,0 +1,57 @@
1
+ require 'observer'
2
+
3
+ module Observr
4
+ module EventHandler
5
+
6
+ # @private
7
+ class AbstractMethod < Exception; end
8
+
9
+ # Base functionality mixin, meant to be included in specific event handlers.
10
+ #
11
+ # @abstract
12
+ module Base
13
+ include Observable
14
+
15
+ # Notify that a file was modified.
16
+ #
17
+ # @param [Pathname, String] path
18
+ # full path or path relative to current working directory
19
+ #
20
+ # @param [Symbol] event
21
+ # event type.
22
+ #
23
+ # @return [undefined]
24
+ #
25
+ def notify(path, event_type = nil)
26
+ changed(true)
27
+ notify_observers(path, event_type)
28
+ end
29
+
30
+ # Begin watching given paths and enter listening loop. Called by the
31
+ # controller.
32
+ #
33
+ # @param [Array<Pathname>] monitored_paths
34
+ # list of paths the application is currently monitoring.
35
+ #
36
+ # @return [undefined]
37
+ #
38
+ # @abstract
39
+ def listen(monitored_paths)
40
+ raise AbstractMethod
41
+ end
42
+
43
+ # Called by the controller when the list of paths monitored by wantchr
44
+ # has changed. It should refresh the list of paths being watched.
45
+ #
46
+ # @param [Array<Pathname>] monitored_paths
47
+ # list of paths the application is currently monitoring.
48
+ #
49
+ # @return [undefined]
50
+ #
51
+ # @abstract
52
+ def refresh(monitored_paths)
53
+ raise AbstractMethod
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,160 @@
1
+ module Observr
2
+ module EventHandler
3
+
4
+ class ::FSEvents
5
+ # Same as Watch.debug, but prefixed with [fsevents] instead.
6
+ #
7
+ # @example
8
+ #
9
+ # FSEvents.debug('missfired')
10
+ #
11
+ # @param [String] message
12
+ # debug message to print
13
+ #
14
+ # @return [nil]
15
+ #
16
+ def self.debug(msg)
17
+ puts "[fsevents] #{msg}" if Observr.options.debug
18
+ end
19
+ end
20
+
21
+ # FSEvents based event handler for Darwin/OSX
22
+ #
23
+ # Uses ruby-fsevents (http://github.com/sandro/ruby-fsevent)
24
+ #
25
+ class Darwin < FSEvent
26
+ include Base
27
+
28
+ def initialize
29
+ super
30
+ self.latency = 0.2
31
+ end
32
+
33
+ # Enter listening loop. Will block control flow until application is
34
+ # explicitly stopped/killed.
35
+ #
36
+ # @return [undefined]
37
+ #
38
+ def listen(monitored_paths)
39
+ register_paths(monitored_paths)
40
+ start
41
+ end
42
+
43
+ # Rebuild file bindings. Will detach all current bindings, and reattach
44
+ # the `monitored_paths`
45
+ #
46
+ # @param [Array<Pathname>] monitored_paths
47
+ # list of paths the application is currently monitoring.
48
+ #
49
+ # @return [undefined]
50
+ #
51
+ def refresh(monitored_paths)
52
+ register_paths(monitored_paths)
53
+ restart
54
+ end
55
+
56
+ private
57
+
58
+ # Callback. Called on file change event. Delegates to
59
+ # {Controller#update}, passing in path and event type
60
+ #
61
+ # @return [undefined]
62
+ #
63
+ def on_change(dirs)
64
+ dirs.each do |dir|
65
+ path, type = detect_change(dir)
66
+ notify(path, type) unless path.nil?
67
+ end
68
+ end
69
+
70
+ # Detected latest updated file within given directory
71
+ #
72
+ # @param [Pathname, String] dir
73
+ # directory reporting event
74
+ #
75
+ # @return [Array(Pathname, Symbol)] path and type
76
+ # path to updated file and event type
77
+ #
78
+ def detect_change(dir)
79
+ paths = monitored_paths_for(dir)
80
+ type = nil
81
+ path = paths.find {|path| type = event_type(path) }
82
+
83
+ FSEvents.debug("event detection error") if type.nil?
84
+
85
+ update_reference_times
86
+ [path, type]
87
+ end
88
+
89
+ # Detect type of event for path, if any
90
+ #
91
+ # Path times (atime, mtime, ctime) are compared to stored references.
92
+ # If any is more recent, the event is reported as a symbol.
93
+ #
94
+ # @param [Pathname] path
95
+ #
96
+ # @return [Symbol, nil] event type
97
+ # Event type if detected, nil otherwise.
98
+ # Symbol is on of :deleted, :modified, :accessed, :changed
99
+ #
100
+ def event_type(path)
101
+ return :deleted if !path.exist?
102
+ return :modified if path.mtime > @reference_times[path][:mtime]
103
+ return :accessed if path.atime > @reference_times[path][:atime]
104
+ return :changed if path.ctime > @reference_times[path][:ctime]
105
+ nil
106
+ end
107
+
108
+ # Monitored paths within given dir
109
+ #
110
+ # @param [Pathname, String] dir
111
+ #
112
+ # @return [Array<Pathname>] monitored_paths
113
+ #
114
+ def monitored_paths_for(dir)
115
+ dir = Pathname(dir).expand_path
116
+ @paths.select {|path| path.dirname.expand_path == dir }
117
+ end
118
+
119
+ # Register watches for paths
120
+ #
121
+ # @param [Array<Pathname>] paths
122
+ #
123
+ # @return [undefined]
124
+ #
125
+ def register_paths(paths)
126
+ @paths = paths
127
+ watch_directories(dirs_for(@paths))
128
+ update_reference_times
129
+ end
130
+
131
+ # Directories for paths
132
+ #
133
+ # A unique list of directories containing given paths
134
+ #
135
+ # @param [Array<Pathname>] paths
136
+ #
137
+ # @return [Array<Pathname>] dirs
138
+ #
139
+ def dirs_for(paths)
140
+ paths.map {|path| path.dirname.to_s }.uniq
141
+ end
142
+
143
+ # Update reference times for registered paths
144
+ #
145
+ # @return [undefined]
146
+ #
147
+ def update_reference_times
148
+ @reference_times = {}
149
+ now = Time.now
150
+ @paths.each do |path|
151
+ @reference_times[path] = {}
152
+ @reference_times[path][:atime] = now
153
+ @reference_times[path][:mtime] = now
154
+ @reference_times[path][:ctime] = now
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+
@@ -0,0 +1,75 @@
1
+ module Observr
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
+ # @param [Array<Pathname>] monitored_paths
15
+ # list of paths the application is currently monitoring.
16
+ #
17
+ # @return [undefined]
18
+ #
19
+ def listen(monitored_paths)
20
+ @monitored_paths = monitored_paths
21
+ loop { trigger; sleep(1) }
22
+ end
23
+
24
+ # See if an event occured, and if so notify observers.
25
+ #
26
+ # @return [undefined]
27
+ #
28
+ # @private
29
+ def trigger
30
+ path, type = detect_event
31
+ notify(path, type) unless path.nil?
32
+ end
33
+
34
+ # Update list of monitored paths.
35
+ #
36
+ # @param [Array<Pathname>] monitored_paths
37
+ # list of paths the application is currently monitoring.
38
+ #
39
+ # @return [undefined]
40
+ #
41
+ def refresh(monitored_paths)
42
+ @monitored_paths = monitored_paths
43
+ end
44
+
45
+ private
46
+
47
+ # Verify mtimes of monitored files.
48
+ #
49
+ # If the latest mtime is more recent than the reference mtime, return
50
+ # that file's path.
51
+ #
52
+ # @return [[Pathname, Symbol]]
53
+ # path and type of event if event occured, nil otherwise
54
+ #
55
+ # @todo improve ENOENT error handling
56
+ #
57
+ def detect_event # OPTIMIZE, REFACTOR
58
+ @monitored_paths.each do |path|
59
+ return [path, :deleted] unless path.exist?
60
+ end
61
+
62
+ mtime_path = @monitored_paths.max {|a,b| a.mtime <=> b.mtime }
63
+ atime_path = @monitored_paths.max {|a,b| a.atime <=> b.atime }
64
+ ctime_path = @monitored_paths.max {|a,b| a.ctime <=> b.ctime }
65
+
66
+ if mtime_path.mtime > @reference_mtime then @reference_mtime = mtime_path.mtime; [mtime_path, :modified]
67
+ elsif atime_path.atime > @reference_atime then @reference_atime = atime_path.atime; [atime_path, :accessed]
68
+ elsif ctime_path.ctime > @reference_ctime then @reference_ctime = ctime_path.ctime; [ctime_path, :changed ]
69
+ else; nil; end
70
+ rescue Errno::ENOENT
71
+ retry
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,125 @@
1
+ module Observr
2
+ module EventHandler
3
+ class Unix
4
+ include Base
5
+
6
+ # Used by Rev. Wraps a monitored path, and `Rev::Loop` will call its
7
+ # callback on file events.
8
+ #
9
+ # @private
10
+ class SingleFileWatcher < Rev::StatWatcher
11
+ class << self
12
+ # Stores a reference back to handler so we can call its {Base#notify notify}
13
+ # method with file event info
14
+ #
15
+ # @return [EventHandler::Base]
16
+ #
17
+ attr_accessor :handler
18
+ end
19
+
20
+ # @param [String] path
21
+ # single file to monitor
22
+ #
23
+ def initialize(path)
24
+ super
25
+ update_reference_times
26
+ end
27
+
28
+ # File's path as a Pathname
29
+ #
30
+ # @return [Pathname]
31
+ #
32
+ def pathname
33
+ @pathname ||= Pathname(@path)
34
+ end
35
+
36
+ # Callback. Called on file change event. Delegates to
37
+ # {Controller#update}, passing in path and event type
38
+ #
39
+ # @return [undefined]
40
+ #
41
+ def on_change
42
+ self.class.handler.notify(path, type)
43
+ update_reference_times unless type == :deleted
44
+ end
45
+
46
+ private
47
+
48
+ # @todo improve ENOENT error handling
49
+ def update_reference_times
50
+ @reference_atime = pathname.atime
51
+ @reference_mtime = pathname.mtime
52
+ @reference_ctime = pathname.ctime
53
+ rescue Errno::ENOENT
54
+ retry
55
+ end
56
+
57
+ # Type of latest event.
58
+ #
59
+ # A single type is determined, even though more than one stat times may
60
+ # have changed on the file. The type is the first to match in the
61
+ # following hierarchy:
62
+ #
63
+ # :deleted, :modified (mtime), :accessed (atime), :changed (ctime)
64
+ #
65
+ # @return [Symbol] type
66
+ # latest event's type
67
+ #
68
+ def type
69
+ return :deleted if !pathname.exist?
70
+ return :modified if pathname.mtime > @reference_mtime
71
+ return :accessed if pathname.atime > @reference_atime
72
+ return :changed if pathname.ctime > @reference_ctime
73
+ end
74
+ end
75
+
76
+ def initialize
77
+ SingleFileWatcher.handler = self
78
+ @loop = Rev::Loop.default
79
+ end
80
+
81
+ # Enters listening loop. Will block control flow until application is
82
+ # explicitly stopped/killed.
83
+ #
84
+ # @return [undefined]
85
+ #
86
+ def listen(monitored_paths)
87
+ @monitored_paths = monitored_paths
88
+ attach
89
+ @loop.run
90
+ end
91
+
92
+ # Rebuilds file bindings. Will detach all current bindings, and reattach
93
+ # the `monitored_paths`
94
+ #
95
+ # @param [Array<Pathname>] monitored_paths
96
+ # list of paths the application is currently monitoring.
97
+ #
98
+ # @return [undefined]
99
+ #
100
+ def refresh(monitored_paths)
101
+ @monitored_paths = monitored_paths
102
+ detach
103
+ attach
104
+ end
105
+
106
+ private
107
+
108
+ # Binds all `monitored_paths` to the listening loop.
109
+ #
110
+ # @return [undefined]
111
+ #
112
+ def attach
113
+ @monitored_paths.each {|path| SingleFileWatcher.new(path.to_s).attach(@loop) }
114
+ end
115
+
116
+ # Unbinds all paths currently attached to listening loop.
117
+ #
118
+ # @return [undefined]
119
+ #
120
+ def detach
121
+ @loop.watchers.each {|watcher| watcher.detach }
122
+ end
123
+ end
124
+ end
125
+ end