listen 2.7.12 → 2.8.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 65b3168861b5ccc57809689d8ad5849a631db822
4
- data.tar.gz: 3640593ce18fa45435a2ff0184e9c7fda77acfce
3
+ metadata.gz: 5a859a235a28efe17024969a47542ebec79836f9
4
+ data.tar.gz: 54b831e943a6b7b773aa9da8684041ee55e9a8fe
5
5
  SHA512:
6
- metadata.gz: 1ba0723ef31cfc026fb71bc89f0d1f951cbd276270bb57329fef1ffa5aae8e982f645a69a2a7bbf39591ae6d8160950fbb71f356417874948c39d010559cca31
7
- data.tar.gz: 1203e42ab3497f1c2d1348330398880f92b1932038f60b7f8f2283e67bfc4e04e034f10b35389bb00a0775db352d91228af98243951f702137cbc19a3ffe2ad5
6
+ metadata.gz: 7058ec4f00876391e3179d8566b6fcce5aa9741f37da3ac32b5f68f08e655520278320f16237e080113a4774a580b7c391f2f08569b4bf4cb3f709787b6334fc
7
+ data.tar.gz: 8eede032561181f9d5add525343992098c4216a59953d519eafaac11df294d0fc98bdc9dd31a0da6fb10a8c3fced456c57ee260a2ef620d57951051546342d91
data/README.md CHANGED
@@ -6,6 +6,21 @@
6
6
 
7
7
  The Listen gem listens to file modifications and notifies you about the changes.
8
8
 
