listen 3.2.1 → 3.5.1

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