listen 3.2.1 → 3.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Listen
2
4
  module Adapter
3
5
  # Adapter implementation for Windows `wdm`.
4
6
  #
5
7
  class Windows < Base
6
- OS_REGEXP = /mswin|mingw|cygwin/i
8
+ OS_REGEXP = /mswin|mingw|cygwin/i.freeze
7
9
 
8
10
  BUNDLER_DECLARE_GEM = <<-EOS.gsub(/^ {6}/, '')
9
11
  Please add the following to your Gemfile to avoid polling for changes:
@@ -15,8 +17,8 @@ module Listen
15
17
  require 'wdm'
16
18
  true
17
19
  rescue LoadError
18
- _log :debug, format('wdm - load failed: %s:%s', $ERROR_INFO,
19
- $ERROR_POSITION * "\n")
20
+ Listen.logger.debug format('wdm - load failed: %s:%s', $ERROR_INFO,
21
+ $ERROR_POSITION * "\n")
20
22
 
21
23
  Kernel.warn BUNDLER_DECLARE_GEM
22
24
  false
@@ -26,7 +28,7 @@ module Listen
26
28
 
27
29
  def _configure(dir)
28
30
  require 'wdm'
29
- _log :debug, 'wdm - starting...'
31
+ Listen.logger.debug 'wdm - starting...'
30
32
  @worker ||= WDM::Monitor.new
31
33
  @worker.watch_recursively(dir.to_s, :files) do |change|
32
34
  yield([:file, change])
@@ -36,8 +38,7 @@ module Listen
36
38
  yield([:dir, change])
37
39
  end
38
40
 
39
- events = [:attributes, :last_write]
40
- @worker.watch_recursively(dir.to_s, *events) do |change|
41
+ @worker.watch_recursively(dir.to_s, :attributes, :last_write) do |change|
41
42
  yield([:attr, change])
42
43
  end
43
44
  end
@@ -46,8 +47,9 @@ module Listen
46
47
  @worker.run!
47
48
  end
48
49
 
50
+ # rubocop:disable Metrics/MethodLength
49
51
  def _process_event(dir, event)
50
- _log :debug, "wdm - callback: #{event.inspect}"
52
+ Listen.logger.debug "wdm - callback: #{event.inspect}"
51
53
 
52
54
  type, change = event
53
55
 
@@ -65,10 +67,11 @@ module Listen
65
67
  _queue_change(:file, dir, rel_path, options)
66
68
  end
67
69
  when :dir
68
- if change.type == :removed
70
+ case change.type
71
+ when :removed
69
72
  # TODO: check if watched dir?
70
73
  _queue_change(:dir, dir, Pathname(rel_path).dirname.to_s, {})
71
- elsif change.type == :added
74
+ when :added
72
75
  _queue_change(:dir, dir, rel_path, {})
73
76
  # do nothing - changed directory means either:
74
77
  # - removed subdirs (handled above)
@@ -78,20 +81,15 @@ module Listen
78
81
  # so what's left?
79
82
  end
80
83
  end
81
- rescue
82
- details = event.inspect
83
- _log :error, format('wdm - callback (%s): %s:%s', details, $ERROR_INFO,
84
- $ERROR_POSITION * "\n")
85
- raise
86
84
  end
85
+ # rubocop:enable Metrics/MethodLength
87
86
 
88
87
  def _change(type)
89
88
  { modified: [:modified, :attrib], # TODO: is attrib really passed?
90
89
  added: [:added, :renamed_new_file],
91
- removed: [:removed, :renamed_old_file] }.each do |change, types|
92
- return change if types.include?(type)
90
+ removed: [:removed, :renamed_old_file] }.find do |change, types|
91
+ types.include?(type) and break change
93
92
  end
94
- nil
95
93
  end
96
94
  end
97
95
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'listen/adapter'
2
4
  require 'listen/adapter/base'
3
5
  require 'listen/adapter/config'
data/lib/listen/change.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'listen/file'
2
4
  require 'listen/directory'
3
5
 
@@ -28,49 +30,40 @@ module Listen
28
30
  end
29
31
 
30
32
  # Invalidate some part of the snapshot/record (dir, file, subtree, etc.)
