sass-listen 3.0.7
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 -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
|