9
+ ## IMPORTANT NOTE
10
+
11
+ Got an issue with Listen and you're "in a hurry"? First, try Polling mode as a workaround (described below).
12
+
13
+ Otherwise ...
14
+
15
+ Output from using `LISTEN_GEM_DEBUGGING` (debug mode) is often *crucial* to quickly diagnosing and fixing issues.
16
+
17
+ There are TOO MANY possible and surprising reasons why Listen "doesn't work as expected" (e.g. Dropbox folders, editor settings, system limits) - and the best way to find out is by going through these 3 steps first:
18
+
19
+ See [TROUBLESHOOTING](https://github.com/guard/listen/blob/master/TROUBLESHOOTING.md)
20
+
21
+ Once you've reproduced the problem in debug mode (`LISTEN_GEM_DEBUGGING`), paste the output it into a Gist and link it to your issue.
22
+
23
+
9
24
  ## Features
10
25
 
11
26
  * Supports watching multiple directories from a single listener.
@@ -25,8 +40,7 @@ Please note that:
25
40
 
26
41
  ## Pending features / issues
27
42
 
28
- * ~~Non-recursive directory scanning~~ [#111](https://github.com/guard/listen/issues/111)
29
- * Symlinks support. [#25](https://github.com/guard/listen/issues/25)
43
+ * Ignored directories are actually still scanned [#274](https://github.com/guard/listen/issues/274) (Except when polling)
30
44
  * Directory/adapter specific configuration options
31
45
  * Support for plugins
32
46
 
@@ -1,7 +1,9 @@
1
+ require 'listen/record/entry'
2
+ require 'listen/record/symlink_detector'
3
+
1
4
  module Listen
2
5
  class Record
3
6
  include Celluloid
4
-
5
7
  # TODO: one Record object per watched directory?
6
8
 
7
9
  # TODO: deprecate
@@ -100,34 +102,32 @@ module Listen
100
102
  end
101
103
  end
102
104
 
105
+ # TODO: test with a file name given
106
+ # TODO: test other permissions
107
+ # TODO: test with mixed encoding
103
108
  def _fast_build(root)
109
+ symlink_detector = SymlinkDetector.new
104
110
  @paths[root] = _auto_hash
105
- left = Queue.new
106
- left << '.'
107
-
108
- until left.empty?
109
- dirname = left.pop
110
- add_dir(root, dirname)
111
-
112
- path = ::File.join(root, dirname)
113
- current = Dir.entries(path.to_s) - %w(. ..)
114
-
115
- current.each do |entry|
116
- full_path = ::File.join(path, entry)
117
-
118
- if Dir.exist?(full_path)
119
- left << (dirname == '.' ? entry : ::File.join(dirname, entry))
120
- else
121
- begin
122
- lstat = ::File.lstat(full_path)
123
- data = { mtime: lstat.mtime.to_f, mode: lstat.mode }
124
- _fast_update_file(root, dirname, entry, data)
125
- rescue SystemCallError
126
- _fast_unset_path(root, dirname, entry)
127
- end
128
- end
129
- end
130
- end
111
+ remaining = Queue.new
112
+ remaining << Entry.new(root, nil, nil)
113
+ _fast_build_dir(remaining, symlink_detector) until remaining.empty?
114
+ end
115
+
116
+ def _fast_build_dir(remaining, symlink_detector)
117
+ entry = remaining.pop
118
+ entry.children.each { |child| remaining << child }
119
+ symlink_detector.verify_unwatched!(entry)
120
+ add_dir(entry.root, entry.record_dir_key)
121
+ rescue Errno::ENOTDIR
122
+ _fast_try_file(entry)
123
+ rescue SystemCallError
124
+ _fast_unset_path(entry.root, entry.relative, entry.name)
125
+ end
126
+
127
+ def _fast_try_file(entry)
128
+ _fast_update_file(entry.root, entry.relative, entry.name, entry.meta)
129
+ rescue SystemCallError
130
+ _fast_unset_path(entry.root, entry.relative, entry.name)
131
131
  end
132
132
  end
133
133
  end
@@ -0,0 +1,51 @@
1
+ module Listen
2
+ # @private api
3
+ class Record
4
+ # Represents a directory entry (dir or file)
5
+ class Entry
6
+ # file: "/home/me/watched_dir", "app/models", "foo.rb"
7
+ # dir, "/home/me/watched_dir", "."
8
+ def initialize(root, relative, name = nil)
9
+ @root, @relative, @name = root, relative, name
10
+ end
11
+
12
+ attr_reader :root, :relative, :name
13
+
14
+ def children
15
+ child_relative = _join
16
+ (Dir.entries(sys_path) - %w(. ..)).map do |name|
17
+ Entry.new(@root, child_relative, name)
18
+ end
19
+ end
20
+
21
+ def meta
22
+ lstat = ::File.lstat(sys_path)
23
+ { mtime: lstat.mtime.to_f, mode: lstat.mode }
24
+ end
25
+
26
+ # record hash is e.g.
27
+ # if @record["/home/me/watched_dir"]["project/app/models"]["foo.rb"]
28
+ # if @record["/home/me/watched_dir"]["project/app"]["models"]
29
+ # record_dir_key is "project/app/models"
30
+ def record_dir_key
31
+ ::File.join(*[@relative, @name].compact)
32
+ end
33
+
34
+ def sys_path
35
+ # Use full path in case someone uses chdir
36
+ ::File.join(*[@root, @relative, @name].compact)
37
+ end
38
+
39
+ def real_path
40
+ @real_path ||= ::File.realpath(sys_path)
41
+ end
42
+
43
+ private
44
+
45
+ def _join
46
+ args = [@relative, @name].compact
47
+ args.empty? ? nil : ::File.join(*args)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,59 @@
1
+ require 'set'
2
+
3
+ module Listen
4
+ # @private api
5
+ class Record
6
+ class SymlinkDetector
7
+ SYMLINK_LOOP_ERROR = <<-EOS
8
+ ** ERROR: Listen detected a duplicate directory being watched! **
9
+
10
+ (This may be due to symlinks pointing to parent directories).
11
+
12
+ Duplicate: %s
13
+
14
+ which already is added as: %s
15
+
16
+ Listen is refusing to continue, because this may likely result in
17
+ an infinite loop.
18
+
19
+ Suggestions:
20
+
21
+ 1) (best option) watch only directories you care about, e.g.
22
+ either symlinked folders or folders with the real directories,
23
+ but not both.
24
+
25
+ 2) reorganize your project so that watched directories do not
26
+ contain symlinked directories
27
+
28
+ 3) submit patches so that Listen can reliably and quickly (!)
29
+ detect symlinks to already watched read directories, skip
30
+ them, and then reasonably choose which symlinked paths to
31
+ report as changed (if any)
32
+
33
+ 4) (not worth it) help implement a "reverse symlink lookup"
34
+ function in Listen, which - given a real directory - would
35
+ return all the symlinks pointing to that directory
36
+
37
+ Issue: https://github.com/guard/listen/issues/259
38
+ EOS
39
+
40
+ def initialize
41
+ @real_dirs = Set.new
42
+ end
43
+
44
+ def verify_unwatched!(entry)
45
+ real_path = entry.real_path
46
+ @real_dirs.add?(real_path) || _fail(entry.sys_path, real_path)
47
+ end
48
+
49
+ private
50
+
51
+ def _fail(symlinked, real_path)
52
+ STDERR.puts format(SYMLINK_LOOP_ERROR, symlinked, real_path)
53
+
54
+ # Note Celluloid eats up abort message anyway
55
+ fail 'Failed due to looped symlinks'
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,3 +1,3 @@
1
1
  module Listen
