listen 2.7.6 → 2.7.7

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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -0
  3. data/.rspec +0 -0
  4. data/.rubocop.yml +0 -0
  5. data/.travis.yml +0 -0
  6. data/.yardopts +0 -0
  7. data/CHANGELOG.md +0 -0
  8. data/CONTRIBUTING.md +0 -0
  9. data/Gemfile +2 -0
  10. data/Guardfile +2 -0
  11. data/LICENSE.txt +0 -0
  12. data/README.md +0 -0
  13. data/Rakefile +0 -0
  14. data/lib/listen.rb +0 -0
  15. data/lib/listen/adapter.rb +0 -0
  16. data/lib/listen/adapter/base.rb +47 -21
  17. data/lib/listen/adapter/bsd.rb +31 -25
  18. data/lib/listen/adapter/darwin.rb +13 -12
  19. data/lib/listen/adapter/linux.rb +45 -36
  20. data/lib/listen/adapter/polling.rb +12 -7
  21. data/lib/listen/adapter/tcp.rb +9 -4
  22. data/lib/listen/adapter/windows.rb +46 -58
  23. data/lib/listen/change.rb +12 -8
  24. data/lib/listen/cli.rb +0 -0
  25. data/lib/listen/directory.rb +30 -22
  26. data/lib/listen/file.rb +9 -8
  27. data/lib/listen/listener.rb +35 -12
  28. data/lib/listen/options.rb +23 -0
  29. data/lib/listen/queue_optimizer.rb +23 -13
  30. data/lib/listen/record.rb +98 -21
  31. data/lib/listen/silencer.rb +21 -40
  32. data/lib/listen/tcp.rb +0 -0
  33. data/lib/listen/tcp/broadcaster.rb +0 -0
  34. data/lib/listen/tcp/message.rb +0 -0
  35. data/lib/listen/version.rb +1 -1
  36. data/listen.gemspec +0 -0
  37. data/spec/acceptance/listen_spec.rb +0 -0
  38. data/spec/acceptance/tcp_spec.rb +0 -0
  39. data/spec/lib/listen/adapter/base_spec.rb +17 -16
  40. data/spec/lib/listen/adapter/bsd_spec.rb +0 -0
  41. data/spec/lib/listen/adapter/darwin_spec.rb +11 -4
  42. data/spec/lib/listen/adapter/linux_spec.rb +20 -29
  43. data/spec/lib/listen/adapter/polling_spec.rb +15 -13
  44. data/spec/lib/listen/adapter/tcp_spec.rb +6 -3
  45. data/spec/lib/listen/adapter/windows_spec.rb +0 -0
  46. data/spec/lib/listen/adapter_spec.rb +0 -0
  47. data/spec/lib/listen/change_spec.rb +21 -27
  48. data/spec/lib/listen/directory_spec.rb +60 -42
  49. data/spec/lib/listen/file_spec.rb +16 -20
  50. data/spec/lib/listen/listener_spec.rb +136 -99
  51. data/spec/lib/listen/record_spec.rb +205 -62
  52. data/spec/lib/listen/silencer_spec.rb +44 -114
  53. data/spec/lib/listen/tcp/broadcaster_spec.rb +0 -0
  54. data/spec/lib/listen/tcp/listener_spec.rb +8 -5
  55. data/spec/lib/listen/tcp/message_spec.rb +0 -0
  56. data/spec/lib/listen_spec.rb +0 -0
  57. data/spec/spec_helper.rb +0 -0
  58. data/spec/support/acceptance_helper.rb +15 -4
  59. data/spec/support/fixtures_helper.rb +0 -0
  60. data/spec/support/platform_helper.rb +0 -0
  61. metadata +3 -2
@@ -5,18 +5,26 @@ module Listen
5
5
  def _smoosh_changes(changes)
6
6
  # TODO: adapter could be nil at this point (shutdown)
7
7
  if _adapter_class.local_fs?
8
- cookies = changes.group_by do |_, _, _, options|
8
+ cookies = changes.group_by do |_, _, _, _, options|
9
9
  (options || {})[:cookie]
10
10
  end
11
11
  _squash_changes(_reinterpret_related_changes(cookies))
12
12
  else
13
13
  smooshed = { modified: [], added: [], removed: [] }
14
- changes.each { |_, change, path, _| smooshed[change] << path.to_s }
14
+ changes.each do |_, change, dir, rel_path, _|
15
+ smooshed[change] << (dir + rel_path).to_s
16
+ end
15
17
  smooshed.tap { |s| s.each { |_, v| v.uniq! } }
