em-dir-watcher 0.1.0 → 0.9.1
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.
- 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
|