smparkes-watchr 0.5.7
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/History.txt +32 -0
- data/LICENSE +19 -0
- data/Manifest +30 -0
- data/README.rdoc +82 -0
- data/Rakefile +51 -0
- data/TODO.txt +40 -0
- data/bin/watchr +77 -0
- data/docs.watchr +26 -0
- data/gem.watchr +32 -0
- data/lib/watchr.rb +101 -0
- data/lib/watchr/controller.rb +93 -0
- data/lib/watchr/event_handlers/base.rb +48 -0
- data/lib/watchr/event_handlers/em.rb +147 -0
- data/lib/watchr/event_handlers/portable.rb +60 -0
- data/lib/watchr/event_handlers/rev.rb +104 -0
- data/lib/watchr/event_handlers/unix.rb +25 -0
- data/lib/watchr/script.rb +230 -0
- data/manifest.watchr +70 -0
- data/specs.watchr +38 -0
- data/test/README +11 -0
- data/test/event_handlers/test_base.rb +24 -0
- data/test/event_handlers/test_em.rb +162 -0
- data/test/event_handlers/test_portable.rb +142 -0
- data/test/event_handlers/test_rev.rb +162 -0
- data/test/test_controller.rb +130 -0
- data/test/test_helper.rb +60 -0
- data/test/test_script.rb +124 -0
- data/test/test_watchr.rb +60 -0
- data/watchr.gemspec +64 -0
- metadata +141 -0
@@ -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
|