listen 3.0.8 → 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.
data/lib/listen/fsm.rb CHANGED
@@ -1,119 +1,123 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Code copied from https://github.com/celluloid/celluloid-fsm
4
+
5
+ require 'thread'
6
+
2
7
  module Listen
3
8
  module FSM
4
- DEFAULT_STATE = :default # Default state name unless one is explicitly set
5
-
6
9
  # Included hook to extend class methods
7
10
  def self.included(klass)
8
11
  klass.send :extend, ClassMethods
9
12
  end
10
13
 
11
14
  module ClassMethods
12
- # Obtain or set the default state
13
- # Passing a state name sets the default state
14
- def default_state(new_default = nil)
15
- if new_default
16
- @default_state = new_default.to_sym
15
+ # Obtain or set the start state
16
+ # Passing a state name sets the start state
17
+ def start_state(new_start_state = nil)
18
+ if new_start_state
19
+ new_start_state.is_a?(Symbol) or raise ArgumentError, "state name must be a Symbol (got #{new_start_state.inspect})"
20
+ @start_state = new_start_state
17
21
  else
18
- defined?(@default_state) ? @default_state : DEFAULT_STATE
22
+ defined?(@start_state) or raise ArgumentError, "`start_state :<state>` must be declared before `new`"
23
+ @start_state
19
24
  end
20
25
  end
21
26
 
22
- # Obtain the valid states for this FSM
27
+ # The valid states for this FSM, as a hash with state name symbols as keys and State objects as values.
23
28
  def states
24
29
  @states ||= {}
25
30
  end
26
31
 
27
- # Declare an FSM state and optionally provide a callback block to fire
32
+ # Declare an FSM state and optionally provide a callback block to fire on state entry
28
33
  # Options:
29
34
  # * to: a state or array of states this state can transition to
30
- def state(*args, &block)
31
- if args.last.is_a? Hash
32
- # Stringify keys :/
33
- options = args.pop.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
34
- else
35
- options = {}
36
- end
37
-
38
- args.each do |name|
39
- name = name.to_sym
40
- default_state name if options['default']
41
- states[name] = State.new(name, options['to'], &block)
42
- end
35
+ def state(state_name, to: nil, &block)
36
+ state_name.is_a?(Symbol) or raise ArgumentError, "state name must be a Symbol (got #{state_name.inspect})"
37
+ states[state_name] = State.new(state_name, to, &block)
43
38
  end
44
39
  end
45
40
 
46
- # Be kind and call super if you must redefine initialize
47
- def initialize
48
- @state = self.class.default_state
41
+ # Note: including classes must call initialize_fsm from their initialize method.
42
+ def initialize_fsm
43
+ @fsm_initialized = true
44
+ @state = self.class.start_state
45
+ @mutex = ::Mutex.new
46
+ @state_changed = ::ConditionVariable.new
49
47
  end
50
48
 
51
- # Obtain the current state of the FSM
49
+ # Current state of the FSM, stored as a symbol
52
50
  attr_reader :state
53
51
 
54
- def transition(state_name)
55
- new_state = validate_and_sanitize_new_state(state_name)
56
- return unless new_state
57
- transition_with_callbacks!(new_state)
52
+ # checks for one of the given states to wait for
53
+ # if not already, waits for a state change (up to timeout seconds--`nil` means infinite)
54
+ # returns truthy iff the transition to one of the desired state has occurred
55
+ def wait_for_state(*wait_for_states, timeout: nil)
56
+ wait_for_states.each do |state|
57
+ state.is_a?(Symbol) or raise ArgumentError, "states must be symbols (got #{state.inspect})"
58
+ end
59
+ @mutex.synchronize do
60
+ if !wait_for_states.include?(@state)
61
+ @state_changed.wait(@mutex, timeout)
62
+ end
63
+ wait_for_states.include?(@state)
64
+ end
58
65
  end
59
66
 
60
- # Immediate state transition with no checks, or callbacks. "Dangerous!"
61
- def transition!(state_name)
62
- @state = state_name
63
- end
67
+ private
64
68
 
65
- protected
69
+ def transition(new_state_name)
70
+ new_state_name.is_a?(Symbol) or raise ArgumentError, "state name must be a Symbol (got #{new_state_name.inspect})"
71
+ if (new_state = validate_and_sanitize_new_state(new_state_name))
72
+ transition_with_callbacks!(new_state)
73
+ end
74
+ end
66
75
 
67
- def validate_and_sanitize_new_state(state_name)
68
- state_name = state_name.to_sym
76
+ # Low-level, immediate state transition with no checks or callbacks.
77
+ def transition!(new_state_name)
78
+ new_state_name.is_a?(Symbol) or raise ArgumentError, "state name must be a Symbol (got #{new_state_name.inspect})"
79
+ @fsm_initialized or raise ArgumentError, "FSM not initialized. You must call initialize_fsm from initialize!"
80
+ @mutex.synchronize do
81
+ yield if block_given?
82
+ @state = new_state_name
83
+ @state_changed.broadcast
84
+ end
85
+ end
69
86
 
