listen 2.7.5 → 2.7.6
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 +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
data/lib/listen/listener.rb
CHANGED
@@ -3,12 +3,20 @@ require 'listen/adapter'
|
|
3
3
|
require 'listen/change'
|
4
4
|
require 'listen/record'
|
5
5
|
require 'listen/silencer'
|
6
|
+
require 'listen/queue_optimizer'
|
6
7
|
require 'English'
|
7
8
|
|
8
9
|
module Listen
|
9
10
|
class Listener
|
10
|
-
|
11
|
-
|
11
|
+
include Celluloid::FSM
|
12
|
+
include QueueOptimizer
|
13
|
+
|
14
|
+
attr_accessor :block
|
15
|
+
|
16
|
+
# TODO: deprecate
|
17
|
+
attr_reader :options, :directories
|
18
|
+
attr_reader :registry, :supervisor
|
19
|
+
attr_reader :host, :port
|
12
20
|
|
13
21
|
# Initializes the directories listener.
|
14
22
|
#
|
@@ -21,94 +29,138 @@ module Listen
|
|
21
29
|
# @yieldparam [Array<String>] removed the list of removed files
|
22
30
|
#
|
23
31
|
def initialize(*args, &block)
|
24
|
-
@options
|
32
|
+
@options = _init_options(args.last.is_a?(Hash) ? args.pop : {})
|
33
|
+
|
34
|
+
# Setup logging first
|
35
|
+
Celluloid.logger.level = _debug_level
|
36
|
+
_log :info, "Celluloid loglevel set to: #{Celluloid.logger.level}"
|
37
|
+
|
38
|
+
@tcp_mode = nil
|
39
|
+
if [:recipient, :broadcaster].include? args[1]
|
40
|
+
target = args.shift
|
41
|
+
@tcp_mode = args.shift
|
42
|
+
_init_tcp_options(target)
|
43
|
+
end
|
44
|
+
|
25
45
|
@directories = args.flatten.map { |path| Pathname.new(path).realpath }
|
26
|
-
@
|
27
|
-
@block
|
28
|
-
@registry
|
29
|
-
|
46
|
+
@queue = Queue.new
|
47
|
+
@block = block
|
48
|
+
@registry = Celluloid::Registry.new
|
49
|
+
|
50
|
+
transition :stopped
|
30
51
|
end
|
31
52
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
53
|
+
default_state :initializing
|
54
|
+
|
55
|
+
state :initializing, to: :stopped
|
56
|
+
state :paused, to: [:processing, :stopped]
|
57
|
+
|
58
|
+
state :stopped, to: [:processing] do
|
59
|
+
_stop_wait_thread
|
60
|
+
if @supervisor
|
61
|
+
@supervisor.terminate
|
62
|
+
@supervisor = nil
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
state :processing, to: [:paused, :stopped] do
|
67
|
+
if wait_thread # means - was paused
|
68
|
+
_wakeup_wait_thread
|
69
|
+
else
|
70
|
+
@last_queue_event_time = nil
|
71
|
+
_start_wait_thread
|
72
|
+
_init_actors
|
73
|
+
|
74
|
+
# Note: make sure building is finished before starting adapter (for
|
75
|
+
# consistent results both in specs and normal usage)
|
76
|
+
sync(:record).build
|
77
|
+
|
78
|
+
_start_adapter
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Starts processing events and starts adapters
|
83
|
+
# or resumes invoking callbacks if paused
|
36
84
|
def start
|
37
|
-
|
38
|
-
unpause
|
39
|
-
@stopping = false
|
40
|
-
registry[:adapter].async.start
|
41
|
-
Thread.new { _wait_for_changes }
|
85
|
+
transition :processing
|
42
86
|
end
|
43
87
|
|
44
|
-
#
|
45
|
-
|
88
|
+
# TODO: depreciate
|
89
|
+
alias_method :unpause, :start
|
90
|
+
|
91
|
+
# Stops processing and terminates all actors
|
46
92
|
def stop
|
47
|
-
|
48
|
-
supervisor.terminate
|
93
|
+
transition :stopped
|
49
94
|
end
|
50
95
|
|
51
|
-
#
|
52
|
-
#
|
96
|
+
# Stops invoking callbacks (messages pile up)
|
53
97
|
def pause
|
54
|
-
|
98
|
+
transition :paused
|
55
99
|
end
|
56
100
|
|
57
|
-
#
|
58
|
-
|
59
|
-
|
60
|
-
registry[:record].build
|
61
|
-
@paused = false
|
101
|
+
# processing means callbacks are called
|
102
|
+
def processing?
|
103
|
+
state == :processing
|
62
104
|
end
|
63
105
|
|
64
|
-
# Returns true if Listener is paused
|
65
|
-
#
|
66
|
-
# @return [Boolean]
|
67
|
-
#
|
68
106
|
def paused?
|
69
|
-
|
107
|
+
state == :paused
|
70
108
|
end
|
71
109
|
|
72
|
-
#
|
73
|
-
|
74
|
-
|
75
|
-
#
|
76
|
-
def
|
77
|
-
|
110
|
+
# TODO: deprecate
|
111
|
+
alias_method :listen?, :processing?
|
112
|
+
|
113
|
+
# TODO: deprecate
|
114
|
+
def paused=(value)
|
115
|
+
transition value ? :paused : :processing
|
78
116
|
end
|
79
117
|
|
80
|
-
#
|
81
|
-
|
82
|
-
|
83
|
-
#
|
118
|
+
# TODO: deprecate
|
119
|
+
alias_method :paused, :paused?
|
120
|
+
|
121
|
+
# Add files and dirs to ignore on top of defaults
|
84
122
|
#
|
85
|
-
# @
|
123
|
+
# (@see Listen::Silencer for default ignored files and dirs)
|
86
124
|
#
|
87
125
|
def ignore(regexps)
|
88
|
-
|
89
|
-
registry[:silencer] = Silencer.new(self)
|
126
|
+
_reconfigure_silencer(ignore: [options[:ignore], regexps])
|
90
127
|
end
|
91
128
|
|
92
|
-
#
|
93
|
-
#
|
94
|
-
# @see DEFAULT_IGNORED_DIRECTORIES and DEFAULT_IGNORED_EXTENSIONS in
|
95
|
-
# Listen::Silencer)
|
96
|
-
#
|
97
|
-
# @param [Regexp, Array<Regexp>] new ignoring patterns.
|
98
|
-
#
|
129
|
+
# Replace default ignore patterns with provided regexp
|
99
130
|
def ignore!(regexps)
|
100
|
-
|
101
|
-
@options[:ignore!] = regexps
|
102
|
-
registry[:silencer] = Silencer.new(self)
|
131
|
+
_reconfigure_silencer(ignore: [], ignore!: regexps)
|
103
132
|
end
|
104
133
|
|
105
|
-
#
|
106
|
-
#
|
107
|
-
# @param [Regexp, Array<Regexp>] new ignoring patterns.
|
108
|
-
#
|
134
|
+
# Listen only to files and dirs matching regexp
|
109
135
|
def only(regexps)
|
110
|
-
|
111
|
-
|
136
|
+
_reconfigure_silencer(only: regexps)
|
137
|
+
end
|
138
|
+
|
139
|
+
def async(type)
|
140
|
+
proxy = sync(type)
|
141
|
+
proxy ? proxy.async : nil
|
142
|
+
end
|
143
|
+
|
144
|
+
def sync(type)
|
145
|
+
@registry[type]
|
146
|
+
end
|
147
|
+
|
148
|
+
def queue(type, change, path, options = {})
|
149
|
+
fail "Invalid type: #{type.inspect}" unless [:dir, :file].include? type
|
150
|
+
fail "Invalid change: #{change.inspect}" unless change.is_a?(Symbol)
|
151
|
+
@queue << [type, change, path, options]
|
152
|
+
|
153
|
+
@last_queue_event_time = Time.now.to_f
|
154
|
+
_wakeup_wait_thread unless state == :paused
|
155
|
+
|
156
|
+
return unless @tcp_mode == :broadcaster
|
157
|
+
|
158
|
+
message = TCP::Message.new(type, change, path, options)
|
159
|
+
registry[:broadcaster].async.broadcast(message.payload)
|
160
|
+
end
|
161
|
+
|
162
|
+
def silencer
|
163
|
+
@registry[:silencer]
|
112
164
|
end
|
113
165
|
|
114
166
|
private
|
@@ -121,16 +173,19 @@ module Listen
|
|
121
173
|
polling_fallback_message: nil }.merge(options)
|
122
174
|
end
|
123
175
|
|
124
|
-
def
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
176
|
+
def _debug_level
|
177
|
+
# TODO: remove? (since there are BSD warnings anyway)
|
178
|
+
bsd = RbConfig::CONFIG['host_os'] =~ /bsd|dragonfly/
|
179
|
+
return Logger::DEBUG if bsd
|
180
|
+
|
181
|
+
debugging = ENV['LISTEN_GEM_DEBUGGING'] || options[:debug]
|
182
|
+
case debugging.to_s
|
183
|
+
when /2/
|
184
|
+
Logger::DEBUG
|
185
|
+
when /true|yes|1/i
|
186
|
+
Logger::INFO
|
132
187
|
else
|
133
|
-
|
188
|
+
Logger::ERROR
|
134
189
|
end
|
135
190
|
end
|
136
191
|
|
@@ -140,119 +195,123 @@ module Listen
|
|
140
195
|
supervisor.add(Record, as: :record, args: self)
|
141
196
|
supervisor.pool(Change, as: :change_pool, args: self)
|
142
197
|
|
143
|
-
|
144
|
-
|
198
|
+
if @tcp_mode == :broadcaster
|
199
|
+
require 'listen/tcp/broadcaster'
|
200
|
+
supervisor.add(TCP::Broadcaster, as: :broadcaster, args: [@host, @port])
|
201
|
+
|
202
|
+
# TODO: should be auto started, because if it crashes
|
203
|
+
# a new instance is spawned by supervisor, but it's 'start' isn't
|
204
|
+
# called
|
205
|
+
registry[:broadcaster].start
|
206
|
+
end
|
207
|
+
|
208
|
+
supervisor.add(_adapter_class, as: :adapter, args: self)
|
145
209
|
end
|
146
210
|
|
147
211
|
def _wait_for_changes
|
212
|
+
latency = options[:wait_for_delay]
|
213
|
+
|
148
214
|
loop do
|
149
|
-
break if
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
new_changes = _pop_changes
|
155
|
-
changes += new_changes
|
156
|
-
end until new_changes.empty?
|
157
|
-
unless changes.empty?
|
158
|
-
hash = _smoosh_changes(changes)
|
159
|
-
block.call(hash[:modified], hash[:added], hash[:removed])
|
215
|
+
break if state == :stopped
|
216
|
+
|
217
|
+
if state == :paused || @queue.empty?
|
218
|
+
sleep
|
219
|
+
break if state == :stopped
|
160
220
|
end
|
221
|
+
|
222
|
+
# Assure there's at least latency between callbacks to allow
|
223
|
+
# for accumulating changes
|
224
|
+
now = Time.now.to_f
|
225
|
+
diff = latency + (@last_queue_event_time || now) - now
|
226
|
+
if diff > 0
|
227
|
+
sleep diff
|
228
|
+
next
|
229
|
+
end
|
230
|
+
|
231
|
+
_process_changes unless state == :paused
|
161
232
|
end
|
162
|
-
rescue
|
233
|
+
rescue RuntimeError
|
163
234
|
Kernel.warn "[Listen warning]: Change block raised an exception: #{$!}"
|
164
|
-
Kernel.warn "Backtrace:\n\t#{
|
235
|
+
Kernel.warn "Backtrace:\n\t#{$@.join("\n\t")}"
|
165
236
|
end
|
166
237
|
|
167
|
-
def
|
168
|
-
|
169
|
-
popped << @changes.shift until @changes.empty?
|
170
|
-
popped
|
238
|
+
def _silenced?(path, type)
|
239
|
+
sync(:silencer).silenced?(path, type)
|
171
240
|
end
|
172
241
|
|
173
|
-
def
|
174
|
-
|
175
|
-
|
176
|
-
_squash_changes(_reinterpret_related_changes(cookies))
|
177
|
-
else
|
178
|
-
smooshed = { modified: [], added: [], removed: [] }
|
179
|
-
changes.map(&:first).each { |type, path| smooshed[type] << path.to_s }
|
180
|
-
smooshed.tap { |s| s.each { |_, v| v.uniq! } }
|
181
|
-
end
|
242
|
+
def _start_adapter
|
243
|
+
# Don't run async, because configuration has to finish first
|
244
|
+
sync(:adapter).start
|
182
245
|
end
|
183
246
|
|
184
|
-
def
|
185
|
-
|
186
|
-
|
187
|
-
end
|
188
|
-
Celluloid.logger.info "listen: raw changes: #{actions.inspect}"
|
247
|
+
def _log(type, message)
|
248
|
+
Celluloid.logger.send(type, message)
|
249
|
+
end
|
189
250
|
|
190
|
-
|
191
|
-
|
192
|
-
squashed[type] << path unless type.nil?
|
193
|
-
end
|
194
|
-
Celluloid.logger.info "listen: final changes: #{squashed.inspect}"
|
195
|
-
end
|
251
|
+
def _adapter_class
|
252
|
+
@adapter_class ||= Adapter.select(options)
|
196
253
|
end
|
197
254
|
|
198
|
-
|
199
|
-
|
200
|
-
|
255
|
+
# for easier testing without sleep loop
|
256
|
+
def _process_changes
|
257
|
+
return if @queue.empty?
|
258
|
+
|
259
|
+
@last_queue_event_time = nil
|
260
|
+
|
261
|
+
changes = []
|
262
|
+
while !@queue.empty?
|
263
|
+
changes << @queue.pop
|
264
|
+
end
|
265
|
+
|
266
|
+
return if block.nil?
|
267
|
+
|
268
|
+
hash = _smoosh_changes(changes)
|
269
|
+
result = [hash[:modified], hash[:added], hash[:removed]]
|
201
270
|
|
202
|
-
|
203
|
-
|
271
|
+
# TODO: condition not tested, but too complex to test ATM
|
272
|
+
block.call(*result) unless result.all?(&:empty?)
|
204
273
|
end
|
205
274
|
|
206
|
-
|
207
|
-
added = actions.count { |x| x == :added }
|
208
|
-
removed = actions.count { |x| x == :removed }
|
209
|
-
diff = added - removed
|
275
|
+
attr_reader :wait_thread
|
210
276
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
277
|
+
def _init_tcp_options(target)
|
278
|
+
# Handle TCP options here
|
279
|
+
require 'listen/tcp'
|
280
|
+
fail ArgumentError, 'missing host/port for TCP' unless target
|
281
|
+
|
282
|
+
if @tcp_mode == :recipient
|
283
|
+
@host = 'localhost'
|
284
|
+
@options[:force_tcp] = true
|
285
|
+
end
|
286
|
+
|
287
|
+
if target.is_a? Fixnum
|
288
|
+
@port = target
|
219
289
|
else
|
220
|
-
|
290
|
+
@host, port = target.split(':')
|
291
|
+
@port = port.to_i
|
221
292
|
end
|
222
293
|
end
|
223
294
|
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
table = { moved_to: :added, moved_from: :removed }
|
228
|
-
cookies.map do |_, changes|
|
229
|
-
file = _detect_possible_editor_save(changes)
|
230
|
-
if file
|
231
|
-
[[:modified, file]]
|
232
|
-
else
|
233
|
-
not_silenced = changes.map(&:first).reject do |_, path|
|
234
|
-
_silenced?(path)
|
235
|
-
end
|
236
|
-
not_silenced.map { |type, path| [table.fetch(type, type), path] }
|
237
|
-
end
|
238
|
-
end.flatten(1)
|
295
|
+
def _reconfigure_silencer(extra_options)
|
296
|
+
@options.merge!(extra_options)
|
297
|
+
registry[:silencer] = Silencer.new(self)
|
239
298
|
end
|
240
299
|
|
241
|
-
def
|
242
|
-
|
243
|
-
|
244
|
-
from, to = changes.sort { |x, y| x.keys.first <=> y.keys.first }
|
245
|
-
from, to = from[:moved_from], to[:moved_to]
|
246
|
-
return unless from && to
|
300
|
+
def _start_wait_thread
|
301
|
+
@wait_thread = Thread.new { _wait_for_changes }
|
302
|
+
end
|
247
303
|
|
248
|
-
|
249
|
-
|
250
|
-
_silenced?(from) && !_silenced?(to) ? to : nil
|
304
|
+
def _wakeup_wait_thread
|
305
|
+
wait_thread.wakeup if wait_thread && wait_thread.alive?
|
251
306
|
end
|
252
307
|
|
253
|
-
def
|
254
|
-
|
255
|
-
|
308
|
+
def _stop_wait_thread
|
309
|
+
return unless wait_thread
|
310
|
+
if wait_thread.alive?
|
311
|
+
wait_thread.wakeup
|
312
|
+
wait_thread.join
|
313
|
+
end
|
314
|
+
@wait_thread = nil
|
256
315
|
end
|
257
316
|
end
|
258
317
|
end
|