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,37 @@
|
|
1
|
+
module Listen
|
2
|
+
module Adapter
|
3
|
+
# Polling Adapter that works cross-platform and
|
4
|
+
# has no dependencies. This is the adapter that
|
5
|
+
# uses the most CPU processing power and has higher
|
6
|
+
# file IO than the other implementations.
|
7
|
+
#
|
8
|
+
class Polling < Base
|
9
|
+
OS_REGEXP = // # match every OS
|
10
|
+
|
11
|
+
DEFAULTS = { latency: 1.0, wait_for_delay: 0.05 }
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def _configure(_, &callback)
|
16
|
+
@polling_callbacks ||= []
|
17
|
+
@polling_callbacks << callback
|
18
|
+
end
|
19
|
+
|
20
|
+
def _run
|
21
|
+
loop do
|
22
|
+
start = Time.now.to_f
|
23
|
+
@polling_callbacks.each do |callback|
|
24
|
+
callback.call(nil)
|
25
|
+
nap_time = options.latency - (Time.now.to_f - start)
|
26
|
+
# TODO: warn if nap_time is negative (polling too slow)
|
27
|
+
sleep(nap_time) if nap_time > 0
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def _process_event(dir, _)
|
33
|
+
_queue_change(:dir, dir, '.', recursive: true)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Listen
|
2
|
+
module Adapter
|
3
|
+
# Adapter implementation for Windows `wdm`.
|
4
|
+
#
|
5
|
+
class Windows < Base
|
6
|
+
OS_REGEXP = /mswin|mingw|cygwin/i
|
7
|
+
|
8
|
+
BUNDLER_DECLARE_GEM = <<-EOS.gsub(/^ {6}/, '')
|
9
|
+
Please add the following to your Gemfile to avoid polling for changes:
|
10
|
+
gem 'wdm', '>= 0.1.0' if Gem.win_platform?
|
11
|
+
EOS
|
12
|
+
|
13
|
+
def self.usable?
|
14
|
+
return false unless super
|
15
|
+
require 'wdm'
|
16
|
+
true
|
17
|
+
rescue LoadError
|
18
|
+
_log :debug, format('wdm - load failed: %s:%s', $ERROR_INFO,
|
19
|
+
$ERROR_POSITION * "\n")
|
20
|
+
|
21
|
+
Kernel.warn BUNDLER_DECLARE_GEM
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def _configure(dir, &callback)
|
28
|
+
require 'wdm'
|
29
|
+
_log :debug, 'wdm - starting...'
|
30
|
+
@worker ||= WDM::Monitor.new
|
31
|
+
@worker.watch_recursively(dir.to_s, :files) do |change|
|
32
|
+
callback.call([:file, change])
|
33
|
+
end
|
34
|
+
|
35
|
+
@worker.watch_recursively(dir.to_s, :directories) do |change|
|
36
|
+
callback.call([:dir, change])
|
37
|
+
end
|
38
|
+
|
39
|
+
events = [:attributes, :last_write]
|
40
|
+
@worker.watch_recursively(dir.to_s, *events) do |change|
|
41
|
+
callback.call([:attr, change])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def _run
|
46
|
+
@worker.run!
|
47
|
+
end
|
48
|
+
|
49
|
+
def _process_event(dir, event)
|
50
|
+
_log :debug, "wdm - callback: #{event.inspect}"
|
51
|
+
|
52
|
+
type, change = event
|
53
|
+
|
54
|
+
full_path = Pathname(change.path)
|
55
|
+
|
56
|
+
rel_path = full_path.relative_path_from(dir).to_s
|
57
|
+
|
58
|
+
options = { change: _change(change.type) }
|
59
|
+
|
60
|
+
case type
|
61
|
+
when :file
|
62
|
+
_queue_change(:file, dir, rel_path, options)
|
63
|
+
when :attr
|
64
|
+
unless full_path.directory?
|
65
|
+
_queue_change(:file, dir, rel_path, options)
|
66
|
+
end
|
67
|
+
when :dir
|
68
|
+
if change.type == :removed
|
69
|
+
# TODO: check if watched dir?
|
70
|
+
_queue_change(:dir, dir, Pathname(rel_path).dirname.to_s, {})
|
71
|
+
elsif change.type == :added
|
72
|
+
_queue_change(:dir, dir, rel_path, {})
|
73
|
+
else
|
74
|
+
# do nothing - changed directory means either:
|
75
|
+
# - removed subdirs (handled above)
|
76
|
+
# - added subdirs (handled above)
|
77
|
+
# - removed files (handled by _file_callback)
|
78
|
+
# - added files (handled by _file_callback)
|
79
|
+
# so what's left?
|
80
|
+
end
|
81
|
+
end
|
82
|
+
rescue
|
83
|
+
details = event.inspect
|
84
|
+
_log :error, format('wdm - callback (%): %s:%s', details, $ERROR_INFO,
|
85
|
+
$ERROR_POSITION * "\n")
|
86
|
+
raise
|
87
|
+
end
|
88
|
+
|
89
|
+
def _change(type)
|
90
|
+
{ modified: [:modified, :attrib], # TODO: is attrib really passed?
|
91
|
+
added: [:added, :renamed_new_file],
|
92
|
+
removed: [:removed, :renamed_old_file] }.each do |change, types|
|
93
|
+
return change if types.include?(type)
|
94
|
+
end
|
95
|
+
nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'listen/adapter'
|
2
|
+
require 'listen/adapter/base'
|
3
|
+
require 'listen/adapter/config'
|
4
|
+
|
5
|
+
# This class just aggregates configuration object to avoid Listener specs
|
6
|
+
# from exploding with huge test setup blocks
|
7
|
+
module Listen
|
8
|
+
class Backend
|
9
|
+
def initialize(directories, queue, silencer, config)
|
10
|
+
adapter_select_opts = config.adapter_select_options
|
11
|
+
|
12
|
+
adapter_class = Adapter.select(adapter_select_opts)
|
13
|
+
|
14
|
+
# Use default from adapter if possible
|
15
|
+
@min_delay_between_events = config.min_delay_between_events
|
16
|
+
@min_delay_between_events ||= adapter_class::DEFAULTS[:wait_for_delay]
|
17
|
+
@min_delay_between_events ||= 0.1
|
18
|
+
|
19
|
+
adapter_opts = config.adapter_instance_options(adapter_class)
|
20
|
+
|
21
|
+
aconfig = Adapter::Config.new(directories, queue, silencer, adapter_opts)
|
22
|
+
@adapter = adapter_class.new(aconfig)
|
23
|
+
end
|
24
|
+
|
25
|
+
def start
|
26
|
+
adapter.start
|
27
|
+
end
|
28
|
+
|
29
|
+
def stop
|
30
|
+
adapter.stop
|
31
|
+
end
|
32
|
+
|
33
|
+
def min_delay_between_events
|
34
|
+
@min_delay_between_events
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
attr_reader :adapter
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'listen/file'
|
2
|
+
require 'listen/directory'
|
3
|
+
|
4
|
+
module Listen
|
5
|
+
# TODO: rename to Snapshot
|
6
|
+
class Change
|
7
|
+
# TODO: test this class for coverage
|
8
|
+
class Config
|
9
|
+
def initialize(queue, silencer)
|
10
|
+
@queue = queue
|
11
|
+
@silencer = silencer
|
12
|
+
end
|
13
|
+
|
14
|
+
def silenced?(path, type)
|
15
|
+
@silencer.silenced?(Pathname(path), type)
|
16
|
+
end
|
17
|
+
|
18
|
+
def queue(*args)
|
19
|
+
@queue << args
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :record
|
24
|
+
|
25
|
+
def initialize(config, record)
|
26
|
+
@config = config
|
27
|
+
@record = record
|
28
|
+
end
|
29
|
+
|
30
|
+
# Invalidate some part of the snapshot/record (dir, file, subtree, etc.)
|
31
|
+
def invalidate(type, rel_path, options)
|
32
|
+
watched_dir = Pathname.new(record.root)
|
33
|
+
|
34
|
+
change = options[:change]
|
35
|
+
cookie = options[:cookie]
|
36
|
+
|
37
|
+
if !cookie && config.silenced?(rel_path, type)
|
38
|
+
Listen::Logger.debug { "(silenced): #{rel_path.inspect}" }
|
39
|
+
return
|
40
|
+
end
|
41
|
+
|
42
|
+
path = watched_dir + rel_path
|
43
|
+
|
44
|
+
Listen::Logger.debug do
|
45
|
+
log_details = options[:silence] && 'recording' || change || 'unknown'
|
46
|
+
"#{log_details}: #{type}:#{path} (#{options.inspect})"
|
47
|
+
end
|
48
|
+
|
49
|
+
if change
|
50
|
+
options = cookie ? { cookie: cookie } : {}
|
51
|
+
config.queue(type, change, watched_dir, rel_path, options)
|
52
|
+
else
|
53
|
+
if type == :dir
|
54
|
+
# NOTE: POSSIBLE RECURSION
|
55
|
+
# TODO: fix - use a queue instead
|
56
|
+
Directory.scan(self, rel_path, options)
|
57
|
+
else
|
58
|
+
change = File.change(record, rel_path)
|
59
|
+
return if !change || options[:silence]
|
60
|
+
config.queue(:file, change, watched_dir, rel_path)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
rescue RuntimeError => ex
|
64
|
+
msg = format(
|
65
|
+
'%s#%s crashed %s:%s',
|
66
|
+
self.class,
|
67
|
+
__method__,
|
68
|
+
exinspect,
|
69
|
+
ex.backtrace * "\n")
|
70
|
+
Listen::Logger.error(msg)
|
71
|
+
raise
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
attr_reader :config
|
77
|
+
end
|
78
|
+
end
|
data/lib/listen/cli.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'listen'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Listen
|
6
|
+
class CLI < Thor
|
7
|
+
default_task :start
|
8
|
+
|
9
|
+
desc 'start', 'Starts Listen'
|
10
|
+
|
11
|
+
class_option :verbose,
|
12
|
+
type: :boolean,
|
13
|
+
default: false,
|
14
|
+
aliases: '-v',
|
15
|
+
banner: 'Verbose'
|
16
|
+
|
17
|
+
class_option :directory,
|
18
|
+
type: :array,
|
19
|
+
default: '.',
|
20
|
+
aliases: '-d',
|
21
|
+
banner: 'The directory to listen to'
|
22
|
+
|
23
|
+
class_option :relative,
|
24
|
+
type: :boolean,
|
25
|
+
default: false,
|
26
|
+
aliases: '-r',
|
27
|
+
banner: 'Convert paths relative to current directory'
|
28
|
+
|
29
|
+
def start
|
30
|
+
Listen::Forwarder.new(options).start
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Forwarder
|
35
|
+
attr_reader :logger
|
36
|
+
def initialize(options)
|
37
|
+
@options = options
|
38
|
+
@logger = ::Logger.new(STDOUT)
|
39
|
+
@logger.level = ::Logger::INFO
|
40
|
+
@logger.formatter = proc { |_, _, _, msg| "#{msg}\n" }
|
41
|
+
end
|
42
|
+
|
43
|
+
def start
|
44
|
+
logger.info 'Starting listen...'
|
45
|
+
directory = @options[:directory]
|
46
|
+
relative = @options[:relative]
|
47
|
+
callback = proc do |modified, added, removed|
|
48
|
+
if @options[:verbose]
|
49
|
+
logger.info "+ #{added}" unless added.empty?
|
50
|
+
logger.info "- #{removed}" unless removed.empty?
|
51
|
+
logger.info "> #{modified}" unless modified.empty?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
listener = Listen.to(
|
56
|
+
directory,
|
57
|
+
relative: relative,
|
58
|
+
&callback)
|
59
|
+
|
60
|
+
listener.start
|
61
|
+
|
62
|
+
sleep 0.5 while listener.processing?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Listen
|
4
|
+
# TODO: refactor (turn it into a normal object, cache the stat, etc)
|
5
|
+
class Directory
|
6
|
+
def self.scan(snapshot, rel_path, options)
|
7
|
+
record = snapshot.record
|
8
|
+
dir = Pathname.new(record.root)
|
9
|
+
previous = record.dir_entries(rel_path)
|
10
|
+
|
11
|
+
record.add_dir(rel_path)
|
12
|
+
|
13
|
+
# TODO: use children(with_directory: false)
|
14
|
+
path = dir + rel_path
|
15
|
+
current = Set.new(path.children)
|
16
|
+
|
17
|
+
Listen::Logger.debug do
|
18
|
+
format('%s: %s(%s): %s -> %s',
|
19
|
+
(options[:silence] ? 'Recording' : 'Scanning'),
|
20
|
+
rel_path, options.inspect, previous.inspect, current.inspect)
|
21
|
+
end
|
22
|
+
|
23
|
+
begin
|
24
|
+
current.each do |full_path|
|
25
|
+
type = ::File.lstat(full_path.to_s).directory? ? :dir : :file
|
26
|
+
item_rel_path = full_path.relative_path_from(dir).to_s
|
27
|
+
_change(snapshot, type, item_rel_path, options)
|
28
|
+
end
|
29
|
+
rescue Errno::ENOENT
|
30
|
+
# The directory changed meanwhile, so rescan it
|
31
|
+
current = Set.new(path.children)
|
32
|
+
retry
|
33
|
+
end
|
34
|
+
|
35
|
+
# TODO: this is not tested properly
|
36
|
+
previous = previous.reject { |entry, _| current.include? path + entry }
|
37
|
+
|
38
|
+
_async_changes(snapshot, Pathname.new(rel_path), previous, options)
|
39
|
+
|
40
|
+
rescue Errno::ENOENT, Errno::EHOSTDOWN
|
41
|
+
record.unset_path(rel_path)
|
42
|
+
_async_changes(snapshot, Pathname.new(rel_path), previous, options)
|
43
|
+
|
44
|
+
rescue Errno::ENOTDIR
|
45
|
+
# TODO: path not tested
|
46
|
+
record.unset_path(rel_path)
|
47
|
+
_async_changes(snapshot, path, previous, options)
|
48
|
+
_change(snapshot, :file, rel_path, options)
|
49
|
+
rescue
|
50
|
+
Listen::Logger.warn do
|
51
|
+
format('scan DIED: %s:%s', $ERROR_INFO, $ERROR_POSITION * "\n")
|
52
|
+
end
|
53
|
+
raise
|
54
|
+
end
|
55
|
+
|
56
|
+
def self._async_changes(snapshot, path, previous, options)
|
57
|
+
fail "Not a Pathname: #{path.inspect}" unless path.respond_to?(:children)
|
58
|
+
previous.each do |entry, data|
|
59
|
+
# TODO: this is a hack with insufficient testing
|
60
|
+
type = data.key?(:mtime) ? :file : :dir
|
61
|
+
rel_path_s = (path + entry).to_s
|
62
|
+
_change(snapshot, type, rel_path_s, options)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def self._change(snapshot, type, path, options)
|
67
|
+
return snapshot.invalidate(type, path, options) if type == :dir
|
68
|
+
|
69
|
+
# Minor param cleanup for tests
|
70
|
+
# TODO: use a dedicated Event class
|
71
|
+
opts = options.dup
|
72
|
+
opts.delete(:recursive)
|
73
|
+
snapshot.invalidate(type, path, opts)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Listen
|
2
|
+
module Event
|
3
|
+
class Config
|
4
|
+
def initialize(
|
5
|
+
listener,
|
6
|
+
event_queue,
|
7
|
+
queue_optimizer,
|
8
|
+
wait_for_delay,
|
9
|
+
&block)
|
10
|
+
|
11
|
+
@listener = listener
|
12
|
+
@event_queue = event_queue
|
13
|
+
@queue_optimizer = queue_optimizer
|
14
|
+
@min_delay_between_events = wait_for_delay
|
15
|
+
@block = block
|
16
|
+
end
|
17
|
+
|
18
|
+
def sleep(*args)
|
19
|
+
Kernel.sleep(*args)
|
20
|
+
end
|
21
|
+
|
22
|
+
def call(*args)
|
23
|
+
@block.call(*args) if @block
|
24
|
+
end
|
25
|
+
|
26
|
+
def timestamp
|
27
|
+
Time.now.to_f
|
28
|
+
end
|
29
|
+
|
30
|
+
def event_queue
|
31
|
+
@event_queue
|
32
|
+
end
|
33
|
+
|
34
|
+
def callable?
|
35
|
+
@block
|
36
|
+
end
|
37
|
+
|
38
|
+
def optimize_changes(changes)
|
39
|
+
@queue_optimizer.smoosh_changes(changes)
|
40
|
+
end
|
41
|
+
|
42
|
+
def min_delay_between_events
|
43
|
+
@min_delay_between_events
|
44
|
+
end
|
45
|
+
|
46
|
+
def stopped?
|
47
|
+
listener.state == :stopped
|
48
|
+
end
|
49
|
+
|
50
|
+
def paused?
|
51
|
+
listener.state == :paused
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
attr_reader :listener
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|