listen 2.7.5 → 2.7.6

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