lib/adhearsion/voip/asterisk/manager_interface.rb in adhearsion-0.8.3 vs lib/adhearsion/voip/asterisk/manager_interface.rb in adhearsion-0.8.4
- old
+ new
@@ -1,168 +1,168 @@
require 'adhearsion/voip/asterisk/manager_interface/ami_lexer'
module Adhearsion
module VoIP
module Asterisk
-
+
##
# Sorry, this AMI class has been deprecated. Please see http://docs.adhearsion.com/Asterisk_Manager_Interface for
# documentation on the new way of handling AMI. This new version is much better and should not require an enormous
# migration on your part.
#
class AMI
def initialize
- raise "Sorry, this AMI class has been deprecated. Please see http://docs.adhearsion.com/Asterisk_Manager_Interface for documentation on the new way of handling AMI. This new version is much better and should not require an enormous migration on your part."
+ raise "Sorry, this AMI class has been deprecated. Please see http://docs.adhearsion.com/display/adhearsion/Asterisk+Manager+Interface for documentation on the new way of handling AMI. This new version is much better and should not require an enormous migration on your part."
end
end
-
+
mattr_accessor :manager_interface
-
+
module Manager
-
+
##
# This class abstracts a connection to the Asterisk Manager Interface. Its purpose is, first and foremost, to make
# the protocol consistent. Though the classes employed to assist this class (ManagerInterfaceAction,
# ManagerInterfaceResponse, ManagerInterfaceError, etc.) are relatively user-friendly, they're designed to be a
# building block on which to build higher-level abstractions of the Asterisk Manager Interface.
#
# For a higher-level abstraction of the Asterisk Manager Interface, see the SuperManager class.
#
class ManagerInterface
-
+
CAUSAL_EVENT_NAMES = ["queuestatus", "sippeers", "parkedcalls", "status", "dahdishowchannels"] unless defined? CAUSAL_EVENT_NAMES
-
+
class << self
-
+
def connect(*args)
returning new(*args) do |connection|
connection.connect!
end
end
-
+
def replies_with_action_id?(name, headers={})
name = name.to_s.downcase
# TODO: Expand this case statement
case name
when "queues", "iaxpeers"
false
else
true
- end
+ end
end
-
+
##
# When sending an action with "causal events" (i.e. events which must be collected to form a proper
# response), AMI should send a particular event which instructs us that no more events will be sent.
# This event is called the "causal event terminator".
#
- # Note: you must supply both the name of the event and any headers because it's possible that some uses of an
+ # Note: you must supply both the name of the event and any headers because it's possible that some uses of an
# action (i.e. same name, different headers) have causal events while other uses don't.
#
# @param [String] name the name of the event
# @param [Hash] the headers associated with this event
# @return [String] the downcase()'d name of the event name for which to wait
#
def has_causal_events?(name, headers={})
CAUSAL_EVENT_NAMES.include? name.to_s.downcase
end
-
+
##
# Used to determine the event name for an action which has causal events.
- #
+ #
# @param [String] action_name
# @return [String] The corresponding event name which signals the completion of the causal event sequence.
- #
+ #
def causal_event_terminator_name_for(action_name)
return nil unless has_causal_events?(action_name)
- action_name = action_name.to_s.downcase
+ action_name = action_name.to_s.downcase
case action_name
when "queuestatus", 'parkedcalls', "status"
action_name + "complete"
when "sippeers"
"peerlistcomplete"
end
end
-
+
end
-
+
DEFAULT_SETTINGS = {
:host => "localhost",
:port => 5038,
:username => "admin",
:password => "secret",
:events => true
}.freeze unless defined? DEFAULT_SETTINGS
-
+
attr_reader *DEFAULT_SETTINGS.keys
-
+
##
# Creates a new Asterisk Manager Interface connection and exposes certain methods to control it. The constructor
# takes named parameters as Symbols. Note: if the :events option is given, this library will establish a separate
# socket for just events. Two sockets are used because some actions actually respond with events, making it very
# complicated to differentiate between response-type events and normal events.
#
# @param [Hash] options Available options are :host, :port, :username, :password, and :events
#
def initialize(options={})
options = parse_options options
-
+
@host = options[:host]
- @username = options[:username]
+ @username = options[:username]
@password = options[:password]
@port = options[:port]
@events = options[:events]
-
+
@sent_messages = {}
@sent_messages_lock = Mutex.new
-
+
@actions_lexer = DelegatingAsteriskManagerInterfaceLexer.new self, \
:message_received => :action_message_received,
:error_received => :action_error_received
-
+
@write_queue = Queue.new
-
+
if @events
@events_lexer = DelegatingAsteriskManagerInterfaceLexer.new self, \
:message_received => :event_message_received,
- :error_received => :event_error_received
+ :error_received => :event_error_received
end
end
-
+
def action_message_received(message)
if message.kind_of? Manager::ManagerInterfaceEvent
# Trigger the return value of the waiting action id...
corresponding_action = @current_action_with_causal_events
event_collection = @event_collection_for_current_action
-
+
if corresponding_action
-
+
# If this is the meta-event which signals no more events will follow and the response is complete.
if message.name.downcase == corresponding_action.causal_event_terminator_name
-
+
# Result found! Wake up any Threads waiting
corresponding_action.future_resource.resource = event_collection.freeze
-
+
@current_action_with_causal_events = nil
@event_collection_for_current_action = nil
-
+
else
event_collection << message
# We have more causal events coming.
end
else
ahn_log.ami.error "Got an unexpected event on actions socket! This may be a bug! #{message.inspect}"
end
-
+
elsif message["ActionID"].nil?
# No ActionID! Release the write lock and wake up the waiter
else
action_id = message["ActionID"]
corresponding_action = data_for_message_received_with_action_id action_id
if corresponding_action
message.action = corresponding_action
-
+
if corresponding_action.has_causal_events?
# By this point the write loop will already have started blocking by calling the response() method on the
# action. Because we must collect more events before we wake the write loop up again, let's create these
# instance variable which will needed when the subsequent causal events come in.
@current_action_with_causal_events = corresponding_action
@@ -174,86 +174,90 @@
else
ahn_log.ami.error "Received an AMI message with an unrecognized ActionID!! This may be an bug! #{message.inspect}"
end
end
end
-
+
def action_error_received(ami_error)
action_id = ami_error["ActionID"]
-
+
corresponding_action = data_for_message_received_with_action_id action_id
if corresponding_action
corresponding_action.future_resource.resource = ami_error
else
ahn_log.ami.error "Received an AMI error with an unrecognized ActionID!! This may be an bug! #{ami_error.inspect}"
end
end
-
+
##
# Called only when this ManagerInterface is instantiated with events enabled.
#
def event_message_received(event)
return if event.kind_of?(ManagerInterfaceResponse) && event["Message"] == "Authentication accepted"
# TODO: convert the event name to a certain namespace.
Events.trigger %w[asterisk manager_interface], event
end
-
+
def event_error_received(message)
# Does this ever even occur?
ahn_log.ami.error "Hmmm, got an error on the AMI events-only socket! This must be a bug! #{message.inspect}"
end
-
+
##
# Called when our Ragel parser encounters some unexpected syntax from Asterisk. Anytime this is called, it should
# be considered a bug in Adhearsion. Note: this same method is called regardless of whether the syntax error
# happened on the actions socket or on the events socket.
#
def syntax_error_encountered(ignored_chunk)
- ahn_log.ami.error "ADHEARSION'S AMI PARSER ENCOUNTERED A SYNTAX ERROR! " +
+ ahn_log.ami.error "ADHEARSION'S AMI PARSER ENCOUNTERED A SYNTAX ERROR! " +
"PLEASE REPORT THIS ON http://bugs.adhearsion.com! OFFENDING TEXT:\n#{ignored_chunk.inspect}"
end
-
+
##
# Must be called after instantiation. Also see ManagerInterface::connect().
#
# @raise [AuthenticationFailedException] if username or password are rejected
#
def connect!
establish_actions_connection
establish_events_connection if @events
self
end
-
+
def actions_connection_established
@actions_state = :connected
@actions_writer_thread = Thread.new(&method(:write_loop))
end
-
+
def actions_connection_disconnected
@actions_state = :disconnected
+ ahn_log.ami.error "AMI connection for ACTION disconnected !!!"
+ establish_actions_connection
end
-
+
def events_connection_established
@events_state = :connected
end
-
- def actions_connection_disconnected
+
+ def events_connection_disconnected
@events_state = :disconnected
+ ahn_log.ami.error "AMI connection for EVENT disconnected !!!"
+ establish_events_connection
end
-
+
def disconnect!
# PSEUDO CODE
# TODO: Go through all the waiting condition variables and raise an exception
#@write_queue << :STOP!
- raise NotImplementedError
+ #raise NotImplementedError
end
-
+
def dynamic
# TODO: Return an object which responds to method_missing
end
-
+
##
# Used to directly send a new action to Asterisk. Note: NEVER supply an ActionID; these are handled internally.
#
# @param [String, Symbol] action_name The name of the action (e.g. Originate)
# @param [Hash] headers Other key/value pairs to send in this action. Note: don't provide an ActionID
@@ -267,11 +271,11 @@
action
else
raise NotImplementedError
end
end
-
+
##
# Sends an action over the AMI connection and blocks your Thread until the response comes in. If there was an error
# for some reason, the error will be raised as an ManagerInterfaceError.
#
# @param [String, Symbol] action_name The name of the action (e.g. Originate)
@@ -282,35 +286,35 @@
def send_action_synchronously(*args)
returning send_action_asynchronously(*args).response do |response|
raise response if response.kind_of?(ManagerInterfaceError)
end
end
-
+
alias send_action send_action_synchronously
-
-
+
+
####### #######
########### ###########
- ################# SOON-DEPRECATED COMMANDS #################
+ ################# SOON-DEPRECATED COMMANDS #################
########### ###########
####### #######
-
+
# ping sends an action to the Asterisk Manager Interface that returns a pong
# more details here: http://www.voip-info.org/wiki/index.php?page=Asterisk+Manager+API+Action+Ping
def ping
deprecation_warning
send_action "Ping"
true
end
-
+
def deprecation_warning
ahn_log.ami.deprecation.warn "The implementation of the ping, originate, introduce, hangup, call_into_context " +
"and call_and_exec methods will soon be moved from this class to SuperManager. At the moment, the " +
"SuperManager abstractions are not completed. Don't worry. The migration to SuperManager will be very easy."+
" See http://docs.adhearsion.com/AMI for more information."
end
-
+
# The originate method launches a call to Asterisk, full details here:
# http://www.voip-info.org/tiki-index.php?page=Asterisk+Manager+API+Action+Originate
# Takes these arguments as a hash:
#
# Channel: Channel on which to originate the call (The same as you specify in the Dial application command)
@@ -322,11 +326,11 @@
# Variable: Channels variables to set (max 32). Variables will be set for both channels (local and connected).
# Account: Account code for the call
# Application: Application to use on connect (use Data for parameters)
# Data : Data if Application parameter is used
# Async: For the origination to be asynchronous (allows multiple calls to be generated without waiting for a response)
- # ActionID: The request identifier. It allows you to identify the response to this request.
+ # ActionID: The request identifier. It allows you to identify the response to this request.
# You may use a number or a string. Useful when you make several simultaneous requests.
#
# For example:
# originate { :channel => 'SIP/1000@sipnetworks.com',
# :context => 'my_context',
@@ -373,35 +377,35 @@
args[:caller_id] = opts[:caller_id] if opts[:caller_id]
args[:data] = opts[:args] if opts[:args]
originate args
end
- # call_into_context is syntactic sugar for the Asterisk originate command that allows you to
+ # call_into_context is syntactic sugar for the Asterisk originate command that allows you to
# lanuch a call into a particular context. For example:
#
# call_into_context('SIP/1000@sipnetworks.com', 'my_context', { :variables => { :session_guid => new_guid }})
def call_into_context(channel, context, options={})
deprecation_warning
args = {:channel => channel, :context => context}
args[:priority] = options[:priority] || 1
args[:exten] = options[:extension] if options[:extension]
args[:caller_id] = options[:caller_id] if options[:caller_id]
if options[:variables] && options[:variables].kind_of?(Hash)
- args[:variable] = options[:variables].map {|pair| pair.join('=')}.join('|')
+ args[:variable] = options[:variables].map {|pair| pair.join('=')}.join(AHN_CONFIG.asterisk.argument_delimiter)
end
originate args
end
-
+
####### #######
########### ###########
- ################# END SOON-DEPRECATED COMMANDS #################
+ ################# END SOON-DEPRECATED COMMANDS #################
########### ###########
####### #######
-
+
protected
-
+
##
# This class will be removed once this AMI library fully supports all known protocol anomalies.
#
class UnsupportedActionName < ArgumentError
UNSUPPORTED_ACTION_NAMES = %w[
@@ -409,34 +413,34 @@
iaxpeers
] unless defined? UNSUPPORTED_ACTION_NAMES
def initialize(name)
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."
end
-
+
end
-
+
def check_action_name(name)
name = name.to_s.downcase
raise UnsupportedActionName.new(name) if UnsupportedActionName::UNSUPPORTED_ACTION_NAMES.include? name
true
end
-
+
def write_loop
loop do
next_action = @write_queue.shift
return :stopped if next_action.equal? :STOP!
register_action_with_metadata next_action
-
+
ahn_log.ami.debug "Sending AMI action: #{"\n>>> " + next_action.to_s.gsub(/(\r\n)+/, "\n>>> ")}"
@actions_connection.send_data next_action.to_s
# If it's "causal event" action, we must wait here until it's fully responded
next_action.response if next_action.has_causal_events?
end
rescue => e
p e
end
-
+
##
# When we send out an AMI action, we need to track the ActionID and have the other Thread handling the socket IO
# notify the sending Thread that a response has been received. This method instantiates a new FutureResource and
# keeps it around in a synchronized Hash for the IO-handling Thread to notify when a response with a matching
# ActionID is seen again. See also data_for_message_received_with_action_id() which is how the IO-handling Thread
@@ -449,17 +453,17 @@
raise ArgumentError, "Must supply an action!" if action.nil?
@sent_messages_lock.synchronize do
@sent_messages[action.action_id] = action
end
end
-
+
def data_for_message_received_with_action_id(action_id)
@sent_messages_lock.synchronize do
@sent_messages.delete action_id
end
end
-
+
##
# Instantiates a new ManagerInterfaceActionsConnection and assigns it to @actions_connection.
#
# @return [EventSocket]
#
@@ -469,18 +473,18 @@
handler.connected { actions_connection_established }
handler.disconnected { actions_connection_disconnected }
end
login_actions
end
-
+
##
# Instantiates a new ManagerInterfaceEventsConnection and assigns it to @events_connection.
#
# @return [EventSocket]
#
def establish_events_connection
-
+
# Note: the @events_connection instance variable is set in login()
@events_connection = EventSocket.connect(@host, @port) do |handler|
handler.receive_data { |data| @events_lexer << data }
handler.connected { events_connection_established }
handler.disconnected { events_connection_disconnected }
@@ -497,95 +501,95 @@
else
ahn_log.ami "Successful AMI actions-only connection into #{@username}@#{@host}"
response
end
end
-
+
##
# Since this method is always called after the login_actions method, an AuthenticationFailedException would have already
# been raised if the username/password were off. Because this is the only action we ever need to send on this socket,
# it goes straight to the EventSocket connection (bypassing the @write_queue).
#
def login_events
login_action = ManagerInterfaceAction.new "Login", "Username" => @username, "Secret" => @password, "Events" => "On"
@events_connection.send_data login_action.to_s
end
-
+
def parse_options(options)
unrecognized_keys = options.keys.map { |key| key.to_sym } - DEFAULT_SETTINGS.keys
if unrecognized_keys.any?
raise ArgumentError, "Unrecognized named argument(s): #{unrecognized_keys.to_sentence}"
end
DEFAULT_SETTINGS.merge options
end
-
+
##
# Raised when calling ManagerInterface#connect!() and the server responds with an error after logging in.
#
class AuthenticationFailedException < Exception; end
-
+
class NotConnectedError < Exception; end
-
+
##
# Each time ManagerInterface#send_action is invoked, a new ManagerInterfaceAction is instantiated.
#
class ManagerInterfaceAction
-
+
attr_reader :name, :headers, :future_resource, :action_id, :causal_event_terminator_name
def initialize(name, headers={})
@name = name.to_s.downcase.freeze
@headers = headers.stringify_keys.freeze
@action_id = new_action_id.freeze
@future_resource = FutureResource.new
@causal_event_terminator_name = ManagerInterface.causal_event_terminator_name_for name
end
-
+
##
# Used internally by ManagerInterface for the actions in AMI which break the protocol's definition and do not
# reply with an ActionID.
#
def replies_with_action_id?
ManagerInterface.replies_with_action_id?(@name, @headers)
end
-
+
##
# Some AMI actions effectively respond with many events which collectively constitute the actual response. These
# Must be handled specially by the protocol parser, so this method helps inform the parser.
#
def has_causal_events?
ManagerInterface.has_causal_events?(@name, @headers)
end
-
+
##
# Abstracts the generation of new ActionIDs. This could be implemented virutally any way, provided each
# invocation returns something unique, so this will generate a GUID and return it.
#
# @return [String] characters in GUID format (e.g. "4C5F4E1C-A0F1-4D13-8751-C62F2F783062")
#
def new_action_id
new_guid # Implemented in lib/adhearsion/foundation/pseudo_guid.rb
end
-
+
##
# Converts this action into a protocol-valid String, ready to be sent over a socket.
#
def to_s
@textual_representation ||= (
"Action: #{@name}\r\nActionID: #{@action_id}\r\n" +
@headers.map { |(key,value)| "#{key}: #{value}" }.join("\r\n") +
(@headers.any? ? "\r\n\r\n" : "\r\n")
)
end
-
+
##
# If the response has simply not been received yet from Asterisk, the calling Thread will block until it comes
# in. Once the response comes in, subsequent calls immediately return a reference to the ManagerInterfaceResponse
# object.
#
def response
future_resource.resource
end
-
+
end
end
end
end
end