Skip to content

Commit 87dc8ce

Browse files
committed
Gracefully deal with dangling symlinks
* Don't add files that are dangling symlinks * When watching a directory, ignore dangling symlinks rather than treating them as forever-stale
1 parent abf91ba commit 87dc8ce

File tree

3 files changed

+63
-6
lines changed

3 files changed

+63
-6
lines changed

lib/spring/test/watcher_test.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,33 @@ def assert_not_stale
162162
watcher.add './foobar'
163163
assert watcher.files.empty?
164164
end
165+
166+
test "add symlink" do
167+
File.write("#{dir}/bar", "bar")
168+
File.symlink("#{dir}/bar", "#{dir}/foo")
169+
watcher.add './foo'
170+
assert_equal ["#{dir}/bar"], watcher.files.to_a
171+
end
172+
173+
test "add dangling symlink" do
174+
File.symlink("#{dir}/bar", "#{dir}/foo")
175+
watcher.add './foo'
176+
assert watcher.files.empty?
177+
end
178+
179+
test "add directory with dangling symlink" do
180+
subdir = "#{@dir}/subdir"
181+
FileUtils.mkdir(subdir)
182+
File.symlink("dangling", "#{subdir}/foo")
183+
184+
watcher.add subdir
185+
assert_not_stale
186+
187+
# Adding a new file should mark as stale despite the dangling symlink.
188+
File.write("#{subdir}/new-file", "new")
189+
watcher.check_stale
190+
assert_stale
191+
end
165192
end
166193
end
167194
end

lib/spring/watcher/abstract.rb

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,30 @@ def add(*items)
4848
end
4949
end
5050

51-
items = items.select(&:exist?)
51+
items = items.select do |item|
52+
if item.symlink?
53+
item.readlink.exist?.tap do |exists|
54+
if !exists
55+
debug { "add: ignoring dangling symlink: #{item.inspect} -> #{item.readlink.inspect}" }
56+
end
57+
end
58+
else
59+
item.exist?
60+
end
61+
end
5262

5363
synchronize {
5464
items.each do |item|
5565
if item.directory?
5666
directories << item.realpath.to_s
5767
else
58-
files << item.realpath.to_s
68+
begin
69+
files << item.realpath.to_s
70+
rescue Errno::ENOENT
71+
# Race condition. Ignore symlinks whose target was removed
72+
# since the check above, or are deeply chained.
73+
debug { "add: ignoring now-dangling symlink: #{item.inspect} -> #{item.readlink.inspect}" }
74+
end
5975
end
6076
end
6177

lib/spring/watcher/polling.rb

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,24 @@ def subjects_changed
6464
private
6565

6666
def compute_mtime
67-
expanded_files.map { |f| File.mtime(f).to_f }.max || 0
68-
rescue Errno::ENOENT
69-
# if a file does no longer exist, the watcher is always stale.
70-
Float::MAX
67+
expanded_files.map do |f|
68+
# Get the mtime of symlink targets. Ignore dangling symlinks.
69+
if File.symlink?(f)
70+
begin
71+
File.mtime(f)
72+
rescue Errno::ENOENT
73+
0
74+
end
75+
# If a file no longer exists, treat it as changed.
76+
else
77+
begin
78+
File.mtime(f)
79+
rescue Errno::ENOENT
80+
debug { "compute_mtime: no longer exists: #{f}" }
81+
Float::MAX
82+
end
83+
end.to_f
84+
end.max || 0
7185
end
7286

7387
def expanded_files

0 commit comments

Comments
 (0)