listen 2.7.6 → 2.7.7

Sign up to get free protection for your applications and to get access to all the features.
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