sass-listen 3.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1 -0
- data/CONTRIBUTING.md +38 -0
- data/LICENSE.txt +22 -0
- data/README.md +297 -0
- data/bin/listen +12 -0
- data/lib/listen.rb +55 -0
- data/lib/listen/adapter.rb +43 -0
- data/lib/listen/adapter/base.rb +137 -0
- data/lib/listen/adapter/bsd.rb +106 -0
- data/lib/listen/adapter/config.rb +26 -0
- data/lib/listen/adapter/darwin.rb +71 -0
- data/lib/listen/adapter/linux.rb +106 -0
- data/lib/listen/adapter/polling.rb +37 -0
- data/lib/listen/adapter/windows.rb +99 -0
- data/lib/listen/backend.rb +41 -0
- data/lib/listen/change.rb +78 -0
- data/lib/listen/cli.rb +65 -0
- data/lib/listen/directory.rb +76 -0
- data/lib/listen/event/config.rb +59 -0
- data/lib/listen/event/loop.rb +117 -0
- data/lib/listen/event/processor.rb +122 -0
- data/lib/listen/event/queue.rb +56 -0
- data/lib/listen/file.rb +80 -0
- data/lib/listen/fsm.rb +131 -0
- data/lib/listen/internals/thread_pool.rb +21 -0
- data/lib/listen/listener.rb +132 -0
- data/lib/listen/listener/config.rb +45 -0
- data/lib/listen/logger.rb +32 -0
- data/lib/listen/options.rb +23 -0
- data/lib/listen/queue_optimizer.rb +132 -0
- data/lib/listen/record.rb +120 -0
- data/lib/listen/record/entry.rb +51 -0
- data/lib/listen/record/symlink_detector.rb +39 -0
- data/lib/listen/silencer.rb +97 -0
- data/lib/listen/silencer/controller.rb +48 -0
- data/lib/listen/version.rb +3 -0
- metadata +124 -0
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'listen/options'
|
2
|
+
require 'listen/record'
|
3
|
+
require 'listen/change'
|
4
|
+
|
5
|
+
module Listen
|
6
|
+
module Adapter
|
7
|
+
class Base
|
8
|
+
attr_reader :options
|
9
|
+
|
10
|
+
# TODO: only used by tests
|
11
|
+
DEFAULTS = {}
|
12
|
+
|
13
|
+
attr_reader :config
|
14
|
+
|
15
|
+
def initialize(config)
|
16
|
+
@started = false
|
17
|
+
@config = config
|
18
|
+
|
19
|
+
@configured = nil
|
20
|
+
|
21
|
+
fail 'No directories to watch!' if config.directories.empty?
|
22
|
+
|
23
|
+
defaults = self.class.const_get('DEFAULTS')
|
24
|
+
@options = Listen::Options.new(config.adapter_options, defaults)
|
25
|
+
rescue
|
26
|
+
_log_exception 'adapter config failed: %s:%s called from: %s', caller
|
27
|
+
raise
|
28
|
+
end
|
29
|
+
|
30
|
+
# TODO: it's a separate method as a temporary workaround for tests
|
31
|
+
def configure
|
32
|
+
if @configured
|
33
|
+
_log(:warn, 'Adapter already configured!')
|
34
|
+
return
|
35
|
+
end
|
36
|
+
|
37
|
+
@configured = true
|
38
|
+
|
39
|
+
@callbacks ||= {}
|
40
|
+
config.directories.each do |dir|
|
41
|
+
callback = @callbacks[dir] || lambda do |event|
|
42
|
+
_process_event(dir, event)
|
43
|
+
end
|
44
|
+
@callbacks[dir] = callback
|
45
|
+
_configure(dir, &callback)
|
46
|
+
end
|
47
|
+
|
48
|
+
@snapshots ||= {}
|
49
|
+
# TODO: separate config per directory (some day maybe)
|
50
|
+
change_config = Change::Config.new(config.queue, config.silencer)
|
51
|
+
config.directories.each do |dir|
|
52
|
+
record = Record.new(dir)
|
53
|
+
snapshot = Change.new(change_config, record)
|
54
|
+
@snapshots[dir] = snapshot
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def started?
|
59
|
+
@started
|
60
|
+
end
|
61
|
+
|
62
|
+
def start
|
63
|
+
configure
|
64
|
+
|
65
|
+
if started?
|
66
|
+
_log(:warn, 'Adapter already started!')
|
67
|
+
return
|
68
|
+
end
|
69
|
+
|
70
|
+
@started = true
|
71
|
+
|
72
|
+
calling_stack = caller.dup
|
73
|
+
Listen::Internals::ThreadPool.add do
|
74
|
+
begin
|
75
|
+
@snapshots.values.each do |snapshot|
|
76
|
+
_timed('Record.build()') { snapshot.record.build }
|
77
|
+
end
|
78
|
+
_run
|
79
|
+
rescue
|
80
|
+
msg = 'run() in thread failed: %s:\n'\
|
81
|
+
' %s\n\ncalled from:\n %s'
|
82
|
+
_log_exception(msg, calling_stack)
|
83
|
+
raise # for unit tests mostly
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def stop
|
89
|
+
_stop
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.usable?
|
93
|
+
const_get('OS_REGEXP') =~ RbConfig::CONFIG['target_os']
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def _stop
|
99
|
+
end
|
100
|
+
|
101
|
+
def _timed(title)
|
102
|
+
start = Time.now.to_f
|
103
|
+
yield
|
104
|
+
diff = Time.now.to_f - start
|
105
|
+
Listen::Logger.info format('%s: %.05f seconds', title, diff)
|
106
|
+
rescue
|
107
|
+
Listen::Logger.warn "#{title} crashed: #{$ERROR_INFO.inspect}"
|
108
|
+
raise
|
109
|
+
end
|
110
|
+
|
111
|
+
# TODO: allow backend adapters to pass specific invalidation objects
|
112
|
+
# e.g. Darwin -> DirRescan, INotify -> MoveScan, etc.
|
113
|
+
def _queue_change(type, dir, rel_path, options)
|
114
|
+
@snapshots[dir].invalidate(type, rel_path, options)
|
115
|
+
end
|
116
|
+
|
117
|
+
def _log(*args, &block)
|
118
|
+
self.class.send(:_log, *args, &block)
|
119
|
+
end
|
120
|
+
|
121
|
+
def _log_exception(msg, caller_stack)
|
122
|
+
formatted = format(
|
123
|
+
msg,
|
124
|
+
$ERROR_INFO,
|
125
|
+
$ERROR_POSITION * "\n",
|
126
|
+
caller_stack * "\n"
|
127
|
+
)
|
128
|
+
|
129
|
+
_log(:error, formatted)
|
130
|
+
end
|
131
|
+
|
132
|
+
def self._log(*args, &block)
|
133
|
+
Listen::Logger.send(*args, &block)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# Listener implementation for BSD's `kqueue`.
|
2
|
+
# @see http://www.freebsd.org/cgi/man.cgi?query=kqueue
|
3
|
+
# @see https://github.com/mat813/rb-kqueue/blob/master/lib/rb-kqueue/queue.rb
|
4
|
+
#
|
5
|
+
module Listen
|
6
|
+
module Adapter
|
7
|
+
class BSD < Base
|
8
|
+
OS_REGEXP = /bsd|dragonfly/i
|
9
|
+
|
10
|
+
DEFAULTS = {
|
11
|
+
events: [
|
12
|
+
:delete,
|
13
|
+
:write,
|
14
|
+
:extend,
|
15
|
+
:attrib,
|
16
|
+
:rename
|
17
|
+
# :link, :revoke
|
18
|
+
]
|
19
|
+
}
|
20
|
+
|
21
|
+
BUNDLER_DECLARE_GEM = <<-EOS.gsub(/^ {6}/, '')
|
22
|
+
Please add the following to your Gemfile to avoid polling for changes:
|
23
|
+
require 'rbconfig'
|
24
|
+
if RbConfig::CONFIG['target_os'] =~ /#{OS_REGEXP}/
|
25
|
+
gem 'rb-kqueue', '>= 0.2'
|
26
|
+
end
|
27
|
+
EOS
|
28
|
+
|
29
|
+
def self.usable?
|
30
|
+
return false unless super
|
31
|
+
require 'rb-kqueue'
|
32
|
+
require 'find'
|
33
|
+
true
|
34
|
+
rescue LoadError
|
35
|
+
Kernel.warn BUNDLER_DECLARE_GEM
|
36
|
+
false
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def _configure(directory, &_callback)
|
42
|
+
@worker ||= KQueue::Queue.new
|
43
|
+
@callback = _callback
|
44
|
+
# use Record to make a snapshot of dir, so we
|
45
|
+
# can detect new files
|
46
|
+
_find(directory.to_s) { |path| _watch_file(path, @worker) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def _run
|
50
|
+
@worker.run
|
51
|
+
end
|
52
|
+
|
53
|
+
def _process_event(dir, event)
|
54
|
+
full_path = _event_path(event)
|
55
|
+
if full_path.directory?
|
56
|
+
# Force dir content tracking to kick in, or we won't have
|
57
|
+
# names of added files
|
58
|
+
_queue_change(:dir, dir, '.', recursive: true)
|
59
|
+
elsif full_path.exist?
|
60
|
+
path = full_path.relative_path_from(dir)
|
61
|
+
_queue_change(:file, dir, path.to_s, change: _change(event.flags))
|
62
|
+
end
|
63
|
+
|
64
|
+
# If it is a directory, and it has a write flag, it means a
|
65
|
+
# file has been added so find out which and deal with it.
|
66
|
+
# No need to check for removed files, kqueue will forget them
|
67
|
+
# when the vfs does.
|
68
|
+
_watch_for_new_file(event) if full_path.directory?
|
69
|
+
end
|
70
|
+
|
71
|
+
def _change(event_flags)
|
72
|
+
{ modified: [:attrib, :extend],
|
73
|
+
added: [:write],
|
74
|
+
removed: [:rename, :delete]
|
75
|
+
}.each do |change, flags|
|
76
|
+
return change unless (flags & event_flags).empty?
|
77
|
+
end
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
|
81
|
+
def _event_path(event)
|
82
|
+
Pathname.new(event.watcher.path)
|
83
|
+
end
|
84
|
+
|
85
|
+
def _watch_for_new_file(event)
|
86
|
+
queue = event.watcher.queue
|
87
|
+
_find(_event_path(event).to_s) do |file_path|
|
88
|
+
unless queue.watchers.detect { |_, v| v.path == file_path.to_s }
|
89
|
+
_watch_file(file_path, queue)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def _watch_file(path, queue)
|
95
|
+
queue.watch_file(path, *options.events, &@callback)
|
96
|
+
rescue Errno::ENOENT => e
|
97
|
+
_log :warn, "kqueue: watch file failed: #{e.message}"
|
98
|
+
end
|
99
|
+
|
100
|
+
# Quick rubocop workaround
|
101
|
+
def _find(*paths, &block)
|
102
|
+
Find.send(:find, *paths, &block)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Listen
|
4
|
+
module Adapter
|
5
|
+
class Config
|
6
|
+
attr_reader :directories
|
7
|
+
attr_reader :silencer
|
8
|
+
attr_reader :queue
|
9
|
+
attr_reader :adapter_options
|
10
|
+
|
11
|
+
def initialize(directories, queue, silencer, adapter_options)
|
12
|
+
# Default to current directory if no directories are supplied
|
13
|
+
directories = [Dir.pwd] if directories.to_a.empty?
|
14
|
+
|
15
|
+
# TODO: fix (flatten, array, compact?)
|
16
|
+
@directories = directories.map do |directory|
|
17
|
+
Pathname.new(directory.to_s).realpath
|
18
|
+
end
|
19
|
+
|
20
|
+
@silencer = silencer
|
21
|
+
@queue = queue
|
22
|
+
@adapter_options = adapter_options
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'listen/internals/thread_pool'
|
3
|
+
|
4
|
+
module Listen
|
5
|
+
module Adapter
|
6
|
+
# Adapter implementation for Mac OS X `FSEvents`.
|
7
|
+
#
|
8
|
+
class Darwin < Base
|
9
|
+
OS_REGEXP = /darwin(1.+)?$/i
|
10
|
+
|
11
|
+
# The default delay between checking for changes.
|
12
|
+
DEFAULTS = { latency: 0.1 }
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
# NOTE: each directory gets a DIFFERENT callback!
|
17
|
+
def _configure(dir, &callback)
|
18
|
+
require 'rb-fsevent'
|
19
|
+
|
20
|
+
opts = { latency: options.latency }
|
21
|
+
|
22
|
+
@workers ||= ::Queue.new
|
23
|
+
@workers << FSEvent.new.tap do |worker|
|
24
|
+
_log :debug, "fsevent: watching: #{dir.to_s.inspect}"
|
25
|
+
worker.watch(dir.to_s, opts, &callback)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def _run
|
30
|
+
first = @workers.pop
|
31
|
+
|
32
|
+
# NOTE: _run is called within a thread, so run every other
|
33
|
+
# worker in it's own thread
|
34
|
+
_run_workers_in_background(_to_array(@workers))
|
35
|
+
_run_worker(first)
|
36
|
+
end
|
37
|
+
|
38
|
+
def _process_event(dir, event)
|
39
|
+
_log :debug, "fsevent: processing event: #{event.inspect}"
|
40
|
+
event.each do |path|
|
41
|
+
new_path = Pathname.new(path.sub(/\/$/, ''))
|
42
|
+
_log :debug, "fsevent: #{new_path}"
|
43
|
+
# TODO: does this preserve symlinks?
|
44
|
+
rel_path = new_path.relative_path_from(dir).to_s
|
45
|
+
_queue_change(:dir, dir, rel_path, recursive: true)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def _run_worker(worker)
|
50
|
+
_log :debug, "fsevent: running worker: #{worker.inspect}"
|
51
|
+
worker.run
|
52
|
+
rescue
|
53
|
+
_log_exception 'fsevent: running worker failed: %s:%s called from: %s', caller
|
54
|
+
end
|
55
|
+
|
56
|
+
def _run_workers_in_background(workers)
|
57
|
+
workers.each do |worker|
|
58
|
+
# NOTE: while passing local variables to the block below is not
|
59
|
+
# thread safe, using 'worker' from the enumerator above is ok
|
60
|
+
Listen::Internals::ThreadPool.add { _run_worker(worker) }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def _to_array(queue)
|
65
|
+
workers = []
|
66
|
+
workers << queue.pop until queue.empty?
|
67
|
+
workers
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Listen
|
2
|
+
module Adapter
|
3
|
+
# @see https://github.com/nex3/rb-inotify
|
4
|
+
class Linux < Base
|
5
|
+
OS_REGEXP = /linux/i
|
6
|
+
|
7
|
+
DEFAULTS = {
|
8
|
+
events: [
|
9
|
+
:recursive,
|
10
|
+
:attrib,
|
11
|
+
:create,
|
12
|
+
:delete,
|
13
|
+
:move,
|
14
|
+
:close_write
|
15
|
+
],
|
16
|
+
wait_for_delay: 0.1
|
17
|
+
}
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
WIKI_URL = 'https://github.com/guard/listen'\
|
22
|
+
'/wiki/Increasing-the-amount-of-inotify-watchers'
|
23
|
+
|
24
|
+
INOTIFY_LIMIT_MESSAGE = <<-EOS.gsub(/^\s*/, '')
|
25
|
+
FATAL: Listen error: unable to monitor directories for changes.
|
26
|
+
Visit #{WIKI_URL} for info on how to fix this.
|
27
|
+
EOS
|
28
|
+
|
29
|
+
def _configure(directory, &callback)
|
30
|
+
require 'rb-inotify'
|
31
|
+
@worker ||= ::INotify::Notifier.new
|
32
|
+
@worker.watch(directory.to_s, *options.events, &callback)
|
33
|
+
rescue Errno::ENOSPC
|
34
|
+
abort(INOTIFY_LIMIT_MESSAGE)
|
35
|
+
end
|
36
|
+
|
37
|
+
def _run
|
38
|
+
@worker.run
|
39
|
+
end
|
40
|
+
|
41
|
+
def _process_event(dir, event)
|
42
|
+
# NOTE: avoid using event.absolute_name since new API
|
43
|
+
# will need to have a custom recursion implemented
|
44
|
+
# to properly match events to configured directories
|
45
|
+
path = Pathname.new(event.watcher.path) + event.name
|
46
|
+
rel_path = path.relative_path_from(dir).to_s
|
47
|
+
|
48
|
+
_log(:debug) { "inotify: #{rel_path} (#{event.flags.inspect})" }
|
49
|
+
|
50
|
+
if /1|true/ =~ ENV['LISTEN_GEM_SIMULATE_FSEVENT']
|
51
|
+
if (event.flags & [:moved_to, :moved_from]) || _dir_event?(event)
|
52
|
+
rel_path = path.dirname.relative_path_from(dir).to_s
|
53
|
+
_queue_change(:dir, dir, rel_path, {})
|
54
|
+
else
|
55
|
+
_queue_change(:dir, dir, rel_path, {})
|
56
|
+
end
|
57
|
+
return
|
58
|
+
end
|
59
|
+
|
60
|
+
return if _skip_event?(event)
|
61
|
+
|
62
|
+
cookie_params = event.cookie.zero? ? {} : { cookie: event.cookie }
|
63
|
+
|
64
|
+
# Note: don't pass options to force rescanning the directory, so we can
|
65
|
+
# detect moving/deleting a whole tree
|
66
|
+
if _dir_event?(event)
|
67
|
+
_queue_change(:dir, dir, rel_path, cookie_params)
|
68
|
+
return
|
69
|
+
end
|
70
|
+
|
71
|
+
params = cookie_params.merge(change: _change(event.flags))
|
72
|
+
|
73
|
+
_queue_change(:file, dir, rel_path, params)
|
74
|
+
end
|
75
|
+
|
76
|
+
def _skip_event?(event)
|
77
|
+
# Event on root directory
|
78
|
+
return true if event.name == ''
|
79
|
+
# INotify reports changes to files inside directories as events
|
80
|
+
# on the directories themselves too.
|
81
|
+
#
|
82
|
+
# @see http://linux.die.net/man/7/inotify
|
83
|
+
_dir_event?(event) && (event.flags & [:close, :modify]).any?
|
84
|
+
end
|
85
|
+
|
86
|
+
def _change(event_flags)
|
87
|
+
{ modified: [:attrib, :close_write],
|
88
|
+
moved_to: [:moved_to],
|
89
|
+
moved_from: [:moved_from],
|
90
|
+
added: [:create],
|
91
|
+
removed: [:delete] }.each do |change, flags|
|
92
|
+
return change unless (flags & event_flags).empty?
|
93
|
+
end
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
|
97
|
+
def _dir_event?(event)
|
98
|
+
event.flags.include?(:isdir)
|
99
|
+
end
|
100
|
+
|
101
|
+
def _stop
|
102
|
+
@worker.close
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|