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.
@@ -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