All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.asteriskjava.manager.internal.ManagerConnectionImpl Maven / Gradle / Ivy

There is a newer version: 3.41.0
Show newest version
/*
 *  Copyright 2004-2006 Stefan Reuter
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */
package org.asteriskjava.manager.internal;

import static org.asteriskjava.manager.ManagerConnectionState.CONNECTED;
import static org.asteriskjava.manager.ManagerConnectionState.CONNECTING;
import static org.asteriskjava.manager.ManagerConnectionState.DISCONNECTED;
import static org.asteriskjava.manager.ManagerConnectionState.DISCONNECTING;
import static org.asteriskjava.manager.ManagerConnectionState.INITIAL;
import static org.asteriskjava.manager.ManagerConnectionState.RECONNECTING;

import java.io.IOException;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import org.asteriskjava.AsteriskVersion;
import org.asteriskjava.manager.AuthenticationFailedException;
import org.asteriskjava.manager.EventTimeoutException;
import org.asteriskjava.manager.ExpectedResponse;
import org.asteriskjava.manager.ManagerConnection;
import org.asteriskjava.manager.ManagerConnectionState;
import org.asteriskjava.manager.ManagerEventListener;
import org.asteriskjava.manager.ResponseEvents;
import org.asteriskjava.manager.SendActionCallback;
import org.asteriskjava.manager.SendEventGeneratingActionCallback;
import org.asteriskjava.manager.TimeoutException;
import org.asteriskjava.manager.action.ChallengeAction;
import org.asteriskjava.manager.action.CommandAction;
import org.asteriskjava.manager.action.CoreSettingsAction;
import org.asteriskjava.manager.action.EventGeneratingAction;
import org.asteriskjava.manager.action.LoginAction;
import org.asteriskjava.manager.action.LogoffAction;
import org.asteriskjava.manager.action.ManagerAction;
import org.asteriskjava.manager.action.UserEventAction;
import org.asteriskjava.manager.event.ConnectEvent;
import org.asteriskjava.manager.event.DialBeginEvent;
import org.asteriskjava.manager.event.DialEvent;
import org.asteriskjava.manager.event.DisconnectEvent;
import org.asteriskjava.manager.event.ManagerEvent;
import org.asteriskjava.manager.event.ProtocolIdentifierReceivedEvent;
import org.asteriskjava.manager.event.ResponseEvent;
import org.asteriskjava.manager.response.ChallengeResponse;
import org.asteriskjava.manager.response.CommandResponse;
import org.asteriskjava.manager.response.CoreSettingsResponse;
import org.asteriskjava.manager.response.ManagerError;
import org.asteriskjava.manager.response.ManagerResponse;
import org.asteriskjava.util.DateUtil;
import org.asteriskjava.util.Log;
import org.asteriskjava.util.LogFactory;
import org.asteriskjava.util.SocketConnectionFacade;
import org.asteriskjava.util.internal.SocketConnectionFacadeImpl;

/**
 * Internal implemention of the ManagerConnection interface.
 *
 * @author srt
 * @version $Id$
 * @see org.asteriskjava.manager.ManagerConnectionFactory
 */
public class ManagerConnectionImpl implements ManagerConnection, Dispatcher
{
    private static final int RECONNECTION_INTERVAL_1 = 50;
    private static final int RECONNECTION_INTERVAL_2 = 5000;
    private static final String DEFAULT_HOSTNAME = "localhost";
    private static final int DEFAULT_PORT = 5038;
    private static final int RECONNECTION_VERSION_INTERVAL = 500;
    private static final int MAX_VERSION_ATTEMPTS = 4;
    private static final String CMD_SHOW_VERSION = "core show version";

    // NOTE: identifier is AMI_VERSION, defined in include/asterisk/manager.h
    // AMI version consists of MAJOR.BREAKING.NON-BREAKING.
    private static final String[] SUPPORTED_AMI_VERSIONS = {

            "2.6", // Asterisk 13
            "2.7", // Asterisk 13.2
            "2.8", // Asterisk >13.5
            "2.9", // Asterisk >13.3
            "3.1", // Asterisk =14.3
            "3.2", // Asterisk 14.4.0
            "4.0", // Asterisk 15
            "5.0", // Asterisk 16
            "6.0", // Asterisk 17
    };

    private static final AtomicLong idCounter = new AtomicLong(0);

    /**
     * Instance logger.
     */
    private final static Log logger = LogFactory.getLog(ManagerConnectionImpl.class);

    private final long id;

    /**
     * Used to construct the internalActionId.
     */
    private AtomicLong actionIdCounter = new AtomicLong(0);

    /* Config attributes */
    /**
     * Hostname of the Asterisk server to connect to.
     */
    private String hostname = DEFAULT_HOSTNAME;

    /**
     * TCP port to connect to.
     */
    private int port = DEFAULT_PORT;

    /**
     * true to use SSL for the connection, false for a
     * plain text connection.
     */
    private boolean ssl = false;

    /**
     * The username to use for login as defined in Asterisk's
     * manager.conf.
     */
    protected String username;

    /**
     * The password to use for login as defined in Asterisk's
     * manager.conf.
     */
    protected String password;

    /**
     * Encoding used for transmission of strings.
     */
    private Charset encoding = StandardCharsets.UTF_8;