2
- VERSION = '2.7.12'
2
+ VERSION = '2.8.0'
3
3
  end
@@ -193,21 +193,24 @@ describe Listen::Record do
193
193
  before do
194
194
  allow(listener).to receive(:directories) { directories }
195
195
 
196
- allow(::File).to receive(:lstat) do |path|
197
- fail "::File.lstat stub called with: #{path.inspect}"
198
- end
199
-
200
- allow(::Dir).to receive(:entries) do |path|
201
- fail "::Dir.entries stub called with: #{path.inspect}"
202
- end
203
-
204
- allow(::Dir).to receive(:exist?) do |path|
205
- fail "::Dir.exist? stub called with: #{path.inspect}"
196
+ stubs = {
197
+ ::File => %w(lstat realpath),
198
+ ::Dir => %w(entries exist?)
199
+ }
200
+
201
+ stubs.each do |klass, meths|
202
+ meths.each do |meth|
203
+ allow(klass).to receive(meth.to_sym) do |*args|
204
+ fail "stub called: #{klass}.#{meth}(#{args.map(&:inspect) * ', '})"
205
+ end
206
+ end
206
207
  end
207
208
  end
208
209
 
209
210
  it 're-inits paths' do
210
- allow(::Dir).to receive(:entries) { [] }
211
+ allow(::Dir).to receive(:entries).and_return([])
212
+ allow(::File).to receive(:realpath).with('/dir1').and_return('/dir1')
213
+ allow(::File).to receive(:realpath).with('/dir2').and_return('/dir2')
211
214
 
212
215
  record.update_file(dir, 'path/file.rb', mtime: 1.1)
213
216
  record.build
@@ -219,18 +222,19 @@ describe Listen::Record do
219
222
  let(:bar_stat) { instance_double(::File::Stat, mtime: 2.3, mode: 0755) }
220
223
 
221
224
  context 'with no subdirs' do
222
-
223
225
  before do
224
- expect(::Dir).to receive(:entries).with('/dir1/.') { %w(foo bar) }
225
- expect(::Dir).to receive(:exist?).with('/dir1/./foo') { false }
226
- expect(::Dir).to receive(:exist?).with('/dir1/./bar') { false }
227
- expect(::File).to receive(:lstat).with('/dir1/./foo') { foo_stat }
228
- expect(::File).to receive(:lstat).with('/dir1/./bar') { bar_stat }
229
-
230
- expect(::Dir).to receive(:entries).with('/dir2/.') { [] }
226
+ allow(::Dir).to receive(:entries).with('/dir1') { %w(foo bar) }
227
+ allow(::Dir).to receive(:entries).with('/dir1/foo').and_raise(Errno::ENOTDIR)
228
+ allow(::Dir).to receive(:entries).with('/dir1/bar').and_raise(Errno::ENOTDIR)
229
+ allow(::File).to receive(:lstat).with('/dir1/foo') { foo_stat }
230
+ allow(::File).to receive(:lstat).with('/dir1/bar') { bar_stat }
231
+ allow(::Dir).to receive(:entries).with('/dir2') { [] }
232
+
233
+ allow(::File).to receive(:realpath).with('/dir1').and_return('/dir1')
234
+ allow(::File).to receive(:realpath).with('/dir2').and_return('/dir2')
231
235
  end
232
236
 
233
- it 'builds record' do
237
+ it 'builds record' do
234
238
  record.build
235
239
  expect(record.paths.keys).to eq %w( /dir1 /dir2 )
236
240
  expect(record.paths['/dir1']).
@@ -242,15 +246,16 @@ describe Listen::Record do
242
246
 
243
247
  context 'with subdir containing files' do
244
248
  before do