33
+ # rubocop:disable Metrics/MethodLength
34
+ # rubocop:disable Metrics/CyclomaticComplexity
35
+ # rubocop:disable Metrics/PerceivedComplexity
31
36
  def invalidate(type, rel_path, options)
32
37
  watched_dir = Pathname.new(record.root)
33
38
 
34
39
  change = options[:change]
35
40
  cookie = options[:cookie]
36
41
 
37
- if !cookie && config.silenced?(rel_path, type)
38
- Listen::Logger.debug { "(silenced): #{rel_path.inspect}" }
42
+ if !cookie && @config.silenced?(rel_path, type)
43
+ Listen.logger.debug { "(silenced): #{rel_path.inspect}" }
39
44
  return
40
45
  end
41
46
 
42
47
  path = watched_dir + rel_path
43
48
 
44
- Listen::Logger.debug do
49
+ Listen.logger.debug do
45
50
  log_details = options[:silence] && 'recording' || change || 'unknown'
46
51
  "#{log_details}: #{type}:#{path} (#{options.inspect})"
47
52
  end
48
53
 
49
54
  if change
50
55
  options = cookie ? { cookie: cookie } : {}
51
- config.queue(type, change, watched_dir, rel_path, options)
56
+ @config.queue(type, change, watched_dir, rel_path, options)
52
57
  elsif type == :dir
53
58
  # NOTE: POSSIBLE RECURSION
54
59
  # TODO: fix - use a queue instead
55
60
  Directory.scan(self, rel_path, options)
56
- else
57
- change = File.change(record, rel_path)
58
- return if !change || options[:silence]
59
- config.queue(:file, change, watched_dir, rel_path)
61
+ elsif (change = File.change(record, rel_path)) && !options[:silence]
62
+ @config.queue(:file, change, watched_dir, rel_path)
60
63
  end
61
- rescue RuntimeError => ex
62
- msg = format(
63
- '%s#%s crashed %s:%s',
64
- self.class,
65
- __method__,
66
- exinspect,
67
- ex.backtrace * "\n")
68
- Listen::Logger.error(msg)
69
- raise
70
64
  end
71
-
72
- private
73
-
74
- attr_reader :config
65
+ # rubocop:enable Metrics/MethodLength
66
+ # rubocop:enable Metrics/CyclomaticComplexity
67
+ # rubocop:enable Metrics/PerceivedComplexity
75
68
  end
76
69
  end
data/lib/listen/cli.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thor'
2
4
  require 'listen'
3
5
  require 'logger'
@@ -33,15 +35,16 @@ module Listen
33
35
 
34
36
  class Forwarder
35
37
  attr_reader :logger
38
+
36
39
  def initialize(options)
37
40
  @options = options
38
- @logger = ::Logger.new(STDOUT)
39
- @logger.level = ::Logger::INFO
41
+ @logger = ::Logger.new(STDOUT, level: ::Logger::INFO)
40
42
  @logger.formatter = proc { |_, _, _, msg| "#{msg}\n" }
41
43
  end
42
44
 
43
45
  def start
44
46
  logger.info 'Starting listen...'
47
+
45
48
  directory = @options[:directory]
46
49
  relative = @options[:relative]
47
50
  callback = proc do |modified, added, removed|
@@ -52,10 +55,7 @@ module Listen
52
55
  end
53
56
  end
54
57
 
55
- listener = Listen.to(
56
- directory,
57
- relative: relative,
58
- &callback)
58
+ listener = Listen.to(directory, relative: relative, &callback)
59
59
 
60
60
  listener.start
61
61
 
@@ -1,8 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'set'
2
4
 
3
5
  module Listen
4
6
  # TODO: refactor (turn it into a normal object, cache the stat, etc)
5
7
  class Directory
8
+ # rubocop:disable Metrics/MethodLength
6
9
  def self.scan(snapshot, rel_path, options)
7
10
  record = snapshot.record
8
11
  dir = Pathname.new(record.root)
@@ -14,7 +17,7 @@ module Listen
14
17
  path = dir + rel_path
15
18
  current = Set.new(_children(path))
16
19
 
17
- Listen::Logger.debug do
20
+ Listen.logger.debug do
18
21
  format('%s: %s(%s): %s -> %s',
19
22
  (options[:silence] ? 'Recording' : 'Scanning'),
20
23
  rel_path, options.inspect, previous.inspect, current.inspect)
