listen 3.1.5 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'English'
2
4
 
3
5
  require 'listen/version'
@@ -19,7 +21,6 @@ require 'listen/listener/config'
19
21
 
20
22
  module Listen
21
23
  class Listener
22
- # TODO: move the state machine's methods private
23
24
  include Listen::FSM
24
25
 
25
26
  # Initializes the directories listener.
@@ -32,13 +33,14 @@ module Listen
32
33
  # @yieldparam [Array<String>] added the list of added files
33
34
  # @yieldparam [Array<String>] removed the list of removed files
34
35
  #
36
+ # rubocop:disable Metrics/MethodLength
35
37
  def initialize(*dirs, &block)
36
38
  options = dirs.last.is_a?(Hash) ? dirs.pop : {}
37
39
 
38
40
  @config = Config.new(options)
39
41
 
40
42
  eq_config = Event::Queue::Config.new(@config.relative?)
41
- queue = Event::Queue.new(eq_config) { @processor.wakeup_on_event }
43
+ queue = Event::Queue.new(eq_config)
42
44
 
43
45
  silencer = Silencer.new
44
46
  rules = @config.silencer_rules
@@ -57,41 +59,43 @@ module Listen
57
59
 
58
60
  @processor = Event::Loop.new(pconfig)
59
61
 
60
- super() # FSM
62
+ initialize_fsm
61
63
  end
64
+ # rubocop:enable Metrics/MethodLength
62
65
 
63
- default_state :initializing
66
+ start_state :initializing
64
67
 
65
68
  state :initializing, to: [:backend_started, :stopped]
66
69
 
67
- state :backend_started, to: [:frontend_ready, :stopped] do
68
- backend.start
69
- end
70
-
71
- state :frontend_ready, to: [:processing_events, :stopped] do
72
- processor.setup
70
+ state :backend_started, to: [:processing_events, :stopped] do
71
+ @backend.start
73
72
  end
74
73
 
75
74
  state :processing_events, to: [:paused, :stopped] do
76
- processor.resume
75
+ @processor.start
77
76
  end
78
77
 
79
78
  state :paused, to: [:processing_events, :stopped] do
80
- processor.pause
79
+ @processor.pause
81
80
  end
82
81
 
83
82
  state :stopped, to: [:backend_started] do
84
- backend.stop # should be before processor.teardown to halt events ASAP
85
- processor.teardown
83
+ @backend.stop # halt events ASAP
84
+ @processor.stop
86
85
  end
87
86
 
88
87
  # Starts processing events and starts adapters
89
88
  # or resumes invoking callbacks if paused
90
89
  def start
91
- transition :backend_started if state == :initializing
92
- transition :frontend_ready if state == :backend_started
93
- transition :processing_events if state == :frontend_ready
94
- transition :processing_events if state == :paused
90
+ case state
91
+ when :initializing
92
+ transition :backend_started
93
+ transition :processing_events
94
+ when :paused
95
+ transition :processing_events
96
+ else
97
+ raise ArgumentError, "cannot start from state #{state.inspect}"
98
+ end
95
99
  end
96
100
 
97
101
  # Stops both listening for events and processing them
@@ -113,6 +117,10 @@ module Listen
113
117
  state == :paused
114
118
  end
115
119
 
120
+ def stopped?
121
+ state == :stopped
122
+ end
123
+
116
124
  def ignore(regexps)
117
125
  @silencer_controller.append_ignores(regexps)
118
126
  end
@@ -124,10 +132,5 @@ module Listen
124
132
  def only(regexps)
125
133
  @silencer_controller.replace_with_only(regexps)
126
134
  end
127
-
128
- private
129
-
130
- attr_reader :processor
131
- attr_reader :backend
132
135
  end
133
136
  end
data/lib/listen/logger.rb CHANGED
@@ -1,32 +1,36 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Listen
2
- def self.logger
3
- @logger ||= nil
4
- end
4
+ @logger = nil
5
5
 
6
- def self.logger=(logger)
7
- @logger = logger
8
- end
6
+ # Listen.logger will always be present.
7
+ # If you don't want logging, set Listen.logger = ::Logger.new('/dev/null', level: ::Logger::UNKNOWN)
9
8
 
