ruby_ami 1.3.4 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 87d233c2b2c4e712089b0f2ce6c1f6ef75b877cf
4
- data.tar.gz: f04dff4a31a2b235babc6c8ab3c0ae5561574841
3
+ metadata.gz: cb7606f130bf3753537649990fbc188a62b31441
4
+ data.tar.gz: eef9574ee77d8721fb9b480978595ec412a82857
5
5
  SHA512:
6
- metadata.gz: d462a5e08948fdd719b76929d60da79983bafa4d4b9bc9155bc787a0937ec3ba930fffd91886f344a40944410186ddd6078b2b732e0d1c3356066a3786f23feb
7
- data.tar.gz: 7e2a487f30a366182462c0301960d864dadfa17235721038d1e261062c79646fde4d16f1c08ce4ff4ad52bdccf648cf2c59b4aa0ab5a74c394d39c8c90fdc973
6
+ metadata.gz: 29b7868e1c9e4a99babf6a169b97741b53ea05dfa5b8a0d31de0caa6e919912b8aab09863ec3c874c6e2fabec9e674789e9524212117a64c198b4b9799eba34e
7
+ data.tar.gz: 4213b880af9d91452b351b15ee5d55261e00954bacd79632f04467343e52371a1ee725d301216046d01ed83005b7f23e90eb8adce24ef63c94c9dce289be075e
data/.travis.yml CHANGED
@@ -2,6 +2,7 @@ language: ruby
2
2
  rvm:
3
3
  - 1.9.2
4
4
  - 1.9.3
5
+ - 2.0.0
5
6
  - jruby-19mode
6
7
  - rbx-19mode
7
8
  - ruby-head
data/CHANGELOG.md CHANGED
@@ -1,7 +1,10 @@
1
1
  # [develop](https://github.com/adhearsion/ruby_ami)
2
2
 
3
- # [1.3.4](https://github.com/adhearsion/ruby_ami/compare/v1.3.3...v1.3.4) - [2013-04-25](https://rubygems.org/gems/ruby_ami/versions/1.3.4)
4
- * Bugfix: Handle AGI 5xx responses
3
+ # [2.0.0](https://github.com/adhearsion/ruby_ami/compare/v1.3.3...v2.0.0) - [2013-04-15](https://rubygems.org/gems/ruby_ami/versions/2.0.0)
4
+ * Major refactoring for simplification and performance
5
+ * Actions are no longer synchronised on the wire since ActionID is now a reliable method of response/event association
6
+ * Callbacks are no longer required. #send_action now simply blocks waiting for a response
7
+ * Client still starts up two Streams, one for actions and one for events, but only for possible performance gains. It is possible to use Stream directly since it now does its own login and response association. Client is a very thin routing layer. It's encouraged that if you expect low traffic, you should use Stream directly. Client may be removed in v3.0.
5
8
 
6
9
  # [1.3.3](https://github.com/adhearsion/ruby_ami/compare/v1.3.2...v1.3.3) - [2013-04-09](https://rubygems.org/gems/ruby_ami/versions/1.3.3)
7
10
  * Bugfix: DBGet actions are now not terminated specially
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # RubyAMI [![Build Status](https://secure.travis-ci.org/adhearsion/ruby_ami.png?branch=master)](http://travis-ci.org/adhearsion/ruby_ami)
2
- RubyAMI is an AMI client library in Ruby and based on EventMachine with the sole purpose of providing an connection to the Asterisk Manager Interface. RubyAMI does not provide any features beyond connection management and protocol parsing. Actions are sent over the wire, and responses come back via callbacks. It's up to you to match these up into something useful. In this regard, RubyAMI is very similar to [Blather](https://github.com/sprsquish/blather) for XMPP or [Punchblock](https://github.com/adhearsion/punchblock), the Ruby 3PCC library. In fact, Punchblock uses RubyAMI under the covers for its Asterisk implementation, including an implementation of AsyncAGI.
2
+ RubyAMI is an AMI client library in Ruby and based on EventMachine with the sole purpose of providing a connection to the Asterisk Manager Interface. RubyAMI does not provide any features beyond connection management and protocol parsing. Actions are sent over the wire, and responses are returned. Events are passed to a callback you define. It's up to you to match these up into something useful. In this regard, RubyAMI is very similar to [Blather](https://github.com/sprsquish/blather) for XMPP or [Punchblock](https://github.com/adhearsion/punchblock), the Ruby 3PCC library. In fact, Punchblock uses RubyAMI under the covers for its Asterisk implementation, including an implementation of AsyncAGI.
3
3
 