    /**
     * The default timeout to wait for a ManagerResponse after sending a
     * ManagerAction.
     */
    private long defaultResponseTimeout = 2000;

    /**
     * The default timeout to wait for the last ResponseEvent after sending an
     * EventGeneratingAction.
     */
    private long defaultEventTimeout = 5000;

    /**
     * The timeout to use when connecting the the Asterisk server.
     */
    private int socketTimeout = 0;

    /**
     * Closes the connection (and reconnects) if no input has been read for the
     * given amount of milliseconds. A timeout of zero is interpreted as an
     * infinite timeout.
     *
     * @see Socket#setSoTimeout(int)
     */
    private int socketReadTimeout = 0;

    /**
     * true to continue to reconnect after an authentication
     * failure.
     */
    private boolean keepAliveAfterAuthenticationFailure = true;

    /**
     * The socket to use for TCP/IP communication with Asterisk.
     */
    private SocketConnectionFacade socket;

    /**
     * The thread that runs the reader.
     */
    private Thread readerThread;
    private final AtomicLong readerThreadCounter = new AtomicLong(0);

    private final AtomicLong reconnectThreadCounter = new AtomicLong(0);

    /**
     * The reader to use to receive events and responses from asterisk.
     */
    private ManagerReader reader;

    /**
     * The writer to use to send actions to asterisk.
     */
    private ManagerWriter writer;

    /**
     * The protocol identifer Asterisk sends on connect wrapped into an object
     * to be used as mutex.
     */
    private final ProtocolIdentifierWrapper protocolIdentifier;

    /**
     * The version of the Asterisk server we are connected to.
     */
    private AsteriskVersion version;

