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.
- checksums.yaml +5 -5
- data/CONTRIBUTING.md +10 -3
- data/README.md +241 -70
- data/bin/listen +3 -4
- data/lib/listen/adapter/base.rb +25 -33
- data/lib/listen/adapter/bsd.rb +9 -8
- data/lib/listen/adapter/config.rb +4 -5
- data/lib/listen/adapter/darwin.rb +35 -46
- data/lib/listen/adapter/linux.rb +16 -13
- data/lib/listen/adapter/polling.rb +9 -6
- data/lib/listen/adapter/windows.rb +19 -22
- data/lib/listen/adapter.rb +25 -25
- data/lib/listen/backend.rb +7 -10
- data/lib/listen/change.rb +18 -27
- data/lib/listen/cli.rb +6 -6
- data/lib/listen/directory.rb +14 -8
- data/lib/listen/error.rb +10 -0
- data/lib/listen/event/config.rb +9 -28
- data/lib/listen/event/loop.rb +44 -67
- data/lib/listen/event/processor.rb +41 -37
- data/lib/listen/event/queue.rb +14 -18
- data/lib/listen/file.rb +16 -3
- data/lib/listen/fsm.rb +74 -72
- data/lib/listen/listener/config.rb +5 -9
- data/lib/listen/listener.rb +26 -22
- data/lib/listen/logger.rb +24 -20
- data/lib/listen/monotonic_time.rb +27 -0
- data/lib/listen/options.rb +11 -8
- data/lib/listen/queue_optimizer.rb +15 -18
- data/lib/listen/record/entry.rb +8 -4
- data/lib/listen/record/symlink_detector.rb +9 -7
- data/lib/listen/record.rb +30 -33
- data/lib/listen/silencer/controller.rb +2 -0
- data/lib/listen/silencer.rb +16 -9
- data/lib/listen/thread.rb +54 -0
- data/lib/listen/version.rb +3 -1
- data/lib/listen.rb +14 -22
- metadata +20 -28
- data/lib/listen/internals/thread_pool.rb +0 -29
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
|
13
|
-
# Passing a state name sets the
|
14
|
-
def
|
15
|
-
if
|
16
|
-
|
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?(@
|
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
|
-
#
|
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(
|
31
|
-
|
32
|
-
|
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
|
-
#
|
47
|
-
def
|
48
|
-
@
|
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
|
-
#
|
49
|
+
# Current state of the FSM, stored as a symbol
|
52
50
|
attr_reader :state
|
53
51
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
61
|
-
def transition!(state_name)
|
62
|
-
@state = state_name
|
63
|
-
end
|
67
|
+
private
|
64
68
|
|
65
|
-
|
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
|
-
|
68
|
-
|
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
|
-
|
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?(
|
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
|
-
|
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[
|
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!(
|
90
|
-
transition!
|
91
|
-
|
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
|
114
|
-
@name
|
115
|
-
@
|
116
|
-
@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
|
125
|
-
|
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
|
-
|
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
|
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
|
data/lib/listen/listener.rb
CHANGED
@@ -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)
|
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
|
-
|
62
|
+
initialize_fsm
|
60
63
|
end
|
64
|
+
# rubocop:enable Metrics/MethodLength
|
61
65
|
|
62
|
-
|
66
|
+
start_state :initializing
|
63
67
|
|
64
68
|
state :initializing, to: [:backend_started, :stopped]
|
65
69
|
|
66
|
-
state :backend_started, to: [:
|
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.
|
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 #
|
84
|
-
processor.
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
3
|
-
@logger ||= nil
|
4
|
-
end
|
4
|
+
@logger = nil
|
5
5
|
|
6
|
-
|
7
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
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
|
data/lib/listen/options.rb
CHANGED
@@ -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.
|
8
|
+
defaults.each_key do |key|
|
7
9
|
@options[key] = given_options.delete(key) || defaults[key]
|
8
10
|
end
|
9
11
|
|
10
|
-
|
12
|
+
given_options.empty? or raise ArgumentError, "Unknown options: #{given_options.inspect}"
|
13
|
+
end
|
11
14
|
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
19
|
-
|
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.
|
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.
|
92
|
-
|
93
|
-
|
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
|
104
|
+
end
|
105
105
|
end
|
106
106
|
|
107
|
-
def
|
107
|
+
def editor_modified?(changes)
|
108
108
|
return unless changes.size == 2
|
109
109
|
|
110
|
-
from_type =
|
111
|
-
to_type =
|
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,
|
116
|
+
from_type, _from_change, _, from, = data
|
117
117
|
when :moved_to
|
118
|
-
to_type,
|
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
|
-
|
129
|
-
|
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
|
data/lib/listen/record/entry.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
-
|
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: #{
|
19
|
+
MORE INFO: #{README_URL}
|
17
20
|
EOS
|
18
21
|
|
19
|
-
|
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)
|
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
|
-
|
35
|
-
|
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
|