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.
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 -1
  6. data/.yardopts +0 -0
  7. data/CHANGELOG.md +0 -0
  8. data/CONTRIBUTING.md +0 -0
  9. data/Gemfile +25 -4
  10. data/Guardfile +0 -0
  11. data/LICENSE.txt +0 -0
  12. data/README.md +18 -10
  13. data/Rakefile +0 -0
  14. data/lib/listen.rb +2 -4
  15. data/lib/listen/adapter.rb +13 -4
  16. data/lib/listen/adapter/base.rb +33 -16
  17. data/lib/listen/adapter/bsd.rb +21 -38
  18. data/lib/listen/adapter/darwin.rb +17 -25
  19. data/lib/listen/adapter/linux.rb +34 -52
  20. data/lib/listen/adapter/polling.rb +9 -25
  21. data/lib/listen/adapter/tcp.rb +27 -14
  22. data/lib/listen/adapter/windows.rb +67 -23
  23. data/lib/listen/change.rb +26 -23
  24. data/lib/listen/cli.rb +0 -0
  25. data/lib/listen/directory.rb +47 -58
  26. data/lib/listen/file.rb +66 -101
  27. data/lib/listen/listener.rb +214 -155
  28. data/lib/listen/queue_optimizer.rb +104 -0
  29. data/lib/listen/record.rb +15 -5
  30. data/lib/listen/silencer.rb +14 -10
  31. data/lib/listen/tcp.rb +0 -1
  32. data/lib/listen/tcp/broadcaster.rb +31 -26
  33. data/lib/listen/tcp/message.rb +2 -2
  34. data/lib/listen/version.rb +1 -1
  35. data/listen.gemspec +1 -1
  36. data/spec/acceptance/listen_spec.rb +151 -239
  37. data/spec/acceptance/tcp_spec.rb +125 -134
  38. data/spec/lib/listen/adapter/base_spec.rb +13 -30
  39. data/spec/lib/listen/adapter/bsd_spec.rb +7 -35
  40. data/spec/lib/listen/adapter/darwin_spec.rb +18 -30
  41. data/spec/lib/listen/adapter/linux_spec.rb +49 -55
  42. data/spec/lib/listen/adapter/polling_spec.rb +20 -35
  43. data/spec/lib/listen/adapter/tcp_spec.rb +25 -27
  44. data/spec/lib/listen/adapter/windows_spec.rb +7 -33
  45. data/spec/lib/listen/adapter_spec.rb +10 -10
  46. data/spec/lib/listen/change_spec.rb +55 -57
  47. data/spec/lib/listen/directory_spec.rb +105 -155
  48. data/spec/lib/listen/file_spec.rb +186 -73
  49. data/spec/lib/listen/listener_spec.rb +233 -216
  50. data/spec/lib/listen/record_spec.rb +60 -22
  51. data/spec/lib/listen/silencer_spec.rb +48 -75
  52. data/spec/lib/listen/tcp/broadcaster_spec.rb +78 -69
  53. data/spec/lib/listen/tcp/listener_spec.rb +28 -71
  54. data/spec/lib/listen/tcp/message_spec.rb +48 -14
  55. data/spec/lib/listen_spec.rb +3 -3
  56. data/spec/spec_helper.rb +6 -3
  57. data/spec/support/acceptance_helper.rb +250 -31
  58. data/spec/support/fixtures_helper.rb +6 -4
  59. data/spec/support/platform_helper.rb +2 -2
  60. metadata +5 -5
  61. data/lib/listen/tcp/listener.rb +0 -108
@@ -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
- attr_accessor :options, :directories, :paused, :changes, :block, :stopping
11
- attr_accessor :registry, :supervisor
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 = _init_options(args.last.is_a?(Hash) ? args.pop : {})
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
- @changes = []
27
- @block = block
28
- @registry = Celluloid::Registry.new
29
- _init_debug
46
+ @queue = Queue.new
47
+ @block = block
48
+ @registry = Celluloid::Registry.new
49
+
50
+ transition :stopped
30
51
  end
31
52
 
32
- # Starts the listener by initializing the adapter and building
33
- # the directory record concurrently, then it starts the adapter to watch
34
- # for changes. The current thread is not blocked after starting.
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
- _init_actors
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
- # Terminates all Listen actors and kill the adapter.
45
- #
88
+ # TODO: depreciate
89
+ alias_method :unpause, :start
90
+
91
+ # Stops processing and terminates all actors
46
92
  def stop
47
- @stopping = true
48
- supervisor.terminate
93
+ transition :stopped
49
94
  end
50
95
 
