listen 2.10.1 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|