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,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
|