16
18
  end
17
19
  end
18
20
 
21
+ # groups changes into the expected structure expected by
22
+ # clients
19
23
  def _squash_changes(changes)
24
+ # We combine here for backward compatibility
25
+ # Newer clients should receive dir and path separately
26
+ changes = changes.map { |change, dir, path| [change, dir + path] }
27
+
20
28
  actions = changes.group_by(&:last).map do |path, action_list|
21
29
  [_logical_action_for(path, action_list.map(&:first)), path.to_s]
22
30
  end
@@ -63,15 +71,16 @@ module Listen
63
71
  def _reinterpret_related_changes(cookies)
64
72
  table = { moved_to: :added, moved_from: :removed }
65
73
  cookies.map do |_, changes|
66
- file = _detect_possible_editor_save(changes)
67
- if file
68
- [[:modified, file]]
74
+ data = _detect_possible_editor_save(changes)
75
+ if data
76
+ to_dir, to_file = data
77
+ [[:modified, to_dir, to_file]]
69
78
  else
70
- not_silenced = changes.reject do |type, _, path, _|
71
- _silenced?(path, type)
79
+ not_silenced = changes.reject do |type, _, _, path, _|
80
+ _silenced?(Pathname(path), type)
72
81
  end
73
- not_silenced.map do |_, change, path, _|
74
- [table.fetch(change, change), path]
82
+ not_silenced.map do |_, change, dir, path, _|
83
+ [table.fetch(change, change), dir, path]
75
84
  end
76
85
  end
77
86
  end.flatten(1)
@@ -81,14 +90,14 @@ module Listen
81
90
  return unless changes.size == 2
82
91
 
83
92
  from_type = from_change = from = nil
84
- to_type = to_change = to = nil
93
+ to_type = to_change = to_dir = to = nil
85
94
 
86
95
  changes.each do |data|
87
96
  case data[1]
88
97
  when :moved_from
89
- from_type, from_change, from, _ = data
98
+ from_type, from_change, _, from, _ = data
90
99
  when :moved_to
91
- to_type, to_change, to, _ = data
100
+ to_type, to_change, to_dir, to, _ = data
92
101
  else
93
102
  return nil
94
103
  end
@@ -98,7 +107,8 @@ module Listen
98
107
 
99
108
  # Expect an ignored moved_from and non-ignored moved_to
100
109
  # to qualify as an "editor modify"
101
- _silenced?(from, from_type) && !_silenced?(to, to_type) ? to : nil
110
+ return unless _silenced?(Pathname(from), from_type)
111
+ _silenced?(Pathname(to), to_type) ? nil : [to_dir, to]
102
112
  end
103
113
  end
104
114
  end
data/lib/listen/record.rb CHANGED
@@ -2,52 +2,129 @@ module Listen
2
2
  class Record
3
3
  include Celluloid
4
4
 
5
+ # TODO: one Record object per watched directory?
6
+
5
7
  # TODO: deprecate
6
8
  attr_accessor :paths, :listener
7
9
 
8
10
  def initialize(listener)
9
11
  @listener = listener
10
- @paths = _init_paths
12
+ @paths = _auto_hash
13
+ end
14
+
15
+ def add_dir(dir, rel_path)
16
+ return if [nil, '', '.'].include? rel_path
17
+ @paths[dir.to_s][rel_path] ||= {}
11
18
  end
12
19
 
13
- def set_path(type, path, data = {})
14
- new_data = file_data(path).merge(data).merge(type: type)
15
- @paths[::File.dirname(path)][::File.basename(path)] = new_data
20
+ def update_file(dir, rel_path, data)
21
+ dirname, basename = Pathname(rel_path).split.map(&:to_s)
22
+ _fast_update_file(dir, dirname, basename, data)
16
23
  end
17
24
 
18
- def unset_path(path)
19
- @paths[::File.dirname(path)].delete(::File.basename(path))
25
+ def unset_path(dir, rel_path)
26
+ dirname, basename = Pathname(rel_path).split.map(&:to_s)
27
+
28
+ @paths[dir.to_s][dirname] ||= {}
29
+ _fast_unset_path(dir, dirname, basename)
20
30
  end
21
31
 
22
- def file_data(path)
23
- @paths[::File.dirname(path)][::File.basename(path)] || {}
32
+ def file_data(dir, rel_path)
33
+ root = @paths[dir.to_s]
34
+ dirname, basename = Pathname(rel_path).split.map(&:to_s)
35
+ if [nil, '', '.'].include? dirname
36
+ root[basename] ||= {}
37
+ root[basename].dup
38
+ else
39
+ root[dirname] ||= {}
40
+ root[dirname][basename] ||= {}
41
+ root[dirname][basename].dup
42
+ end
24
43
  end