10
- def self.setup_default_logger_if_unset
11
- self.logger ||= ::Logger.new(STDERR).tap do |logger|
12
- debugging = ENV['LISTEN_GEM_DEBUGGING']
13
- logger.level =
14
- case debugging.to_s
15
- when /2/
9
+ class << self
10
+ attr_writer :logger
11
+
12
+ def logger
13
+ @logger ||= default_logger
14
+ end
15
+
16
+ private
17
+
18
+ def default_logger
19
+ level =
20
+ case ENV['LISTEN_GEM_DEBUGGING'].to_s
21
+ when /debug|2/i
16
22
  ::Logger::DEBUG
17
- when /true|yes|1/i
23
+ when /info|true|yes|1/i
18
24
  ::Logger::INFO
25
+ when /warn/i
26
+ ::Logger::WARN
27
+ when /fatal/i
28
+ ::Logger::FATAL
19
29
  else
20
30
  ::Logger::ERROR
21
31
  end
22
- end
23
- end
24
32
 
25
- class Logger
26
- [:fatal, :error, :warn, :info, :debug].each do |meth|
27
- define_singleton_method(meth) do |*args, &block|
28
- Listen.logger.public_send(meth, *args, &block) if Listen.logger
29
- end
33
+ ::Logger.new(STDERR, level: level)
30
34
  end
31
35
  end
32
36
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Listen
4
+ module MonotonicTime
5
+ class << self
6
+ if defined?(Process::CLOCK_MONOTONIC)
7
+
8
+ def now
9
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
10
+ end
11
+
12
+ elsif defined?(Process::CLOCK_MONOTONIC_RAW)
13
+
14
+ def now
15
+ Process.clock_gettime(Process::CLOCK_MONOTONIC_RAW)
16
+ end
17
+
18
+ else
19
+
20
+ def now
21
+ Time.now.to_f
22
+ end
23
+
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,23 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Listen
2
4
  class Options
3
5
  def initialize(opts, defaults)
4
6
  @options = {}
5
7
  given_options = opts.dup
6
- defaults.keys.each do |key|
8
+ defaults.each_key do |key|
7
9
  @options[key] = given_options.delete(key) || defaults[key]
8
10
  end
9
11
 
10
- return if given_options.empty?
12
+ given_options.empty? or raise ArgumentError, "Unknown options: #{given_options.inspect}"
13
+ end
11
14
 
12
- msg = "Unknown options: #{given_options.inspect}"
13
- Listen::Logger.warn msg
14
- fail msg
15
+ # rubocop:disable Lint/MissingSuper
16
+ def respond_to_missing?(name, *_)
17
+ @options.has_key?(name)
15
18
  end
16
19
 
17
20
  def method_missing(name, *_)
18
- return @options[name] if @options.key?(name)
19
- msg = "Bad option: #{name.inspect} (valid:#{@options.keys.inspect})"
20
- fail NameError, msg
21
+ respond_to_missing?(name) or raise NameError, "Bad option: #{name.inspect} (valid:#{@options.keys.inspect})"
22
+ @options[name]
21
23
  end
24
+ # rubocop:enable Lint/MissingSuper
22
25
  end
23
26
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Listen
2
4
  class QueueOptimizer
3
5
  class Config
@@ -60,7 +62,7 @@ module Listen
60
62
  actions << :added if actions.delete(:moved_to)
61
63
  actions << :removed if actions.delete(:moved_from)
62
64
 
63
- modified = actions.detect { |x| x == :modified }
65
+ modified = actions.find { |x| x == :modified }
64
66
  _calculate_add_remove_difference(actions, path, modified)
65
67
  end
66
68
 
@@ -89,10 +91,8 @@ module Listen
89
91
  def _reinterpret_related_changes(cookies)
90
92
  table = { moved_to: :added, moved_from: :removed }
91
93
  cookies.flat_map do |_, changes|
92
- data = _detect_possible_editor_save(changes)
93
- if data
94
- to_dir, to_file = data
95
- [[:modified, to_dir, to_file]]
94
+ if (editor_modified = editor_modified?(changes))
95
+ [[:modified, *editor_modified]]
96
96
  else
97
97
  not_silenced = changes.reject do |type, _, _, path, _|
98
98
  config.silenced?(Pathname(path), type)
@@ -104,29 +104,26 @@ module Listen
104
104
  end
