listen 2.10.1 → 3.0.0
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 +4 -4
- data/README.md +11 -46
- data/lib/listen.rb +19 -40
- data/lib/listen/adapter.rb +1 -3
- data/lib/listen/adapter/base.rb +66 -36
- data/lib/listen/adapter/config.rb +21 -0
- data/lib/listen/adapter/linux.rb +5 -7
- data/lib/listen/adapter/polling.rb +2 -1
- data/lib/listen/backend.rb +41 -0
- data/lib/listen/change.rb +45 -26
- data/lib/listen/cli.rb +3 -11
- data/lib/listen/directory.rb +31 -29
- data/lib/listen/event/config.rb +59 -0
- data/lib/listen/event/loop.rb +113 -0
- data/lib/listen/event/processor.rb +122 -0
- data/lib/listen/event/queue.rb +54 -0
- data/lib/listen/file.rb +9 -9
- data/lib/listen/fsm.rb +131 -0
- data/lib/listen/internals/thread_pool.rb +1 -1
- data/lib/listen/listener.rb +66 -305
- data/lib/listen/listener/config.rb +45 -0
- data/lib/listen/logger.rb +32 -0
- data/lib/listen/options.rb +1 -1
- data/lib/listen/queue_optimizer.rb +38 -20
- data/lib/listen/record.rb +50 -65
- data/lib/listen/silencer/controller.rb +48 -0
- data/lib/listen/version.rb +1 -1
- metadata +12 -21
- data/lib/listen/adapter/tcp.rb +0 -88
- data/lib/listen/internals/logging.rb +0 -35
- data/lib/listen/tcp.rb +0 -8
- data/lib/listen/tcp/broadcaster.rb +0 -79
- data/lib/listen/tcp/message.rb +0 -50
@@ -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
|
+
# TODO: does nothing
|
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
|
data/lib/listen/change.rb
CHANGED
@@ -2,58 +2,77 @@ require 'listen/file'
|
|
2
2
|
require 'listen/directory'
|
3
3
|
|
4
4
|
module Listen
|
5
|
+
# TODO: rename to Snapshot
|
5
6
|
class Change
|
6
|
-
|
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
|
7
22
|
|
8
|
-
|
23
|
+
attr_reader :record
|
9
24
|
|
10
|
-
def initialize(
|
11
|
-
@
|
25
|
+
def initialize(config, record)
|
26
|
+
@config = config
|
27
|
+
@record = record
|
12
28
|
end
|
13
29
|
|
14
|
-
|
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
|
+
|
15
34
|
change = options[:change]
|
16
35
|
cookie = options[:cookie]
|
17
36
|
|
18
|
-
if !cookie &&
|
19
|
-
|
37
|
+
if !cookie && config.silenced?(rel_path, type)
|
38
|
+
Listen::Logger.debug { "(silenced): #{rel_path.inspect}" }
|
20
39
|
return
|
21
40
|
end
|
22
41
|
|
23
42
|
path = watched_dir + rel_path
|
24
43
|
|
25
|
-
|
26
|
-
|
44
|
+
Listen::Logger.debug do
|
45
|
+
log_details = options[:silence] && 'recording' || change || 'unknown'
|
46
|
+
"#{log_details}: #{type}:#{path} (#{options.inspect})"
|
47
|
+
end
|
27
48
|
|
28
49
|
if change
|
29
|
-
# TODO: move this to Listener to avoid Celluloid overhead
|
30
|
-
# from caller
|
31
50
|
options = cookie ? { cookie: cookie } : {}
|
32
|
-
|
51
|
+
config.queue(type, change, watched_dir, rel_path, options)
|
33
52
|
else
|
34
|
-
return unless (record = listener.sync(:record))
|
35
|
-
|
36
53
|
if type == :dir
|
37
|
-
|
38
|
-
|
54
|
+
# NOTE: POSSIBLE RECURSION
|
55
|
+
# TODO: fix - use a queue instead
|
56
|
+
Directory.scan(self, rel_path, options)
|
39
57
|
else
|
40
|
-
change = File.change(record,
|
58
|
+
change = File.change(record, rel_path)
|
41
59
|
return if !change || options[:silence]
|
42
|
-
|
60
|
+
config.queue(:file, change, watched_dir, rel_path)
|
43
61
|
end
|
44
62
|
end
|
45
|
-
rescue
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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)
|
50
71
|
raise
|
51
72
|
end
|
52
73
|
|
53
74
|
private
|
54
75
|
|
55
|
-
|
56
|
-
Celluloid::Logger.send(type, message)
|
57
|
-
end
|
76
|
+
attr_reader :config
|
58
77
|
end
|
59
78
|
end
|
data/lib/listen/cli.rb
CHANGED
@@ -14,12 +14,6 @@ module Listen
|
|
14
14
|
aliases: '-v',
|
15
15
|
banner: 'Verbose'
|
16
16
|
|
17
|
-
class_option :forward,
|
18
|
-
type: :string,
|
19
|
-
default: '127.0.0.1:4000',
|
20
|
-
aliases: '-f',
|
21
|
-
banner: 'The address to forward filesystem events'
|
22
|
-
|
23
17
|
class_option :directory,
|
24
18
|
type: :array,
|
25
19
|
default: '.',
|
@@ -41,14 +35,13 @@ module Listen
|
|
41
35
|
attr_reader :logger
|
42
36
|
def initialize(options)
|
43
37
|
@options = options
|
44
|
-
@logger = Logger.new(STDOUT)
|
45
|
-
@logger.level = Logger::INFO
|
38
|
+
@logger = ::Logger.new(STDOUT)
|
39
|
+
@logger.level = ::Logger::INFO
|
46
40
|
@logger.formatter = proc { |_, _, _, msg| "#{msg}\n" }
|
47
41
|
end
|
48
42
|
|
49
43
|
def start
|
50
44
|
logger.info 'Starting listen...'
|
51
|
-
address = @options[:forward]
|
52
45
|
directory = @options[:directory]
|
53
46
|
relative = @options[:relative]
|
54
47
|
callback = proc do |modified, added, removed|
|
@@ -61,13 +54,12 @@ module Listen
|
|
61
54
|
|
62
55
|
listener = Listen.to(
|
63
56
|
directory,
|
64
|
-
forward_to: address,
|
65
57
|
relative: relative,
|
66
58
|
&callback)
|
67
59
|
|
68
60
|
listener.start
|
69
61
|
|
70
|
-
sleep 0.5 while listener.
|
62
|
+
sleep 0.5 while listener.processing?
|
71
63
|
end
|
72
64
|
end
|
73
65
|
end
|
data/lib/listen/directory.rb
CHANGED
@@ -1,78 +1,80 @@
|
|
1
1
|
require 'set'
|
2
2
|
|
3
3
|
module Listen
|
4
|
+
# TODO: refactor (turn it into a normal object, cache the stat, etc)
|
4
5
|
class Directory
|
5
|
-
def self.scan(
|
6
|
-
|
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)
|
7
10
|
|
8
|
-
|
9
|
-
|
10
|
-
record.add_dir(dir, rel_path)
|
11
|
+
record.add_dir(rel_path)
|
11
12
|
|
12
13
|
# TODO: use children(with_directory: false)
|
13
14
|
path = dir + rel_path
|
14
15
|
current = Set.new(path.children)
|
15
16
|
|
16
|
-
|
17
|
+
Listen::Logger.debug do
|
17
18
|
format('%s: %s(%s): %s -> %s',
|
18
19
|
(options[:silence] ? 'Recording' : 'Scanning'),
|
19
20
|
rel_path, options.inspect, previous.inspect, current.inspect)
|
20
21
|
end
|
21
22
|
|
22
23
|
current.each do |full_path|
|
23
|
-
type = full_path
|
24
|
+
type = detect_type(full_path)
|
24
25
|
item_rel_path = full_path.relative_path_from(dir).to_s
|
25
|
-
_change(
|
26
|
+
_change(snapshot, type, item_rel_path, options)
|
26
27
|
end
|
27
28
|
|
28
29
|
# TODO: this is not tested properly
|
29
30
|
previous = previous.reject { |entry, _| current.include? path + entry }
|
30
31
|
|
31
|
-
_async_changes(
|
32
|
+
_async_changes(snapshot, Pathname.new(rel_path), previous, options)
|
32
33
|
|
33
34
|
rescue Errno::ENOENT, Errno::EHOSTDOWN
|
34
|
-
record.unset_path(
|
35
|
-
_async_changes(
|
35
|
+
record.unset_path(rel_path)
|
36
|
+
_async_changes(snapshot, Pathname.new(rel_path), previous, options)
|
36
37
|
|
37
38
|
rescue Errno::ENOTDIR
|
38
39
|
# TODO: path not tested
|
39
|
-
record.unset_path(
|
40
|
-
_async_changes(
|
41
|
-
_change(
|
40
|
+
record.unset_path(rel_path)
|
41
|
+
_async_changes(snapshot, path, previous, options)
|
42
|
+
_change(snapshot, :file, rel_path, options)
|
42
43
|
rescue
|
43
|
-
|
44
|
+
Listen::Logger.warn do
|
44
45
|
format('scan DIED: %s:%s', $ERROR_INFO, $ERROR_POSITION * "\n")
|
45
46
|
end
|
46
47
|
raise
|
47
48
|
end
|
48
49
|
|
49
|
-
def self._async_changes(
|
50
|
+
def self._async_changes(snapshot, path, previous, options)
|
51
|
+
fail "Not a Pathname: #{path.inspect}" unless path.respond_to?(:children)
|
50
52
|
previous.each do |entry, data|
|
51
53
|
# TODO: this is a hack with insufficient testing
|
52
54
|
type = data.key?(:mtime) ? :file : :dir
|
53
|
-
|
55
|
+
rel_path_s = (path + entry).to_s
|
56
|
+
_change(snapshot, type, rel_path_s, options)
|
54
57
|
end
|
55
58
|
end
|
56
59
|
|
57
|
-
def self._change(
|
58
|
-
return
|
60
|
+
def self._change(snapshot, type, path, options)
|
61
|
+
return snapshot.invalidate(type, path, options) if type == :dir
|
59
62
|
|
60
63
|
# Minor param cleanup for tests
|
61
64
|
# TODO: use a dedicated Event class
|
62
65
|
opts = options.dup
|
63
66
|
opts.delete(:recursive)
|
64
|
-
|
65
|
-
queue.change(type, dir, path)
|
66
|
-
else
|
67
|
-
queue.change(type, dir, path, opts)
|
68
|
-
end
|
67
|
+
snapshot.invalidate(type, path, opts)
|
69
68
|
end
|
70
69
|
|
71
|
-
def self.
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
70
|
+
def self.detect_type(full_path)
|
71
|
+
# TODO: should probably check record first
|
72
|
+
stat = ::File.lstat(full_path.to_s)
|
73
|
+
stat.directory? ? :dir : :file
|
74
|
+
rescue Errno::ENOENT
|
75
|
+
# TODO: ok, it should really check the record here
|
76
|
+
# report as dir for scanning
|
77
|
+
:dir
|
76
78
|
end
|
77
79
|
end
|
78
80
|
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
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'listen/event/processor'
|
2
|
+
|
3
|
+
module Listen
|
4
|
+
module Event
|
5
|
+
class Loop
|
6
|
+
class Error < RuntimeError
|
7
|
+
class NotStarted < Error
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(config)
|
12
|
+
@config = config
|
13
|
+
@wait_thread = nil
|
14
|
+
@state = :paused
|
15
|
+
@reasons = Thread::Queue.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def wakeup_on_event
|
19
|
+
return if stopped?
|
20
|
+
return unless processing?
|
21
|
+
return unless wait_thread.alive?
|
22
|
+
_wakeup(:event)
|
23
|
+
end
|
24
|
+
|
25
|
+
def paused?
|
26
|
+
wait_thread && state == :paused
|
27
|
+
end
|
28
|
+
|
29
|
+
def processing?
|
30
|
+
return false if stopped?
|
31
|
+
return false if paused?
|
32
|
+
state == :processing
|
33
|
+
end
|
34
|
+
|
35
|
+
def setup
|
36
|
+
# TODO: use a Fiber instead?
|
37
|
+
q = Thread::Queue.new
|
38
|
+
@wait_thread = Internals::ThreadPool.add do
|
39
|
+
_wait_for_changes(q, config)
|
40
|
+
end
|
41
|
+
|
42
|
+
Listen::Logger.debug('Waiting for processing to start...')
|
43
|
+
Timeout.timeout(5) { q.pop }
|
44
|
+
end
|
45
|
+
|
46
|
+
def resume
|
47
|
+
fail Error::NotStarted if stopped?
|
48
|
+
return unless wait_thread
|
49
|
+
_wakeup(:resume)
|
50
|
+
end
|
51
|
+
|
52
|
+
def pause
|
53
|
+
fail NotImplementedError
|
54
|
+
end
|
55
|
+
|
56
|
+
def teardown
|
57
|
+
return unless wait_thread
|
58
|
+
if wait_thread.alive?
|
59
|
+
_wakeup(:teardown)
|
60
|
+
wait_thread.join
|
61
|
+
end
|
62
|
+
@wait_thread = nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def stopped?
|
66
|
+
!wait_thread
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
attr_reader :config
|
72
|
+
attr_reader :wait_thread
|
73
|
+
|
74
|
+
attr_accessor :state
|
75
|
+
|
76
|
+
def _wait_for_changes(ready_queue, config)
|
77
|
+
processor = Event::Processor.new(config, @reasons)
|
78
|
+
|
79
|
+
_wait_until_resumed(ready_queue)
|
80
|
+
processor.loop_for(config.min_delay_between_events)
|
81
|
+
rescue StandardError => ex
|
82
|
+
_nice_error(ex)
|
83
|
+
end
|
84
|
+
|
85
|
+
def _sleep(*args)
|
86
|
+
Kernel.sleep(*args)
|
87
|
+
end
|
88
|
+
|
89
|
+
def _wait_until_resumed(ready_queue)
|
90
|
+
self.state = :paused
|
91
|
+
ready_queue << :ready
|
92
|
+
sleep
|
93
|
+
self.state = :processing
|
94
|
+
end
|
95
|
+
|
96
|
+
def _nice_error(ex)
|
97
|
+
indent = "\n -- "
|
98
|
+
msg = format(
|
99
|
+
'exception while processing events: %s Backtrace:%s%s',
|
100
|
+
ex,
|
101
|
+
indent,
|
102
|
+
ex.backtrace * indent
|
103
|
+
)
|
104
|
+
Listen::Logger.error(msg)
|
105
|
+
end
|
106
|
+
|
107
|
+
def _wakeup(reason)
|
108
|
+
@reasons << reason
|
109
|
+
wait_thread.wakeup
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|