51
- # Pauses listening callback (adapter still running)
52
- #
96
+ # Stops invoking callbacks (messages pile up)
53
97
  def pause
54
- @paused = true
98
+ transition :paused
55
99
  end
56
100
 
57
- # Unpauses listening callback
58
- #
59
- def unpause
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
- @paused == true
107
+ state == :paused
70
108
  end
71
109
 
72
- # Returns true if Listener is neither paused nor stopped
73
- #
74
- # @return [Boolean]
75
- #
76
- def listen?
77
- @paused == false && @stopping == false
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
- # Adds ignore patterns to the existing one
81
- #
82
- # @see DEFAULT_IGNORED_DIRECTORIES and DEFAULT_IGNORED_EXTENSIONS in
83
- # Listen::Silencer)
118
+ # TODO: deprecate
119
+ alias_method :paused, :paused?
120
+
121
+ # Add files and dirs to ignore on top of defaults
84
122
  #
85
- # @param [Regexp, Array<Regexp>] new ignoring patterns.
123
+ # (@see Listen::Silencer for default ignored files and dirs)
86
124
  #
87
125
  def ignore(regexps)
88
- @options[:ignore] = [options[:ignore], regexps]
89
- registry[:silencer] = Silencer.new(self)
126
+ _reconfigure_silencer(ignore: [options[:ignore], regexps])
90
127
  end
91
128
 
92
- # Overwrites ignore patterns
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
- @options.delete(:ignore)
101
- @options[:ignore!] = regexps
102
- registry[:silencer] = Silencer.new(self)
131
+ _reconfigure_silencer(ignore: [], ignore!: regexps)
103
132
  end
104
133
 
105
- # Sets only patterns, to listen only to specific regexps
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
- @options[:only] = regexps
111
- registry[:silencer] = Silencer.new(self)
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 _init_debug
125
- if options[:debug] || ENV['LISTEN_GEM_DEBUGGING'] =~ /true|1/i
126
- if RbConfig::CONFIG['host_os'] =~ /bsd|dragonfly/
127
- Celluloid.logger.level = Logger::INFO
128
- else
129
- # BSDs silently fail ;;(
130
- Celluloid.logger.level = Logger::DEBUG
131
- end
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
- Celluloid.logger.level = Logger::FATAL
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
- adapter_class = Adapter.select(options)
144
- supervisor.add(adapter_class, as: :adapter, args: self)
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 @stopping
150
-
151
- changes = []
152
- begin
153
- sleep options[:wait_for_delay] # wait for changes to accumulate
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 => ex
233
+ rescue RuntimeError
163
234
  Kernel.warn "[Listen warning]: Change block raised an exception: #{$!}"
164
- Kernel.warn "Backtrace:\n\t#{ex.backtrace.join("\n\t")}"
235
+ Kernel.warn "Backtrace:\n\t#{$@.join("\n\t")}"
165
236
  end
166
237
 
167
- def _pop_changes
168
- popped = []
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 _smoosh_changes(changes)
174
- if registry[:adapter].class.local_fs?
175
- cookies = changes.group_by { |x| x[:cookie] }
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 _squash_changes(changes)
185
- actions = changes.group_by(&:last).map do |path, action_list|
186
- [_logical_action_for(path, action_list.map(&:first)), path.to_s]
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
- { modified: [], added: [], removed: [] }.tap do |squashed|
191
- actions.each do |type, path|
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
- def _logical_action_for(path, actions)
199
- actions << :added if actions.delete(:moved_to)
200
- actions << :removed if actions.delete(:moved_from)
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
- modified = actions.detect { |x| x == :modified }
203
- _calculate_add_remove_difference(actions, path, modified)
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
- def _calculate_add_remove_difference(actions, path, default_if_exists)
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
- if path.exist?
212
- if diff > 0
213
- :added
214
- elsif diff.zero? && added > 0
215
- :modified
216
- else
217
- default_if_exists
218
- end
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
- diff < 0 ? :removed : nil
290
+ @host, port = target.split(':')
291
+ @port = port.to_i
221
292
  end
222
293
  end
223
294
 
224
- # remove extraneous rb-inotify events, keeping them only if it's a possible
225
- # editor rename() call (e.g. Kate and Sublime)
226
- def _reinterpret_related_changes(cookies)
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 _detect_possible_editor_save(changes)
242
- return unless changes.size == 2
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
- # Expect an ignored moved_from and non-ignored moved_to
249
- # to qualify as an "editor modify"
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 _silenced?(path)
254
- type = path.directory? ? 'Dir' : 'File'
255
- registry[:silencer].silenced?(path, type)
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