105
105
  end
106
106
 
107
- def _detect_possible_editor_save(changes)
107
+ def editor_modified?(changes)
108
108
  return unless changes.size == 2
109
109
 
110
- from_type = from_change = from = nil
111
- to_type = to_change = to_dir = to = nil
110
+ from_type = from = nil
111
+ to_type = to_dir = to = nil
112
112
 
113
113
  changes.each do |data|
114
114
  case data[1]
115
115
  when :moved_from
116
- from_type, from_change, _, from, = data
116
+ from_type, _from_change, _, from, = data
117
117
  when :moved_to
118
- to_type, to_change, to_dir, to, = data
119
- else
120
- return nil
118
+ to_type, _to_change, to_dir, to, = data
121
119
  end
122
120
  end
123
121
 
124
- return unless from && to
125
-
126
122
  # Expect an ignored moved_from and non-ignored moved_to
127
123
  # to qualify as an "editor modify"
128
- return unless config.silenced?(Pathname(from), from_type)
129
- config.silenced?(Pathname(to), to_type) ? nil : [to_dir, to]
124
+ if from && to && config.silenced?(Pathname(from), from_type) && !config.silenced?(Pathname(to), to_type)
125
+ [to_dir, to]
126
+ end
130
127
  end
131
128
  end
132
129
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Listen
2
4
  # @private api
3
5
  class Record
@@ -15,14 +17,14 @@ module Listen
15
17
 
16
18
  def children
17
19
  child_relative = _join
18
- (_entries(sys_path) - %w(. ..)).map do |name|
20
+ (_entries(sys_path) - %w[. ..]).map do |name|
19
21
  Entry.new(@root, child_relative, name)
20
22
  end
21
23
  end
22
24
 
23
25
  def meta
24
26
  lstat = ::File.lstat(sys_path)
25
- { mtime: lstat.mtime.to_f, mode: lstat.mode }
27
+ { mtime: lstat.mtime.to_f, mode: lstat.mode, size: lstat.size }
26
28
  end
27
29
 
28
30
  # record hash is e.g.
@@ -1,23 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'set'
4
+ require 'listen/error'
2
5
 
3
6
  module Listen
4
7
  # @private api
5
8
  class Record
6
9
  class SymlinkDetector
7
- WIKI = 'https://github.com/guard/listen/wiki/Duplicate-directory-errors'.freeze
10
+ README_URL = 'https://github.com/guard/listen/blob/master/README.md'
8
11
 
9
- SYMLINK_LOOP_ERROR = <<-EOS.freeze
12
+ SYMLINK_LOOP_ERROR = <<-EOS
10
13
  ** ERROR: directory is already being watched! **
11
14
 
12
15
  Directory: %s
13
16
 
14
17
  is already being watched through: %s
15
18
 
16
- MORE INFO: #{WIKI}
19
+ MORE INFO: #{README_URL}
17
20
  EOS
18
21
 
19
- class Error < RuntimeError
20
- end
22
+ Error = ::Listen::Error # for backward compatibility
21
23
 
22
24
  def initialize
23
25
  @real_dirs = Set.new
@@ -25,14 +27,14 @@ module Listen
25
27
 
26
28
  def verify_unwatched!(entry)
27
29
  real_path = entry.real_path
28
- @real_dirs.add?(real_path) || _fail(entry.sys_path, real_path)
30
+ @real_dirs.add?(real_path) or _fail(entry.sys_path, real_path)
29
31
  end
30
32
 
31
33
  private
32
34
 
33
35
  def _fail(symlinked, real_path)
34
- STDERR.puts format(SYMLINK_LOOP_ERROR, symlinked, real_path)
35
- fail Error, 'Failed due to looped symlinks'
36
+ warn(format(SYMLINK_LOOP_ERROR, symlinked, real_path))
37
+ raise ::Listen::Error::SymlinkLoop, 'Failed due to looped symlinks'
36
38
  end
37
39
  end
38
40
  end
data/lib/listen/record.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thread'
2
4
  require 'listen/record/entry'
3
5
  require 'listen/record/symlink_detector'
@@ -8,14 +10,17 @@ module Listen
8
10
  # TODO: deprecate
9
11
 
10
12
  attr_reader :root
