listen 3.1.3 → 3.3.0.pre.2

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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thread'
2
4
 
3
5
  require 'forwardable'
@@ -17,9 +19,8 @@ module Listen
17
19
  end
18
20
  end
19
21
 
20
- def initialize(config, &block)
22
+ def initialize(config)
21
23
  @event_queue = ::Queue.new
22
- @block = block
23
24
  @config = config
24
25
  end
25
26
 
@@ -31,17 +32,15 @@ module Listen
31
32
 
32
33
  dir = _safe_relative_from_cwd(dir)
33
34
  event_queue.public_send(:<<, [type, change, dir, path, options])
34
-
35
- block.call(args) if block
36
35
  end
37
36
 
38
37
  delegate empty?: :event_queue
39
38
  delegate pop: :event_queue
39
+ delegate close: :event_queue
40
40
 
41
41
  private
42
42
 
43
43
  attr_reader :event_queue
44
- attr_reader :block
45
44
  attr_reader :config
46
45
 
47
46
  def _safe_relative_from_cwd(dir)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'digest/md5'
2
4
 
3
5
  module Listen
@@ -6,7 +8,7 @@ module Listen
6
8
  path = Pathname.new(record.root) + rel_path
7
9
  lstat = path.lstat
8
10
 
9
- data = { mtime: lstat.mtime.to_f, mode: lstat.mode }
11
+ data = { mtime: lstat.mtime.to_f, mode: lstat.mode, size: lstat.size }
10
12
 
11
13
  record_data = record.file_data(rel_path)
12
14
 
@@ -25,6 +27,11 @@ module Listen
25
27
  return :modified
26
28
  end
27
29
 
30
+ if data[:size] != record_data[:size]
31
+ record.update_file(rel_path, data)
32
+ return :modified
33
+ end
34
+
28
35
  return if /1|true/ =~ ENV['LISTEN_GEM_DISABLE_HASHING']
29
36
  return unless inaccurate_mac_time?(lstat)
30
37
 
@@ -64,7 +71,7 @@ module Listen
64
71
  record.unset_path(rel_path)
65
72
  :removed
66
73
  rescue
67
- Listen::Logger.debug "lstat failed for: #{rel_path} (#{$ERROR_INFO})"
74
+ Listen.logger.debug "lstat failed for: #{rel_path} (#{$ERROR_INFO})"
68
75
  raise
69
76
  end
70
77
 
@@ -1,120 +1,120 @@
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
+ @mutex.synchronize do
57
+ if !wait_for_states.include?(@state)
58
+ @state_changed.wait(@mutex, timeout)
59
+ end
60
+ wait_for_states.include?(@state)
61
+ end
58
62
  end
59
63
 
60
- # Immediate state transition with no checks, or callbacks. "Dangerous!"
61
- def transition!(state_name)
62
- @state = state_name
63
- end
64
+ private
64
65
 
65
- protected
66
+ def transition(new_state_name)
67
+ new_state_name.is_a?(Symbol) or raise ArgumentError, "state name must be a Symbol (got #{new_state_name.inspect})"
68
+ if (new_state = validate_and_sanitize_new_state(new_state_name))
69
+ transition_with_callbacks!(new_state)
70
+ end
71
+ end
66
72
 
67
- def validate_and_sanitize_new_state(state_name)
68
- state_name = state_name.to_sym
73
+ # Low-level, immediate state transition with no checks or callbacks.
74
+ def transition!(new_state_name)
75
+ new_state_name.is_a?(Symbol) or raise ArgumentError, "state name must be a Symbol (got #{new_state_name.inspect})"
76
+ @fsm_initialized or raise ArgumentError, "FSM not initialized. You must call initialize_fsm from initialize!"
77
+ @mutex.synchronize do
78
+ yield if block_given?
79
+ @state = new_state_name
80
+ @state_changed.broadcast
81
+ end
82
+ end
69
83
 
70
- return if current_state_name == state_name
84
+ def validate_and_sanitize_new_state(new_state_name)
85
+ return nil if @state == new_state_name
71
86
 
72
- if current_state && !current_state.valid_transition?(state_name)
87
+ if current_state && !current_state.valid_transition?(new_state_name)
73
88
  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
89
+ msg = "#{self.class} can't change state from '#{@state}' to '#{new_state_name}', only to: #{valid}"
90
+ raise ArgumentError, msg
77
91
  end
78
92
 
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}"
93
+ unless (new_state = self.class.states[new_state_name])
94
+ new_state_name == self.class.start_state or raise ArgumentError, "invalid state for #{self.class}: #{new_state_name}"
84
95
  end
85
96
 
86
97
  new_state
87
98
  end
88
99
 
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
100
+ def transition_with_callbacks!(new_state)
101
+ transition! new_state.name
102
+ new_state.call(self)
100
103
  end
101
104
 
102
105
  def current_state
103
- states[@state]
104
- end
105
-
106
- def current_state_name
107
- current_state && current_state.name || ''
106
+ self.class.states[@state]
108
107
  end
109
108
 
110
109
  class State
111
110
  attr_reader :name, :transitions
112
111
 