25
44
 
26
- def dir_entries(path)
27
- @paths[path.to_s].dup
45
+ def dir_entries(dir, rel_path)
46
+ tree = if [nil, '', '.'].include? rel_path.to_s
47
+ @paths[dir.to_s]
48
+ else
49
+ @paths[dir.to_s][rel_path.to_s] ||= _auto_hash
50
+ @paths[dir.to_s][rel_path.to_s]
51
+ end
52
+
53
+ result = {}
54
+ tree.each do |key, values|
55
+ # only get data for file entries
56
+ result[key] = values.key?(:mtime) ? values : {}
57
+ end
58
+ result
28
59
  end
29
60
 
30
61
  def build
31
- @last_build_at = Time.now
32
- @paths = _init_paths
33
- listener.directories.each do |path|
34
- options = { recursive: true, silence: true, build: true }
35
- listener.sync(:change_pool).change(:dir, path, options)
62
+ start = Time.now.to_f
63
+ @paths = _auto_hash
64
+
65
+ # TODO: refactor this out (1 Record = 1 watched dir)
66
+ listener.directories.each do |directory|
67
+ _fast_build(directory.to_s)
36
68
  end
37
- sleep 0.01 until @last_build_at + 0.1 < Time.now
69
+
70
+ Celluloid.logger.info "Record.build took #{Time.now.to_f - start} seconds"
38
71
  rescue
39
72
  Celluloid.logger.warn "build crashed: #{$!.inspect}"
40
73
  raise
41
74
  end
42
75
 
43
- def still_building!
44
- @last_build_at = Time.now
45
- end
46
-
47
76
  private
48
77
 
49
- def _init_paths
78
+ def _auto_hash
50
79
  Hash.new { |h, k| h[k] = Hash.new }
51
80
  end
81
+
82
+ def _fast_update_file(dir, dirname, basename, data)
83
+ root = @paths[dir.to_s]
84
+ if [nil, '', '.'].include? dirname
85
+ root[basename] = (root[basename] || {}).merge(data)
86
+ else
87
+ root[dirname] ||= {}
88
+ root[dirname][basename] = (root[dirname][basename] || {}).merge(data)
89
+ end
90
+ end
91
+
92
+ def _fast_unset_path(dir, dirname, basename)
93
+ root = @paths[dir.to_s]
94
+ # this may need to be reworked to properly remove
95
+ # entries from a tree, without adding non-existing dirs to the record
96
+ return unless root.key?(dirname)
97
+ root[dirname].delete(basename)
98
+ end
99
+
100
+ def _fast_build(root)
101
+ @paths[root] = _auto_hash
102
+ left = Queue.new
103
+ left << '.'
104
+
105
+ while !left.empty?
106
+ dirname = left.pop
107
+ add_dir(root, dirname)
108
+
109
+ path = ::File.join(root, dirname)
110
+ current = Dir.entries(path.to_s) - %w(. ..)
111
+
112
+ current.each do |entry|
113
+ full_path = ::File.join(path, entry)
114
+
115
+ if Dir.exist?(full_path)
116
+ left << (dirname == '.' ? entry : ::File.join(dirname, entry))
117
+ else
118
+ begin
119
+ lstat = ::File.lstat(full_path)
120
+ data = { mtime: lstat.mtime.to_f, mode: lstat.mode }
121
+ _fast_update_file(root, dirname, entry, data)
122
+ rescue SystemCallError
123
+ _fast_unset_path(root, dirname, entry)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
52
129
  end
53
130
  end
@@ -1,7 +1,5 @@
1
1
  module Listen
2
2
  class Silencer
3
- include Celluloid
4
-
5
3
  # The default list of directories that get ignored.
6
4
  DEFAULT_IGNORED_DIRECTORIES = %r{^(?:
7
5
  \.git
@@ -40,63 +38,46 @@ module Listen
40
38
  | ~
41
39
  )$}x
42
40
 
43
- attr_accessor :listener, :only_patterns, :ignore_patterns
41
+ attr_accessor :only_patterns, :ignore_patterns
42
+
43
+ def initialize
44
+ configure({})
45
+ end
44
46
 