4
4
  NB: If you're looking to develop an application on Asterisk, you should take a look at the [Adhearsion](http://adhearsion.com) framework first. This library is much lower level.
5
5
 
@@ -10,31 +10,30 @@ NB: If you're looking to develop an application on Asterisk, you should take a l
10
10
  ```ruby
11
11
  require 'ruby_ami'
12
12
 
13
- include RubyAMI
14
-
15
- client = Client.new :username => 'test',
16
- :password => 'test',
17
- :host => '127.0.0.1',
18
- :port => 5038,
19
- :event_handler => lambda { |e| handle_event e },
20
- :logger => Logger.new(STDOUT),
21
- :log_level => Logger::DEBUG,
22
- :timeout => 10
13
+ client = RubyAMI::Client.new username: 'test',
14
+ password: 'test',
15
+ host: '127.0.0.1',
16
+ port: 5038,
17
+ event_handler: ->(e) { handle_event e },
18
+ logger: Logger.new(STDOUT),
19
+ log_level: Logger::DEBUG,
20
+ timeout: 10
23
21
 
24
22
  def handle_event(event)
25
23
  case event.name
26
24
  when 'FullyBooted'
27
- client.send_action 'Originate', 'Channel' => 'SIP/foo'
25
+ client.async.send_action 'Originate', 'Channel' => 'SIP/foo'
28
26
  end
29
27
  end
30
28
 
31
29
  client.start
30
+
31
+ Celluloid::Actor.join client
32
32
  ```
33
33
 
34
34
  ## Development Requirements
35
35
 
