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.
- checksums.yaml +15 -0
- data/.gitignore +6 -0
- data/History.txt +32 -0
- data/LICENSE +19 -0
- data/README.md +119 -0
- data/Rakefile +34 -0
- data/TODO.md +31 -0
- data/bin/observr +105 -0
- data/contributions.txt +7 -0
- data/docs.observr +26 -0
- data/gem.observr +22 -0
- data/lib/observr.rb +133 -0
- data/lib/observr/controller.rb +87 -0
- data/lib/observr/event_handlers/base.rb +57 -0
- data/lib/observr/event_handlers/darwin.rb +160 -0
- data/lib/observr/event_handlers/portable.rb +75 -0
- data/lib/observr/event_handlers/unix.rb +125 -0
- data/lib/observr/script.rb +257 -0
- data/observr.gemspec +21 -0
- data/specs.watchr +37 -0
- data/test/README +11 -0
- data/test/event_handlers/test_base.rb +24 -0
- data/test/event_handlers/test_darwin.rb +111 -0
- data/test/event_handlers/test_portable.rb +142 -0
- data/test/event_handlers/test_unix.rb +162 -0
- data/test/test_controller.rb +121 -0
- data/test/test_helper.rb +37 -0
- data/test/test_script.rb +163 -0
- data/test/test_watchr.rb +76 -0
- metadata +114 -0
@@ -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
|