45
- def initialize(listener)
46
- @listener = listener
47
- _init_only_patterns
48
- _init_ignore_patterns
47
+ def configure(options)
48
+ @only_patterns = options[:only] ? Array(options[:only]) : nil
49
+ @ignore_patterns = _init_ignores(options[:ignore], options[:ignore!])
49
50
  end
50
51
 
51
- def silenced?(path, type)
52
- silenced = false
52
+ # Note: relative_path is temporarily expected to be a relative Pathname to
53
+ # make refactoring easier (ideally, it would take a string)
53
54
 
54
- relative_path = _relative_path(path)
55
+ # TODO: switch type and path places - and verify
56
+ def silenced?(relative_path, type)
57
+ path = relative_path.to_s
55
58
 
56
59
  if only_patterns && type == :file
57
- silenced = !only_patterns.any? { |pattern| relative_path =~ pattern }
60
+ return true unless only_patterns.any? { |pattern| path =~ pattern }
58
61
  end
59
62
 
60
- silenced || ignore_patterns.any? { |pattern| relative_path =~ pattern }
63
+ ignore_patterns.any? { |pattern| path =~ pattern }
61
64
  end
62
65
 
63
66
  private
64
67
 
65
- def _init_only_patterns
66
- return unless listener.options[:only]
67
-
68
- @only_patterns = Array(listener.options[:only])
69
- end
70
-
71
- def _init_ignore_patterns
72
- options = listener.options
68
+ attr_reader :options
73
69
 
70
+ def _init_ignores(ignores, overrides)
74
71
  patterns = []
75
- unless options[:ignore!]
72
+ unless overrides
76
73
  patterns << DEFAULT_IGNORED_DIRECTORIES
77
74
  patterns << DEFAULT_IGNORED_EXTENSIONS
78
75
  end
79
76
 
80
- patterns << options[:ignore]
81
- patterns << options[:ignore!]
82
-
83
- patterns.compact!
84
- patterns.flatten!
77
+ patterns << ignores
78
+ patterns << overrides
85
79
 
86
- @ignore_patterns = patterns
87
- end
88
-
89
- def _relative_path(path)
90
- relative_paths = listener.directories.map do |dir|
91
- begin
92
- path.relative_path_from(dir).to_s
93
- rescue ArgumentError
94
- # Windows raises errors across drives, e.g. when 'C:/' and 'E:/dir'
95
- # So, here's a Dirty hack to fool the detect() below..
96
- '../'
97
- end
98
- end
99
- relative_paths.detect { |rel_path| !rel_path.start_with?('../') }
80
+ patterns.compact.flatten
100
81
  end
101
82
  end
102
83
  end
data/lib/listen/tcp.rb CHANGED
File without changes
File without changes
File without changes
@@ -1,3 +1,3 @@
1
1
  module Listen
2
- VERSION = '2.7.6'
2
+ VERSION = '2.7.7'
3
3
  end
data/listen.gemspec CHANGED
File without changes
File without changes
File without changes
@@ -1,29 +1,30 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Listen::Adapter::Base do
4
- subject { described_class.new(listener) }
3
+ include Listen
5
4
 
6
- let(:listener) { instance_double(Listen::Listener) }
5
+ describe Adapter::Base do
7
6
 
8
- before { allow(listener).to receive(:async).with(:change_pool) { worker } }
7
+ class FakeAdapter < described_class
8
+ def initialize(*args)
9
+ super(*args)
10
+ end
11
+ end
12
+
13
+ subject { FakeAdapter.new(mq: mq, directories: []) }
14
+
15
+ let(:mq) { instance_double(Listener) }
9
16
 
10
17
  describe '#_notify_change' do
18
+ let(:dir) { Pathname.pwd }
19
+
11
20
  context 'listener is listening or paused' do
12
- let(:worker) { instance_double(Listen::Change) }
21
+ let(:worker) { instance_double(Change) }
13
22
 
14
23
  it 'calls change on change_pool asynchronously' do
15
- expect(worker).to receive(:change).
16
- with(:dir, 'path', recursive: true)
17
- subject.send(:_notify_change, :dir, 'path', recursive: true)
18
- end
19
- end
20
-
21
- context 'listener is stopped' do
22
- let(:worker) { nil }
24
+ expect(mq).to receive(:_queue_raw_change).
25
+ with(:dir, dir, 'path', recursive: true)
23
26
 
24
- it 'does not fail when no worker is available' do
25
- expect(worker).to_not receive(:change)
26
- subject.send(:_notify_change, :dir, 'path', recursive: true)
27
+ subject.send(:_queue_change, :dir, dir, 'path', recursive: true)
27
28
  end
