listen 0.5.3 → 3.7.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 +7 -0
- data/CHANGELOG.md +1 -186
- data/CONTRIBUTING.md +45 -0
- data/{LICENSE → LICENSE.txt} +3 -1
- data/README.md +332 -181
- data/bin/listen +11 -0
- data/lib/listen/adapter/base.rb +129 -0
- data/lib/listen/adapter/bsd.rb +107 -0
- data/lib/listen/adapter/config.rb +25 -0
- data/lib/listen/adapter/darwin.rb +77 -0
- data/lib/listen/adapter/linux.rb +108 -0
- data/lib/listen/adapter/polling.rb +40 -0
- data/lib/listen/adapter/windows.rb +96 -0
- data/lib/listen/adapter.rb +32 -201
- data/lib/listen/backend.rb +40 -0
- data/lib/listen/change.rb +69 -0
- data/lib/listen/cli.rb +65 -0
- data/lib/listen/directory.rb +93 -0
- data/lib/listen/error.rb +11 -0
- data/lib/listen/event/config.rb +40 -0
- data/lib/listen/event/loop.rb +94 -0
- data/lib/listen/event/processor.rb +126 -0
- data/lib/listen/event/queue.rb +54 -0
- data/lib/listen/file.rb +95 -0
- data/lib/listen/fsm.rb +133 -0
- data/lib/listen/listener/config.rb +41 -0
- data/lib/listen/listener.rb +93 -160
- data/lib/listen/logger.rb +36 -0
- data/lib/listen/monotonic_time.rb +27 -0
- data/lib/listen/options.rb +26 -0
- data/lib/listen/queue_optimizer.rb +129 -0
- data/lib/listen/record/entry.rb +66 -0
- data/lib/listen/record/symlink_detector.rb +41 -0
- data/lib/listen/record.rb +123 -0
- data/lib/listen/silencer/controller.rb +50 -0
- data/lib/listen/silencer.rb +106 -0
- data/lib/listen/thread.rb +54 -0
- data/lib/listen/version.rb +3 -1
- data/lib/listen.rb +40 -32
- metadata +87 -38
- data/lib/listen/adapters/darwin.rb +0 -85
- data/lib/listen/adapters/linux.rb +0 -113
- data/lib/listen/adapters/polling.rb +0 -67
- data/lib/listen/adapters/windows.rb +0 -87
- data/lib/listen/dependency_manager.rb +0 -126
- data/lib/listen/directory_record.rb +0 -344
- data/lib/listen/multi_listener.rb +0 -121
- data/lib/listen/turnstile.rb +0 -28
data/lib/listen/listener.rb
CHANGED
@@ -1,203 +1,136 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'English'
|
4
|
+
|
5
|
+
require 'listen/version'
|
6
|
+
|
7
|
+
require 'listen/backend'
|
8
|
+
|
9
|
+
require 'listen/silencer'
|
10
|
+
require 'listen/silencer/controller'
|
11
|
+
|
12
|
+
require 'listen/queue_optimizer'
|
13
|
+
|
14
|
+
require 'listen/fsm'
|
15
|
+
|
16
|
+
require 'listen/event/loop'
|
17
|
+
require 'listen/event/queue'
|
18
|
+
require 'listen/event/config'
|
19
|
+
|
20
|
+
require 'listen/listener/config'
|
2
21
|
|
3
22
|
module Listen
|
4
23
|
class Listener
|
5
|
-
|
24
|
+
include Listen::FSM
|
6
25
|
|
7
|
-
#
|
8
|
-
DEFAULT_TO_RELATIVE_PATHS = false
|
9
|
-
|
10
|
-
# Initializes the directory listener.
|
26
|
+
# Initializes the directories listener.
|
11
27
|
#
|
12
|
-
# @param [String] directory the
|
13
|
-
# @param [Hash] options the listen options
|
14
|
-
# @option options [Regexp] ignore a pattern for ignoring paths
|
15
|
-
# @option options [Regexp] filter a pattern for filtering paths
|
16
|
-
# @option options [Float] latency the delay between checking for changes in seconds
|
17
|
-
# @option options [Boolean] relative_paths whether or not to use relative-paths in the callback
|
18
|
-
# @option options [Boolean] force_polling whether to force the polling adapter or not
|
19
|
-
# @option options [String, Boolean] polling_fallback_message to change polling fallback message or remove it
|
28
|
+
# @param [String] directory the directories to listen to
|
29
|
+
# @param [Hash] options the listen options (see Listen::Listener::Options)
|
20
30
|
#
|
21
31
|
# @yield [modified, added, removed] the changed files
|
22
32
|
# @yieldparam [Array<String>] modified the list of modified files
|
23
33
|
# @yieldparam [Array<String>] added the list of added files
|
24
34
|
# @yieldparam [Array<String>] removed the list of removed files
|
25
35
|
#
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
@directory_record = DirectoryRecord.new(@directory)
|
30
|
-
@use_relative_paths = DEFAULT_TO_RELATIVE_PATHS
|
36
|
+
# rubocop:disable Metrics/MethodLength
|
37
|
+
def initialize(*dirs, &block)
|
38
|
+
options = dirs.last.is_a?(Hash) ? dirs.pop : {}
|
31
39
|
|
32
|
-
@
|
33
|
-
@directory_record.ignore(*options.delete(:ignore)) if options[:ignore]
|
34
|
-
@directory_record.filter(*options.delete(:filter)) if options[:filter]
|
40
|
+
@config = Config.new(options)
|
35
41
|
|
36
|
-
|
37
|
-
|
42
|
+
eq_config = Event::Queue::Config.new(@config.relative?)
|
43
|
+
queue = Event::Queue.new(eq_config)
|
38
44
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
#
|
43
|
-
# @param [Boolean] blocking whether or not to block the current thread after starting
|
44
|
-
#
|
45
|
-
def start(blocking = true)
|
46
|
-
t = Thread.new { @directory_record.build }
|
47
|
-
@adapter = initialize_adapter
|
48
|
-
t.join
|
49
|
-
@adapter.start(blocking)
|
50
|
-
end
|
45
|
+
silencer = Silencer.new
|
46
|
+
rules = @config.silencer_rules
|
47
|
+
@silencer_controller = Silencer::Controller.new(silencer, rules)
|
51
48
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
49
|
+
@backend = Backend.new(dirs, queue, silencer, @config)
|
50
|
+
|
51
|
+
optimizer_config = QueueOptimizer::Config.new(@backend, silencer)
|
52
|
+
|
53
|
+
pconfig = Event::Config.new(
|
54
|
+
self,
|
55
|
+
queue,
|
56
|
+
QueueOptimizer.new(optimizer_config),
|
57
|
+
@backend.min_delay_between_events,
|
58
|
+
&block)
|
59
|
+
|
60
|
+
@processor = Event::Loop.new(pconfig)
|
61
|
+
|
62
|
+
initialize_fsm
|
56
63
|
end
|
64
|
+
# rubocop:enable Metrics/MethodLength
|
57
65
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
@
|
64
|
-
self
|
66
|
+
start_state :initializing
|
67
|
+
|
68
|
+
state :initializing, to: [:backend_started, :stopped]
|
69
|
+
|
70
|
+
state :backend_started, to: [:processing_events, :stopped] do
|
71
|
+
@backend.start
|
65
72
|
end
|
66
73
|
|
67
|
-
|
68
|
-
|
69
|
-
# @return [Listen::Listener] the listener
|
70
|
-
#
|
71
|
-
def unpause
|
72
|
-
@directory_record.build
|
73
|
-
@adapter.paused = false
|
74
|
-
self
|
74
|
+
state :processing_events, to: [:paused, :stopped] do
|
75
|
+
@processor.start
|
75
76
|
end
|
76
77
|
|
77
|
-
|
78
|
-
|
79
|
-
# @return [Boolean] adapter paused status
|
80
|
-
#
|
81
|
-
def paused?
|
82
|
-
!!@adapter && @adapter.paused == true
|
78
|
+
state :paused, to: [:processing_events, :stopped] do
|
79
|
+
@processor.pause
|
83
80
|
end
|
84
81
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
#
|
89
|
-
# @return [Listen::Listener] the listener
|
90
|
-
#
|
91
|
-
def ignore(*regexps)
|
92
|
-
@directory_record.ignore(*regexps)
|
93
|
-
self
|
82
|
+
state :stopped, to: [:backend_started] do
|
83
|
+
@backend.stop # halt events ASAP
|
84
|
+
@processor.stop
|
94
85
|
end
|
95
86
|
|
96
|
-
#
|
97
|
-
#
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
87
|
+
# Starts processing events and starts adapters
|
88
|
+
# or resumes invoking callbacks if paused
|
89
|
+
def start
|
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
|
105
99
|
end
|
106
100
|
|
107
|
-
#
|
108
|
-
|
109
|
-
|
110
|
-
# @example Wait 0.5 seconds each time before checking changes
|
111
|
-
# latency 0.5
|
112
|
-
#
|
113
|
-
# @param [Float] seconds the amount of delay, in seconds
|
114
|
-
#
|
115
|
-
# @return [Listen::Listener] the listener
|
116
|
-
#
|
117
|
-
def latency(seconds)
|
118
|
-
@adapter_options[:latency] = seconds
|
119
|
-
self
|
101
|
+
# Stops both listening for events and processing them
|
102
|
+
def stop
|
103
|
+
transition :stopped
|
120
104
|
end
|
121
105
|
|
122
|
-
#
|
123
|
-
|
124
|
-
|
125
|
-
# @example Forcing the use of the polling adapter
|
126
|
-
# force_polling true
|
127
|
-
#
|
128
|
-
# @param [Boolean] value whether to force the polling adapter or not
|
129
|
-
#
|
130
|
-
# @return [Listen::Listener] the listener
|
131
|
-
#
|
132
|
-
def force_polling(value)
|
133
|
-
@adapter_options[:force_polling] = value
|
134
|
-
self
|
106
|
+
# Stops invoking callbacks (messages pile up)
|
107
|
+
def pause
|
108
|
+
transition :paused
|
135
109
|
end
|
136
110
|
|
137
|
-
#
|
138
|
-
|
139
|
-
|
140
|
-
# @example Enabling relative paths in the callback
|
141
|
-
# relative_paths true
|
142
|
-
#
|
143
|
-
# @param [Boolean] value whether to enable relative paths in the callback or not
|
144
|
-
#
|
145
|
-
# @return [Listen::Listener] the listener
|
146
|
-
#
|
147
|
-
def relative_paths(value)
|
148
|
-
@use_relative_paths = value
|
149
|
-
self
|
111
|
+
# processing means callbacks are called
|
112
|
+
def processing?
|
113
|
+
state == :processing_events
|
150
114
|
end
|
151
115
|
|
152
|
-
|
153
|
-
|
154
|
-
# @example Disabling the polling fallback message
|
155
|
-
# polling_fallback_message false
|
156
|
-
#
|
157
|
-
# @param [String, Boolean] value to change polling fallback message or remove it
|
158
|
-
#
|
159
|
-
# @return [Listen::Listener] the listener
|
160
|
-
#
|
161
|
-
def polling_fallback_message(value)
|
162
|
-
@adapter_options[:polling_fallback_message] = value
|
163
|
-
self
|
116
|
+
def paused?
|
117
|
+
state == :paused
|
164
118
|
end
|
165
119
|
|
166
|
-
|
167
|
-
|
168
|
-
# @example Assign a callback to be called on changes
|
169
|
-
# callback = lambda { |modified, added, removed| ... }
|
170
|
-
# change &callback
|
171
|
-
#
|
172
|
-
# @param [Proc] block the callback proc
|
173
|
-
#
|
174
|
-
# @return [Listen::Listener] the listener
|
175
|
-
#
|
176
|
-
def change(&block) # modified, added, removed
|
177
|
-
@block = block
|
178
|
-
self
|
120
|
+
def stopped?
|
121
|
+
state == :stopped
|
179
122
|
end
|
180
123
|
|
181
|
-
|
182
|
-
|
183
|
-
# @param (see Listen::DirectoryRecord#fetch_changes)
|
184
|
-
#
|
185
|
-
def on_change(directories, options = {})
|
186
|
-
changes = @directory_record.fetch_changes(directories, options.merge(
|
187
|
-
:relative_paths => @use_relative_paths
|
188
|
-
))
|
189
|
-
unless changes.values.all? { |paths| paths.empty? }
|
190
|
-
@block.call(changes[:modified],changes[:added],changes[:removed])
|
191
|
-
end
|
124
|
+
def ignore(regexps)
|
125
|
+
@silencer_controller.append_ignores(regexps)
|
192
126
|
end
|
193
127
|
|
194
|
-
|
128
|
+
def ignore!(regexps)
|
129
|
+
@silencer_controller.replace_with_bang_ignores(regexps)
|
130
|
+
end
|
195
131
|
|
196
|
-
|
197
|
-
|
198
|
-
def initialize_adapter
|
199
|
-
callback = lambda { |changed_dirs, options| self.on_change(changed_dirs, options) }
|
200
|
-
Adapter.select_and_initialize(@directory, @adapter_options, &callback)
|
132
|
+
def only(regexps)
|
133
|
+
@silencer_controller.replace_with_only(regexps)
|
201
134
|
end
|
202
135
|
end
|
203
136
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Listen
|
4
|
+
@logger = nil
|
5
|
+
|
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
|
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
|
22
|
+
::Logger::DEBUG
|
23
|
+
when /info|true|yes|1/i
|
24
|
+
::Logger::INFO
|
25
|
+
when /warn/i
|
26
|
+
::Logger::WARN
|
27
|
+
when /fatal/i
|
28
|
+
::Logger::FATAL
|
29
|
+
else
|
30
|
+
::Logger::ERROR
|
31
|
+
end
|
32
|
+
|
33
|
+
::Logger.new(STDERR, level: level)
|
34
|
+
end
|
35
|
+
end
|
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
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Listen
|
4
|
+
class Options
|
5
|
+
def initialize(opts, defaults)
|
6
|
+
@options = {}
|
7
|
+
given_options = opts.dup
|
8
|
+
defaults.each_key do |key|
|
9
|
+
@options[key] = given_options.delete(key) || defaults[key]
|
10
|
+
end
|
11
|
+
|
12
|
+
given_options.empty? or raise ArgumentError, "Unknown options: #{given_options.inspect}"
|
13
|
+
end
|
14
|
+
|
15
|
+
# rubocop:disable Lint/MissingSuper
|
16
|
+
def respond_to_missing?(name, *_)
|
17
|
+
@options.has_key?(name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def method_missing(name, *_)
|
21
|
+
respond_to_missing?(name) or raise NameError, "Bad option: #{name.inspect} (valid:#{@options.keys.inspect})"
|
22
|
+
@options[name]
|
23
|
+
end
|
24
|
+
# rubocop:enable Lint/MissingSuper
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Listen
|
4
|
+
class QueueOptimizer
|
5
|
+
class Config
|
6
|
+
def initialize(adapter_class, silencer)
|
7
|
+
@adapter_class = adapter_class
|
8
|
+
@silencer = silencer
|
9
|
+
end
|
10
|
+
|
11
|
+
def exist?(path)
|
12
|
+
Pathname(path).exist?
|
13
|
+
end
|
14
|
+
|
15
|
+
def silenced?(path, type)
|
16
|
+
@silencer.silenced?(path, type)
|
17
|
+
end
|
18
|
+
|
19
|
+
def debug(*args, &block)
|
20
|
+
Listen.logger.debug(*args, &block)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def smoosh_changes(changes)
|
25
|
+
# TODO: adapter could be nil at this point (shutdown)
|
26
|
+
cookies = changes.group_by do |_, _, _, _, options|
|
27
|
+
(options || {})[:cookie]
|
28
|
+
end
|
29
|
+
_squash_changes(_reinterpret_related_changes(cookies))
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(config)
|
33
|
+
@config = config
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_reader :config
|
39
|
+
|
40
|
+
# groups changes into the expected structure expected by
|
41
|
+
# clients
|
42
|
+
def _squash_changes(changes)
|
43
|
+
# We combine here for backward compatibility
|
44
|
+
# Newer clients should receive dir and path separately
|
45
|
+
changes = changes.map { |change, dir, path| [change, dir + path] }
|
46
|
+
|
47
|
+
actions = changes.group_by(&:last).map do |path, action_list|
|
48
|
+
[_logical_action_for(path, action_list.map(&:first)), path.to_s]
|
49
|
+
end
|
50
|
+
|
51
|
+
config.debug("listen: raw changes: #{actions.inspect}")
|
52
|
+
|
53
|
+
{ modified: [], added: [], removed: [] }.tap do |squashed|
|
54
|
+
actions.each do |type, path|
|
55
|
+
squashed[type] << path unless type.nil?
|
56
|
+
end
|
57
|
+
config.debug("listen: final changes: #{squashed.inspect}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def _logical_action_for(path, actions)
|
62
|
+
actions << :added if actions.delete(:moved_to)
|
63
|
+
actions << :removed if actions.delete(:moved_from)
|
64
|
+
|
65
|
+
modified = actions.find { |x| x == :modified }
|
66
|
+
_calculate_add_remove_difference(actions, path, modified)
|
67
|
+
end
|
68
|
+
|
69
|
+
def _calculate_add_remove_difference(actions, path, default_if_exists)
|
70
|
+
added = actions.count { |x| x == :added }
|
71
|
+
removed = actions.count { |x| x == :removed }
|
72
|
+
diff = added - removed
|
73
|
+
|
74
|
+
# TODO: avoid checking if path exists and instead assume the events are
|
75
|
+
# in order (if last is :removed, it doesn't exist, etc.)
|
76
|
+
if config.exist?(path)
|
77
|
+
if diff > 0
|
78
|
+
:added
|
79
|
+
elsif diff.zero? && added > 0
|
80
|
+
:modified
|
81
|
+
else
|
82
|
+
default_if_exists
|
83
|
+
end
|
84
|
+
else
|
85
|
+
diff < 0 ? :removed : nil
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# remove extraneous rb-inotify events, keeping them only if it's a possible
|
90
|
+
# editor rename() call (e.g. Kate and Sublime)
|
91
|
+
def _reinterpret_related_changes(cookies)
|
92
|
+
table = { moved_to: :added, moved_from: :removed }
|
93
|
+
cookies.flat_map do |_, changes|
|
94
|
+
if (editor_modified = editor_modified?(changes))
|
95
|
+
[[:modified, *editor_modified]]
|
96
|
+
else
|
97
|
+
not_silenced = changes.reject do |type, _, _, path, _|
|
98
|
+
config.silenced?(Pathname(path), type)
|
99
|
+
end
|
100
|
+
not_silenced.map do |_, change, dir, path, _|
|
101
|
+
[table.fetch(change, change), dir, path]
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def editor_modified?(changes)
|
108
|
+
return unless changes.size == 2
|
109
|
+
|
110
|
+
from_type = from = nil
|
111
|
+
to_type = to_dir = to = nil
|
112
|
+
|
113
|
+
changes.each do |data|
|
114
|
+
case data[1]
|
115
|
+
when :moved_from
|
116
|
+
from_type, _from_change, _, from, = data
|
117
|
+
when :moved_to
|
118
|
+
to_type, _to_change, to_dir, to, = data
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Expect an ignored moved_from and non-ignored moved_to
|
123
|
+
# to qualify as an "editor modify"
|
124
|
+
if from && to && config.silenced?(Pathname(from), from_type) && !config.silenced?(Pathname(to), to_type)
|
125
|
+
[to_dir, to]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Listen
|
4
|
+
# @private api
|
5
|
+
class Record
|
6
|
+
# Represents a directory entry (dir or file)
|
7
|
+
class Entry
|
8
|
+
# file: "/home/me/watched_dir", "app/models", "foo.rb"
|
9
|
+
# dir, "/home/me/watched_dir", "."
|
10
|
+
def initialize(root, relative, name = nil)
|
11
|
+
@root = root
|
12
|
+
@relative = relative
|
13
|
+
@name = name
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :root, :relative, :name
|
17
|
+
|
18
|
+
def children
|
19
|
+
child_relative = _join
|
20
|
+
(_entries(sys_path) - %w[. ..]).map do |name|
|
21
|
+
Entry.new(@root, child_relative, name)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def meta
|
26
|
+
lstat = ::File.lstat(sys_path)
|
27
|
+
{ mtime: lstat.mtime.to_f, mode: lstat.mode, size: lstat.size }
|
28
|
+
end
|
29
|
+
|
30
|
+
# record hash is e.g.
|
31
|
+
# if @record["/home/me/watched_dir"]["project/app/models"]["foo.rb"]
|
32
|
+
# if @record["/home/me/watched_dir"]["project/app"]["models"]
|
33
|
+
# record_dir_key is "project/app/models"
|
34
|
+
def record_dir_key
|
35
|
+
::File.join(*[@relative, @name].compact)
|
36
|
+
end
|
37
|
+
|
38
|
+
def sys_path
|
39
|
+
# Use full path in case someone uses chdir
|
40
|
+
::File.join(*[@root, @relative, @name].compact)
|
41
|
+
end
|
42
|
+
|
43
|
+
def real_path
|
44
|
+
@real_path ||= ::File.realpath(sys_path)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def _join
|
50
|
+
args = [@relative, @name].compact
|
51
|
+
args.empty? ? nil : ::File.join(*args)
|
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
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
require 'listen/error'
|
5
|
+
|
6
|
+
module Listen
|
7
|
+
# @private api
|
8
|
+
class Record
|
9
|
+
class SymlinkDetector
|
10
|
+
README_URL = 'https://github.com/guard/listen/blob/master/README.md'
|
11
|
+
|
12
|
+
SYMLINK_LOOP_ERROR = <<-EOS
|
13
|
+
** ERROR: directory is already being watched! **
|
14
|
+
|
15
|
+
Directory: %s
|
16
|
+
|
17
|
+
is already being watched through: %s
|
18
|
+
|
19
|
+
MORE INFO: #{README_URL}
|
20
|
+
EOS
|
21
|
+
|
22
|
+
Error = ::Listen::Error # for backward compatibility
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@real_dirs = Set.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def verify_unwatched!(entry)
|
29
|
+
real_path = entry.real_path
|
30
|
+
@real_dirs.add?(real_path) or _fail(entry.sys_path, real_path)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def _fail(symlinked, real_path)
|
36
|
+
warn(format(SYMLINK_LOOP_ERROR, symlinked, real_path))
|
37
|
+
raise ::Listen::Error::SymlinkLoop, 'Failed due to looped symlinks'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|