11
- def initialize(directory)
13
+
14
+ def initialize(directory, silencer)
12
15
  @tree = _auto_hash
13
16
  @root = directory.to_s
17
+ @silencer = silencer
14
18
  end
15
19
 
16
20
  def add_dir(rel_path)
17
- return if [nil, '', '.'].include? rel_path
18
- @tree[rel_path] ||= {}
21
+ if ![nil, '', '.'].include?(rel_path)
22
+ @tree[rel_path] ||= {}
23
+ end
19
24
  end
20
25
 
21
26
  def update_file(rel_path, data)
@@ -31,30 +36,27 @@ module Listen
31
36
  def file_data(rel_path)
32
37
  dirname, basename = Pathname(rel_path).split.map(&:to_s)
33
38
  if [nil, '', '.'].include? dirname
34
- tree[basename] ||= {}
35
- tree[basename].dup
39
+ @tree[basename] ||= {}
40
+ @tree[basename].dup
36
41
  else
37
- tree[dirname] ||= {}
38
- tree[dirname][basename] ||= {}
39
- tree[dirname][basename].dup
42
+ @tree[dirname] ||= {}
43
+ @tree[dirname][basename] ||= {}
44
+ @tree[dirname][basename].dup
40
45
  end
41
46
  end
42
47
 
43
48
  def dir_entries(rel_path)
44
- subtree =
45
- if [nil, '', '.'].include? rel_path.to_s
46
- tree
47
- else
48
- tree[rel_path.to_s] ||= _auto_hash
49
- tree[rel_path.to_s]
50
- end
49
+ subtree = if ['', '.'].include? rel_path.to_s
50
+ @tree
51
+ else
52
+ @tree[rel_path.to_s] ||= _auto_hash
53
+ @tree[rel_path.to_s]
54
+ end
51
55
 
52
- result = {}
53
- subtree.each do |key, values|
56
+ subtree.transform_values do |values|
54
57
  # only get data for file entries
55
- result[key] = values.key?(:mtime) ? values : {}
58
+ values.key?(:mtime) ? values : {}
56
59
  end
57
- result
58
60
  end
59
61
 
60
62
  def build
@@ -71,34 +73,34 @@ module Listen
71
73
  private
72
74
 
73
75
  def _auto_hash
74
- Hash.new { |h, k| h[k] = Hash.new }
76
+ Hash.new { |h, k| h[k] = {} }
75
77
  end
76
78
 
77
- attr_reader :tree
78
-
79
79
  def _fast_update_file(dirname, basename, data)
80
- if [nil, '', '.'].include? dirname
81
- tree[basename] = (tree[basename] || {}).merge(data)
80
+ if [nil, '', '.'].include?(dirname)
81
+ @tree[basename] = (@tree[basename] || {}).merge(data)
82
82
  else
83
- tree[dirname] ||= {}
84
- tree[dirname][basename] = (tree[dirname][basename] || {}).merge(data)
83
+ @tree[dirname] ||= {}
84
+ @tree[dirname][basename] = (@tree[dirname][basename] || {}).merge(data)
85
85
  end
86
86
  end
87
87
 
88
88
  def _fast_unset_path(dirname, basename)
89
89
  # this may need to be reworked to properly remove
90
90
  # entries from a tree, without adding non-existing dirs to the record
91
- if [nil, '', '.'].include? dirname
92
- return unless tree.key?(basename)
93
- tree.delete(basename)
94
- else
95
- return unless tree.key?(dirname)
96
- tree[dirname].delete(basename)
91
+ if [nil, '', '.'].include?(dirname)
92
+ if @tree.key?(basename)
93
+ @tree.delete(basename)
94
+ end
95
+ elsif @tree.key?(dirname)
96
+ @tree[dirname].delete(basename)
97
97
  end
98
98
  end
99
99
 
100
100
  def _fast_build_dir(remaining, symlink_detector)
101
101
  entry = remaining.pop
102
+ return if @silencer.silenced?(entry.record_dir_key, :dir)
103
+
102
104
  children = entry.children # NOTE: children() implicitly tests if dir
103
105
  symlink_detector.verify_unwatched!(entry)
104
106
  children.each { |child| remaining << child }
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Listen
2
4
  class Silencer
3
5
  class Controller
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Listen
2
4
  class Silencer
3
5
  # The default list of directories that get ignored.