@@ -36,22 +39,19 @@ module Listen
36
39
  previous = previous.reject { |entry, _| current.include? path + entry }
37
40
 
38
41
  _async_changes(snapshot, Pathname.new(rel_path), previous, options)
39
-
40
42
  rescue Errno::ENOENT, Errno::EHOSTDOWN
41
43
  record.unset_path(rel_path)
42
44
  _async_changes(snapshot, Pathname.new(rel_path), previous, options)
43
-
44
45
  rescue Errno::ENOTDIR
45
46
  # TODO: path not tested
46
47
  record.unset_path(rel_path)
47
48
  _async_changes(snapshot, path, previous, options)
48
49
  _change(snapshot, :file, rel_path, options)
49
50
  rescue
50
- Listen::Logger.warn do
51
- format('scan DIED: %s:%s', $ERROR_INFO, $ERROR_POSITION * "\n")
52
- end
51
+ Listen.logger.warn { format('scan DIED: %s:%s', $ERROR_INFO, $ERROR_POSITION * "\n") }
53
52
  raise
54
53
  end
54
+ # rubocop:enable Metrics/MethodLength
55
55
 
56
56
  def self.ascendant_of?(base, other)
57
57
  other.ascend do |ascendant|
@@ -86,8 +86,8 @@ module Listen
86
86
  # https://github.com/jruby/jruby/issues/3840
87
87
  exists = path.exist?
88
88
  directory = path.directory?
89
- return path.children unless exists && !directory
90
- raise Errno::ENOTDIR, path.to_s
89
+ exists && !directory and raise Errno::ENOTDIR, path.to_s
90
+ path.children
91
91
  end
92
92
  end
93
93
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Besides programming error exceptions like ArgumentError,
4
+ # all public interface exceptions should be declared here and inherit from Listen::Error.
5
+ module Listen
6
+ class Error < RuntimeError
7
+ class NotStarted < Error; end
8
+ class SymlinkLoop < Error; end
9
+ end
10
+ end
@@ -1,12 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Listen
2
4
  module Event
3
5
  class Config
6
+ attr_reader :listener, :event_queue, :min_delay_between_events
7
+
4
8
  def initialize(
5
9
  listener,
6
10
  event_queue,
7
11
  queue_optimizer,
8
12
  wait_for_delay,
9
- &block)
13
+ &block
14
+ )
10
15
 
11
16
  @listener = listener
12
17
  @event_queue = event_queue
@@ -15,20 +20,14 @@ module Listen
15
20
  @block = block
16
21
  end
17
22
 
18
- def sleep(*args)
19
- Kernel.sleep(*args)
23
+ def sleep(seconds)
24
+ Kernel.sleep(seconds)
20
25
  end
21
26
 
22
27
  def call(*args)
23
- @block.call(*args) if @block
24
- end
25
-
26
- def timestamp
27
- Time.now.to_f
28
+ @block&.call(*args)
28
29
  end
29
30
 
30
- attr_reader :event_queue
31
-
32
31
  def callable?
33
32
  @block
34
33
  end
@@ -36,20 +35,6 @@ module Listen
36
35
  def optimize_changes(changes)
37
36
  @queue_optimizer.smoosh_changes(changes)
38
37
  end
39
-
40
- attr_reader :min_delay_between_events
41
-
42
- def stopped?
43
- listener.state == :stopped
44
- end
45
-
46
- def paused?
47
- listener.state == :paused
48
- end
49
-
50
- private
51
-
52
- attr_reader :listener
53
38
  end
54
39
  end
55
40
  end
@@ -1,55 +1,62 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thread'
2
4
 
3
5
  require 'timeout'
4
6
  require 'listen/event/processor'
7
+ require 'listen/thread'
8
+ require 'listen/error'
5
9
 
6
10
  module Listen
7
11
  module Event
8
12
  class Loop
9
- class Error < RuntimeError
10
- class NotStarted < Error
11
- end
12
- end
13
+ include Listen::FSM
14
+
15
+ Error = ::Listen::Error
16
+ NotStarted = ::Listen::Error::NotStarted # for backward compatibility
17
+
18
+ start_state :pre_start
19
+ state :pre_start
20
+ state :starting
21
+ state :started
22
+ state :stopped
13
23
 
