listen 2.7.5 → 2.7.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +0 -0
- data/.rspec +0 -0
- data/.rubocop.yml +0 -0
- data/.travis.yml +0 -1
- data/.yardopts +0 -0
- data/CHANGELOG.md +0 -0
- data/CONTRIBUTING.md +0 -0
- data/Gemfile +25 -4
- data/Guardfile +0 -0
- data/LICENSE.txt +0 -0
- data/README.md +18 -10
- data/Rakefile +0 -0
- data/lib/listen.rb +2 -4
- data/lib/listen/adapter.rb +13 -4
- data/lib/listen/adapter/base.rb +33 -16
- data/lib/listen/adapter/bsd.rb +21 -38
- data/lib/listen/adapter/darwin.rb +17 -25
- data/lib/listen/adapter/linux.rb +34 -52
- data/lib/listen/adapter/polling.rb +9 -25
- data/lib/listen/adapter/tcp.rb +27 -14
- data/lib/listen/adapter/windows.rb +67 -23
- data/lib/listen/change.rb +26 -23
- data/lib/listen/cli.rb +0 -0
- data/lib/listen/directory.rb +47 -58
- data/lib/listen/file.rb +66 -101
- data/lib/listen/listener.rb +214 -155
- data/lib/listen/queue_optimizer.rb +104 -0
- data/lib/listen/record.rb +15 -5
- data/lib/listen/silencer.rb +14 -10
- data/lib/listen/tcp.rb +0 -1
- data/lib/listen/tcp/broadcaster.rb +31 -26
- data/lib/listen/tcp/message.rb +2 -2
- data/lib/listen/version.rb +1 -1
- data/listen.gemspec +1 -1
- data/spec/acceptance/listen_spec.rb +151 -239
- data/spec/acceptance/tcp_spec.rb +125 -134
- data/spec/lib/listen/adapter/base_spec.rb +13 -30
- data/spec/lib/listen/adapter/bsd_spec.rb +7 -35
- data/spec/lib/listen/adapter/darwin_spec.rb +18 -30
- data/spec/lib/listen/adapter/linux_spec.rb +49 -55
- data/spec/lib/listen/adapter/polling_spec.rb +20 -35
- data/spec/lib/listen/adapter/tcp_spec.rb +25 -27
- data/spec/lib/listen/adapter/windows_spec.rb +7 -33
- data/spec/lib/listen/adapter_spec.rb +10 -10
- data/spec/lib/listen/change_spec.rb +55 -57
- data/spec/lib/listen/directory_spec.rb +105 -155
- data/spec/lib/listen/file_spec.rb +186 -73
- data/spec/lib/listen/listener_spec.rb +233 -216
- data/spec/lib/listen/record_spec.rb +60 -22
- data/spec/lib/listen/silencer_spec.rb +48 -75
- data/spec/lib/listen/tcp/broadcaster_spec.rb +78 -69
- data/spec/lib/listen/tcp/listener_spec.rb +28 -71
- data/spec/lib/listen/tcp/message_spec.rb +48 -14
- data/spec/lib/listen_spec.rb +3 -3
- data/spec/spec_helper.rb +6 -3
- data/spec/support/acceptance_helper.rb +250 -31
- data/spec/support/fixtures_helper.rb +6 -4
- data/spec/support/platform_helper.rb +2 -2
- metadata +5 -5
- data/lib/listen/tcp/listener.rb +0 -108
@@ -1,45 +1,39 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
|
3
|
+
require 'listen/tcp/message'
|
4
|
+
require 'listen/tcp/broadcaster'
|
5
|
+
|
6
|
+
describe Listen::Listener do
|
4
7
|
|
5
8
|
let(:host) { '10.0.0.2' }
|
6
9
|
let(:port) { 4000 }
|
7
10
|
|
8
11
|
subject { described_class.new("#{host}:#{port}", :recipient, options) }
|
9
12
|
let(:options) { {} }
|
10
|
-
let(:registry) {
|
13
|
+
let(:registry) { instance_double(Celluloid::Registry, :[]= => true) }
|
11
14
|
|
12
15
|
let(:supervisor) do
|
13
|
-
|
16
|
+
instance_double(Celluloid::SupervisionGroup, add: true, pool: true)
|
14
17
|
end
|
15
18
|
|
16
|
-
let(:record) {
|
17
|
-
let(:silencer) {
|
18
|
-
let(:adapter) {
|
19
|
-
let(:
|
20
|
-
let(:
|
21
|
-
let(:
|
19
|
+
let(:record) { instance_double(Listen::Record, terminate: true, build: true) }
|
20
|
+
let(:silencer) { instance_double(Listen::Silencer, terminate: true) }
|
21
|
+
let(:adapter) { instance_double(Listen::Adapter::Base) }
|
22
|
+
let(:async) { instance_double(Listen::TCP::Broadcaster, broadcast: true) }
|
23
|
+
let(:broadcaster) { instance_double(Listen::TCP::Broadcaster, async: async) }
|
24
|
+
let(:change_pool) { instance_double(Listen::Change, terminate: true) }
|
25
|
+
let(:change_pool_async) { instance_double('ChangePoolAsync') }
|
22
26
|
before do
|
23
|
-
Celluloid::Registry.
|
24
|
-
Celluloid::SupervisionGroup.
|
25
|
-
registry.
|
26
|
-
registry.
|
27
|
-
registry.
|
28
|
-
registry.
|
29
|
-
registry.
|
27
|
+
allow(Celluloid::Registry).to receive(:new) { registry }
|
28
|
+
allow(Celluloid::SupervisionGroup).to receive(:run!) { supervisor }
|
29
|
+
allow(registry).to receive(:[]).with(:silencer) { silencer }
|
30
|
+
allow(registry).to receive(:[]).with(:adapter) { adapter }
|
31
|
+
allow(registry).to receive(:[]).with(:record) { record }
|
32
|
+
allow(registry).to receive(:[]).with(:change_pool) { change_pool }
|
33
|
+
allow(registry).to receive(:[]).with(:broadcaster) { broadcaster }
|
30
34
|
end
|
31
35
|
|
32
36
|
describe '#initialize' do
|
33
|
-
its(:mode) { should be :recipient }
|
34
|
-
its(:host) { should eq host }
|
35
|
-
its(:port) { should eq port }
|
36
|
-
|
37
|
-
it 'raises on invalid mode' do
|
38
|
-
expect do
|
39
|
-
described_class.new(port, :foo)
|
40
|
-
end.to raise_error ArgumentError
|
41
|
-
end
|
42
|
-
|
43
37
|
it 'raises on omitted target' do
|
44
38
|
expect do
|
45
39
|
described_class.new(nil, :recipient)
|
@@ -50,21 +44,14 @@ describe Listen::TCP::Listener do
|
|
50
44
|
context 'when broadcaster' do
|
51
45
|
subject { described_class.new(port, :broadcaster) }
|
52
46
|
|
53
|
-
it { should be_a_broadcaster }
|
54
|
-
it { should_not be_a_recipient }
|
55
|
-
|
56
47
|
it 'does not force TCP adapter through options' do
|
57
48
|
expect(subject.options).not_to include(force_tcp: true)
|
58
49
|
end
|
59
50
|
|
60
|
-
context 'when host is omitted' do
|
61
|
-
its(:host) { should be_nil }
|
62
|
-
end
|
63
|
-
|
64
51
|
describe '#start' do
|
65
52
|
before do
|
66
|
-
|
67
|
-
broadcaster.
|
53
|
+
allow(subject).to receive(:_start_adapter)
|
54
|
+
allow(broadcaster).to receive(:start)
|
68
55
|
end
|
69
56
|
|
70
57
|
it 'registers broadcaster' do
|
@@ -79,49 +66,27 @@ describe Listen::TCP::Listener do
|
|
79
66
|
end
|
80
67
|
end
|
81
68
|
|
82
|
-
describe '
|
83
|
-
let(:async) { double('TCP broadcaster async', broadcast: true) }
|
84
|
-
let(:callback) { double(call: true) }
|
85
|
-
let(:changes) do
|
86
|
-
{ modified: ['/foo'], added: [], removed: [] }
|
87
|
-
end
|
88
|
-
|
69
|
+
describe 'queue' do
|
89
70
|
before do
|
90
|
-
broadcaster.
|
91
|
-
end
|
92
|
-
|
93
|
-
after do
|
94
|
-
subject.block.call changes.values
|
95
|
-
end
|
96
|
-
|
97
|
-
context 'when paused' do
|
98
|
-
it 'honours paused state and does nothing' do
|
99
|
-
subject.pause
|
100
|
-
expect(broadcaster).not_to receive(:async)
|
101
|
-
expect(callback).not_to receive(:call)
|
102
|
-
end
|
71
|
+
allow(broadcaster).to receive(:async).and_return async
|
103
72
|
end
|
104
73
|
|
105
74
|
context 'when stopped' do
|
106
75
|
it 'honours stopped state and does nothing' do
|
107
76
|
allow(subject).to receive(:supervisor) do
|
108
|
-
|
77
|
+
instance_double(Celluloid::SupervisionGroup, terminate: true)
|
109
78
|
end
|
110
79
|
|
111
80
|
subject.stop
|
81
|
+
subject.queue(:file, :modified, 'foo')
|
112
82
|
expect(broadcaster).not_to receive(:async)
|
113
|
-
expect(callback).not_to receive(:call)
|
114
83
|
end
|
115
84
|
end
|
116
85
|
|
117
86
|
it 'broadcasts changes asynchronously' do
|
118
|
-
message = Listen::TCP::Message.new
|
87
|
+
message = Listen::TCP::Message.new(:file, :modified, 'foo', {})
|
119
88
|
expect(async).to receive(:broadcast).with message.payload
|
120
|
-
|
121
|
-
|
122
|
-
it 'invokes original callback block' do
|
123
|
-
subject.block = callback
|
124
|
-
expect(callback).to receive(:call).with(*changes.values)
|
89
|
+
subject.queue(:file, :modified, 'foo')
|
125
90
|
end
|
126
91
|
end
|
127
92
|
end
|
@@ -132,13 +97,5 @@ describe Listen::TCP::Listener do
|
|
132
97
|
it 'forces TCP adapter through options' do
|
133
98
|
expect(subject.options).to include(force_tcp: true)
|
134
99
|
end
|
135
|
-
|
136
|
-
it { should_not be_a_broadcaster }
|
137
|
-
it { should be_a_recipient }
|
138
|
-
|
139
|
-
context 'when host is omitted' do
|
140
|
-
its(:host) { should eq described_class::DEFAULT_HOST }
|
141
|
-
end
|
142
100
|
end
|
143
|
-
|
144
101
|
end
|
@@ -11,31 +11,65 @@ describe Listen::TCP::Message do
|
|
11
11
|
|
12
12
|
describe '#initialize' do
|
13
13
|
it 'initializes with an object' do
|
14
|
-
message = described_class.new(
|
15
|
-
expect(message.object).to
|
14
|
+
message = described_class.new(1, 2, 3)
|
15
|
+
expect(message.object).to eq [1, 2, 3]
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
19
|
describe '#object=' do
|
20
|
-
|
21
|
-
|
20
|
+
subject do
|
21
|
+
described_class.new(object).tap do |message|
|
22
|
+
message.object = object
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#object' do
|
27
|
+
subject { super().object }
|
28
|
+
it { is_expected.to be object }
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#body' do
|
32
|
+
subject { super().body }
|
33
|
+
it { is_expected.to eq body }
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#size' do
|
37
|
+
subject { super().size }
|
38
|
+
it { is_expected.to eq size }
|
22
39
|
end
|
23
40
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
41
|
+
describe '#payload' do
|
42
|
+
subject { super().payload }
|
43
|
+
it { is_expected.to eq payload }
|
44
|
+
end
|
28
45
|
end
|
29
46
|
|
30
47
|
describe '#payload=' do
|
31
|
-
|
32
|
-
|
48
|
+
subject do
|
49
|
+
described_class.new(object).tap do |message|
|
50
|
+
message.payload = payload
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#object' do
|
55
|
+
subject { super().object }
|
56
|
+
it { is_expected.to eq object }
|
57
|
+
end
|
58
|
+
|
59
|
+
describe '#body' do
|
60
|
+
subject { super().body }
|
61
|
+
it { is_expected.to eq body }
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '#size' do
|
65
|
+
subject { super().size }
|
66
|
+
it { is_expected.to eq size }
|
33
67
|
end
|
34
68
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
69
|
+
describe '#payload' do
|
70
|
+
subject { super().payload }
|
71
|
+
it { is_expected.to be payload }
|
72
|
+
end
|
39
73
|
end
|
40
74
|
|
41
75
|
describe '.from_buffer' do
|
data/spec/lib/listen_spec.rb
CHANGED
@@ -9,7 +9,7 @@ describe Listen do
|
|
9
9
|
|
10
10
|
context 'when using :forward_to option' do
|
11
11
|
it 'initializes TCP-listener in broadcast-mode' do
|
12
|
-
expect(Listen::
|
12
|
+
expect(Listen::Listener).to receive(:new).
|
13
13
|
with(4000, :broadcaster, '/path', {})
|
14
14
|
described_class.to('/path', forward_to: 4000)
|
15
15
|
end
|
@@ -18,7 +18,7 @@ describe Listen do
|
|
18
18
|
it 'sets stopping at false' do
|
19
19
|
allow(Listen::Listener).to receive(:new)
|
20
20
|
Listen.to('/path')
|
21
|
-
expect(Listen.stopping).to
|
21
|
+
expect(Listen.stopping).to be_falsey
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
@@ -41,7 +41,7 @@ describe Listen do
|
|
41
41
|
|
42
42
|
describe '.on' do
|
43
43
|
it 'initializes TCP-listener in recipient-mode' do
|
44
|
-
expect(Listen::
|
44
|
+
expect(Listen::Listener).to receive(:new).
|
45
45
|
with(4000, :recipient, '/path')
|
46
46
|
described_class.on(4000, '/path')
|
47
47
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -16,15 +16,18 @@ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
|
16
16
|
|
17
17
|
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
18
18
|
RSpec.configure do |config|
|
19
|
-
config.color_enabled = true
|
20
19
|
config.order = :random
|
21
20
|
config.filter_run focus: true
|
22
|
-
config.treat_symbols_as_metadata_keys_with_true_values = true
|
23
21
|
config.run_all_when_everything_filtered = true
|
24
|
-
config.fail_fast = !ci?
|
22
|
+
# config.fail_fast = !ci?
|
25
23
|
config.expect_with :rspec do |c|
|
26
24
|
c.syntax = :expect
|
27
25
|
end
|
26
|
+
|
27
|
+
config.mock_with :rspec do |mocks|
|
28
|
+
mocks.verify_doubled_constant_names = true
|
29
|
+
mocks.verify_partial_doubles = true
|
30
|
+
end
|
28
31
|
end
|
29
32
|
|
30
33
|
require 'rspec/retry'
|
@@ -1,47 +1,266 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
end
|
1
|
+
{
|
2
|
+
modification: :modified,
|
3
|
+
addition: :added,
|
4
|
+
removal: :removed,
|
5
|
+
queued_modification: :modified,
|
6
|
+
queued_addition: :added,
|
7
|
+
}.each do |description, type|
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
9
|
+
RSpec::Matchers.define "process_#{description}_of".to_sym do |expected|
|
10
|
+
match do |actual|
|
11
|
+
# Use cases:
|
12
|
+
# 1. reset the changes so they don't have leftovers
|
13
|
+
# 2. keep the queue if we're testing for existing accumulated changes
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
# if were testing the queue (e.g. after unpause), don't reset
|
16
|
+
check_already_queued = /queued_/ =~ description
|
17
|
+
reset_queue = !check_already_queued
|
18
|
+
|
19
|
+
actual.listen(reset_queue) do
|
20
|
+
change_fs(type, expected) unless check_already_queued
|
21
|
+
end
|
22
|
+
actual.changes[type].include? expected
|
23
|
+
end
|
18
24
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
25
|
+
failure_message do |actual|
|
26
|
+
result = actual.changes.inspect
|
27
|
+
"expected #{result} to include #{description} of #{expected}"
|
28
|
+
end
|
29
|
+
|
30
|
+
failure_message_when_negated do |actual|
|
31
|
+
result = actual.changes.inspect
|
32
|
+
"expected #{result} to not include #{description} of #{expected}"
|
33
|
+
end
|
34
|
+
end
|
23
35
|
end
|
24
36
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
37
|
+
def change_fs(type, path)
|
38
|
+
case type
|
39
|
+
when :modified
|
40
|
+
unless File.exist?(path)
|
41
|
+
fail "Bad test: cannot modify #{path.inspect} (it doesn't exist)"
|
42
|
+
end
|
43
|
+
|
44
|
+
# wait until full second, because this might be followed by a modification
|
45
|
+
# event (which otherwise may not be detected every time)
|
46
|
+
_sleep_until_next_second(Pathname.pwd)
|
47
|
+
|
48
|
+
open(path, 'a') { |f| f.write('foo') }
|
49
|
+
|
50
|
+
# separate it from upcoming modifications"
|
51
|
+
_sleep_to_separate_events
|
52
|
+
when :added
|
53
|
+
if File.exist?(path)
|
54
|
+
fail "Bad test: cannot add #{path.inspect} (it already exists)"
|
55
|
+
end
|
56
|
+
|
57
|
+
# wait until full second, because this might be followed by a modification
|
58
|
+
# event (which otherwise may not be detected every time)
|
59
|
+
_sleep_until_next_second(Pathname.pwd)
|
60
|
+
|
61
|
+
open(path, 'w') { |f| f.write('foo') }
|
62
|
+
|
63
|
+
# separate it from upcoming modifications"
|
64
|
+
_sleep_to_separate_events
|
65
|
+
when :removed
|
66
|
+
unless File.exist?(path)
|
67
|
+
fail "Bad test: cannot remove #{path.inspect} (it doesn't exist)"
|
68
|
+
end
|
69
|
+
File.unlink(path)
|
70
|
+
else
|
71
|
+
fail "bad test: unknown type: #{type.inspect}"
|
30
72
|
end
|
31
73
|
end
|
32
74
|
|
33
|
-
#
|
34
|
-
#
|
75
|
+
# Used by change_fs() above so that the FS change (e.g. file created) happens
|
76
|
+
# as close to the start of a new second (time) as possible.
|
35
77
|
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
78
|
+
# E.g. if file is created at 1234567.999 (unix time), it's mtime on some
|
79
|
+
# filesystems is rounded, so it becomes 1234567.0, but if the change
|
80
|
+
# notification happens a little while later, e.g. at 1234568.111, now the file
|
81
|
+
# mtime and the current time in seconds are different (1234567 vs 1234568), and
|
82
|
+
# so the MD5 test won't kick in (see file.rb) - the file will not be considered
|
83
|
+
# for content checking (md5), so File.change will consider the file unmodified.
|
39
84
|
#
|
40
|
-
|
41
|
-
|
85
|
+
# This means, that if a file is added at 1234567.888 (and updated in Record),
|
86
|
+
# and then its content is modified at 1234567.999, and checking for changes
|
87
|
+
# happens at 1234568.111, the modification won't be detected.
|
88
|
+
# (because Record mtime is 1234567.0, current FS mtime from stat() is the
|
89
|
+
# same, and the checking happens in another second - 1234568).
|
90
|
+
#
|
91
|
+
# So basically, adding a file and detecting its later modification should all
|
92
|
+
# happen within 1 second (which makes testing and debugging difficult).
|
93
|
+
#
|
94
|
+
def _sleep_until_next_second(path)
|
95
|
+
Listen::File.inaccurate_mac_time?(path)
|
42
96
|
|
43
97
|
t = Time.now
|
44
98
|
diff = t.to_f - t.to_i
|
45
99
|
|
46
100
|
sleep(1.05 - diff)
|
47
101
|
end
|
102
|
+
|
103
|
+
# Special class to only allow changes within a specific time window
|
104
|
+
|
105
|
+
class TimedChanges
|
106
|
+
attr_reader :changes
|
107
|
+
|
108
|
+
def initialize
|
109
|
+
# Set to non-nil, because changes can immediately come after unpausing
|
110
|
+
# listener in an Rspec 'before()' block
|
111
|
+
@changes = { modified: [], added: [], removed: [] }
|
112
|
+
end
|
113
|
+
|
114
|
+
def change_offset
|
115
|
+
Time.now.to_f - @yield_time
|
116
|
+
end
|
117
|
+
|
118
|
+
def freeze_offset
|
119
|
+
result = @freeze_time - @yield_time
|
120
|
+
# Make an "almost zero" value more readable
|
121
|
+
result < 1e-4 ? 1e-4 : result
|
122
|
+
end
|
123
|
+
|
124
|
+
# Allow changes only during specific time wine
|
125
|
+
def allow_changes(reset_queue = true)
|
126
|
+
@freeze_time = nil
|
127
|
+
if reset_queue
|
128
|
+
# Clear to prepare for collecting new FS events
|
129
|
+
@changes = { modified: [], added: [], removed: [] }
|
130
|
+
else
|
131
|
+
# Since we're testing the queue and the listener callback is adding
|
132
|
+
# changes to the same hash (e.g. after a pause), copy the existing data
|
133
|
+
# to a new, unfrozen hash
|
134
|
+
@changes = @changes.dup if @changes.frozen?
|
135
|
+
@changes ||= { modified: [], added: [], removed: [] }
|
136
|
+
end
|
137
|
+
|
138
|
+
@yield_time = Time.now.to_f
|
139
|
+
yield
|
140
|
+
# Prevent recording changes after timeout
|
141
|
+
@changes.freeze
|
142
|
+
@freeze_time = Time.now.to_f
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Conveniently wrap a Listener instance for testing
|
147
|
+
class ListenerWrapper
|
148
|
+
attr_reader :listener, :changes
|
149
|
+
attr_accessor :lag
|
150
|
+
|
151
|
+
def initialize(callback, paths, *args)
|
152
|
+
# Lag depends mostly on wait_for_delay On Linux desktop, it's 0.06 - 0.11
|
153
|
+
#
|
154
|
+
# On Travis it used to be > 0.5, but that was before broadcaster sent
|
155
|
+
# changes immediately, so 0.2-0.4 might be enough for Travis, but we set it
|
156
|
+
# to 0.6
|
157
|
+
#
|
158
|
+
# The value should be 2-3 x wait_for_delay + time between fs operation and
|
159
|
+
# notification, which for polling and FSEvent means the configured latency
|
160
|
+
@lag = 0.6
|
161
|
+
|
162
|
+
@paths = paths
|
163
|
+
|
164
|
+
# Isolate collected changes between tests/listener instances
|
165
|
+
@timed_changes = TimedChanges.new
|
166
|
+
|
167
|
+
if callback
|
168
|
+
@listener = Listen.send(*args) do |modified, added, removed|
|
169
|
+
# Add changes to trigger frozen Hash error, making sure lag is enough
|
170
|
+
_add_changes(:modified, modified, changes)
|
171
|
+
_add_changes(:added, added, changes)
|
172
|
+
_add_changes(:removed, removed, changes)
|
173
|
+
|
174
|
+
unless callback == :track_changes
|
175
|
+
callback.call(modified, added, removed)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
else
|
179
|
+
@listener = Listen.send(*args)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def changes
|
184
|
+
@timed_changes.changes
|
185
|
+
end
|
186
|
+
|
187
|
+
def listen(reset_queue = true)
|
188
|
+
@timed_changes.allow_changes(reset_queue) do
|
189
|
+
|
190
|
+
# give events time to be received, queued and processed
|
191
|
+
sleep lag
|
192
|
+
|
193
|
+
yield
|
194
|
+
|
195
|
+
sleep lag # wait for changes
|
196
|
+
end
|
197
|
+
|
198
|
+
# Keep this to detect a lag too small (changes during this sleep
|
199
|
+
# will trigger "frozen hash" error caught below (and displaying timeout
|
200
|
+
# details)
|
201
|
+
sleep 1
|
202
|
+
|
203
|
+
changes
|
204
|
+
end
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
def _add_changes(type, changes, dst)
|
209
|
+
dst[type] += _relative_path(changes)
|
210
|
+
dst[type].uniq!
|
211
|
+
dst[type].sort!
|
212
|
+
|
213
|
+
rescue RuntimeError => e
|
214
|
+
raise unless e.message == "can't modify frozen Hash"
|
215
|
+
|
216
|
+
# Show how by much the changes missed the timeout
|
217
|
+
change_offset = @timed_changes.change_offset
|
218
|
+
freeze_offset = @timed_changes.freeze_offset
|
219
|
+
|
220
|
+
msg = "Changes took #{change_offset}s (allowed lag: #{freeze_offset})s"
|
221
|
+
|
222
|
+
# Use STDERR (workaround for Celluloid, since it catches abort)
|
223
|
+
STDERR.puts msg
|
224
|
+
abort(msg)
|
225
|
+
end
|
226
|
+
|
227
|
+
def _relative_path(changes)
|
228
|
+
changes.map do |change|
|
229
|
+
unfrozen_copy = change.dup
|
230
|
+
[@paths].flatten.each do |path|
|
231
|
+
sub = path.sub(/\/$/, '').to_s
|
232
|
+
unfrozen_copy.gsub!(/^#{sub}\//, '')
|
233
|
+
end
|
234
|
+
unfrozen_copy
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def setup_listener(options, callback = nil)
|
240
|
+
ListenerWrapper.new(callback, paths, :to, paths, options)
|
241
|
+
end
|
242
|
+
|
243
|
+
def setup_recipient(port, callback = nil)
|
244
|
+
ListenerWrapper.new(callback, paths, :on, port)
|
245
|
+
end
|
246
|
+
|
247
|
+
def _sleep_to_separate_events
|
248
|
+
# separate the events or Darwin and Polling
|
249
|
+
# will detect only the :added event
|
250
|
+
#
|
251
|
+
# (This is because both use directory scanning
|
252
|
+
# through Celluloid tasks, which may not kick in
|
253
|
+
# time before the next filesystem change)
|
254
|
+
#
|
255
|
+
# The minimum for this is the time it takes between a syscall
|
256
|
+
# changing the filesystem ... and ... an async
|
257
|
+
# Listen::File.scan to finish comparing the file with the
|
258
|
+
# Record
|
259
|
+
#
|
260
|
+
# This necessary for:
|
261
|
+
# - Darwin Adapter
|
262
|
+
# - Polling Adapter
|
263
|
+
# - Linux Adapter in FSEvent emulation mode
|
264
|
+
# - maybe Windows adapter (probably not)
|
265
|
+
sleep 0.4
|
266
|
+
end
|