70
- return if current_state_name == state_name
87
+ def validate_and_sanitize_new_state(new_state_name)
88
+ return nil if @state == new_state_name
71
89
 
72
- if current_state && !current_state.valid_transition?(state_name)
90
+ if current_state && !current_state.valid_transition?(new_state_name)
73
91
  valid = current_state.transitions.map(&:to_s).join(', ')
74
- msg = "#{self.class} can't change state from '#{@state}'"\
75
- " to '#{state_name}', only to: #{valid}"
76
- fail ArgumentError, msg
92
+ msg = "#{self.class} can't change state from '#{@state}' to '#{new_state_name}', only to: #{valid}"
93
+ raise ArgumentError, msg
77
94
  end
78
95
 
79
- new_state = states[state_name]
80
-
81
- unless new_state
82
- return if state_name == default_state
83
- fail ArgumentError, "invalid state for #{self.class}: #{state_name}"
96
+ unless (new_state = self.class.states[new_state_name])
97
+ new_state_name == self.class.start_state or raise ArgumentError, "invalid state for #{self.class}: #{new_state_name}"
84
98
  end
85
99
 
86
100
  new_state
87
101
  end
88
102
 
89
- def transition_with_callbacks!(state_name)
90
- transition! state_name.name
91
- state_name.call(self)
92
- end
93
-
94
- def states
95
- self.class.states
96
- end
97
-
98
- def default_state
99
- self.class.default_state
103
+ def transition_with_callbacks!(new_state)
104
+ transition! new_state.name
105
+ new_state.call(self)
100
106
  end
101
107
 
102
108
  def current_state
103
- states[@state]
104
- end
105
-
106
- def current_state_name
107
- current_state && current_state.name || ''
109
+ self.class.states[@state]
108
110
  end
109
111
 
110
112
  class State
111
113
  attr_reader :name, :transitions
112
114
 
113
- def initialize(name, transitions = nil, &block)
114
- @name, @block = name, block
115
- @transitions = nil
116
- @transitions = Array(transitions).map(&:to_sym) if transitions
115
+ def initialize(name, transitions, &block)
116
+ @name = name
117
+ @block = block
118
+ @transitions = if transitions
119
+ Array(transitions).map(&:to_sym)
120
+ end
117
121
  end
118
122
 
119
123
  def call(obj)
@@ -121,10 +125,8 @@ module Listen
121
125
  end
122
126
 
123
127
  def valid_transition?(new_state)
124
- # All transitions are allowed unless expressly
125
- return true unless @transitions
126
-
127
- @transitions.include? new_state.to_sym
128
+ # All transitions are allowed if none are expressly declared
129
+ !@transitions || @transitions.include?(new_state.to_sym)
128
130
  end
129
131
  end
130
132
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Listen
2
4
  class Listener
3
5
  class Config
@@ -10,7 +12,7 @@ module Listen
10
12
  # Backend selecting options
11
13
  force_polling: false,
12
14
  polling_fallback_message: nil
13
- }
15
+ }.freeze
14
16
 
15
17
  def initialize(opts)
16
18
  @options = DEFAULTS.merge(opts)
@@ -23,13 +25,7 @@ module Listen
23
25
  @relative
24
26
  end
25
27
 
26
- def min_delay_between_events
27
- @min_delay_between_events
28
- end
29
-
30
- def silencer_rules
31
- @silencer_rules
32
- end
28
+ attr_reader :min_delay_between_events, :silencer_rules
33
29
 
34
30
  def adapter_instance_options(klass)
35
31
  valid_keys = klass.const_get('DEFAULTS').keys
@@ -37,7 +33,7 @@ module Listen
37
33
  end
38
34
 
39
35
  def adapter_select_options
40
- valid_keys = %w(force_polling polling_fallback_message).map(&:to_sym)
36
+ valid_keys = %w[force_polling polling_fallback_message].map(&:to_sym)
41
37
  Hash[@options.select { |key, _| valid_keys.include?(key) }]
42
38
  end
43
39
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'English'
2
4
 
3
5
  require 'listen/version'
@@ -31,13 +33,14 @@ module Listen
31
33
  # @yieldparam [Array<String>] added the list of added files
32
34
  # @yieldparam [Array<String>] removed the list of removed files
33
35
  #
36
+ # rubocop:disable Metrics/MethodLength
34
37
  def initialize(*dirs, &block)
35
38
  options = dirs.last.is_a?(Hash) ? dirs.pop : {}
36
39
 
37
40
  @config = Config.new(options)
38
41
 
39
42
  eq_config = Event::Queue::Config.new(@config.relative?)