36
- ruby_ami uses [ragel](http://www.complang.org/ragel/) to generate some of it's
37
- files.
36
+ ruby_ami uses [ragel](http://www.complang.org/ragel/) to generate some of it's files.
38
37
 
39
38
  On OS X (if you use homebrew):
40
39
 
@@ -64,4 +63,4 @@ Once you are inside the repository, before anything else, you will want to run:
64
63
 
65
64
  ## Copyright
66
65
 
67
- Copyright (c) 2011 Ben Langfeld, Jay Phillips. MIT licence (see LICENSE for details).
66
+ Copyright (c) 2013 Ben Langfeld, Jay Phillips. MIT licence (see LICENSE for details).
@@ -1,5 +1,23 @@
1
1
  FIXTURES = YAML.load_file File.dirname(__FILE__) + "/ami_fixtures.yml"
2
2
 
3
+ class Object
4
+ def metaclass
5
+ class << self
6
+ self
7
+ end
8
+ end
9
+
10
+ def meta_eval(&block)
11
+ metaclass.instance_eval &block
12
+ end
13
+
14
+ def meta_def(name, &block)
15
+ meta_eval do
16
+ define_method name, &block
17
+ end
18
+ end
19
+ end
20
+
3
21
  def fixture(path, overrides = {})
4
22
  path_segments = path.split '/'
5
23
  selected_event = path_segments.inject(FIXTURES.clone) do |hash, segment|
@@ -1,8 +1,6 @@
1
1
  module RubyAMI
2
2
  class Action
3
- attr_reader :name, :headers, :action_id
4
-
5
- attr_accessor :state
3
+ attr_reader :name, :headers, :action_id, :response
6
4
 
7
5
  CAUSAL_EVENT_NAMES = %w[queuestatus sippeers iaxpeers parkedcalls dahdishowchannels coreshowchannels
8
6
  dbget status agents konferencelist confbridgelist confbridgelistrooms] unless defined? CAUSAL_EVENT_NAMES
@@ -11,19 +9,14 @@ module RubyAMI
11
9
  @name = name.to_s.downcase.freeze
12
10
  @headers = headers.freeze
13
11
  @action_id = RubyAMI.new_uuid
14
- @response = FutureResource.new
15
- @response_callback = block
16
- @state = :new
12
+ @response = nil
13
+ @complete = false
17
14
  @events = []
18
- @event_lock = Mutex.new
19
- end
20
-
21
- [:new, :sent, :complete].each do |state|
22
- define_method("#{state}?") { @state == state }
15
+ @callback = block
23
16
  end
24
17
 
25
- def replies_with_action_id?
26
- !UnsupportedActionName::UNSUPPORTED_ACTION_NAMES.include? name
18
+ def complete?
19
+ @complete
27
20
  end
28
21
 
29
22
  ##
@@ -71,46 +64,20 @@ module RubyAMI
71
64
  )
72
65
  end
73
66
 
74
- #
75
- # If the response has simply not been received yet from Asterisk, the calling Thread will block until it comes
76
- # in. Once the response comes in, subsequent calls immediately return a reference to the ManagerInterfaceResponse
77
- # object.
78
- #
79
- def response(timeout = nil)
80
- @response.resource(timeout).tap do |resp|
81
- raise resp if resp.is_a? Exception
82
- end
83
- end
84
-
85
- def response=(other)
86
- @state = :complete
87
- @response.resource = other
88
- @response_callback.call other if @response_callback
89
- end
90
-
91
67
  def <<(message)
92
68
  case message
93
69
  when Error
94
70
  self.response = message
71
+ complete!
95
72
  when Event
96
73
  raise StandardError, 'This action should not trigger events. Maybe it is now a causal action? This is most likely a bug in RubyAMI' unless has_causal_events?
97
- @event_lock.synchronize do
98
- @events << message
99
- end
100
- self.response = @pending_response if message.name.downcase == causal_event_terminator_name
74
+ response.events << message
75
+ complete! if message.name.downcase == causal_event_terminator_name
101
76
  when Response
102
- if has_causal_events?
103
- @pending_response = message
104
- else
105
- self.response = message
106
- end
107
- end
108
- end
109
-
110
- def events
111
- @event_lock.synchronize do
112
- @events.dup
77
+ self.response = message
78
+ complete! unless has_causal_events?
113
79
  end
80
+ self
114
81
  end
115
82
 
116
83
  def eql?(other)
@@ -118,28 +85,15 @@ module RubyAMI
118
85
  end
119
86
  alias :== :eql?
120
87
 
121
- def sync_timeout
122
- name.downcase == 'originate' && !headers[:async] ? 60 : 10
123
- end
88
+ private
124
89
 
125
- ##
126
- # This class will be removed once this AMI library fully supports all known protocol anomalies.
127
- #
128
- class UnsupportedActionName < ArgumentError
129
- UNSUPPORTED_ACTION_NAMES = %w[queues] unless defined? UNSUPPORTED_ACTION_NAMES
130
-
131
- # Blacklist some actions depends on the Asterisk version
132
- def self.preinitialize(version)
133
- if version < 1.8
134
- %w[iaxpeers muteaudio mixmonitormute aocmessage].each do |action|
135
- UNSUPPORTED_ACTION_NAMES << action
136
- end
137
- end
138
- end
90
+ def response=(other)
91
+ @response = other
92
+ end
139
93
 
140
- def initialize(name)
141
- super "At the moment this AMI library doesn't support the #{name.inspect} action because it causes a protocol anomaly. Support for it will be coming shortly."
142
- end
94
+ def complete!
95
+ @complete = true
96
+ @callback.call response if @callback
143
97
  end
144
98
  end
145
99
  end # RubyAMI
@@ -4,7 +4,7 @@ module RubyAMI
4
4
  class AGIResultParser
5
5
  attr_reader :code, :result, :data
6
6
 
7
- FORMAT = /^(?<code>\d{3})( result=(?<result>-?\d*))? ?(?<data>\(?.*\)?)?$/.freeze
7
+ FORMAT = /^(?<code>\d{3}) result=(?<result>-?\d*) ?(?<data>\(?.*\)?)?$/.freeze
8
8
  DATA_KV_FORMAT = /(?<key>[\w\d]+)=(?<value>[\w\d]*)/.freeze
9
9
  DATA_CLEANER = /(^\()|(\)$)/.freeze
10
10
 
@@ -31,7 +31,7 @@ module RubyAMI
31
31
 
32
32
  def parse
33
33
  @code = match[:code].to_i
34
- @result = match[:result] ? match[:result].to_i : nil
34
+ @result = match[:result].to_i
35
35
  @data = match[:data] ? match[:data].gsub(DATA_CLEANER, '').freeze : nil
36
36
  end
37
37
 
@@ -1,44 +1,19 @@
1
1
  # encoding: utf-8
2
2
  module RubyAMI
3
3
  class Client
4
- attr_reader :options, :action_queue, :events_stream, :actions_stream
4
+ include Celluloid
5
5
 
6
- def initialize(options)
7
- @options = options
8
- @logger = options[:logger] || Logger.new(STDOUT)
9
- @logger.level = options[:log_level] || Logger::DEBUG if @logger
10
- @event_handler = @options[:event_handler]
11
- @state = :stopped
12
-
13
- if RubyAMI.rbx?
14
- logger.warn 'The "timeout" parameter is not supported when using Rubinius'
15
- end
16
-
17
- stop_writing_actions
18
-
19
- @pending_actions = {}
20
- @sent_actions = {}
21
- @actions_lock = Mutex.new
22
-
23
- @action_queue = GirlFriday::WorkQueue.new(:actions, :size => 1, :error_handler => ErrorHandler) do |action|
24
- @actions_write_blocker.wait
25
- _send_action action
26
- begin
27
- action.response action.sync_timeout
28
- rescue Timeout::Error => e
29
- logger.error "Timed out waiting for a response to #{action}"
30
- rescue RubyAMI::Error
31
- nil
32
- end
33
- end
6
+ trap_exit :stream_died
34
7
 
35
- @message_processor = GirlFriday::WorkQueue.new(:messages, :size => 1, :error_handler => ErrorHandler) do |message|
36
- handle_message message
37
- end
8
+ attr_reader :events_stream, :actions_stream
38
9
 
39
- @event_processor = GirlFriday::WorkQueue.new(:events, :size => 2, :error_handler => ErrorHandler) do |event|
40
- handle_event event
41
- end
10
+ def initialize(options)
11
+ @options = options
12
+ @event_handler = @options[:event_handler]
13
+ @state = :stopped
14
+ client = current_actor
15
+ @events_stream = new_stream ->(event) { client.async.handle_event event }
16
+ @actions_stream = new_stream ->(message) { client.async.handle_message message }
42
17
  end
43
18
 
44
19
  [:started, :stopped, :ready].each do |state|
@@ -46,162 +21,56 @@ module RubyAMI
46
21
  end
47
22
 
48
23
  def start
49
- @events_stream = new_stream lambda { |event| @event_processor << event }
50
- @actions_stream = new_stream lambda { |message| @message_processor << message }
51
- streams.each { |stream| stream.async.run }
24
+ @events_stream.async.run
25
+ @actions_stream.async.run
52
26
  @state = :started
53
- streams.each { |s| Celluloid::Actor.join s }
54
- end
55
-
56
- def stop
57
- streams.each do |stream|
58
- begin
59
- stream.terminate if stream.alive?
60
- rescue => e
61
- logger.error e if logger
62
- end
63
- end
64
27
  end
65
28
 
66
- def send_action(action, headers = {}, &block)
67
- (action.is_a?(Action) ? action : Action.new(action, headers, &block)).tap do |action|
68
- logger.trace "[QUEUE]: #{action.inspect}" if logger
69
- register_pending_action action
70
- action_queue << action
71
- end
29
+ def send_action(*args)
30
+ actions_stream.send_action *args
72
31
  end
73
32
 
74
33
  def handle_message(message)
75
- logger.trace "[RECV-ACTIONS]: #{message.inspect}" if logger
34
+ logger.trace "[RECV-ACTIONS]: #{message.inspect}"
76
35
  case message
77
36
  when Stream::Connected
78
- login_actions
37
+ send_action 'Events', 'EventMask' => 'Off'
79
38
  when Stream::Disconnected
80
- stop_writing_actions
81
- stop
82
39
  when Event
83
- action = @current_action_with_causal_events
84
- if action
85
- message.action = action
86
- action << message
87
- @current_action_with_causal_events = nil if action.complete?
88
- else
89
- if message.name == 'FullyBooted'
90
- pass_event message
91
- start_writing_actions
92
- else
93
- 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}"
94
- end
95
- end
96
- when Response, Error
97
- action = sent_action_with_id message.action_id
98
- raise StandardError, "Received an AMI response with an unrecognized ActionID!! This may be an bug! #{message.inspect}" unless action
99
- message.action = action
100
-
101
- # By this point the write loop will already have started blocking by calling the response() method on the
102
- # action. Because we must collect more events before we wake the write loop up again, let's create these
103
- # instance variable which will needed when the subsequent causal events come in.
104
- @current_action_with_causal_events = action if action.has_causal_events?
105
-
106
- action << message
40
+ pass_event message
107
41
  end
