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
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
|