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.
@@ -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
@@ -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-directory-watch'
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