driskell-listen 3.0.6.10
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 +293 -0
- data/bin/driskell-listen +12 -0
- data/lib/driskell-listen.rb +61 -0
- data/lib/driskell-listen/adapter.rb +44 -0
- data/lib/driskell-listen/adapter/base.rb +147 -0
- data/lib/driskell-listen/adapter/bsd.rb +106 -0
- data/lib/driskell-listen/adapter/config.rb +26 -0
- data/lib/driskell-listen/adapter/darwin.rb +71 -0
- data/lib/driskell-listen/adapter/linux.rb +96 -0
- data/lib/driskell-listen/adapter/polling.rb +37 -0
- data/lib/driskell-listen/adapter/simulated_darwin.rb +65 -0
- data/lib/driskell-listen/adapter/windows.rb +99 -0
- data/lib/driskell-listen/backend.rb +45 -0
- data/lib/driskell-listen/change.rb +80 -0
- data/lib/driskell-listen/cli.rb +65 -0
- data/lib/driskell-listen/directory.rb +83 -0
- data/lib/driskell-listen/event/config.rb +59 -0
- data/lib/driskell-listen/event/loop.rb +117 -0
- data/lib/driskell-listen/event/processor.rb +122 -0
- data/lib/driskell-listen/event/queue.rb +56 -0
- data/lib/driskell-listen/file.rb +80 -0
- data/lib/driskell-listen/fsm.rb +131 -0
- data/lib/driskell-listen/internals/thread_pool.rb +21 -0
- data/lib/driskell-listen/listener.rb +132 -0
- data/lib/driskell-listen/listener/config.rb +45 -0
- data/lib/driskell-listen/logger.rb +32 -0
- data/lib/driskell-listen/options.rb +23 -0
- data/lib/driskell-listen/queue_optimizer.rb +132 -0
- data/lib/driskell-listen/record.rb +104 -0
- data/lib/driskell-listen/record/entry.rb +56 -0
- data/lib/driskell-listen/silencer.rb +97 -0
- data/lib/driskell-listen/silencer/controller.rb +48 -0
- data/lib/driskell-listen/version.rb +5 -0
- metadata +126 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
module Driskell::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(:tree, dir, '.')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Driskell::Listen
|
2
|
+
module Adapter
|
3
|
+
class SimulatedDarwin < Linux
|
4
|
+
def self.usable?
|
5
|
+
os = RbConfig::CONFIG['target_os']
|
6
|
+
return false unless const_get('OS_REGEXP') =~ os
|
7
|
+
/1|true/ =~ ENV['LISTEN_GEM_SIMULATE_FSEVENT']
|
8
|
+
end
|
9
|
+
|
10
|
+
class FakeEvent
|
11
|
+
attr_reader :dir
|
12
|
+
|
13
|
+
def initialize(watched_dir, event)
|
14
|
+
# NOTE: avoid using event.absolute_name since new API
|
15
|
+
# will need to have a custom recursion implemented
|
16
|
+
# to properly match events to configured directories
|
17
|
+
@real_path = full_path(event).relative_path_from(watched_dir)
|
18
|
+
@dir = "#{Pathname(watched_dir) + dir_for_event(event, @real_path)}/"
|
19
|
+
end
|
20
|
+
|
21
|
+
def real_path
|
22
|
+
@real_path.to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def dir?(event)
|
28
|
+
event.flags.include?(:isdir)
|
29
|
+
end
|
30
|
+
|
31
|
+
def moved?(event)
|
32
|
+
(event.flags & [:moved_to, :moved_from])
|
33
|
+
end
|
34
|
+
|
35
|
+
def dir_for_event(event, rel_path)
|
36
|
+
(moved?(event) || dir?(event)) ? rel_path.dirname : rel_path
|
37
|
+
end
|
38
|
+
|
39
|
+
def full_path(event)
|
40
|
+
Pathname.new(event.watcher.path) + event.name
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def _process_event(watched_dir, event)
|
47
|
+
ev = FakeEvent.new(watched_dir, event)
|
48
|
+
|
49
|
+
_log(
|
50
|
+
:debug,
|
51
|
+
"fake_fsevent: #{ev.dir}(#{ev.real_path}=#{event.flags.inspect})")
|
52
|
+
|
53
|
+
_darwin.send(:_process_event, watched_dir, [ev.dir])
|
54
|
+
end
|
55
|
+
|
56
|
+
def _darwin
|
57
|
+
@darwin ||= Class.new(Darwin) do
|
58
|
+
def _configure(*_args)
|
59
|
+
# Skip FSEvent setup
|
60
|
+
end
|
61
|
+
end.new(mq: @mq)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Driskell::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,45 @@
|
|
1
|
+
require 'driskell-listen/adapter'
|
2
|
+
require 'driskell-listen/adapter/base'
|
3
|
+
require 'driskell-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 Driskell::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 preempt_change(dir, rel_path, data)
|
34
|
+
adapter.preempt_change dir, rel_path, data
|
35
|
+
end
|
36
|
+
|
37
|
+
def min_delay_between_events
|
38
|
+
@min_delay_between_events
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
attr_reader :adapter
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'driskell-listen/file'
|
2
|
+
require 'driskell-listen/directory'
|
3
|
+
|
4
|
+
module Driskell::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
|
+
Driskell::Listen::Logger.debug { "(silenced): #{rel_path.inspect}" }
|
39
|
+
return
|
40
|
+
end
|
41
|
+
|
42
|
+
path = watched_dir + rel_path
|
43
|
+
|
44
|
+
Driskell::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 == :tree
|
54
|
+
# Invalid the entire directory tree
|
55
|
+
Directory.scan(self, rel_path, **options, recurse: true)
|
56
|
+
elsif type == :dir
|
57
|
+
# Invalid directory contents, but do not recurse
|
58
|
+
Directory.scan(self, rel_path, **options, recurse: false)
|
59
|
+
else
|
60
|
+
change = File.change(record, rel_path)
|
61
|
+
return if !change || options[:silence]
|
62
|
+
config.queue(:file, change, watched_dir, rel_path)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
rescue RuntimeError => ex
|
66
|
+
msg = format(
|
67
|
+
'%s#%s crashed %s:%s',
|
68
|
+
self.class,
|
69
|
+
__method__,
|
70
|
+
exinspect,
|
71
|
+
ex.backtrace * "\n")
|
72
|
+
Driskell::Listen::Logger.error(msg)
|
73
|
+
raise
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
attr_reader :config
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'driskell-listen'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Driskell::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
|
+
Driskell::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 = Driskell::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,83 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Driskell::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
|
+
|
9
|
+
previous = record.dir_entries(rel_path)
|
10
|
+
|
11
|
+
dir = Pathname.new(record.root)
|
12
|
+
path = dir + rel_path
|
13
|
+
current = Set.new(path.children)
|
14
|
+
|
15
|
+
Driskell::Listen::Logger.debug do
|
16
|
+
format('%s: %s(%s): %s -> %s',
|
17
|
+
(options[:silence] ? 'Recording' : 'Scanning'),
|
18
|
+
rel_path, options.inspect, previous.inspect, current.inspect)
|
19
|
+
end
|
20
|
+
|
21
|
+
record.update_dir(rel_path)
|
22
|
+
|
23
|
+
current.each do |full_path|
|
24
|
+
# Find old type so we can ensure we invalidate directory contents
|
25
|
+
# if we were previously a file, and vice versa
|
26
|
+
if previous.key?(full_path.basename)
|
27
|
+
old = previous.delete(full_path.basename)
|
28
|
+
old_type = old.key?(:mtime) ? :dir : :file
|
29
|
+
else
|
30
|
+
old_type = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
item_rel_path = full_path.relative_path_from(dir).to_s
|
34
|
+
if detect_type(full_path) == :dir
|
35
|
+
if old_type == :file
|
36
|
+
snapshot.invalidate(:file, item_rel_path, options)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Only invalidate subdirectories if we're recursing or it is new
|
40
|
+
if options[:recurse] || old_type.nil?
|
41
|
+
snapshot.invalidate(:tree, item_rel_path, options)
|
42
|
+
end
|
43
|
+
else
|
44
|
+
if old_type == :dir
|
45
|
+
snapshot.invalidate(:tree, item_rel_path, options)
|
46
|
+
end
|
47
|
+
|
48
|
+
snapshot.invalidate(:file, item_rel_path, options)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
process_previous(snapshot, Pathname.new(rel_path), previous, options)
|
53
|
+
rescue Errno::ENOENT, Errno::EHOSTDOWN
|
54
|
+
record.unset_path(rel_path)
|
55
|
+
process_previous(snapshot, Pathname.new(rel_path), previous, options)
|
56
|
+
rescue Errno::ENOTDIR
|
57
|
+
record.unset_path(rel_path)
|
58
|
+
process_previous(snapshot, path, previous, options)
|
59
|
+
snapshot.invalidate(:file, rel_path, options)
|
60
|
+
rescue
|
61
|
+
Driskell::Listen::Logger.warn do
|
62
|
+
format('scan DIED: %s:%s', $ERROR_INFO, $ERROR_POSITION * "\n")
|
63
|
+
end
|
64
|
+
raise
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.process_previous(snapshot, path, previous, options)
|
68
|
+
previous.each do |entry, data|
|
69
|
+
type = data.key?(:mtime) ? :file : :tree
|
70
|
+
rel_path_s = (path + entry).to_s
|
71
|
+
snapshot.invalidate(type, rel_path_s, options)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.detect_type(full_path)
|
76
|
+
stat = ::File.lstat(full_path.to_s)
|
77
|
+
stat.directory? ? :dir : :file
|
78
|
+
rescue Errno::ENOENT
|
79
|
+
# report as dir for scanning
|
80
|
+
:dir
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|