28
29
  end
29
30
  end
File without changes
@@ -1,6 +1,10 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Listen::Adapter::Darwin do
3
+ require 'listen/adapter/darwin'
4
+
5
+ include Listen
6
+
7
+ describe Adapter::Darwin do
4
8
  describe 'class' do
5
9
  subject { described_class }
6
10
  it { should be_local_fs }
@@ -13,13 +17,16 @@ describe Listen::Adapter::Darwin do
13
17
  end
14
18
 
15
19
  let(:options) { {} }
16
- let(:listener) { instance_double(Listen::Listener, options: options) }
20
+ let(:mq) { instance_double(Listener, options: options) }
17
21
 
18
22
  describe '#_latency' do
19
- subject { described_class.new(listener).send(:_latency) }
23
+ subject do
24
+ adapter = described_class.new(options.merge(mq: mq, directories: []))
25
+ adapter.options.latency
26
+ end
20
27
 
21
28
  context 'with no overriding option' do
22
- it { should eq described_class.const_get('DEFAULT_LATENCY') }
29
+ it { should eq 0.1 }
23
30
  end
24
31
 
25
32
  context 'with custom latency overriding' do
@@ -13,26 +13,17 @@ describe Listen::Adapter::Linux do
13
13
  end
14
14
 
15
15
  if linux?
16
- let(:listener) { instance_double(Listen::Listener) }
17
- let(:adapter) { described_class.new(listener) }
16
+ let(:directories) { [] }
17
+ let(:mq) { instance_double(Listen::Listener) }
18
18
 
19
- describe '#initialize' do
20
- before do
21
- allow(listener).to receive(:directories) { [] }
22
- end
23
- it 'requires rb-inotify gem' do
24
- adapter.send(:_configure)
25
- expect(defined?(INotify)).to be
26
- end
27
- end
19
+ subject { described_class.new(mq: mq, directories: directories) }
28
20
 
29
21
  # workaround: Celluloid ignores SystemExit exception messages
30
22
  describe 'inotify limit message' do
31
- let!(:adapter) { described_class.new(listener) }
23
+ let(:directories) { [Pathname.pwd] }
32
24
 
33
25
  before do
34
26
  require 'rb-inotify'
35
- allow(listener).to receive(:directories) { ['foo/dir'] }
36
27
  fake_worker = double(:fake_worker)
37
28
  allow(fake_worker).to receive(:watch).and_raise(Errno::ENOSPC)
38
29
 
@@ -46,23 +37,21 @@ describe Listen::Adapter::Linux do
46
37
 
47
38
  # Expect RuntimeError here, for the sake of unit testing (actual
48
39
  # handling depends on Celluloid supervisor setup, which is beyond the
49
- # scope of adapter tests)
50
- expect { adapter.start }.to raise_error RuntimeError, expected_message
40
+ # scope of subject tests)
41
+ expect { subject.start }.to raise_error RuntimeError, expected_message
51
42
  end
52
43
  end
53
44
 
54
45
  describe '_callback' do
55
- before do
56
- allow(listener).to receive(:directories) { [] }
57
- end
58
-
46
+ let(:directories) { [Pathname.pwd] }
47
+ before { subject.configure }
59
48
  let(:expect_change) do
60
49
  lambda do |change|
61
- allow_any_instance_of(Listen::Adapter::Base).
62
- to receive(:_notify_change).
50
+ allow(mq).to receive(:_queue_raw_change).
63
51
  with(
64
52
  :file,
65
- Pathname.new('path/foo.txt'),
53
+ Pathname.pwd,
54
+ 'path/foo.txt',
66
55
  change: change,
67
56
  cookie: 123)
68
57
  end
@@ -70,13 +59,15 @@ describe Listen::Adapter::Linux do
70
59
 
71
60
  let(:event_callback) do
72
61
  lambda do |flags|
73
- callback = adapter.send(:_callback)
74
- callback.call double(
75
- :inotify_event,
76
- name: 'foo.txt',
77
- watcher: double(:watcher, path: 'path'),
78
- flags: flags,
79
- cookie: 123)
62
+ callbacks = subject.instance_variable_get(:'@callbacks')
63
+ callbacks.values.flatten.each do |callback|
64
+ callback.call double(
65
+ :inotify_event,
66
+ name: 'foo.txt',
67
+ watcher: double(:watcher, path: (Pathname.pwd + 'path').to_s),
68
+ flags: flags,
69
+ cookie: 123)
70
+ end
80
71
  end
81
72
  end
82
73