@@ -12,7 +14,7 @@ module Listen
12
14
  | log
13
15
  | tmp
14
16
  |vendor/ruby
15
- )(/|$)}x
17
+ )(/|$)}x.freeze
16
18
 
17
19
  # The default list of files that get ignored.
18
20
  DEFAULT_IGNORED_EXTENSIONS = %r{(?:
@@ -46,40 +48,43 @@ module Listen
46
48
  )
47
49
  )
48
50
 
51
+ # Mutagen sync temporary files
52
+ | \.mutagen-temporary.*
53
+
49
54
  # other files
50
55
  | \.DS_Store
51
56
  | \.tmp
52
57
  | ~
53
- )$}x
58
+ )$}x.freeze
54
59
 
60
+ # TODO: deprecate these mutators; use attr_reader instead
55
61
  attr_accessor :only_patterns, :ignore_patterns
56
62
 
57
- def initialize
58
- configure({})
63
+ def initialize(**options)
64
+ configure(options)
59
65
  end
60
66
 
67
+ # TODO: deprecate this mutator
61
68
  def configure(options)
62
69
  @only_patterns = options[:only] ? Array(options[:only]) : nil
63
70
  @ignore_patterns = _init_ignores(options[:ignore], options[:ignore!])
64
71
  end
65
72
 
66
- # Note: relative_path is temporarily expected to be a relative Pathname to
67
- # make refactoring easier (ideally, it would take a string)
68
-
69
- # TODO: switch type and path places - and verify
70
73
  def silenced?(relative_path, type)
71
- path = relative_path.to_s
72
-
73
- if only_patterns && type == :file
74
- return true unless only_patterns.any? { |pattern| path =~ pattern }
75
- end
74
+ path = relative_path.to_s # in case it is a Pathname
76
75
 
77
- ignore_patterns.any? { |pattern| path =~ pattern }
76
+ _ignore?(path) || (only_patterns && type == :file && !_only?(path))
78
77
  end
79
78
 
80
79
  private
81
80
 
82
- attr_reader :options
81
+ def _ignore?(path)
82
+ ignore_patterns.any? { |pattern| path =~ pattern }
83
+ end
84
+
85
+ def _only?(path)
86
+ only_patterns.any? { |pattern| path =~ pattern }
87
+ end
83
88
 
84
89
  def _init_ignores(ignores, overrides)
85
90
  patterns = []
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thread'
4
+
5
+ require_relative 'logger'
6
+
7
+ module Listen
8
+ module Thread
9
+ class << self
10
+ # Creates a new thread with the given name.
11
+ # Any exceptions raised by the thread will be logged with the thread name and complete backtrace.
12
+ # rubocop:disable Style/MultilineBlockChain
13
+ def new(name, &block)
14
+ thread_name = "listen-#{name}"
15
+ caller_stack = caller
16
+
17
+ ::Thread.new do
18
+ rescue_and_log(thread_name, caller_stack: caller_stack, &block)
19
+ end.tap do |thread|
20
+ thread.name = thread_name
21
+ end
22
+ end
23
+ # rubocop:enable Style/MultilineBlockChain
24
+
25
+ def rescue_and_log(method_name, *args, caller_stack: nil)
26
+ yield(*args)
27
+ rescue => exception
28
+ _log_exception(exception, method_name, caller_stack: caller_stack)
29
+ end
30
+
31
+ private
32
+
33
+ def _log_exception(exception, thread_name, caller_stack: nil)
34
+ complete_backtrace = if caller_stack
35
+ [*exception.backtrace, "--- Thread.new ---", *caller_stack]
36
+ else
37
+ exception.backtrace
38
+ end
39
+ message = "Exception rescued in #{thread_name}:\n#{_exception_with_causes(exception)}\n#{complete_backtrace * "\n"}"
40
+ Listen.logger.error(message)
41
+ end
42
+
43
+ def _exception_with_causes(exception)
44
+ result = +"#{exception.class}: #{exception}"
45
+ if exception.cause
46
+ result << "\n"
47
+ result << "--- Caused by: ---\n"
48
+ result << _exception_with_causes(exception.cause)
49
+ end
50
+ result
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Listen
2
- VERSION = '3.1.5'.freeze
4
+ VERSION = '3.7.0'
3
5
  end