108
42
  end
109
43
 
110
44
  def handle_event(event)
111
- logger.trace "[RECV-EVENTS]: #{event.inspect}" if logger
45
+ logger.trace "[RECV-EVENTS]: #{event.inspect}"
112
46
  case event
113
- when Stream::Connected
114
- login_events
115
- when Stream::Disconnected
116
- stop
47
+ when Stream::Connected, Stream::Disconnected
117
48
  else
118
49
  pass_event event
119
50
  end
120
51
  end
121
52
 
122
- def _send_action(action)
123
- logger.trace "[SEND]: #{action.inspect}" if logger
124
- transition_action_to_sent action
125
- actions_stream.send_action action
126
- action.state = :sent
127
- action
128
- end
129
-
130
53
  private
131
54
 
132
55
  def pass_event(event)
133
56
  @event_handler.call event if @event_handler.respond_to? :call
134
57
  end
135
58
 
136
- def register_pending_action(action)
137
- @actions_lock.synchronize do
138
- @pending_actions[action.action_id] = action
139
- end
140
- end
141
-
142
- def transition_action_to_sent(action)
143
- @actions_lock.synchronize do
144
- @pending_actions.delete action.action_id
145
- @sent_actions[action.action_id] = action
146
- end
147
- end
148
-
149
- def sent_action_with_id(action_id)
150
- @actions_lock.synchronize do
151
- @sent_actions.delete action_id
152
- end
153
- end
154
-
155
- def start_writing_actions
156
- @actions_write_blocker.countdown!
157
- end
158
-
159
- def stop_writing_actions
160
- @actions_write_blocker = CountDownLatch.new 1
161
- end
162
-
163
- def login_actions
164
- action = login_action do |response|
165
- pass_event response if response.is_a? Error
166
- send_action 'Events', 'EventMask' => 'Off'
167
- end
168
-
169
- register_pending_action action
170
- Thread.new { _send_action action }
171
- end
172
-
173
- def login_events
174
- login_action.tap do |action|
175
- events_stream.send_action action
176
- end
177
- end
178
-
179
- def login_action(&block)
180
- Action.new 'Login',
181
- 'Username' => options[:username],
182
- 'Secret' => options[:password],
183
- 'Events' => 'On',
184
- &block
59
+ def new_stream(callback)
60
+ Stream.new_link @options[:host], @options[:port], @options[:username], @options[:password], callback, logger, @options[:timeout]
185
61
  end
