em-dir-watcher 0.1.0 → 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +2 -1
- data/README.md +126 -9
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/examples/monitor.rb +11 -3
- data/lib/em-dir-watcher/invokers/subprocess_invoker.rb +100 -0
- data/lib/em-dir-watcher/monitor.rb +47 -0
- data/lib/em-dir-watcher/platform/linux.rb +51 -0
- data/lib/em-dir-watcher/platform/mac/ffi_fsevents_watcher.rb +80 -0
- data/lib/em-dir-watcher/platform/mac/rubycocoa_watcher.rb +51 -0
- data/lib/em-dir-watcher/platform/mac.rb +50 -0
- data/lib/em-dir-watcher/platform/windows/monitor.rb +0 -2
- data/lib/em-dir-watcher/platform/windows/path_to_ruby_exe.rb +0 -3
- data/lib/em-dir-watcher/platform/windows.rb +29 -7
- data/lib/em-dir-watcher/tree.rb +216 -0
- data/lib/em-dir-watcher.rb +4 -9
- data/test/helper.rb +9 -1
- data/test/test_monitor.rb +161 -0
- data/test/test_tree.rb +440 -0
- data/testloop +11 -0
- metadata +16 -8
- data/.document +0 -5
- data/lib/em-dir-watcher/platform/nix.rb +0 -107
- data/lib/em-dir-watcher/platform/windows/monitor-nix.rb +0 -29
- data/test/test_em-dir-watcher.rb +0 -7
@@ -0,0 +1,216 @@
|
|
1
|
+
|
2
|
+
require 'set'
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
module EMDirWatcher
|
6
|
+
|
7
|
+
class Entry
|
8
|
+
|
9
|
+
attr_reader :relative_path
|
10
|
+
|
11
|
+
def initialize tree, relative_path, ancestor_matches_inclusions
|
12
|
+
@tree = tree
|
13
|
+
@matches_inclusions = ancestor_matches_inclusions || @tree.includes?(relative_path)
|
14
|
+
@relative_path = relative_path
|
15
|
+
@is_file = false
|
16
|
+
@file_mtime = Time.at(0)
|
17
|
+
@entries = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def full_path
|
21
|
+
if @relative_path.empty? then @tree.full_path else File.join(@tree.full_path, @relative_path) end
|
22
|
+
end
|
23
|
+
|
24
|
+
def exists?
|
25
|
+
File.exists? full_path
|
26
|
+
end
|
27
|
+
|
28
|
+
def compute_is_file
|
29
|
+
FileTest.file? full_path
|
30
|
+
end
|
31
|
+
|
32
|
+
def compute_file_mtime
|
33
|
+
if FileTest.symlink?(full_path) then Time.at(0) else File.mtime(full_path) end
|
34
|
+
rescue Errno::ENOENT, Errno::ENOTDIR
|
35
|
+
Time.at(0)
|
36
|
+
end
|
37
|
+
|
38
|
+
def compute_entry_names
|
39
|
+
Dir.entries(full_path) - ['.', '..']
|
40
|
+
rescue Errno::ENOENT, Errno::ENOTDIR
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
|
44
|
+
def relative_path_of entry_name
|
45
|
+
if @relative_path.empty? then entry_name else File.join(@relative_path, entry_name) end
|
46
|
+
end
|
47
|
+
|
48
|
+
def compute_entries
|
49
|
+
new_entries = {}
|
50
|
+
compute_entry_names.each do |entry_name|
|
51
|
+
entry_relative_path = relative_path_of entry_name
|
52
|
+
next if @tree.excludes? entry_relative_path
|
53
|
+
new_entries[entry_name] = @entries[entry_name] || Entry.new(@tree, entry_relative_path, @matches_inclusions)
|
54
|
+
end
|
55
|
+
new_entries
|
56
|
+
end
|
57
|
+
|
58
|
+
def refresh_file! changed_relative_paths
|
59
|
+
if @matches_inclusions
|
60
|
+
used_to_be_file, @is_file = @is_file, compute_is_file
|
61
|
+
previous_file_mtime, @file_mtime = @file_mtime, compute_file_mtime
|
62
|
+
if used_to_be_file
|
63
|
+
changed_relative_paths << @relative_path if not @is_file or previous_file_mtime != @file_mtime
|
64
|
+
elsif @is_file
|
65
|
+
changed_relative_paths << @relative_path
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def refresh! changed_relative_paths, refresh_subtree
|
71
|
+
refresh_file! changed_relative_paths
|
72
|
+
old_entries, @entries = @entries, compute_entries
|
73
|
+
|
74
|
+
if refresh_subtree
|
75
|
+
entries_to_refresh = Set.new(@entries.values) + Set.new(old_entries.values)
|
76
|
+
entries_to_refresh.each { |entry| entry.refresh! changed_relative_paths, true }
|
77
|
+
else
|
78
|
+
new_set, old_set = Set.new(@entries.values), Set.new(old_entries.values)
|
79
|
+
removed_entries, added_entries = old_set - new_set, new_set - old_set
|
80
|
+
still_existing_entries = old_set - removed_entries
|
81
|
+
added_or_removed_entries = added_entries + removed_entries
|
82
|
+
|
83
|
+
added_or_removed_entries.each { |entry| entry.refresh! changed_relative_paths, true }
|
84
|
+
still_existing_entries.each { |entry| entry.refresh_file! changed_relative_paths }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def scoped_refresh! changed_relative_paths, relative_scope, refresh_subtree
|
89
|
+
if relative_scope.size == 0
|
90
|
+
refresh! changed_relative_paths, refresh_subtree
|
91
|
+
else
|
92
|
+
entry_name, children_relative_scope = relative_scope[0], relative_scope[1..-1]
|
93
|
+
entry_relative_path = relative_path_of entry_name
|
94
|
+
return if @tree.excludes? entry_relative_path
|
95
|
+
entry = (@entries[entry_name] ||= Entry.new(@tree, entry_relative_path, @matches_inclusions))
|
96
|
+
entry.scoped_refresh! changed_relative_paths, children_relative_scope, refresh_subtree
|
97
|
+
if relative_scope.size == 1
|
98
|
+
@entries.delete entry_name unless entry.exists?
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def recursive_file_entries
|
104
|
+
if @is_file
|
105
|
+
self
|
106
|
+
else
|
107
|
+
@entries.values.collect { |entry| entry.recursive_file_entries }.flatten
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
class RegexpMatcher
|
114
|
+
def initialize re
|
115
|
+
@re = re
|
116
|
+
end
|
117
|
+
def matches? relative_path
|
118
|
+
relative_path =~ @re
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class PathGlobMatcher
|
123
|
+
def initialize glob
|
124
|
+
@glob = glob.gsub(%r-^/+-, '')
|
125
|
+
end
|
126
|
+
def matches? relative_path
|
127
|
+
File.fnmatch?(@glob, relative_path)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
class NameGlobMatcher
|
132
|
+
def initialize glob
|
133
|
+
@glob = glob
|
134
|
+
end
|
135
|
+
def matches? relative_path
|
136
|
+
File.fnmatch?(@glob, File.basename(relative_path))
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Computes fine-grained (per-file) change events for a given directory tree,
|
141
|
+
# using coarse-grained (per-subtree) events for optimization.
|
142
|
+
class Tree
|
143
|
+
|
144
|
+
attr_reader :full_path
|
145
|
+
attr_reader :inclusions
|
146
|
+
attr_reader :exclusions
|
147
|
+
|
148
|
+
def self.parse_matchers matcher_expressions
|
149
|
+
matcher_expressions.collect do |expr|
|
150
|
+
if Regexp === expr
|
151
|
+
RegexpMatcher.new expr
|
152
|
+
elsif expr.include? '/'
|
153
|
+
PathGlobMatcher.new expr
|
154
|
+
else
|
155
|
+
NameGlobMatcher.new expr
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.resolve_real_path_where_possible expanded_path
|
161
|
+
Pathname.new(expanded_path).realpath().to_s
|
162
|
+
rescue Errno::ENOENT
|
163
|
+
dirname, basename = File.dirname(expanded_path), File.basename(expanded_path)
|
164
|
+
return expanded_path if dirname == '/' || dirname == '.'
|
165
|
+
return File.join(resolve_real_path_where_possible(dirname), basename)
|
166
|
+
end
|
167
|
+
|
168
|
+
def initialize full_path, inclusions=nil, exclusions=[]
|
169
|
+
@full_path = self.class.resolve_real_path_where_possible(File.expand_path(full_path))
|
170
|
+
self.inclusions = inclusions
|
171
|
+
self.exclusions = exclusions
|
172
|
+
@root_entry = Entry.new self, '', false
|
173
|
+
@root_entry.refresh! [], true
|
174
|
+
end
|
175
|
+
|
176
|
+
def refresh! scope=nil, refresh_subtree=true
|
177
|
+
scope = self.class.resolve_real_path_where_possible(File.expand_path(scope || @full_path))
|
178
|
+
|
179
|
+
scope_with_slash = File.join(scope, '')
|
180
|
+
full_path_with_slash = File.join(@full_path, '')
|
181
|
+
return [] unless scope_with_slash.downcase[0..full_path_with_slash.size-1] == full_path_with_slash.downcase
|
182
|
+
|
183
|
+
relative_scope = (scope[full_path_with_slash.size..-1] || '').split('/')
|
184
|
+
relative_scope = relative_scope.reject { |item| item == '' }
|
185
|
+
|
186
|
+
changed_relative_paths = []
|
187
|
+
@root_entry.scoped_refresh! changed_relative_paths, relative_scope, refresh_subtree
|
188
|
+
changed_relative_paths.sort
|
189
|
+
end
|
190
|
+
|
191
|
+
def includes? relative_path
|
192
|
+
@inclusion_matchers.nil? || @inclusion_matchers.any? { |matcher| matcher.matches? relative_path }
|
193
|
+
end
|
194
|
+
|
195
|
+
def excludes? relative_path
|
196
|
+
@exclusion_matchers.any? { |matcher| matcher.matches? relative_path }
|
197
|
+
end
|
198
|
+
|
199
|
+
def full_file_list
|
200
|
+
@root_entry.recursive_file_entries.collect { |entry| entry.relative_path }.sort
|
201
|
+
end
|
202
|
+
|
203
|
+
private
|
204
|
+
|
205
|
+
def inclusions= new_inclusions
|
206
|
+
@inclusions = new_inclusions
|
207
|
+
@inclusion_matchers = new_inclusions && self.class.parse_matchers(new_inclusions)
|
208
|
+
end
|
209
|
+
|
210
|
+
def exclusions= new_exclusions
|
211
|
+
@exclusions = new_exclusions
|
212
|
+
@exclusion_matchers = self.class.parse_matchers(new_exclusions)
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
end
|
data/lib/em-dir-watcher.rb
CHANGED
@@ -6,17 +6,12 @@ module EMDirWatcher
|
|
6
6
|
PLATFORM = ENV['EM_DIR_WATCHER_PLATFORM'] ||
|
7
7
|
case Config::CONFIG['target_os']
|
8
8
|
when /mswin|mingw/ then 'Windows'
|
9
|
+
when /darwin/ then 'Mac'
|
10
|
+
when /linux/ then 'Linux'
|
9
11
|
else 'NIX'
|
10
12
|
end
|
11
13
|
end
|
12
14
|
|
15
|
+
require "em-dir-watcher/tree"
|
13
16
|
require "em-dir-watcher/platform/#{EMDirWatcher::PLATFORM.downcase}"
|
14
|
-
|
15
|
-
module EMDirWatcher
|
16
|
-
Watcher = Platform.const_get(PLATFORM)::Watcher
|
17
|
-
|
18
|
-
def self.watch path, globs, &handler
|
19
|
-
Watcher.new path, globs, &handler
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
17
|
+
require "em-dir-watcher/monitor"
|
data/test/helper.rb
CHANGED
@@ -4,7 +4,15 @@ require 'shoulda'
|
|
4
4
|
|
5
5
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
6
6
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
7
|
-
require 'em-
|
7
|
+
require 'em-dir-watcher'
|
8
|
+
|
9
|
+
TEST_DIR = if EMDirWatcher::PLATFORM == 'Windows' then 'c:/tmp/emdwtest' else '/tmp/emdwtest' end # Dir.mktmpdir
|
10
|
+
ALT_TEST_DIR = TEST_DIR + 'alt' # Dir.mktmpdir
|
8
11
|
|
9
12
|
class Test::Unit::TestCase
|
13
|
+
|
14
|
+
def join list
|
15
|
+
list.join(", ").strip
|
16
|
+
end
|
17
|
+
|
10
18
|
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestMonitor < Test::Unit::TestCase
|
4
|
+
|
5
|
+
# A sufficiently reliable maximum file system change reporting lag, in seconds.
|
6
|
+
# See README for explaination of its effect.
|
7
|
+
UNIT_DELAY = 0.5
|
8
|
+
|
9
|
+
def setup
|
10
|
+
FileUtils.rm_rf TEST_DIR
|
11
|
+
FileUtils.mkdir_p TEST_DIR
|
12
|
+
FileUtils.rm_rf ALT_TEST_DIR
|
13
|
+
FileUtils.mkdir_p ALT_TEST_DIR
|
14
|
+
|
15
|
+
FileUtils.mkdir File.join(TEST_DIR, 'bar')
|
16
|
+
FileUtils.mkdir File.join(TEST_DIR, 'bar', 'boo')
|
17
|
+
|
18
|
+
FileUtils.touch File.join(TEST_DIR, 'aa')
|
19
|
+
FileUtils.touch File.join(TEST_DIR, 'biz')
|
20
|
+
FileUtils.touch File.join(TEST_DIR, 'zz')
|
21
|
+
FileUtils.touch File.join(TEST_DIR, 'bar', 'foo')
|
22
|
+
FileUtils.touch File.join(TEST_DIR, 'bar', 'biz')
|
23
|
+
FileUtils.touch File.join(TEST_DIR, 'bar', 'biz.html')
|
24
|
+
FileUtils.touch File.join(TEST_DIR, 'bar', 'boo', 'bizzz')
|
25
|
+
|
26
|
+
@list = ['aa', 'biz', 'zz', 'bar/foo', 'bar/biz', 'bar/biz.html', 'bar/boo/bizzz'].sort
|
27
|
+
end
|
28
|
+
|
29
|
+
should "should report a deletion" do
|
30
|
+
all_changed_paths = []
|
31
|
+
stopped = false
|
32
|
+
watcher = nil
|
33
|
+
EM.run {
|
34
|
+
watcher = EMDirWatcher.watch TEST_DIR, :include_only => ['/bar'], :exclude => ['*.html'] do |changed_paths|
|
35
|
+
all_changed_paths += changed_paths
|
36
|
+
end
|
37
|
+
watcher.when_ready_to_use do
|
38
|
+
FileUtils.rm_rf File.join(TEST_DIR, 'bar')
|
39
|
+
EM.add_timer UNIT_DELAY do EM.stop end
|
40
|
+
end
|
41
|
+
}
|
42
|
+
watcher.stop
|
43
|
+
assert_equal join(['bar/foo', 'bar/biz', 'bar/boo/bizzz'].sort), join(all_changed_paths.sort)
|
44
|
+
end
|
45
|
+
|
46
|
+
should "choke on invalid option keys" do
|
47
|
+
assert_raise StandardError do
|
48
|
+
EM.run {
|
49
|
+
EMDirWatcher.watch TEST_DIR, :bogus_option => true
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
should "report each change individually when using a zero grace period" do
|
55
|
+
changed_1 = []
|
56
|
+
changed_2 = []
|
57
|
+
changed_cur = changed_1
|
58
|
+
stopped = false
|
59
|
+
watcher = nil
|
60
|
+
EM.run {
|
61
|
+
watcher = EMDirWatcher.watch TEST_DIR, :include_only => ['/bar'], :exclude => ['*.html'] do |changed_paths|
|
62
|
+
changed_cur.push *changed_paths
|
63
|
+
end
|
64
|
+
watcher.when_ready_to_use do
|
65
|
+
FileUtils.rm_rf File.join(TEST_DIR, 'bar', 'foo')
|
66
|
+
EM.add_timer 1 do
|
67
|
+
changed_cur = changed_2
|
68
|
+
FileUtils.rm_rf File.join(TEST_DIR, 'bar', 'biz')
|
69
|
+
EM.add_timer 0.5 do EM.stop end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
}
|
73
|
+
watcher.stop
|
74
|
+
assert_equal 'bar/foo >> bar/biz', join(changed_1.sort) + " >> " + join(changed_cur.sort)
|
75
|
+
end
|
76
|
+
|
77
|
+
should "combine changes when using a non-zero grace period" do
|
78
|
+
changed_1 = []
|
79
|
+
changed_2 = []
|
80
|
+
changed_cur = changed_1
|
81
|
+
stopped = false
|
82
|
+
watcher = nil
|
83
|
+
EM.run {
|
84
|
+
watcher = EMDirWatcher.watch TEST_DIR, :include_only => ['/bar'], :exclude => ['*.html'], :grace_period => 2*UNIT_DELAY do |changed_paths|
|
85
|
+
changed_cur.push *changed_paths
|
86
|
+
end
|
87
|
+
watcher.when_ready_to_use do
|
88
|
+
FileUtils.rm_rf File.join(TEST_DIR, 'bar', 'foo')
|
89
|
+
EM.add_timer UNIT_DELAY do
|
90
|
+
changed_cur = changed_2
|
91
|
+
FileUtils.rm_rf File.join(TEST_DIR, 'bar', 'biz')
|
92
|
+
EM.add_timer 2*UNIT_DELAY do EM.stop end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
}
|
96
|
+
watcher.stop
|
97
|
+
assert_equal ' >> bar/biz, bar/foo', join(changed_1.sort) + " >> " + join(changed_cur.sort)
|
98
|
+
end
|
99
|
+
|
100
|
+
should "not report duplicate changes when using a non-zero grace period" do
|
101
|
+
changed_1 = []
|
102
|
+
changed_2 = []
|
103
|
+
changed_cur = changed_1
|
104
|
+
stopped = false
|
105
|
+
watcher = nil
|
106
|
+
EM.run {
|
107
|
+
watcher = EMDirWatcher.watch TEST_DIR, :include_only => ['/bar'], :exclude => ['*.html'], :grace_period => 3*UNIT_DELAY do |changed_paths|
|
108
|
+
changed_cur.push *changed_paths
|
109
|
+
end
|
110
|
+
watcher.when_ready_to_use do
|
111
|
+
FileUtils.rm_rf File.join(TEST_DIR, 'bar', 'foo')
|
112
|
+
EM.add_timer UNIT_DELAY do
|
113
|
+
FileUtils.touch File.join(TEST_DIR, 'bar', 'foo')
|
114
|
+
EM.add_timer UNIT_DELAY do
|
115
|
+
changed_cur = changed_2
|
116
|
+
FileUtils.rm_rf File.join(TEST_DIR, 'bar', 'foo')
|
117
|
+
FileUtils.rm_rf File.join(TEST_DIR, 'bar', 'biz')
|
118
|
+
EM.add_timer 2*UNIT_DELAY do EM.stop end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
}
|
123
|
+
watcher.stop
|
124
|
+
assert_equal ' >> bar/biz, bar/foo', join(changed_1.sort) + " >> " + join(changed_cur.sort)
|
125
|
+
end
|
126
|
+
|
127
|
+
should "should report entire subtree as changed when a directory is moved away" do
|
128
|
+
all_changed_paths = []
|
129
|
+
stopped = false
|
130
|
+
watcher = nil
|
131
|
+
EM.run {
|
132
|
+
watcher = EMDirWatcher.watch TEST_DIR, :include_only => ['/bar'], :exclude => ['*.html'] do |changed_paths|
|
133
|
+
all_changed_paths += changed_paths
|
134
|
+
end
|
135
|
+
watcher.when_ready_to_use do
|
136
|
+
FileUtils.mv File.join(TEST_DIR, 'bar'), ALT_TEST_DIR
|
137
|
+
EM.add_timer UNIT_DELAY do EM.stop end
|
138
|
+
end
|
139
|
+
}
|
140
|
+
watcher.stop
|
141
|
+
assert_equal join(['bar/foo', 'bar/biz', 'bar/boo/bizzz'].sort), join(all_changed_paths.sort)
|
142
|
+
end
|
143
|
+
|
144
|
+
should "should report entire subtree as changed when a directory is moved in" do
|
145
|
+
all_changed_paths = []
|
146
|
+
stopped = false
|
147
|
+
watcher = nil
|
148
|
+
EM.run {
|
149
|
+
watcher = EMDirWatcher.watch ALT_TEST_DIR, :exclude => ['*.html'] do |changed_paths|
|
150
|
+
all_changed_paths += changed_paths
|
151
|
+
end
|
152
|
+
watcher.when_ready_to_use do
|
153
|
+
FileUtils.mv File.join(TEST_DIR, 'bar'), ALT_TEST_DIR
|
154
|
+
EM.add_timer UNIT_DELAY do EM.stop end
|
155
|
+
end
|
156
|
+
}
|
157
|
+
watcher.stop
|
158
|
+
assert_equal join(['bar/foo', 'bar/biz', 'bar/boo/bizzz'].sort), join(all_changed_paths.sort)
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|