14
24
  def initialize(config)
15
25
  @config = config
16
26
  @wait_thread = nil
17
- @state = :paused
18
27
  @reasons = ::Queue.new
28
+ initialize_fsm
19
29
  end
20
30
 
21
31
  def wakeup_on_event
22
- return if stopped?
23
- return unless processing?
24
- return unless wait_thread.alive?
25
- _wakeup(:event)
32
+ if started? && @wait_thread&.alive?
33
+ _wakeup(:event)
34
+ end
26
35
  end
27
36
 
28
- def paused?
29
- wait_thread && state == :paused
37
+ def started?
38
+ state == :started
30
39
  end
31
40
 
32
- def processing?
33
- return false if stopped?
34
- return false if paused?
35
- state == :processing
36
- end
41
+ MAX_STARTUP_SECONDS = 5.0
37
42
 
38
- def setup
43
+ # @raises Error::NotStarted if background thread hasn't started in MAX_STARTUP_SECONDS
44
+ def start
39
45
  # TODO: use a Fiber instead?
40
- q = ::Queue.new
41
- @wait_thread = Internals::ThreadPool.add do
42
- _wait_for_changes(q, config)
46
+ return unless state == :pre_start
47
+
48
+ transition! :starting
49
+
50
+ @wait_thread = Listen::Thread.new("wait_thread") do
51
+ _process_changes
43
52
  end
44
53
 
45
- Listen::Logger.debug('Waiting for processing to start...')
46
- Timeout.timeout(5) { q.pop }
47
- end
54
+ Listen.logger.debug("Waiting for processing to start...")
48
55
 
49
- def resume
50
- fail Error::NotStarted if stopped?
51
- return unless wait_thread
52
- _wakeup(:resume)
56
+ wait_for_state(:started, timeout: MAX_STARTUP_SECONDS) or
57
+ raise Error::NotStarted, "thread didn't start in #{MAX_STARTUP_SECONDS} seconds (in state: #{state.inspect})"
58
+
59
+ Listen.logger.debug('Processing started.')
53
60
  end
54
61
 
55
62
  def pause
@@ -57,60 +64,30 @@ module Listen
57
64
  # fail NotImplementedError
58
65
  end
59
66
 
60
- def teardown
61
- return unless wait_thread
62
- if wait_thread.alive?
63
- _wakeup(:teardown)
64
- wait_thread.join
65
- end
67
+ def stop
68
+ transition! :stopped
69
+
70
+ @wait_thread&.join
66
71
  @wait_thread = nil
67
72
  end
68
73
 
69
74
  def stopped?
70
- !wait_thread
75
+ state == :stopped
71
76
  end
72
77
 
73
78
  private
74
79
 
75
- attr_reader :config
76
- attr_reader :wait_thread
77
-
78
- attr_accessor :state
79
-
80
- def _wait_for_changes(ready_queue, config)
81
- processor = Event::Processor.new(config, @reasons)
80
+ def _process_changes
81
+ processor = Event::Processor.new(@config, @reasons)
82
82
 
83
- _wait_until_resumed(ready_queue)
84
- processor.loop_for(config.min_delay_between_events)
85
- rescue StandardError => ex
86
- _nice_error(ex)
87
- end
88
-
89
- def _sleep(*args)
90
- Kernel.sleep(*args)
91
- end
92
-
93
- def _wait_until_resumed(ready_queue)
94
- self.state = :paused
95
- ready_queue << :ready
96
- sleep
97
- self.state = :processing
98
- end
83
+ transition! :started
99
84
 
100
- def _nice_error(ex)
101
- indent = "\n -- "
102
- msg = format(
103
- 'exception while processing events: %s Backtrace:%s%s',
104
- ex,
105
- indent,
106
- ex.backtrace * indent
107
- )
108
- Listen::Logger.error(msg)
85
+ processor.loop_for(@config.min_delay_between_events)
109
86
  end
110
87
 
111
88
  def _wakeup(reason)
112
89
  @reasons << reason
113
- wait_thread.wakeup
90
+ @wait_thread.wakeup
114
91
  end
115
92
  end
116
93
  end