113
- def initialize(name, transitions = nil, &block)
112
+ def initialize(name, transitions, &block)
114
113
  @name = name
115
114
  @block = block
116
- @transitions = nil
117
- @transitions = Array(transitions).map(&:to_sym) if transitions
115
+ @transitions = if transitions
116
+ Array(transitions).map(&:to_sym)
117
+ end
118
118
  end
119
119
 
120
120
  def call(obj)
@@ -122,10 +122,8 @@ module Listen
122
122
  end
123
123
 
124
124
  def valid_transition?(new_state)
125
- # All transitions are allowed unless expressly
126
- return true unless @transitions
127
-
128
- @transitions.include? new_state.to_sym
125
+ # All transitions are allowed if none are expressly declared
126
+ !@transitions || @transitions.include?(new_state.to_sym)
129
127
  end
130
128
  end
131
129
  end
@@ -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.
@@ -38,7 +39,7 @@ module Listen
38
39
  @config = Config.new(options)
39
40
 
40
41
  eq_config = Event::Queue::Config.new(@config.relative?)
41
- queue = Event::Queue.new(eq_config) { @processor.wakeup_on_event }
42
+ queue = Event::Queue.new(eq_config)
42
43
 
43
44
  silencer = Silencer.new
44
45
  rules = @config.silencer_rules
@@ -57,41 +58,42 @@ module Listen
57
58
 
58
59
  @processor = Event::Loop.new(pconfig)
59
60
 
60
- super() # FSM
61
+ initialize_fsm
61
62
  end
62
63
 
63
- default_state :initializing
64
-
65
- state :initializing, to: :backend_started
64
+ start_state :initializing
66
65
 
67
- state :backend_started, to: [:frontend_ready, :stopped] do
68
- backend.start
69
- end
66
+ state :initializing, to: [:backend_started, :stopped]
70
67
 
71
- state :frontend_ready, to: [:processing_events, :stopped] do
72
- processor.setup
68
+ state :backend_started, to: [:processing_events, :stopped] do
69
+ @backend.start
73
70
  end
74
71
 
75
72
  state :processing_events, to: [:paused, :stopped] do
76
- processor.resume
73
+ @processor.start
77
74
  end
78
75
 
79
76
  state :paused, to: [:processing_events, :stopped] do
80
- processor.pause
77
+ @processor.pause
81
78
  end
82
79
 
83
80
  state :stopped, to: [:backend_started] do
84
- backend.stop # should be before processor.teardown to halt events ASAP
85
- processor.teardown
81
+ @backend.stop # halt events ASAP
82
+ @processor.stop
86
83
  end
87
84
 
88
85
  # Starts processing events and starts adapters
89
86
  # or resumes invoking callbacks if paused
90
87
  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
88
+ case state
89
+ when :initializing
90
+ transition :backend_started
91
+ transition :processing_events
92
+ when :paused
93
+ transition :processing_events
94
+ else
95
+ raise ArgumentError, "cannot start from state #{state.inspect}"
96
+ end
95
97
  end
96
98
 
97
99
  # Stops both listening for events and processing them
@@ -113,6 +115,10 @@ module Listen
113
115
  state == :paused
114
116
  end
115
117
 
118
+ def stopped?
119
+ state == :stopped
120
+ end
121
+
116
122
  def ignore(regexps)
117
123
  @silencer_controller.append_ignores(regexps)
118
124
  end
@@ -124,10 +130,5 @@ module Listen
124
130
  def only(regexps)
125
131
  @silencer_controller.replace_with_only(regexps)
126
132
  end
127
-
128
- private
129
-
130
- attr_reader :processor
131
- attr_reader :backend
132
133
  end
133
134
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Listen
2
4
  class Listener
3
5
  class Config
@@ -1,32 +1,35 @@
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)
8
+
9
+ class << self
10
+ attr_writer :logger
9
11
 
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/
16
- ::Logger::DEBUG
17
- when /true|yes|1/i
18
- ::Logger::INFO
19
- else
20
- ::Logger::ERROR
21
- end
12
+ def logger
13
+ @logger ||= default_logger
22
14
  end
23
- end
24
15
 
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
16
+ private
17
+
18
+ def default_logger
19
+ level = case ENV['LISTEN_GEM_DEBUGGING'].to_s
20
+ when /debug|2/i
21
+ ::Logger::DEBUG
22
+ when /info|true|yes|1/i
23
+ ::Logger::INFO
24
+ when /warn/i
25
+ ::Logger::WARN
26
+ when /fatal/i
27
+ ::Logger::FATAL
28
+ else
29
+ ::Logger::ERROR
30
+ end
31
+
32
+ ::Logger.new(STDERR, level: level)
30
33
  end
31
34
  end
32
35
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Listen
2
4
  class Options
3
5
  def initialize(opts, defaults)
@@ -10,7 +12,7 @@ module Listen
10
12
  return if given_options.empty?
11
13
 
12
14
  msg = "Unknown options: #{given_options.inspect}"
13
- Listen::Logger.warn msg
15
+ Listen.logger.warn msg
14
16
  fail msg
15
17
  end
16
18