ruby_ami 0.1.1
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.
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile +9 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +20 -0
- data/README.md +49 -0
- data/Rakefile +69 -0
- data/cucumber.yml +2 -0
- data/features/lexer.feature +260 -0
- data/features/step_definitions/lexer_steps.rb +207 -0
- data/features/support/ami_fixtures.yml +30 -0
- data/features/support/env.rb +16 -0
- data/features/support/introspective_lexer.rb +22 -0
- data/features/support/lexer_helper.rb +103 -0
- data/lib/ruby_ami.rb +29 -0
- data/lib/ruby_ami/Guardfile +6 -0
- data/lib/ruby_ami/action.rb +143 -0
- data/lib/ruby_ami/client.rb +187 -0
- data/lib/ruby_ami/error.rb +21 -0
- data/lib/ruby_ami/event.rb +10 -0
- data/lib/ruby_ami/lexer.rl.rb +302 -0
- data/lib/ruby_ami/lexer_machine.rl +87 -0
- data/lib/ruby_ami/metaprogramming.rb +17 -0
- data/lib/ruby_ami/response.rb +44 -0
- data/lib/ruby_ami/stream.rb +60 -0
- data/lib/ruby_ami/version.rb +3 -0
- data/ruby_ami.gemspec +40 -0
- data/spec/ruby_ami/action_spec.rb +163 -0
- data/spec/ruby_ami/client_spec.rb +324 -0
- data/spec/ruby_ami/error_spec.rb +7 -0
- data/spec/ruby_ami/event_spec.rb +7 -0
- data/spec/ruby_ami/response_spec.rb +7 -0
- data/spec/ruby_ami/stream_spec.rb +153 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/support/mock_server.rb +16 -0
- metadata +296 -0
@@ -0,0 +1,187 @@
|
|
1
|
+
module RubyAMI
|
2
|
+
class Client
|
3
|
+
attr_reader :options, :action_queue, :events_stream, :actions_stream
|
4
|
+
|
5
|
+
def initialize(options)
|
6
|
+
@options = options
|
7
|
+
@logger = options[:logger]
|
8
|
+
@logger.level = options[:log_level] || Logger::DEBUG if @logger
|
9
|
+
@event_handler = @options[:event_handler]
|
10
|
+
@state = :stopped
|
11
|
+
|
12
|
+
stop_writing_actions
|
13
|
+
|
14
|
+
@pending_actions = {}
|
15
|
+
@sent_actions = {}
|
16
|
+
@actions_lock = Mutex.new
|
17
|
+
|
18
|
+
@action_queue = GirlFriday::WorkQueue.new(:actions, :size => 1, :error_handler => ErrorHandler) do |action|
|
19
|
+
@actions_write_blocker.wait
|
20
|
+
_send_action action
|
21
|
+
begin
|
22
|
+
action.response
|
23
|
+
rescue RubyAMI::Error
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
@message_processor = GirlFriday::WorkQueue.new(:messages, :size => 1, :error_handler => ErrorHandler) do |message|
|
29
|
+
handle_message message
|
30
|
+
end
|
31
|
+
|
32
|
+
@event_processor = GirlFriday::WorkQueue.new(:events, :size => 2, :error_handler => ErrorHandler) do |event|
|
33
|
+
handle_event event
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
[:started, :stopped, :ready].each do |state|
|
38
|
+
define_method("#{state}?") { @state == state }
|
39
|
+
end
|
40
|
+
|
41
|
+
def start
|
42
|
+
EventMachine.run do
|
43
|
+
yield if block_given?
|
44
|
+
@events_stream = start_stream lambda { |event| @event_processor << event }
|
45
|
+
@actions_stream = start_stream lambda { |message| @message_processor << message }
|
46
|
+
@state = :started
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def stop
|
51
|
+
streams.each { |s| s.close_connection_after_writing }
|
52
|
+
end
|
53
|
+
|
54
|
+
def send_action(action, headers = {}, &block)
|
55
|
+
(action.is_a?(Action) ? action : Action.new(action, headers, &block)).tap do |action|
|
56
|
+
logger.trace "[QUEUE]: #{action.inspect}" if logger
|
57
|
+
register_pending_action action
|
58
|
+
action_queue << action
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def handle_message(message)
|
63
|
+
logger.trace "[RECV-ACTIONS]: #{message.inspect}" if logger
|
64
|
+
case message
|
65
|
+
when Stream::Connected
|
66
|
+
start_writing_actions
|
67
|
+
login_actions
|
68
|
+
when Stream::Disconnected
|
69
|
+
stop_writing_actions
|
70
|
+
unbind
|
71
|
+
when Event
|
72
|
+
action = @current_action_with_causal_events
|
73
|
+
raise StandardError, "Got an unexpected event on actions socket! This AMI command may have a multi-message response. Try making Adhearsion treat it as causal action #{message.inspect}" unless action
|
74
|
+
message.action = action
|
75
|
+
action << message
|
76
|
+
@current_action_with_causal_events = nil if action.complete?
|
77
|
+
when Response, Error
|
78
|
+
action = sent_action_with_id message.action_id
|
79
|
+
raise StandardError, "Received an AMI response with an unrecognized ActionID!! This may be an bug! #{message.inspect}" unless action
|
80
|
+
message.action = action
|
81
|
+
|
82
|
+
# By this point the write loop will already have started blocking by calling the response() method on the
|
83
|
+
# action. Because we must collect more events before we wake the write loop up again, let's create these
|
84
|
+
# instance variable which will needed when the subsequent causal events come in.
|
85
|
+
@current_action_with_causal_events = action if action.has_causal_events?
|
86
|
+
|
87
|
+
action << message
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def handle_event(event)
|
92
|
+
logger.trace "[RECV-EVENTS]: #{event.inspect}" if logger
|
93
|
+
pass_event event
|
94
|
+
case event
|
95
|
+
when Stream::Connected
|
96
|
+
login_events
|
97
|
+
when Stream::Disconnected
|
98
|
+
unbind
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def _send_action(action)
|
103
|
+
logger.trace "[SEND]: #{action.inspect}" if logger
|
104
|
+
transition_action_to_sent action
|
105
|
+
actions_stream.send_action action
|
106
|
+
action.state = :sent
|
107
|
+
end
|
108
|
+
|
109
|
+
def unbind
|
110
|
+
EM.reactor_running? && EM.stop
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def pass_event(event)
|
116
|
+
@event_handler.call event if @event_handler.respond_to? :call
|
117
|
+
end
|
118
|
+
|
119
|
+
def register_pending_action(action)
|
120
|
+
@actions_lock.synchronize do
|
121
|
+
@pending_actions[action.action_id] = action
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def transition_action_to_sent(action)
|
126
|
+
@actions_lock.synchronize do
|
127
|
+
@pending_actions.delete action.action_id
|
128
|
+
@sent_actions[action.action_id] = action
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def sent_action_with_id(action_id)
|
133
|
+
@actions_lock.synchronize do
|
134
|
+
@sent_actions.delete action_id
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def start_writing_actions
|
139
|
+
@actions_write_blocker.countdown!
|
140
|
+
end
|
141
|
+
|
142
|
+
def stop_writing_actions
|
143
|
+
@actions_write_blocker = CountDownLatch.new 1
|
144
|
+
end
|
145
|
+
|
146
|
+
def login_actions
|
147
|
+
login_action do |response|
|
148
|
+
pass_event response if response.is_a? Error
|
149
|
+
end.tap { |action| send_action action }
|
150
|
+
end
|
151
|
+
|
152
|
+
def login_events
|
153
|
+
login_action('On').tap do |action|
|
154
|
+
events_stream.send_action action
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def login_action(events = 'Off', &block)
|
159
|
+
Action.new 'Login',
|
160
|
+
'Username' => options[:username],
|
161
|
+
'Secret' => options[:password],
|
162
|
+
'Events' => events,
|
163
|
+
&block
|
164
|
+
end
|
165
|
+
|
166
|
+
def start_stream(callback)
|
167
|
+
Stream.start @options[:host], @options[:port], callback
|
168
|
+
end
|
169
|
+
|
170
|
+
def logger
|
171
|
+
super
|
172
|
+
rescue NoMethodError
|
173
|
+
@logger
|
174
|
+
end
|
175
|
+
|
176
|
+
def streams
|
177
|
+
[actions_stream, events_stream]
|
178
|
+
end
|
179
|
+
|
180
|
+
class ErrorHandler
|
181
|
+
def handle(error)
|
182
|
+
puts error.message
|
183
|
+
puts error.backtrace.join("\n")
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module RubyAMI
|
2
|
+
class Error < StandardError
|
3
|
+
attr_accessor :message, :action
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@headers = HashWithIndifferentAccess.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def [](key)
|
10
|
+
@headers[key]
|
11
|
+
end
|
12
|
+
|
13
|
+
def []=(key,value)
|
14
|
+
@headers[key] = value
|
15
|
+
end
|
16
|
+
|
17
|
+
def action_id
|
18
|
+
@headers['ActionID']
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end # RubyAMI
|
@@ -0,0 +1,302 @@
|
|
1
|
+
module RubyAMI
|
2
|
+
class Lexer
|
3
|
+
|
4
|
+
BUFFER_SIZE = 128.kilobytes unless defined? BUFFER_SIZE
|
5
|
+
|
6
|
+
##
|
7
|
+
# IMPORTANT! See method documentation for adjust_pointers!
|
8
|
+
#
|
9
|
+
# @see adjust_pointers
|
10
|
+
#
|
11
|
+
POINTERS = [
|
12
|
+
:@current_pointer,
|
13
|
+
:@token_start,
|
14
|
+
:@token_end,
|
15
|
+
:@version_start,
|
16
|
+
:@event_name_start,
|
17
|
+
:@current_key_position,
|
18
|
+
:@current_value_position,
|
19
|
+
:@last_seen_value_end,
|
20
|
+
:@error_reason_start,
|
21
|
+
:@follows_text_start,
|
22
|
+
:@current_syntax_error_start,
|
23
|
+
:@immediate_response_start
|
24
|
+
]
|
25
|
+
|
26
|
+
%%{
|
27
|
+
machine ami_protocol_parser;
|
28
|
+
|
29
|
+
# All required Ragel actions are implemented as Ruby methods.
|
30
|
+
|
31
|
+
# Executed after a "Response: Success" or "Response: Pong"
|
32
|
+
action init_success { init_success }
|
33
|
+
|
34
|
+
action init_response_follows { init_response_follows }
|
35
|
+
|
36
|
+
action init_error { init_error }
|
37
|
+
|
38
|
+
action message_received { message_received @current_message }
|
39
|
+
action error_received { error_received @current_message }
|
40
|
+
|
41
|
+
action version_starts { version_starts }
|
42
|
+
action version_stops { version_stops }
|
43
|
+
|
44
|
+
action key_starts { key_starts }
|
45
|
+
action key_stops { key_stops }
|
46
|
+
|
47
|
+
action value_starts { value_starts }
|
48
|
+
action value_stops { value_stops }
|
49
|
+
|
50
|
+
action error_reason_starts { error_reason_starts }
|
51
|
+
action error_reason_stops { error_reason_stops }
|
52
|
+
|
53
|
+
action syntax_error_starts { syntax_error_starts }
|
54
|
+
action syntax_error_stops { syntax_error_stops }
|
55
|
+
|
56
|
+
action immediate_response_starts { immediate_response_starts }
|
57
|
+
action immediate_response_stops { immediate_response_stops }
|
58
|
+
|
59
|
+
action follows_text_starts { follows_text_starts }
|
60
|
+
action follows_text_stops { follows_text_stops }
|
61
|
+
|
62
|
+
action event_name_starts { event_name_starts }
|
63
|
+
action event_name_stops { event_name_stops }
|
64
|
+
|
65
|
+
include ami_protocol_parser_machine "lexer_machine.rl";
|
66
|
+
|
67
|
+
}%%##
|
68
|
+
|
69
|
+
attr_accessor :ami_version
|
70
|
+
|
71
|
+
def initialize(delegate = nil)
|
72
|
+
@delegate = delegate
|
73
|
+
@data = ""
|
74
|
+
@current_pointer = 0
|
75
|
+
@ragel_stack = []
|
76
|
+
@ami_version = 0.0
|
77
|
+
|
78
|
+
%%{
|
79
|
+
# All other variables become local, letting Ruby garbage collect them. This
|
80
|
+
# prevents us from having to manually reset them.
|
81
|
+
|
82
|
+
variable data @data;
|
83
|
+
variable p @current_pointer;
|
84
|
+
variable pe @data_ending_pointer;
|
85
|
+
variable cs @current_state;
|
86
|
+
variable ts @token_start;
|
87
|
+
variable te @token_end;
|
88
|
+
variable act @ragel_act;
|
89
|
+
variable eof @eof;
|
90
|
+
variable stack @ragel_stack;
|
91
|
+
variable top @ragel_stack_top;
|
92
|
+
|
93
|
+
write data;
|
94
|
+
write init;
|
95
|
+
}%%##
|
96
|
+
end
|
97
|
+
|
98
|
+
def <<(new_data)
|
99
|
+
extend_buffer_with new_data
|
100
|
+
resume!
|
101
|
+
end
|
102
|
+
|
103
|
+
def resume!
|
104
|
+
%%{ write exec; }%%##
|
105
|
+
end
|
106
|
+
|
107
|
+
def extend_buffer_with(new_data)
|
108
|
+
length = new_data.size
|
109
|
+
|
110
|
+
if length > BUFFER_SIZE
|
111
|
+
raise Exception, "ERROR: Buffer overrun! Input size (#{new_data.size}) larger than buffer (#{BUFFER_SIZE})"
|
112
|
+
end
|
113
|
+
|
114
|
+
if length + @data.size > BUFFER_SIZE
|
115
|
+
if @data.size != @current_pointer
|
116
|
+
if @current_pointer < length
|
117
|
+
# We are about to shift more bytes off the array than we have
|
118
|
+
# parsed. This will cause the parser to lose state so
|
119
|
+
# integrity cannot be guaranteed.
|
120
|
+
raise Exception, "ERROR: Buffer overrun! AMI parser cannot guarantee sanity. New data size: #{new_data.size}; Current pointer at #{@current_pointer}; Data size: #{@data.size}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
@data.slice! 0...length
|
124
|
+
adjust_pointers -length
|
125
|
+
end
|
126
|
+
@data << new_data
|
127
|
+
@data_ending_pointer = @data.size
|
128
|
+
end
|
129
|
+
|
130
|
+
protected
|
131
|
+
|
132
|
+
##
|
133
|
+
# This method will adjust all pointers into the buffer according
|
134
|
+
# to the supplied offset. This is necessary any time the buffer
|
135
|
+
# changes, for example when the sliding window is incremented forward
|
136
|
+
# after new data is received.
|
137
|
+
#
|
138
|
+
# It is VERY IMPORTANT that when any additional pointers are defined
|
139
|
+
# that they are added to this method. Unpredictable results may
|
140
|
+
# otherwise occur!
|
141
|
+
#
|
142
|
+
# @see https://adhearsion.lighthouseapp.com/projects/5871-adhearsion/tickets/72-ami-lexer-buffer-offset#ticket-72-26
|
143
|
+
#
|
144
|
+
# @param offset Adjust pointers by offset. May be negative.
|
145
|
+
#
|
146
|
+
def adjust_pointers(offset)
|
147
|
+
POINTERS.each do |ptr|
|
148
|
+
value = instance_variable_get(ptr)
|
149
|
+
instance_variable_set(ptr, value + offset) if !value.nil?
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
##
|
154
|
+
# Called after a response or event has been successfully parsed.
|
155
|
+
#
|
156
|
+
# @param [Response, Event] message The message just received
|
157
|
+
#
|
158
|
+
def message_received(message)
|
159
|
+
@delegate.message_received message
|
160
|
+
end
|
161
|
+
|
162
|
+
##
|
163
|
+
# Called when there is an Error: stanza on the socket. Could be caused by executing an unrecognized command, trying
|
164
|
+
# to originate into an invalid priority, etc. Note: many errors' responses are actually tightly coupled to a
|
165
|
+
# Event which comes directly after it. Often the message will say something like "Channel status
|
166
|
+
# will follow".
|
167
|
+
#
|
168
|
+
# @param [String] reason The reason given in the Message: header for the error stanza.
|
169
|
+
#
|
170
|
+
def error_received(message)
|
171
|
+
@delegate.error_received message
|
172
|
+
end
|
173
|
+
|
174
|
+
##
|
175
|
+
# Called when there's a syntax error on the socket. This doesn't happen as often as it should because, in many cases,
|
176
|
+
# it's impossible to distinguish between a syntax error and an immediate packet.
|
177
|
+
#
|
178
|
+
# @param [String] ignored_chunk The offending text which caused the syntax error.
|
179
|
+
def syntax_error_encountered(ignored_chunk)
|
180
|
+
@delegate.syntax_error_encountered ignored_chunk
|
181
|
+
end
|
182
|
+
|
183
|
+
def init_success
|
184
|
+
@current_message = Response.new
|
185
|
+
end
|
186
|
+
|
187
|
+
def init_response_follows
|
188
|
+
@current_message = Response.new
|
189
|
+
end
|
190
|
+
|
191
|
+
def init_error
|
192
|
+
@current_message = Error.new
|
193
|
+
end
|
194
|
+
|
195
|
+
def version_starts
|
196
|
+
@version_start = @current_pointer
|
197
|
+
end
|
198
|
+
|
199
|
+
def version_stops
|
200
|
+
self.ami_version = @data[@version_start...@current_pointer].to_f
|
201
|
+
@version_start = nil
|
202
|
+
end
|
203
|
+
|
204
|
+
def event_name_starts
|
205
|
+
@event_name_start = @current_pointer
|
206
|
+
end
|
207
|
+
|
208
|
+
def event_name_stops
|
209
|
+
event_name = @data[@event_name_start...@current_pointer]
|
210
|
+
@event_name_start = nil
|
211
|
+
@current_message = Event.new(event_name)
|
212
|
+
end
|
213
|
+
|
214
|
+
def key_starts
|
215
|
+
@current_key_position = @current_pointer
|
216
|
+
end
|
217
|
+
|
218
|
+
def key_stops
|
219
|
+
@current_key = @data[@current_key_position...@current_pointer]
|
220
|
+
end
|
221
|
+
|
222
|
+
def value_starts
|
223
|
+
@current_value_position = @current_pointer
|
224
|
+
end
|
225
|
+
|
226
|
+
def value_stops
|
227
|
+
@current_value = @data[@current_value_position...@current_pointer]
|
228
|
+
@last_seen_value_end = @current_pointer + 2 # 2 for \r\n
|
229
|
+
add_pair_to_current_message
|
230
|
+
end
|
231
|
+
|
232
|
+
def error_reason_starts
|
233
|
+
@error_reason_start = @current_pointer
|
234
|
+
end
|
235
|
+
|
236
|
+
def error_reason_stops
|
237
|
+
@current_message.message = @data[@error_reason_start...@current_pointer]
|
238
|
+
end
|
239
|
+
|
240
|
+
def follows_text_starts
|
241
|
+
@follows_text_start = @current_pointer
|
242
|
+
end
|
243
|
+
|
244
|
+
def follows_text_stops
|
245
|
+
text = @data[@last_seen_value_end..@current_pointer]
|
246
|
+
text.sub! /\r?\n--END COMMAND--/, ""
|
247
|
+
@current_message.text_body = text
|
248
|
+
@follows_text_start = nil
|
249
|
+
end
|
250
|
+
|
251
|
+
def add_pair_to_current_message
|
252
|
+
@current_message[@current_key] = @current_value
|
253
|
+
reset_key_and_value_positions
|
254
|
+
end
|
255
|
+
|
256
|
+
def reset_key_and_value_positions
|
257
|
+
@current_key, @current_value, @current_key_position, @current_value_position = nil
|
258
|
+
end
|
259
|
+
|
260
|
+
def syntax_error_starts
|
261
|
+
@current_syntax_error_start = @current_pointer # Adding 1 since the pointer is still set to the last successful match
|
262
|
+
end
|
263
|
+
|
264
|
+
def syntax_error_stops
|
265
|
+
# Subtracting 3 from @current_pointer below for "\r\n" which separates a stanza
|
266
|
+
offending_data = @data[@current_syntax_error_start...@current_pointer - 1]
|
267
|
+
syntax_error_encountered offending_data
|
268
|
+
@current_syntax_error_start = nil
|
269
|
+
end
|
270
|
+
|
271
|
+
def immediate_response_starts
|
272
|
+
@immediate_response_start = @current_pointer
|
273
|
+
end
|
274
|
+
|
275
|
+
def immediate_response_stops
|
276
|
+
message = @data[@immediate_response_start...(@current_pointer -1)]
|
277
|
+
message_received Response.from_immediate_response(message)
|
278
|
+
end
|
279
|
+
|
280
|
+
##
|
281
|
+
# This method is used primarily in debugging.
|
282
|
+
#
|
283
|
+
def view_buffer(message = nil)
|
284
|
+
message ||= "Viewing the buffer"
|
285
|
+
|
286
|
+
buffer = @data.clone
|
287
|
+
buffer.insert(@current_pointer, "\033[0;31m\033[1;31m^\033[0m")
|
288
|
+
|
289
|
+
buffer.gsub!("\r", "\\\\r")
|
290
|
+
buffer.gsub!("\n", "\\n\n")
|
291
|
+
|
292
|
+
puts <<-INSPECTION
|
293
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
294
|
+
#### #{message}
|
295
|
+
#############################
|
296
|
+
#{buffer}
|
297
|
+
#############################
|
298
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
299
|
+
INSPECTION
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|