    /**
     * Contains the registered handlers that process the ManagerResponses.
     * 

* Key is the internalActionId of the Action sent and value the * corresponding ResponseListener. */ private final Map responseListeners; /** * Contains the event handlers that handle ResponseEvents for the * sendEventGeneratingAction methods. *

* Key is the internalActionId of the Action sent and value the * corresponding EventHandler. */ private final Map responseEventListeners; /** * Contains the event handlers that users registered. */ private final List eventListeners; protected ManagerConnectionState state = INITIAL; private String eventMask; /** * Creates a new instance. */ public ManagerConnectionImpl() { this.id = idCounter.getAndIncrement(); this.responseListeners = new HashMap<>(); this.responseEventListeners = new HashMap<>(); this.eventListeners = new ArrayList<>(); this.protocolIdentifier = new ProtocolIdentifierWrapper(); } // the following two methods can be overriden when running test cases to // return a mock object protected ManagerReader createReader(Dispatcher dispatcher, Object source) { return new ManagerReaderImpl(dispatcher, source); } protected ManagerWriter createWriter() { return new ManagerWriterImpl(); } /** * Sets the hostname of the asterisk server to connect to. *

* Default is localhost. * * @param hostname the hostname to connect to */ public void setHostname(String hostname) { this.hostname = hostname; } /** * Sets the port to use to connect to the asterisk server. This is the port * specified in asterisk's manager.conf file. *

* Default is 5038. * * @param port the port to connect to */ public void setPort(int port) { if (port <= 0) { this.port = DEFAULT_PORT; } else { this.port = port; } } /** * Sets whether to use SSL.
* Default is false. * * @param ssl true to use SSL for the connection, * false for a plain text connection. * @since 0.3 */ public void setSsl(boolean ssl) { this.ssl = ssl; } /** * Sets the username to use to connect to the asterisk server. This is the * username specified in asterisk's manager.conf file. * * @param username the username to use for login */ public void setUsername(String username) { this.username = username; } /** * Sets the password to use to connect to the asterisk server. This is the * password specified in Asterisk's manager.conf file. * * @param password the password to use for login */ public void setPassword(String password) { this.password = password; } @Override public void setEncoding(Charset encoding) { this.encoding = encoding; } /** * Sets the time in milliseconds the synchronous method * {@link #sendAction(ManagerAction)} will wait for a response before * throwing a TimeoutException.
* Default is 2000. * * @param defaultResponseTimeout default response timeout in milliseconds * @since 0.2 */ public void setDefaultResponseTimeout(long defaultResponseTimeout) { this.defaultResponseTimeout = defaultResponseTimeout; } /** * Sets the time in milliseconds the synchronous method * {@link #sendEventGeneratingAction(EventGeneratingAction)} will wait for a * response and the last response event before throwing a TimeoutException. *
* Default is 5000. * * @param defaultEventTimeout default event timeout in milliseconds * @since 0.2 */ public void setDefaultEventTimeout(long defaultEventTimeout) { this.defaultEventTimeout = defaultEventTimeout; } /** * Set to true to try reconnecting to ther asterisk serve even * if the reconnection attempt threw an AuthenticationFailedException.
* Default is true. * * @param keepAliveAfterAuthenticationFailure true to try * reconnecting to ther asterisk serve even if the reconnection * attempt threw an AuthenticationFailedException, * false otherwise. */ public void setKeepAliveAfterAuthenticationFailure(boolean keepAliveAfterAuthenticationFailure) { this.keepAliveAfterAuthenticationFailure = keepAliveAfterAuthenticationFailure; } /* Implementation of ManagerConnection interface */ public String getUsername() { return username; } public String getPassword() { return password; } @Override public Charset getEncoding() { return encoding; } public AsteriskVersion getVersion() { return version; } public String getHostname() { return hostname; } public int getPort() { return port; } public boolean isSsl() { return ssl; } public InetAddress getLocalAddress() { return socket.getLocalAddress(); } public int getLocalPort() { return socket.getLocalPort(); } public InetAddress getRemoteAddress() { return socket.getRemoteAddress(); } public int getRemotePort() { return socket.getRemotePort(); } public void registerUserEventClass(Class< ? extends ManagerEvent> userEventClass) { if (reader == null) { reader = createReader(this, this); } reader.registerEventClass(userEventClass); } public void setSocketTimeout(int socketTimeout) { this.socketTimeout = socketTimeout; } public void setSocketReadTimeout(int socketReadTimeout) { this.socketReadTimeout = socketReadTimeout; } public synchronized void login() throws IOException, AuthenticationFailedException, TimeoutException { login(null); } public synchronized void login(String eventMask) throws IOException, AuthenticationFailedException, TimeoutException { if (state != INITIAL && state != DISCONNECTED) { throw new IllegalStateException("Login may only be perfomed when in state " + "INITIAL or DISCONNECTED, but connection is in state " + state); } state = CONNECTING; this.eventMask = eventMask; try { doLogin(defaultResponseTimeout, eventMask); } finally { if (state != CONNECTED) { state = DISCONNECTED; } } } /** * Does the real login, following the steps outlined below.
*

    *
  1. Connects to the asterisk server by calling {@link #connect()} if not * already connected *
  2. Waits until the protocol identifier is received but not longer than * timeout ms. *
  3. Sends a {@link ChallengeAction} requesting a challenge for authType * MD5. *
  4. When the {@link ChallengeResponse} is received a {@link LoginAction} * is sent using the calculated key (MD5 hash of the password appended to * the received challenge). *
* * @param timeout the maximum time to wait for the protocol identifier (in * ms) * @param eventMask the event mask. Set to "on" if all events should be * send, "off" if not events should be sent or a combination of * "system", "call" and "log" (separated by ',') to specify what * kind of events should be sent. * @throws IOException if there is an i/o problem. * @throws AuthenticationFailedException if username or password are * incorrect and the login action returns an error or if the MD5 * hash cannot be computed. The connection is closed in this * case. * @throws TimeoutException if a timeout occurs while waiting for the * protocol identifier. The connection is closed in this case. */ protected synchronized void doLogin(long timeout, String eventMask) throws IOException, AuthenticationFailedException, TimeoutException { ChallengeAction challengeAction; ManagerResponse challengeResponse; String challenge; String key; LoginAction loginAction; ManagerResponse loginResponse; if (socket == null) { connect(); } if (protocolIdentifier.getValue() == null) { try { protocolIdentifier.await(timeout); } catch (InterruptedException e) // NOPMD { Thread.currentThread().interrupt(); } } if (protocolIdentifier.getValue() == null) { disconnect(); if (reader != null && reader.getTerminationException() != null) { throw reader.getTerminationException(); } throw new TimeoutException("Timeout waiting for protocol identifier"); } challengeAction = new ChallengeAction("MD5"); try { challengeResponse = sendAction(challengeAction); } catch (Exception e) { disconnect(); throw new AuthenticationFailedException("Unable to send challenge action", e); } if (challengeResponse instanceof ChallengeResponse) { challenge = ((ChallengeResponse) challengeResponse).getChallenge(); } else { disconnect(); throw new AuthenticationFailedException( "Unable to get challenge from Asterisk. ChallengeAction returned: " + challengeResponse.getMessage()); } try { MessageDigest md; md = MessageDigest.getInstance("MD5"); if (challenge != null) { md.update(challenge.getBytes(StandardCharsets.UTF_8)); } if (password != null) { md.update(password.getBytes(StandardCharsets.UTF_8)); } key = ManagerUtil.toHexString(md.digest()); } catch (NoSuchAlgorithmException ex) { disconnect(); throw new AuthenticationFailedException("Unable to create login key using MD5 Message Digest", ex); } loginAction = new LoginAction(username, "MD5", key, eventMask); try { loginResponse = sendAction(loginAction); } catch (Exception e) { disconnect(); throw new AuthenticationFailedException("Unable to send login action", e); } if (loginResponse instanceof ManagerError) { disconnect(); throw new AuthenticationFailedException(loginResponse.getMessage()); } logger.info("Successfully logged in"); version = determineVersion(); state = CONNECTED; writer.setTargetVersion(version); logger.info("Determined Asterisk version: " + version); // generate pseudo event indicating a successful login ConnectEvent connectEvent = new ConnectEvent(this); connectEvent.setProtocolIdentifier(getProtocolIdentifier()); connectEvent.setDateReceived(DateUtil.getDate()); // TODO could this cause a deadlock? fireEvent(connectEvent); } protected AsteriskVersion determineVersion() throws IOException, TimeoutException { int attempts = 0; logger.info("Got asterisk protocol identifier version " + protocolIdentifier.getValue()); while (attempts++ < MAX_VERSION_ATTEMPTS) { try { AsteriskVersion version = determineVersionByCoreSettings(); if (version != null) return version; } catch (Exception e) { } try { AsteriskVersion version = determineVersionByCoreShowVersion(); if (version != null) return version; } catch (Exception e) { } try { Thread.sleep(RECONNECTION_VERSION_INTERVAL); } catch (Exception ex) { // ignore } // NOPMD } logger.error("Unable to determine asterisk version, assuming " + AsteriskVersion.DEFAULT_VERSION + "... you should expect problems to follow."); return AsteriskVersion.DEFAULT_VERSION; } /** * Get asterisk version by 'core settings' actions. This is supported from * Asterisk 1.6 onwards. * * @return * @throws Exception */ protected AsteriskVersion determineVersionByCoreSettings() throws Exception { ManagerResponse response = sendAction(new CoreSettingsAction()); if (!(response instanceof CoreSettingsResponse)) { // NOTE: you need system or reporting permissions logger.info("Could not get core settings, do we have the necessary permissions?"); return null; } String ver = ((CoreSettingsResponse) response).getAsteriskVersion(); return AsteriskVersion.getDetermineVersionFromString("Asterisk " + ver); } /** * Determine version by the 'core show version' command. This needs * 'command' permissions. * * @return * @throws Exception */ protected AsteriskVersion determineVersionByCoreShowVersion() throws Exception { final ManagerResponse coreShowVersionResponse = sendAction(new CommandAction(CMD_SHOW_VERSION)); if (coreShowVersionResponse == null || !(coreShowVersionResponse instanceof CommandResponse)) { // this needs 'command' permissions logger.info("Could not get response for 'core show version'"); return null; } final List coreShowVersionResult = ((CommandResponse) coreShowVersionResponse).getResult(); if (coreShowVersionResult == null || coreShowVersionResult.isEmpty()) { logger.warn("Got empty response for 'core show version'"); return null; } final String coreLine = coreShowVersionResult.get(0); return AsteriskVersion.getDetermineVersionFromString(coreLine); } protected synchronized void connect() throws IOException { logger.info("Connecting to " + hostname + ":" + port); if (reader == null) { logger.debug("Creating reader for " + hostname + ":" + port); reader = createReader(this, this); } if (writer == null) { logger.debug("Creating writer"); writer = createWriter(); } logger.debug("Creating socket"); socket = createSocket(); logger.debug("Passing socket to reader"); reader.setSocket(socket); if (readerThread == null || !readerThread.isAlive() || reader.isDead()) { logger.debug("Creating and starting reader thread"); readerThread = new Thread(reader); readerThread .setName("Asterisk-Java ManagerConnection-" + id + "-Reader-" + readerThreadCounter.getAndIncrement()); readerThread.setDaemon(true); readerThread.start(); } logger.debug("Passing socket to writer"); writer.setSocket(socket); } protected SocketConnectionFacade createSocket() throws IOException { return new SocketConnectionFacadeImpl(hostname, port, ssl, socketTimeout, socketReadTimeout, encoding); } public synchronized void logoff() throws IllegalStateException { if (state != CONNECTED && state != RECONNECTING) { throw new IllegalStateException("Logoff may only be perfomed when in state " + "CONNECTED or RECONNECTING, but connection is in state " + state); } state = DISCONNECTING; if (socket != null) { try { sendAction(new LogoffAction()); } catch (Exception e) { logger.warn("Unable to send LogOff action", e); } } cleanup(); state = DISCONNECTED; } /** * Closes the socket connection. */ protected synchronized void disconnect() { if (socket != null) { logger.info("Closing socket."); try { socket.close(); } catch (IOException ex) { logger.warn("Unable to close socket: " + ex.getMessage()); } socket = null; } protocolIdentifier.reset(); } public ManagerResponse sendAction(ManagerAction action) throws IOException, TimeoutException, IllegalArgumentException, IllegalStateException { return sendAction(action, defaultResponseTimeout); } /** * Implements synchronous sending of "simple" actions. * * @param timeout - in milliseconds */ public ManagerResponse sendAction(ManagerAction action, long timeout) throws IOException, TimeoutException, IllegalArgumentException, IllegalStateException { ResponseHandlerResult result = new ResponseHandlerResult(); try { SendActionCallback callbackHandler = new DefaultSendActionCallback(result); sendAction(action, callbackHandler); // definitely return null for the response of user events if (action instanceof UserEventAction) { return null; } // only wait if we did not yet receive the response. // Responses may be returned really fast. if (result.getResponse() == null) { try { result.await(timeout); } catch (InterruptedException ex) { logger.warn("Interrupted while waiting for result"); Thread.currentThread().interrupt(); } } // still no response? if (result.getResponse() == null) { throw new TimeoutException("Timeout waiting for response to " + action.getAction() + (action.getActionId() == null ? "" : " (actionId: " + action.getActionId() + "), Timeout=" + timeout + " Action=" + action.getAction())); } return result.getResponse(); } finally { result.dispose(); } } public void sendAction(ManagerAction action, SendActionCallback callback) throws IOException, IllegalArgumentException, IllegalStateException { final String internalActionId; if (action == null) { throw new IllegalArgumentException("Unable to send action: action is null."); } // In general sending actions is only allowed while connected, though // there are a few exceptions, these are handled here: if ((state == CONNECTING || state == RECONNECTING) && (action instanceof ChallengeAction || action instanceof LoginAction || isShowVersionCommandAction(action))) { // when (re-)connecting challenge and login actions are ok. } // NOPMD else if (state == DISCONNECTING && action instanceof LogoffAction) { // when disconnecting logoff action is ok. } // NOPMD else if (state != CONNECTED) { throw new IllegalStateException( "Actions may only be sent when in state " + "CONNECTED, but connection is in state " + state); } if (socket == null) { throw new IllegalStateException("Unable to send " + action.getAction() + " action: socket not connected."); } internalActionId = createInternalActionId(); // if the callbackHandler is null the user is obviously not interested // in the response, thats fine. if (callback != null) { synchronized (this.responseListeners) { this.responseListeners.put(internalActionId, callback); } } Class< ? extends ManagerResponse> responseClass = getExpectedResponseClass(action.getClass()); if (responseClass != null) { reader.expectResponseClass(internalActionId, responseClass); } writer.sendAction(action, internalActionId); } boolean isShowVersionCommandAction(ManagerAction action) { if (action instanceof CoreSettingsAction) return true; if (action instanceof CommandAction) { String cmd = ((CommandAction) action).getCommand(); return CMD_SHOW_VERSION.equals(cmd); } return false; } private Class< ? extends ManagerResponse> getExpectedResponseClass(Class< ? extends ManagerAction> actionClass) { final ExpectedResponse annotation = actionClass.getAnnotation(ExpectedResponse.class); if (annotation == null) { return null; } return annotation.value(); } public ResponseEvents sendEventGeneratingAction(EventGeneratingAction action) throws IOException, EventTimeoutException, IllegalArgumentException, IllegalStateException { return sendEventGeneratingAction(action, defaultEventTimeout); } /* * Implements synchronous sending of event generating actions. */ public ResponseEvents sendEventGeneratingAction(EventGeneratingAction action, long timeout) throws IOException, EventTimeoutException, IllegalArgumentException, IllegalStateException { final ResponseEventsImpl responseEvents; final ResponseEventHandler responseEventHandler; final String internalActionId; if (action == null) { throw new IllegalArgumentException("Unable to send action: action is null."); } else if (action.getActionCompleteEventClass() == null) { throw new IllegalArgumentException( "Unable to send action: actionCompleteEventClass for " + action.getClass().getName() + " is null."); } else if (!ResponseEvent.class.isAssignableFrom(action.getActionCompleteEventClass())) { throw new IllegalArgumentException( "Unable to send action: actionCompleteEventClass (" + action.getActionCompleteEventClass().getName() + ") for " + action.getClass().getName() + " is not a ResponseEvent."); } if (state != CONNECTED) { throw new IllegalStateException( "Actions may only be sent when in state " + "CONNECTED but connection is in state " + state); } responseEvents = new ResponseEventsImpl(); responseEventHandler = new ResponseEventHandler(responseEvents, action.getActionCompleteEventClass()); internalActionId = createInternalActionId(); try { // register response handler... synchronized (this.responseListeners) { this.responseListeners.put(internalActionId, responseEventHandler); } // ...and event handler. synchronized (this.responseEventListeners) { this.responseEventListeners.put(internalActionId, responseEventHandler); } writer.sendAction(action, internalActionId); // only wait if response has not yet arrived. if (responseEvents.getResponse() == null || !responseEvents.isComplete()) { try { responseEvents.await(timeout); } catch (InterruptedException e) { logger.warn("Interrupted while waiting for response events."); Thread.currentThread().interrupt(); } } // still no response or not all events received and timed out? if (responseEvents.getResponse() == null || !responseEvents.isComplete()) { throw new EventTimeoutException( "Timeout waiting for response or response events to " + action.getAction() + (action.getActionId() == null ? "" : " (actionId: " + action.getActionId() + ")"), responseEvents); } } finally { // remove the event handler synchronized (this.responseEventListeners) { this.responseEventListeners.remove(internalActionId); } // Note: The response handler should have already been removed // when the response was received, however we remove it here // just in case it was never received. synchronized (this.responseListeners) { this.responseListeners.remove(internalActionId); } } return responseEvents; } public void sendEventGeneratingAction(EventGeneratingAction action, SendEventGeneratingActionCallback callback) throws IOException, IllegalArgumentException, IllegalStateException { if (action == null) { throw new IllegalArgumentException("Unable to send action: action is null."); } else if (action.getActionCompleteEventClass() == null) { throw new IllegalArgumentException( "Unable to send action: actionCompleteEventClass for " + action.getClass().getName() + " is null."); } else if (!ResponseEvent.class.isAssignableFrom(action.getActionCompleteEventClass())) { throw new IllegalArgumentException( "Unable to send action: actionCompleteEventClass (" + action.getActionCompleteEventClass().getName() + ") for " + action.getClass().getName() + " is not a ResponseEvent."); } if (state != CONNECTED) { throw new IllegalStateException( "Actions may only be sent when in state " + "CONNECTED but connection is in state " + state); } final String internalActionId = createInternalActionId(); if (callback != null) { AsyncEventGeneratingResponseHandler responseEventHandler = new AsyncEventGeneratingResponseHandler( action.getActionCompleteEventClass(), callback); // register response handler... synchronized (this.responseListeners) { this.responseListeners.put(internalActionId, responseEventHandler); } // ...and event handler. synchronized (this.responseEventListeners) { this.responseEventListeners.put(internalActionId, responseEventHandler); } } writer.sendAction(action, internalActionId); } /** * Creates a new unique internal action id based on the hash code of this * connection and a sequence. * * @return a new internal action id * @see ManagerUtil#addInternalActionId(String,String) * @see ManagerUtil#getInternalActionId(String) * @see ManagerUtil#stripInternalActionId(String) */ private String createInternalActionId() { final StringBuilder sb; sb = new StringBuilder(); sb.append(this.hashCode()); sb.append("_"); sb.append(actionIdCounter.getAndIncrement()); return sb.toString(); } public void addEventListener(final ManagerEventListener listener) { synchronized (this.eventListeners) { // only add it if its not already there if (!this.eventListeners.contains(listener)) { this.eventListeners.add(listener); } } } public void removeEventListener(final ManagerEventListener listener) { synchronized (this.eventListeners) { if (this.eventListeners.contains(listener)) { this.eventListeners.remove(listener); } } } public String getProtocolIdentifier() { return protocolIdentifier.getValue(); } public ManagerConnectionState getState() { return state; } /* Implementation of Dispatcher: callbacks for ManagerReader */ /** * This method is called by the reader whenever a {@link ManagerResponse} is * received. The response is dispatched to the associated * {@link SendActionCallback}. * * @param response the response received by the reader * @see ManagerReader */ public void dispatchResponse(ManagerResponse response) { final String actionId; String internalActionId; SendActionCallback listener; // shouldn't happen if (response == null) { logger.error("Unable to dispatch null response. This should never happen. Please file a bug."); return; } actionId = response.getActionId(); internalActionId = null; listener = null; if (actionId != null) { internalActionId = ManagerUtil.getInternalActionId(actionId); response.setActionId(ManagerUtil.stripInternalActionId(actionId)); } if (logger.isDebugEnabled()) { logger.debug("Dispatching response with internalActionId '" + internalActionId + "':\n" + response); } if (internalActionId != null) { synchronized (this.responseListeners) { listener = responseListeners.get(internalActionId); if (listener != null) { this.responseListeners.remove(internalActionId); } else { // when using the async sendAction it's ok not to register a // callback so if we don't find a response handler thats ok logger.debug("No response listener registered for " + "internalActionId '" + internalActionId + "'"); } } } else { logger.error( "Unable to retrieve internalActionId from response: " + "actionId '" + actionId + "':\n" + response); } if (listener != null) { try { listener.onResponse(response); } catch (Exception e) { logger.warn("Unexpected exception in response listener " + listener.getClass().getName(), e); } } } /** * This method is called by the reader whenever a ManagerEvent is received. * The event is dispatched to all registered ManagerEventHandlers. * * @param event the event received by the reader * @see #addEventListener(ManagerEventListener) * @see #removeEventListener(ManagerEventListener) * @see ManagerReader */ public void dispatchEvent(ManagerEvent event) { // shouldn't happen if (event == null) { logger.error("Unable to dispatch null event. This should never happen. Please file a bug."); return; } dispatchLegacyEventIfNeeded(event); if (logger.isDebugEnabled()) { logger.debug("Dispatching event:\n" + event.toString()); } // Some events need special treatment besides forwarding them to the // registered eventListeners (clients) // These events are handled here at first: // Dispatch ResponseEvents to the appropriate responseEventListener if (event instanceof ResponseEvent) { ResponseEvent responseEvent; String internalActionId; responseEvent = (ResponseEvent) event; internalActionId = responseEvent.getInternalActionId(); if (internalActionId != null) { synchronized (responseEventListeners) { ManagerEventListener listener; listener = responseEventListeners.get(internalActionId); if (listener != null) { try { listener.onManagerEvent(event); } catch (Exception e) { logger.warn("Unexpected exception in response event listener " + listener.getClass().getName(), e); } } } } else { // ResponseEvent without internalActionId: // this happens if the same event class is used as response // event // and as an event that is not triggered by a Manager command // Example: QueueMemberStatusEvent. // logger.debug("ResponseEvent without " // + "internalActionId:\n" + responseEvent); } // NOPMD } if (event instanceof DisconnectEvent) { cleanupActionListeners((DisconnectEvent) event); // When we receive get disconnected while we are connected start // a new reconnect thread and set the state to RECONNECTING. synchronized (this) { if (state == CONNECTED) { state = RECONNECTING; // close socket if still open and remove reference to // readerThread // After sending the DisconnectThread that thread will die // anyway. cleanup(); Thread reconnectThread = new Thread(new Runnable() { public void run() { reconnect(); } }); reconnectThread.setName("Asterisk-Java ManagerConnection-" + id + "-Reconnect-" + reconnectThreadCounter.getAndIncrement()); reconnectThread.setDaemon(true); reconnectThread.start(); // now the DisconnectEvent is dispatched to registered // eventListeners // (clients) and after that the ManagerReaderThread is gone. // So effectively we replaced the reader thread by a // ReconnectThread. } else { // when we receive a DisconnectEvent while not connected we // ignore it and do not send it to clients return; } } } if (event instanceof ProtocolIdentifierReceivedEvent) { ProtocolIdentifierReceivedEvent protocolIdentifierReceivedEvent; String protocolIdentifier; protocolIdentifierReceivedEvent = (ProtocolIdentifierReceivedEvent) event; protocolIdentifier = protocolIdentifierReceivedEvent.getProtocolIdentifier(); setProtocolIdentifier(protocolIdentifier); // no need to send this event to clients return; } fireEvent(event); } /** * Enro 2015-03 Workaround to continue having Legacy Events from Asterisk * 13. */ private void dispatchLegacyEventIfNeeded(ManagerEvent event) { if (event instanceof DialBeginEvent) { DialEvent legacyEvent = new DialEvent((DialBeginEvent) event); dispatchEvent(legacyEvent); } } /** * Notifies all {@link ManagerEventListener}s registered by users. * * @param event the event to propagate */ private void fireEvent(ManagerEvent event) { synchronized (eventListeners) { for (ManagerEventListener listener : eventListeners) { try { listener.onManagerEvent(event); } catch (RuntimeException e) { logger.warn("Unexpected exception in eventHandler " + listener.getClass().getName(), e); } } } } private boolean isSupportedProtocolIdentifier(final String identifier) { // Normal version checks for (String supportedVersion : SUPPORTED_AMI_VERSIONS) { String prefix = "Asterisk Call Manager/" + supportedVersion + "."; if (identifier.startsWith(prefix)) { return true; } } // Other cases if ("OpenPBX Call Manager/1.0".equals(identifier)) return true; if ("CallWeaver Call Manager/1.0".equals(identifier)) return true; if (identifier.startsWith("Asterisk Call Manager Proxy/")) return true; return false; } /** * This method is called when a {@link ProtocolIdentifierReceivedEvent} is * received from the reader. Having received a correct protocol identifier * is the precondition for logging in. * * @param identifier the protocol version used by the Asterisk server. */ private void setProtocolIdentifier(final String identifier) { logger.info("Connected via " + identifier); if (identifier == null || !isSupportedProtocolIdentifier(identifier)) { logger.warn("Unsupported protocol version '" + identifier + "'. Use at your own risk."); } protocolIdentifier.setValue(identifier); protocolIdentifier.countDown(); } /** * Reconnects to the asterisk server when the connection is lost.
* While keepAlive is true we will try to reconnect. * Reconnection attempts will be stopped when the {@link #logoff()} method * is called or when the login after a successful reconnect results in an * {@link AuthenticationFailedException} suggesting that the manager * credentials have changed and keepAliveAfterAuthenticationFailure is not * set.
* This method is called when a {@link DisconnectEvent} is received from the * reader. */ private void reconnect() { int numTries; // try to reconnect numTries = 0; while (true) { try { if (numTries < 10) { // try to reconnect quite fast for the firt 10 times // this succeeds if the server has just been restarted Thread.sleep(RECONNECTION_INTERVAL_1); } else { // slow down after 10 unsuccessful attempts asuming a // shutdown of the server Thread.sleep(RECONNECTION_INTERVAL_2); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } try { synchronized (this) { if (state != RECONNECTING) { break; } connect(); try { doLogin(defaultResponseTimeout, eventMask); logger.info("Successfully reconnected."); // everything is ok again, so we leave // when successful doLogin set the state to CONNECTED so // no // need to adjust it break; } catch (AuthenticationFailedException e1) { if (keepAliveAfterAuthenticationFailure) { logger.error("Unable to log in after reconnect: " + e1.getMessage()); } else { logger.error("Unable to log in after reconnect: " + e1.getMessage() + ". Giving up."); state = DISCONNECTED; } } catch (TimeoutException e1) { // shouldn't happen - but happens! logger.error("TimeoutException while trying to log in " + "after reconnect."); } } } catch (IOException e) { // server seems to be still down, just continue to attempt // reconnection String message = e.getClass().getSimpleName(); if (e.getMessage() != null) { message = e.getMessage(); } logger.warn("Exception while trying to reconnect: " + message); try { // Where multiple connections are present, spread out // their reconnect attempts and prevent hard loop. long randomSleep = (long) (Math.random() * 100); TimeUnit.MILLISECONDS.sleep(50 + randomSleep); } catch (InterruptedException e1) { logger.error(e1); } } numTries++; } } /** * Notify pending {@link #responseListeners} and * {@link #responseEventListeners} so the synchronous ones can unblock, * clears those listener collections. * * @param event */ private void cleanupActionListeners(DisconnectEvent event) { HashMap oldResponseListeners = null; synchronized (responseListeners) { // Store remaining response listeners to be notified outside of // synchronized oldResponseListeners = new HashMap(responseListeners); responseListeners.clear(); } // Clear pending responseListeners that will not receive their responses for (SendActionCallback responseListener : oldResponseListeners.values()) { // Allows to unblock waiting sendAction() calls try { responseListener.onResponse(null); } catch (Exception ex) { logger.warn("Exception notifying responseListener.onResponse(null)", ex); } } HashMap oldResponseEventListeners = null; synchronized (responseEventListeners) { // Store remaining responseEventListeners to be notified outside of // synchronized oldResponseEventListeners = new HashMap(responseEventListeners); responseEventListeners.clear(); } // Remove those already cleaned up via oldResponseListeners // TODO or should all be notified? for (String discardedInternalActionId : oldResponseListeners.keySet()) { oldResponseEventListeners.remove(discardedInternalActionId); } // Notify remaining responseEventListeners. // These could be EventGeneratingAction handlers that have received a // response but have not yet received the end event. for (ManagerEventListener responseEventListener : oldResponseEventListeners.values()) { try { // Allows to unblock waiting sendAction() calls responseEventListener.onManagerEvent(event); } catch (Exception ex) { logger.warn("Exception notifying responseListener.onManagerEvent(DisconnectEvent)", ex); } } } private void cleanup() { disconnect(); this.readerThread = null; } @Override public String toString() { StringBuilder sb; sb = new StringBuilder("ManagerConnection["); sb.append("id='").append(id).append("',"); sb.append("hostname='").append(hostname).append("',"); sb.append("port=").append(port).append(","); sb.append("systemHashcode=").append(System.identityHashCode(this)).append("]"); return sb.toString(); } /* Helper classes */ /** * A simple data object to store a ManagerResult. */ private static class ResponseHandlerResult implements Serializable { /** * Serializable version identifier. */ private static final long serialVersionUID = 7831097958568769220L; private ManagerResponse response; private final CountDownLatch latch = new CountDownLatch(1); private volatile boolean disposed = false; public ResponseHandlerResult() { } public ManagerResponse getResponse() { return this.response; } public void setResponse(ManagerResponse response) { this.response = response; if (disposed) { logger.error("Response arrived after Disposal and assumably Timeout " + response); } } public void dispose() { disposed = true; } private void countDown() { latch.countDown(); } /** * @param millis - milliseconds to wait * @return * @throws InterruptedException */ private boolean await(long millis) throws InterruptedException { return latch.await(millis, TimeUnit.MILLISECONDS); } } /** * A simple response handler that stores the received response in a * ResponseHandlerResult for further processing. */ private static class DefaultSendActionCallback implements SendActionCallback, Serializable { /** * Serializable version identifier. */ private static final long serialVersionUID = 2926598671855316803L; private final ResponseHandlerResult result; /** * Creates a new instance. * * @param result the result to store the response in */ public DefaultSendActionCallback(ResponseHandlerResult result) { this.result = result; } public void onResponse(ManagerResponse response) { // null response happens when connection is lost if (response != null) { result.setResponse(response); } result.countDown(); } } /** * A combinded event and response handler that adds received events and the * response to a ResponseEvents object. */ private static class ResponseEventHandler implements ManagerEventListener, SendActionCallback { private final ResponseEventsImpl events; private final Class< ? > actionCompleteEventClass; /** * Creates a new instance. * * @param events the ResponseEventsImpl to store the events in * @param actionCompleteEventClass the type of event that indicates that * all events have been received */ public ResponseEventHandler(ResponseEventsImpl events, Class< ? > actionCompleteEventClass) { this.events = events; this.actionCompleteEventClass = actionCompleteEventClass; } public void onManagerEvent(ManagerEvent event) { if (event instanceof DisconnectEvent) { // Set flag that must not wait for the response events.setComplete(true); // unblock potentially waiting synchronous call to // sendEventGeneratingAction(EventGeneratingAction action, long // timeout) events.countDown(); return; } // should always be a ResponseEvent, anyway... if (event instanceof ResponseEvent) { ResponseEvent responseEvent; responseEvent = (ResponseEvent) event; events.addEvent(responseEvent); } // finished? if (actionCompleteEventClass.isAssignableFrom(event.getClass())) { events.setComplete(true); // notify if action complete event and response have been // received if (events.getResponse() != null) { events.countDown(); } } } public void onResponse(ManagerResponse response) { // If disconnected if (response == null) { // Set flag that must not wait for the response events.setComplete(true); // unblock potentially waiting synchronous call to // sendEventGeneratingAction(EventGeneratingAction action, long // timeout) events.countDown(); return; } events.setRepsonse(response); if (response instanceof ManagerError) { events.setComplete(true); } // finished? // notify if action complete event and response have been // received if (events.isComplete()) { events.countDown(); } } } private class AsyncEventGeneratingResponseHandler implements SendActionCallback, ManagerEventListener { private final Class< ? extends ResponseEvent> actionCompleteEventClass; private final SendEventGeneratingActionCallback callback; private final ResponseEventsImpl events; public AsyncEventGeneratingResponseHandler(Class< ? extends ResponseEvent> actionCompleteEventClass, SendEventGeneratingActionCallback callback) { this.actionCompleteEventClass = actionCompleteEventClass; this.callback = callback; this.events = new ResponseEventsImpl(); } @Override public void onManagerEvent(ManagerEvent event) { if (event instanceof DisconnectEvent) { callback.onResponse(events); return; } // should always be a ResponseEvent, anyway... if (false == (event instanceof ResponseEvent)) { return; } ResponseEvent responseEvent = (ResponseEvent) event; events.addEvent(responseEvent); if (actionCompleteEventClass.isAssignableFrom(event.getClass())) { events.setComplete(true); String internalActionId = responseEvent.getInternalActionId(); synchronized (responseEventListeners) { responseEventListeners.remove(internalActionId); } callback.onResponse(events); } } @Override public void onResponse(ManagerResponse response) { // If disconnected if (response == null) { callback.onResponse(events); return; } events.setRepsonse(response); if (response instanceof ManagerError) { events.setComplete(true); } // finished? if (events.isComplete()) { // invoke callback callback.onResponse(events); } } } @Override public void deregisterEventClass(Class< ? extends ManagerEvent> eventClass) { if (reader == null) { reader = createReader(this, this); } reader.deregisterEventClass(eventClass); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy