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,122 @@
|
|
1
|
+
module Listen
|
2
|
+
module Event
|
3
|
+
class Processor
|
4
|
+
def initialize(config, reasons)
|
5
|
+
@config = config
|
6
|
+
@reasons = reasons
|
7
|
+
_reset_no_unprocessed_events
|
8
|
+
end
|
9
|
+
|
10
|
+
# TODO: implement this properly instead of checking the state at arbitrary
|
11
|
+
# points in time
|
12
|
+
def loop_for(latency)
|
13
|
+
@latency = latency
|
14
|
+
|
15
|
+
loop do
|
16
|
+
_wait_until_events
|
17
|
+
_wait_until_events_calm_down
|
18
|
+
_wait_until_no_longer_paused
|
19
|
+
_process_changes
|
20
|
+
end
|
21
|
+
rescue Stopped
|
22
|
+
Listen::Logger.debug('Processing stopped')
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
class Stopped < RuntimeError
|
28
|
+
end
|
29
|
+
|
30
|
+
def _wait_until_events_calm_down
|
31
|
+
loop do
|
32
|
+
now = _timestamp
|
33
|
+
|
34
|
+
# Assure there's at least latency between callbacks to allow
|
35
|
+
# for accumulating changes
|
36
|
+
diff = _deadline - now
|
37
|
+
break if diff <= 0
|
38
|
+
|
39
|
+
# give events a bit of time to accumulate so they can be
|
40
|
+
# compressed/optimized
|
41
|
+
_sleep(:waiting_until_latency, diff)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def _wait_until_no_longer_paused
|
46
|
+
# TODO: may not be a good idea?
|
47
|
+
_sleep(:waiting_for_unpause) while config.paused?
|
48
|
+
end
|
49
|
+
|
50
|
+
def _check_stopped
|
51
|
+
return unless config.stopped?
|
52
|
+
|
53
|
+
_flush_wakeup_reasons
|
54
|
+
raise Stopped
|
55
|
+
end
|
56
|
+
|
57
|
+
def _sleep(_local_reason, *args)
|
58
|
+
_check_stopped
|
59
|
+
sleep_duration = config.sleep(*args)
|
60
|
+
_check_stopped
|
61
|
+
|
62
|
+
_flush_wakeup_reasons do |reason|
|
63
|
+
next unless reason == :event
|
64
|
+
_remember_time_of_first_unprocessed_event unless config.paused?
|
65
|
+
end
|
66
|
+
|
67
|
+
sleep_duration
|
68
|
+
end
|
69
|
+
|
70
|
+
def _remember_time_of_first_unprocessed_event
|
71
|
+
@first_unprocessed_event_time ||= _timestamp
|
72
|
+
end
|
73
|
+
|
74
|
+
def _reset_no_unprocessed_events
|
75
|
+
@first_unprocessed_event_time = nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def _deadline
|
79
|
+
@first_unprocessed_event_time + @latency
|
80
|
+
end
|
81
|
+
|
82
|
+
def _wait_until_events
|
83
|
+
# TODO: long sleep may not be a good idea?
|
84
|
+
_sleep(:waiting_for_events) while config.event_queue.empty?
|
85
|
+
@first_unprocessed_event_time ||= _timestamp
|
86
|
+
end
|
87
|
+
|
88
|
+
def _flush_wakeup_reasons
|
89
|
+
reasons = @reasons
|
90
|
+
until reasons.empty?
|
91
|
+
reason = reasons.pop
|
92
|
+
yield reason if block_given?
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def _timestamp
|
97
|
+
config.timestamp
|
98
|
+
end
|
99
|
+
|
100
|
+
# for easier testing without sleep loop
|
101
|
+
def _process_changes
|
102
|
+
_reset_no_unprocessed_events
|
103
|
+
|
104
|
+
changes = []
|
105
|
+
changes << config.event_queue.pop until config.event_queue.empty?
|
106
|
+
|
107
|
+
callable = config.callable?
|
108
|
+
return unless callable
|
109
|
+
|
110
|
+
hash = config.optimize_changes(changes)
|
111
|
+
result = [hash[:modified], hash[:added], hash[:removed]]
|
112
|
+
return if result.all?(&:empty?)
|
113
|
+
|
114
|
+
block_start = _timestamp
|
115
|
+
config.call(*result)
|
116
|
+
Listen::Logger.debug "Callback took #{_timestamp - block_start} sec"
|
117
|
+
end
|
118
|
+
|
119
|
+
attr_reader :config
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Listen
|
2
|
+
module Event
|
3
|
+
class Queue
|
4
|
+
class Config
|
5
|
+
def initialize(relative)
|
6
|
+
@relative = relative
|
7
|
+
end
|
8
|
+
|
9
|
+
def relative?
|
10
|
+
@relative
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(config, &block)
|
15
|
+
@event_queue = Thread::Queue.new
|
16
|
+
@block = block
|
17
|
+
@config = config
|
18
|
+
end
|
19
|
+
|
20
|
+
def <<(args)
|
21
|
+
type, change, dir, path, options = *args
|
22
|
+
fail "Invalid type: #{type.inspect}" unless [:dir, :file].include? type
|
23
|
+
fail "Invalid change: #{change.inspect}" unless change.is_a?(Symbol)
|
24
|
+
fail "Invalid path: #{path.inspect}" unless path.is_a?(String)
|
25
|
+
|
26
|
+
dir = _safe_relative_from_cwd(dir)
|
27
|
+
event_queue.public_send(:<<, [type, change, dir, path, options])
|
28
|
+
|
29
|
+
block.call(args) if block
|
30
|
+
end
|
31
|
+
|
32
|
+
def empty?
|
33
|
+
event_queue.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
def pop
|
37
|
+
event_queue.pop
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :event_queue
|
43
|
+
attr_reader :block
|
44
|
+
attr_reader :config
|
45
|
+
|
46
|
+
def _safe_relative_from_cwd(dir)
|
47
|
+
return dir unless config.relative?
|
48
|
+
dir.relative_path_from(Pathname.pwd)
|
49
|
+
rescue ArgumentError
|
50
|
+
dir
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/listen/file.rb
CHANGED
@@ -1,25 +1,25 @@
|
|
1
1
|
module Listen
|
2
2
|
class File
|
3
|
-
def self.change(record,
|
4
|
-
path =
|
3
|
+
def self.change(record, rel_path)
|
4
|
+
path = Pathname.new(record.root) + rel_path
|
5
5
|
lstat = path.lstat
|
6
6
|
|
7
7
|
data = { mtime: lstat.mtime.to_f, mode: lstat.mode }
|
8
8
|
|
9
|
-
record_data = record.file_data(
|
9
|
+
record_data = record.file_data(rel_path)
|
10
10
|
|
11
11
|
if record_data.empty?
|
12
|
-
record.
|
12
|
+
record.update_file(rel_path, data)
|
13
13
|
return :added
|
14
14
|
end
|
15
15
|
|
16
16
|
if data[:mode] != record_data[:mode]
|
17
|
-
record.
|
17
|
+
record.update_file(rel_path, data)
|
18
18
|
return :modified
|
19
19
|
end
|
20
20
|
|
21
21
|
if data[:mtime] != record_data[:mtime]
|
22
|
-
record.
|
22
|
+
record.update_file(rel_path, data)
|
23
23
|
return :modified
|
24
24
|
end
|
25
25
|
|
@@ -56,13 +56,13 @@ module Listen
|
|
56
56
|
return if data[:mtime].to_i + 2 <= Time.now.to_f
|
57
57
|
|
58
58
|
md5 = Digest::MD5.file(path).digest
|
59
|
-
record.
|
59
|
+
record.update_file(rel_path, data.merge(md5: md5))
|
60
60
|
:modified if record_data[:md5] && md5 != record_data[:md5]
|
61
61
|
rescue SystemCallError
|
62
|
-
record.
|
62
|
+
record.unset_path(rel_path)
|
63
63
|
:removed
|
64
64
|
rescue
|
65
|
-
|
65
|
+
Listen::Logger.debug "lstat failed for: #{rel_path} (#{$ERROR_INFO})"
|
66
66
|
raise
|
67
67
|
end
|
68
68
|
|
data/lib/listen/fsm.rb
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
# Code copied from https://github.com/celluloid/celluloid-fsm
|
2
|
+
module Listen
|
3
|
+
module FSM
|
4
|
+
DEFAULT_STATE = :default # Default state name unless one is explicitly set
|
5
|
+
|
6
|
+
# Included hook to extend class methods
|
7
|
+
def self.included(klass)
|
8
|
+
klass.send :extend, ClassMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
# Obtain or set the default state
|
13
|
+
# Passing a state name sets the default state
|
14
|
+
def default_state(new_default = nil)
|
15
|
+
if new_default
|
16
|
+
@default_state = new_default.to_sym
|
17
|
+
else
|
18
|
+
defined?(@default_state) ? @default_state : DEFAULT_STATE
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Obtain the valid states for this FSM
|
23
|
+
def states
|
24
|
+
@states ||= {}
|
25
|
+
end
|
26
|
+
|
27
|
+
# Declare an FSM state and optionally provide a callback block to fire
|
28
|
+
# Options:
|
29
|
+
# * to: a state or array of states this state can transition to
|
30
|
+
def state(*args, &block)
|
31
|
+
if args.last.is_a? Hash
|
32
|
+
# Stringify keys :/
|
33
|
+
options = args.pop.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
|
34
|
+
else
|
35
|
+
options = {}
|
36
|
+
end
|
37
|
+
|
38
|
+
args.each do |name|
|
39
|
+
name = name.to_sym
|
40
|
+
default_state name if options['default']
|
41
|
+
states[name] = State.new(name, options['to'], &block)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Be kind and call super if you must redefine initialize
|
47
|
+
def initialize
|
48
|
+
@state = self.class.default_state
|
49
|
+
end
|
50
|
+
|
51
|
+
# Obtain the current state of the FSM
|
52
|
+
attr_reader :state
|
53
|
+
|
54
|
+
def transition(state_name)
|
55
|
+
new_state = validate_and_sanitize_new_state(state_name)
|
56
|
+
return unless new_state
|
57
|
+
transition_with_callbacks!(new_state)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Immediate state transition with no checks, or callbacks. "Dangerous!"
|
61
|
+
def transition!(state_name)
|
62
|
+
@state = state_name
|
63
|
+
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
|
67
|
+
def validate_and_sanitize_new_state(state_name)
|
68
|
+
state_name = state_name.to_sym
|
69
|
+
|
70
|
+
return if current_state_name == state_name
|
71
|
+
|
72
|
+
if current_state && !current_state.valid_transition?(state_name)
|
73
|
+
valid = current_state.transitions.map(&:to_s).join(', ')
|
74
|
+
msg = "#{self.class} can't change state from '#{@state}'"\
|
75
|
+
" to '#{state_name}', only to: #{valid}"
|
76
|
+
fail ArgumentError, msg
|
77
|
+
end
|
78
|
+
|
79
|
+
new_state = states[state_name]
|
80
|
+
|
81
|
+
unless new_state
|
82
|
+
return if state_name == default_state
|
83
|
+
fail ArgumentError, "invalid state for #{self.class}: #{state_name}"
|
84
|
+
end
|
85
|
+
|
86
|
+
new_state
|
87
|
+
end
|
88
|
+
|
89
|
+
def transition_with_callbacks!(state_name)
|
90
|
+
transition! state_name.name
|
91
|
+
state_name.call(self)
|
92
|
+
end
|
93
|
+
|
94
|
+
def states
|
95
|
+
self.class.states
|
96
|
+
end
|
97
|
+
|
98
|
+
def default_state
|
99
|
+
self.class.default_state
|
100
|
+
end
|
101
|
+
|
102
|
+
def current_state
|
103
|
+
states[@state]
|
104
|
+
end
|
105
|
+
|
106
|
+
def current_state_name
|
107
|
+
current_state && current_state.name || ''
|
108
|
+
end
|
109
|
+
|
110
|
+
class State
|
111
|
+
attr_reader :name, :transitions
|
112
|
+
|
113
|
+
def initialize(name, transitions = nil, &block)
|
114
|
+
@name, @block = name, block
|
115
|
+
@transitions = nil
|
116
|
+
@transitions = Array(transitions).map(&:to_sym) if transitions
|
117
|
+
end
|
118
|
+
|
119
|
+
def call(obj)
|
120
|
+
obj.instance_eval(&@block) if @block
|
121
|
+
end
|
122
|
+
|
123
|
+
def valid_transition?(new_state)
|
124
|
+
# All transitions are allowed unless expressly
|
125
|
+
return true unless @transitions
|
126
|
+
|
127
|
+
@transitions.include? new_state.to_sym
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -1,7 +1,6 @@
|
|
1
1
|
module Listen
|
2
2
|
# @private api
|
3
3
|
module Internals
|
4
|
-
# Just a wrapper for tests to avoid interfereing with Celluloid's threads
|
5
4
|
module ThreadPool
|
6
5
|
def self.add(&block)
|
7
6
|
Thread.new { block.call }.tap do |th|
|
@@ -11,6 +10,7 @@ module Listen
|
|
11
10
|
|
12
11
|
def self.stop
|
13
12
|
return unless @threads ||= nil
|
13
|
+
return if @threads.empty? # return to avoid using possibly stubbed Queue
|
14
14
|
|
15
15
|
killed = Queue.new
|
16
16
|
killed << @threads.pop.kill until @threads.empty?
|
data/lib/listen/listener.rb
CHANGED
@@ -1,31 +1,25 @@
|
|
1
|
-
require '
|
1
|
+
require 'English'
|
2
2
|
|
3
3
|
require 'listen/version'
|
4
|
-
require 'listen/adapter'
|
5
|
-
require 'listen/change'
|
6
|
-
require 'listen/record'
|
7
|
-
require 'listen/silencer'
|
8
|
-
require 'listen/queue_optimizer'
|
9
|
-
require 'English'
|
10
4
|
|
11
|
-
require 'listen/
|
5
|
+
require 'listen/backend'
|
12
6
|
|
13
|
-
|
14
|
-
|
15
|
-
include Celluloid::FSM
|
16
|
-
include QueueOptimizer
|
7
|
+
require 'listen/silencer'
|
8
|
+
require 'listen/silencer/controller'
|
17
9
|
|
18
|
-
|
10
|
+
require 'listen/queue_optimizer'
|
19
11
|
|
20
|
-
|
12
|
+
require 'listen/fsm'
|
21
13
|
|
22
|
-
|
23
|
-
|
24
|
-
|
14
|
+
require 'listen/event/loop'
|
15
|
+
require 'listen/event/queue'
|
16
|
+
require 'listen/event/config'
|
25
17
|
|
26
|
-
|
27
|
-
|
28
|
-
|
18
|
+
require 'listen/listener/config'
|
19
|
+
|
20
|
+
module Listen
|
21
|
+
class Listener
|
22
|
+
include Listen::FSM
|
29
23
|
|
30
24
|
# Initializes the directories listener.
|
31
25
|
#
|
@@ -37,73 +31,69 @@ module Listen
|
|
37
31
|
# @yieldparam [Array<String>] added the list of added files
|
38
32
|
# @yieldparam [Array<String>] removed the list of removed files
|
39
33
|
#
|
40
|
-
def initialize(*
|
41
|
-
|
42
|
-
|
43
|
-
# Setup logging first
|
44
|
-
if Celluloid.logger
|
45
|
-
Celluloid.logger.level = _debug_level
|
46
|
-
_info "Celluloid loglevel set to: #{Celluloid.logger.level}"
|
47
|
-
_info "Listen version: #{Listen::VERSION}"
|
48
|
-
end
|
49
|
-
|
50
|
-
@silencer = Silencer.new
|
51
|
-
_reconfigure_silencer({})
|
52
|
-
|
53
|
-
@tcp_mode = nil
|
54
|
-
if [:recipient, :broadcaster].include? args[1]
|
55
|
-
target = args.shift
|
56
|
-
@tcp_mode = args.shift
|
57
|
-
_init_tcp_options(target)
|
58
|
-
end
|
59
|
-
|
60
|
-
@directories = args.flatten.map { |path| Pathname.new(path).realpath }
|
61
|
-
@queue = Queue.new
|
62
|
-
@block = block
|
63
|
-
@registry = Celluloid::Registry.new
|
34
|
+
def initialize(*dirs, &block)
|
35
|
+
options = dirs.last.is_a?(Hash) ? dirs.pop : {}
|
64
36
|
|
65
|
-
|
37
|
+
@config = Config.new(options)
|
38
|
+
|
39
|
+
eq_config = Event::Queue::Config.new(@config.relative?)
|
40
|
+
queue = Event::Queue.new(eq_config) { @processor.wakeup_on_event }
|
41
|
+
|
42
|
+
silencer = Silencer.new
|
43
|
+
rules = @config.silencer_rules
|
44
|
+
@silencer_controller = Silencer::Controller.new(silencer, rules)
|
45
|
+
|
46
|
+
@backend = Backend.new(dirs, queue, silencer, @config)
|
47
|
+
|
48
|
+
optimizer_config = QueueOptimizer::Config.new(@backend, silencer)
|
49
|
+
|
50
|
+
pconfig = Event::Config.new(
|
51
|
+
self,
|
52
|
+
queue,
|
53
|
+
QueueOptimizer.new(optimizer_config),
|
54
|
+
@backend.min_delay_between_events,
|
55
|
+
&block)
|
56
|
+
|
57
|
+
@processor = Event::Loop.new(pconfig)
|
58
|
+
|
59
|
+
super() # FSM
|
66
60
|
end
|
67
61
|
|
68
62
|
default_state :initializing
|
69
63
|
|
70
|
-
state :initializing, to: :
|
71
|
-
state :paused, to: [:processing, :stopped]
|
64
|
+
state :initializing, to: :backend_started
|
72
65
|
|
73
|
-
state :
|
74
|
-
|
75
|
-
if @supervisor
|
76
|
-
@supervisor.terminate
|
77
|
-
@supervisor = nil
|
78
|
-
end
|
66
|
+
state :backend_started, to: [:frontend_ready] do
|
67
|
+
backend.start
|
79
68
|
end
|
80
69
|
|
81
|
-
state :
|
82
|
-
|
83
|
-
|
84
|
-
else
|
85
|
-
@last_queue_event_time = nil
|
86
|
-
_start_wait_thread
|
87
|
-
_init_actors
|
70
|
+
state :frontend_ready, to: [:processing_events] do
|
71
|
+
processor.setup
|
72
|
+
end
|
88
73
|
|
89
|
-
|
90
|
-
|
91
|
-
|
74
|
+
state :processing_events, to: [:paused, :stopped] do
|
75
|
+
processor.resume
|
76
|
+
end
|
77
|
+
|
78
|
+
state :paused, to: [:processing_events, :stopped] do
|
79
|
+
processor.pause
|
80
|
+
end
|
92
81
|
|
93
|
-
|
94
|
-
|
82
|
+
state :stopped, to: [:backend_started] do
|
83
|
+
backend.stop # should be before processor.teardown to halt events ASAP
|
84
|
+
processor.teardown
|
95
85
|
end
|
96
86
|
|
97
87
|
# Starts processing events and starts adapters
|
98
88
|
# or resumes invoking callbacks if paused
|
99
89
|
def start
|
100
|
-
transition :
|
90
|
+
transition :backend_started if state == :initializing
|
91
|
+
transition :frontend_ready if state == :backend_started
|
92
|
+
transition :processing_events if state == :frontend_ready
|
93
|
+
transition :processing_events if state == :paused
|
101
94
|
end
|
102
95
|
|
103
|
-
#
|
104
|
-
alias_method :unpause, :start
|
105
|
-
|
106
|
-
# Stops processing and terminates all actors
|
96
|
+
# Stops both listening for events and processing them
|
107
97
|
def stop
|
108
98
|
transition :stopped
|
109
99
|
end
|
@@ -115,257 +105,28 @@ module Listen
|
|
115
105
|
|
116
106
|
# processing means callbacks are called
|
117
107
|
def processing?
|
118
|
-
state == :
|
108
|
+
state == :processing_events
|
119
109
|
end
|
120
110
|
|
121
111
|
def paused?
|
122
112
|
state == :paused
|
123
113
|
end
|
124
114
|
|
125
|
-
# TODO: deprecate
|
126
|
-
alias_method :listen?, :processing?
|
127
|
-
|
128
|
-
# TODO: deprecate
|
129
|
-
def paused=(value)
|
130
|
-
transition value ? :paused : :processing
|
131
|
-
end
|
132
|
-
|
133
|
-
# TODO: deprecate
|
134
|
-
alias_method :paused, :paused?
|
135
|
-
|
136
|
-
# Add files and dirs to ignore on top of defaults
|
137
|
-
#
|
138
|
-
# (@see Listen::Silencer for default ignored files and dirs)
|
139
|
-
#
|
140
115
|
def ignore(regexps)
|
141
|
-
|
116
|
+
@silencer_controller.append_ignores(regexps)
|
142
117
|
end
|
143
118
|
|
144
|
-
# Replace default ignore patterns with provided regexp
|
145
119
|
def ignore!(regexps)
|
146
|
-
|
120
|
+
@silencer_controller.replace_with_bang_ignores(regexps)
|
147
121
|
end
|
148
122
|
|
149
|
-
# Listen only to files and dirs matching regexp
|
150
123
|
def only(regexps)
|
151
|
-
|
152
|
-
end
|
153
|
-
|
154
|
-
def async(type)
|
155
|
-
proxy = sync(type)
|
156
|
-
proxy ? proxy.async : nil
|
157
|
-
end
|
158
|
-
|
159
|
-
def sync(type)
|
160
|
-
@registry[type]
|
161
|
-
end
|
162
|
-
|
163
|
-
def queue(type, change, dir, path, options = {})
|
164
|
-
fail "Invalid type: #{type.inspect}" unless [:dir, :file].include? type
|
165
|
-
fail "Invalid change: #{change.inspect}" unless change.is_a?(Symbol)
|
166
|
-
fail "Invalid path: #{path.inspect}" unless path.is_a?(String)
|
167
|
-
if @options[:relative]
|
168
|
-
dir = begin
|
169
|
-
cwd = Pathname.pwd
|
170
|
-
dir.relative_path_from(cwd)
|
171
|
-
rescue ArgumentError
|
172
|
-
dir
|
173
|
-
end
|
174
|
-
end
|
175
|
-
@queue << [type, change, dir, path, options]
|
176
|
-
|
177
|
-
@last_queue_event_time = Time.now.to_f
|
178
|
-
_wakeup_wait_thread unless state == :paused
|
179
|
-
|
180
|
-
return unless @tcp_mode == :broadcaster
|
181
|
-
|
182
|
-
message = TCP::Message.new(type, change, dir, path, options)
|
183
|
-
registry[:broadcaster].async.broadcast(message.payload)
|
124
|
+
@silencer_controller.replace_with_only(regexps)
|
184
125
|
end
|
185
126
|
|
186
127
|
private
|
187
128
|
|
188
|
-
|
189
|
-
|
190
|
-
def _init_options(options = {})
|
191
|
-
{
|
192
|
-
# Listener options
|
193
|
-
debug: false,
|
194
|
-
wait_for_delay: 0.1,
|
195
|
-
relative: false,
|
196
|
-
|
197
|
-
# Backend selecting options
|
198
|
-
force_polling: false,
|
199
|
-
polling_fallback_message: nil,
|
200
|
-
|
201
|
-
}.merge(options)
|
202
|
-
end
|
203
|
-
|
204
|
-
def _debug_level
|
205
|
-
debugging = ENV['LISTEN_GEM_DEBUGGING'] || options[:debug]
|
206
|
-
case debugging.to_s
|
207
|
-
when /2/
|
208
|
-
Logger::DEBUG
|
209
|
-
when /true|yes|1/i
|
210
|
-
Logger::INFO
|
211
|
-
else
|
212
|
-
Logger::ERROR
|
213
|
-
end
|
214
|
-
end
|
215
|
-
|
216
|
-
def _init_actors
|
217
|
-
adapter_options = { mq: self, directories: directories }
|
218
|
-
|
219
|
-
@supervisor = Celluloid::SupervisionGroup.run!(registry)
|
220
|
-
supervisor.add(Record, as: :record, args: self)
|
221
|
-
supervisor.pool(Change, as: :change_pool, args: self)
|
222
|
-
|
223
|
-
# TODO: broadcaster should be a separate plugin
|
224
|
-
if @tcp_mode == :broadcaster
|
225
|
-
require 'listen/tcp/broadcaster'
|
226
|
-
|
227
|
-
# TODO: pass a TCP::Config class to make sure host and port are properly
|
228
|
-
# passed, even when nil
|
229
|
-
supervisor.add(TCP::Broadcaster, as: :broadcaster, args: [@host, @port])
|
230
|
-
|
231
|
-
# TODO: should be auto started, because if it crashes
|
232
|
-
# a new instance is spawned by supervisor, but it's 'start' isn't
|
233
|
-
# called
|
234
|
-
registry[:broadcaster].start
|
235
|
-
elsif @tcp_mode == :recipient
|
236
|
-
# TODO: adapter options should be configured in Listen.{on/to}
|
237
|
-
adapter_options.merge!(host: @host, port: @port)
|
238
|
-
end
|
239
|
-
|
240
|
-
# TODO: refactor
|
241
|
-
valid_adapter_options = _adapter_class.const_get(:DEFAULTS).keys
|
242
|
-
valid_adapter_options.each do |key|
|
243
|
-
adapter_options.merge!(key => options[key]) if options.key?(key)
|
244
|
-
end
|
245
|
-
|
246
|
-
supervisor.add(_adapter_class, as: :adapter, args: [adapter_options])
|
247
|
-
end
|
248
|
-
|
249
|
-
def _wait_for_changes
|
250
|
-
latency = options[:wait_for_delay]
|
251
|
-
|
252
|
-
loop do
|
253
|
-
break if state == :stopped
|
254
|
-
|
255
|
-
if state == :paused || @queue.empty?
|
256
|
-
sleep
|
257
|
-
break if state == :stopped
|
258
|
-
end
|
259
|
-
|
260
|
-
# Assure there's at least latency between callbacks to allow
|
261
|
-
# for accumulating changes
|
262
|
-
now = Time.now.to_f
|
263
|
-
diff = latency + (@last_queue_event_time || now) - now
|
264
|
-
if diff > 0
|
265
|
-
sleep diff
|
266
|
-
next
|
267
|
-
end
|
268
|
-
|
269
|
-
_process_changes unless state == :paused
|
270
|
-
end
|
271
|
-
rescue RuntimeError
|
272
|
-
Kernel.warn _format_error('exception while processing events: %s %s')
|
273
|
-
end
|
274
|
-
|
275
|
-
def _silenced?(path, type)
|
276
|
-
@silencer.silenced?(path, type)
|
277
|
-
end
|
278
|
-
|
279
|
-
def _start_adapter
|
280
|
-
# Don't run async, because configuration has to finish first
|
281
|
-
adapter = sync(:adapter)
|
282
|
-
adapter.start
|
283
|
-
end
|
284
|
-
|
285
|
-
def _adapter_class
|
286
|
-
@adapter_class ||= Adapter.select(options)
|
287
|
-
end
|
288
|
-
|
289
|
-
# for easier testing without sleep loop
|
290
|
-
def _process_changes
|
291
|
-
return if @queue.empty?
|
292
|
-
|
293
|
-
@last_queue_event_time = nil
|
294
|
-
|
295
|
-
changes = []
|
296
|
-
changes << @queue.pop until @queue.empty?
|
297
|
-
|
298
|
-
return if block.nil?
|
299
|
-
|
300
|
-
hash = _smoosh_changes(changes)
|
301
|
-
result = [hash[:modified], hash[:added], hash[:removed]]
|
302
|
-
|
303
|
-
block_start = Time.now.to_f
|
304
|
-
# TODO: condition not tested, but too complex to test ATM
|
305
|
-
block.call(*result) unless result.all?(&:empty?)
|
306
|
-
_debug "Callback took #{Time.now.to_f - block_start} seconds"
|
307
|
-
end
|
308
|
-
|
309
|
-
attr_reader :wait_thread
|
310
|
-
|
311
|
-
def _init_tcp_options(target)
|
312
|
-
# Handle TCP options here
|
313
|
-
require 'listen/tcp'
|
314
|
-
fail ArgumentError, 'missing host/port for TCP' unless target
|
315
|
-
|
316
|
-
if @tcp_mode == :recipient
|
317
|
-
@host = 'localhost'
|
318
|
-
@options[:force_tcp] = true
|
319
|
-
end
|
320
|
-
|
321
|
-
if target.is_a? Fixnum
|
322
|
-
@port = target
|
323
|
-
else
|
324
|
-
@host, port = target.split(':')
|
325
|
-
@port = port.to_i
|
326
|
-
end
|
327
|
-
end
|
328
|
-
|
329
|
-
def _reconfigure_silencer(extra_options)
|
330
|
-
@options.merge!(extra_options)
|
331
|
-
|
332
|
-
# TODO: this should be directory specific
|
333
|
-
rules = [:only, :ignore, :ignore!].map do |option|
|
334
|
-
[option, @options[option]] if @options.key? option
|
335
|
-
end
|
336
|
-
|
337
|
-
@silencer.configure(Hash[rules.compact])
|
338
|
-
end
|
339
|
-
|
340
|
-
def _start_wait_thread
|
341
|
-
@wait_thread = Thread.new { _wait_for_changes }
|
342
|
-
end
|
343
|
-
|
344
|
-
def _wakeup_wait_thread
|
345
|
-
wait_thread.wakeup if wait_thread && wait_thread.alive?
|
346
|
-
end
|
347
|
-
|
348
|
-
def _stop_wait_thread
|
349
|
-
return unless wait_thread
|
350
|
-
if wait_thread.alive?
|
351
|
-
wait_thread.wakeup
|
352
|
-
wait_thread.join
|
353
|
-
end
|
354
|
-
@wait_thread = nil
|
355
|
-
end
|
356
|
-
|
357
|
-
def _queue_raw_change(type, dir, rel_path, options)
|
358
|
-
_debug { "raw queue: #{[type, dir, rel_path, options].inspect}" }
|
359
|
-
|
360
|
-
unless (worker = async(:change_pool))
|
361
|
-
_warn 'Failed to allocate worker from change pool'
|
362
|
-
return
|
363
|
-
end
|
364
|
-
|
365
|
-
worker.change(type, dir, rel_path, options)
|
366
|
-
rescue RuntimeError
|
367
|
-
_error_exception "_queue_raw_change exception %s:\n%s:\n"
|
368
|
-
raise
|
369
|
-
end
|
129
|
+
attr_reader :processor
|
130
|
+
attr_reader :backend
|
370
131
|
end
|
371
132
|
end
|