listen 3.0.2 → 3.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/listen/file.rb CHANGED
@@ -1,12 +1,17 @@
1
- require 'digest/md5'
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
2
4
 
3
5
  module Listen
4
6
  class File
7
+ # rubocop:disable Metrics/MethodLength
8
+ # rubocop:disable Metrics/CyclomaticComplexity
9
+ # rubocop:disable Metrics/PerceivedComplexity
5
10
  def self.change(record, rel_path)
6
11
  path = Pathname.new(record.root) + rel_path
7
12
  lstat = path.lstat
8
13
 
9
- data = { mtime: lstat.mtime.to_f, mode: lstat.mode }
14
+ data = { mtime: lstat.mtime.to_f, mode: lstat.mode, size: lstat.size }
10
15
 
11
16
  record_data = record.file_data(rel_path)
12
17
 
@@ -25,8 +30,13 @@ module Listen
25
30
  return :modified
26
31
  end
27
32
 
33
+ if data[:size] != record_data[:size]
34
+ record.update_file(rel_path, data)
35
+ return :modified
36
+ end
37
+
28
38
  return if /1|true/ =~ ENV['LISTEN_GEM_DISABLE_HASHING']
29
- return unless self.inaccurate_mac_time?(lstat)
39
+ return unless inaccurate_mac_time?(lstat)
30
40
 
31
41
  # Check if change happened within 1 second (maybe it's even
32
42
  # too much, e.g. 0.3-0.5 could be sufficient).
@@ -43,7 +53,7 @@ module Listen
43
53
  # then at ???14.998, but the fstat time would be ???14.0 in
44
54
  # both cases).
45
55
  #
46
- # If change happend at ???14.999997, the mtime is 14.0, so for
56
+ # If change happened at ???14.999997, the mtime is 14.0, so for
47
57
  # an mtime=???14.0 we assume it could even be almost ???15.0
48
58
  #
49
59
  # So if Time.now.to_f is ???15.999998 and stat reports mtime
@@ -57,16 +67,21 @@ module Listen
57
67
  #
58
68
  return if data[:mtime].to_i + 2 <= Time.now.to_f
59
69
 
60
- md5 = Digest::MD5.file(path).digest
61
- record.update_file(rel_path, data.merge(md5: md5))
62
- :modified if record_data[:md5] && md5 != record_data[:md5]
70
+ sha = Digest::SHA256.file(path).digest
71
+ record.update_file(rel_path, data.merge(sha: sha))
72
+ if record_data[:sha] && sha != record_data[:sha]
73
+ :modified
74
+ end
63
75
  rescue SystemCallError
64
76
  record.unset_path(rel_path)
65
77
  :removed
66
78
  rescue
67
- Listen::Logger.debug "lstat failed for: #{rel_path} (#{$ERROR_INFO})"
79
+ Listen.logger.debug "lstat failed for: #{rel_path} (#{$ERROR_INFO})"
68
80
  raise
69
81
  end
82
+ # rubocop:enable Metrics/MethodLength
83
+ # rubocop:enable Metrics/CyclomaticComplexity
84
+ # rubocop:enable Metrics/PerceivedComplexity
70
85
 
71
86
  def self.inaccurate_mac_time?(stat)
72
87
  # 'mac' means Modified/Accessed/Created
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
63
-
64
- state :initializing, to: :backend_started
66
+ start_state :initializing
65
67
 
66
- state :backend_started, to: [:frontend_ready] do
67
- backend.start
68
- end
68
+ state :initializing, to: [:backend_started, :stopped]
69
69
 
70
- state :frontend_ready, to: [:processing_events] 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
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
- (Dir.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.
@@ -46,6 +50,17 @@ module Listen
46
50
  args = [@relative, @name].compact
47
51
  args.empty? ? nil : ::File.join(*args)
48
52
  end
53
+
54
+ def _entries(dir)
55
+ return Dir.entries(dir) unless RUBY_ENGINE == 'jruby'
56
+
57
+ # JRuby inconsistency workaround, see:
58
+ # https://github.com/jruby/jruby/issues/3840
59
+ exists = ::File.exist?(dir)
60
+ directory = ::File.directory?(dir)
61
+ return Dir.entries(dir) unless exists && !directory
62
+ raise Errno::ENOTDIR, dir
63
+ end
49
64
  end
50
65
  end
51
66
  end