245
- expect(::Dir).to receive(:entries).with('/dir1/.') { %w(foo) }
246
- expect(::Dir).to receive(:exist?).with('/dir1/./foo') { true }
247
-
248
- expect(::Dir).to receive(:entries).with('/dir1/foo') { %w(bar) }
249
-
250
- expect(::Dir).to receive(:exist?).with('/dir1/foo/bar') { false }
251
- expect(::File).to receive(:lstat).with('/dir1/foo/bar') { bar_stat }
252
-
253
- expect(::Dir).to receive(:entries).with('/dir2/.') { [] }
249
+ allow(::Dir).to receive(:entries).with('/dir1') { %w(foo) }
250
+ allow(::Dir).to receive(:entries).with('/dir1/foo') { %w(bar) }
251
+ allow(::Dir).to receive(:entries).with('/dir1/foo/bar').and_raise(Errno::ENOTDIR)
252
+ allow(::File).to receive(:lstat).with('/dir1/foo/bar') { bar_stat }
253
+ allow(::Dir).to receive(:entries).with('/dir2') { [] }
254
+
255
+ allow(::File).to receive(:realpath).with('/dir1').and_return('/dir1')
256
+ allow(::File).to receive(:realpath).with('/dir2').and_return('/dir2')
257
+ allow(::File).to receive(:realpath).with('/dir1/foo').
258
+ and_return('/dir1/foo')
254
259
  end
255
260
 
256
261
  it 'builds record' do
@@ -264,18 +269,12 @@ describe Listen::Record do
264
269
 
265
270
  context 'with subdir containing dirs' do
266
271
  before do
267
- expect(::Dir).to receive(:entries).with('/dir1/.') { %w(foo) }
268
- expect(::Dir).to receive(:exist?).with('/dir1/./foo') { true }
269
-
270
- expect(::Dir).to receive(:entries).with('/dir1/foo') { %w(bar baz) }
271
-
272
- expect(::Dir).to receive(:exist?).with('/dir1/foo/bar') { true }
273
- expect(::Dir).to receive(:entries).with('/dir1/foo/bar') { [] }
274
-
275
- expect(::Dir).to receive(:exist?).with('/dir1/foo/baz') { true }
276
- expect(::Dir).to receive(:entries).with('/dir1/foo/baz') { [] }
277
-
278
- expect(::Dir).to receive(:entries).with('/dir2/.') { [] }
272
+ allow(::File).to receive(:realpath) { |path| path }
273
+ allow(::Dir).to receive(:entries).with('/dir1') { %w(foo) }
274
+ allow(::Dir).to receive(:entries).with('/dir1/foo') { %w(bar baz) }
275
+ allow(::Dir).to receive(:entries).with('/dir1/foo/bar') { [] }
276
+ allow(::Dir).to receive(:entries).with('/dir1/foo/baz') { [] }
277
+ allow(::Dir).to receive(:entries).with('/dir2') { [] }
279
278
  end
280
279
 
281
280
  it 'builds record' do
@@ -290,5 +289,22 @@ describe Listen::Record do
290
289
  expect(record.paths['/dir2']).to eq({})
291
290
  end
292
291
  end
292
+
293
+ context 'with subdir containing symlink to parent' do
294
+ subject { record.paths }
295
+ before do
296
+ allow(::Dir).to receive(:entries).with('/dir1') { %w(foo) }
297
+ allow(::Dir).to receive(:entries).with('/dir1/foo') { %w(foo) }
298
+ allow(::File).to receive(:realpath).with('/dir1').and_return('/bar')
299
+ allow(::File).to receive(:realpath).with('/dir1/foo').and_return('/bar')
300
+ end
301
+
302
+ it 'shows message and aborts with error' do
303
+ expect(STDERR).to receive(:puts).with(/detected a duplicate directory/)
304
+
305
+ expect { record.build }.to raise_error(RuntimeError,
306
+ /Failed due to looped symlinks/)
307
+ end
308
+ end
293
309
  end
294
310
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: listen
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.12
4
+ version: 2.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thibaud Guillaume-Gentil
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-11-13 00:00:00.000000000 Z
11
+ date: 2014-11-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: celluloid
@@ -165,6 +165,8 @@ files:
165
165
  - lib/listen/options.rb
166
166
  - lib/listen/queue_optimizer.rb
167
167
  - lib/listen/record.rb
168
+ - lib/listen/record/entry.rb
169
+ - lib/listen/record/symlink_detector.rb
168
170
  - lib/listen/silencer.rb
169
171
  - lib/listen/tcp.rb
170
172
  - lib/listen/tcp/broadcaster.rb