40
- queue = Event::Queue.new(eq_config) { @processor.wakeup_on_event }
43
+ queue = Event::Queue.new(eq_config)
41
44
 
42
45
  silencer = Silencer.new
43
46
  rules = @config.silencer_rules
@@ -56,41 +59,43 @@ module Listen
56
59
 
57
60
  @processor = Event::Loop.new(pconfig)
58
61
 
59
- super() # FSM
62
+ initialize_fsm
60
63
  end
64
+ # rubocop:enable Metrics/MethodLength
61
65
 
62
- default_state :initializing
66
+ start_state :initializing
63
67
 
64
68
  state :initializing, to: [:backend_started, :stopped]
65
69
 
66
- state :backend_started, to: [:frontend_ready, :stopped] do
67
- backend.start
68
- end
69
-
70
- state :frontend_ready, to: [:processing_events, :stopped] do
71
- processor.setup
70
+ state :backend_started, to: [:processing_events, :stopped] do
71
+ @backend.start
72
72
  end
73
73
 
74
74
  state :processing_events, to: [:paused, :stopped] do
75
- processor.resume
75
+ @processor.start
76
76
  end
77
77
 
78
78
  state :paused, to: [:processing_events, :stopped] do
79
- processor.pause
79
+ @processor.pause
80
80
  end
81
81
 
82
82
  state :stopped, to: [:backend_started] do
83
- backend.stop # should be before processor.teardown to halt events ASAP
84
- processor.teardown
83
+ @backend.stop # halt events ASAP
84
+ @processor.stop
85
85
  end
86
86
 
87
87
  # Starts processing events and starts adapters
88
88
  # or resumes invoking callbacks if paused
89
89
  def start
90
- transition :backend_started if state == :initializing
91
- transition :frontend_ready if state == :backend_started
92
- transition :processing_events if state == :frontend_ready
93
- 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
94
99
  end
95
100
 
96
101
  # Stops both listening for events and processing them
@@ -112,6 +117,10 @@ module Listen
112
117
  state == :paused
113
118
  end
114
119
 
120
+ def stopped?
121
+ state == :stopped
122
+ end
123
+
115
124
  def ignore(regexps)
116
125
  @silencer_controller.append_ignores(regexps)
117
126
  end
@@ -123,10 +132,5 @@ module Listen
123
132
  def only(regexps)
124
133
  @silencer_controller.replace_with_only(regexps)
125
134
  end
126
-
127
- private
128
-
129
- attr_reader :processor
130
- attr_reader :backend
131
135
  end
132
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
 
@@ -88,11 +90,9 @@ module Listen
88
90
  # editor rename() call (e.g. Kate and Sublime)
89
91
  def _reinterpret_related_changes(cookies)
90
92
  table = { moved_to: :added, moved_from: :removed }
91
- cookies.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]]
93
+ cookies.flat_map do |_, changes|
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)
@@ -101,32 +101,29 @@ module Listen
101
101
  [table.fetch(change, change), dir, path]
102
102
  end
103
103
  end
104
- end.flatten(1)
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
@@ -6,21 +8,23 @@ module Listen
6
8
  # file: "/home/me/watched_dir", "app/models", "foo.rb"
7
9
  # dir, "/home/me/watched_dir", "."
8
10
  def initialize(root, relative, name = nil)
9
- @root, @relative, @name = root, relative, name
11
+ @root = root
12
+ @relative = relative
13
+ @name = name
10
14
  end
11
15
 
12
16
  attr_reader :root, :relative, :name
13
17
 
14
18
  def children
15
19
  child_relative = _join
16
- (_entries(sys_path) - %w(. ..)).map do |name|
20
+ (_entries(sys_path) - %w[. ..]).map do |name|
17
21
  Entry.new(@root, child_relative, name)
18
22
  end
19
23
  end
20
24
 
21
25
  def meta
22
26
  lstat = ::File.lstat(sys_path)
23
- { mtime: lstat.mtime.to_f, mode: lstat.mode }
27
+ { mtime: lstat.mtime.to_f, mode: lstat.mode, size: lstat.size }
24
28
  end
25
29
 
26
30
  # record hash is e.g.
@@ -54,7 +58,7 @@ module Listen
54
58
  # https://github.com/jruby/jruby/issues/3840
55
59
  exists = ::File.exist?(dir)
56
60
  directory = ::File.directory?(dir)
57
- return Dir.entries(dir) unless (exists && !directory)
61
+ return Dir.entries(dir) unless exists && !directory
58
62
  raise Errno::ENOTDIR, dir
59
63
  end
60
64
  end
@@ -1,10 +1,13 @@
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'
10
+ README_URL = 'https://github.com/guard/listen/blob/master/README.md'
8
11
 
9
12
  SYMLINK_LOOP_ERROR = <<-EOS
10
13
  ** ERROR: directory is already being watched! **
@@ -13,11 +16,10 @@ module Listen
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