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