186
62
 
187
- def new_stream(callback)
188
- Stream.new @options[:host], @options[:port], callback, logger, @options[:timeout]
63
+ def stream_died(stream, reason = nil)
64
+ terminate
189
65
  end
190
66
 
191
67
  def logger
192
68
  super
193
- rescue NoMethodError
194
- @logger
195
- end
196
-
197
- def streams
198
- [actions_stream, events_stream].compact
199
- end
200
-
201
- class ErrorHandler
202
- def handle(error)
203
- puts error.message
204
- puts error.backtrace.join("\n")
69
+ rescue
70
+ @logger ||= begin
71
+ logger = Logger
72
+ logger.define_singleton_method :trace, logger.method(:debug)
73
+ logger
205
74
  end
206
75
  end
207
76
  end
@@ -3,8 +3,8 @@ module RubyAMI
3
3
  class Error < StandardError
4
4
  attr_accessor :message, :action
5
5
 
6
- def initialize
7
- @headers = Hash.new
6
+ def initialize(headers = {})
7
+ @headers = headers
8
8
  end
9
9
 
10
10
  def [](key)
@@ -5,8 +5,8 @@ module RubyAMI
5
5
  class Event < Response
6
6
  attr_reader :name
7
7
 
8
- def initialize(name)
9
- super()
8
+ def initialize(name, headers = {})
9
+ super headers
10
10
  @name = name
11
11
  end
12
12
 
@@ -3,8 +3,6 @@ module RubyAMI
3
3
  ##
4
4
  # This is the object containing a response from Asterisk.
5
5
  #
6
- # Note: not all responses have an ActionID!
7
- #
8
6
  class Response
9
7
  class << self
10
8
  def from_immediate_response(text)
@@ -14,12 +12,12 @@ module RubyAMI
14
12
  end
15
13
  end
16
14
 
17
- attr_accessor :action,
18
- :text_body # For "Response: Follows" sections
19
- attr_reader :events
15
+ attr_accessor :text_body, # For "Response: Follows" sections
16
+ :events
20
17
 
21
- def initialize
22
- @headers = Hash.new
18
+ def initialize(headers = {})
19
+ @headers = headers
20
+ @events = []
23
21
  end
24
22
 
25
23
  def has_text_body?
@@ -47,7 +45,7 @@ module RubyAMI
47
45
  end
48
46
 
49
47
  def inspect_attributes
50
- [:headers, :text_body, :events, :action]
48
+ [:headers, :text_body, :events]
51
49
  end
52
50
 
53
51
  def eql?(o, *fields)