ruby_ami 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,10 @@
1
+ module RubyAMI
2
+ class Event < Response
3
+ attr_reader :name
4
+
5
+ def initialize(name)
6
+ super()
7
+ @name = name
8
+ end
9
+ end
10
+ 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