Add framework to connect to, and read and write protocol based messages from and to an external application using an Asterisk External Application Protocol (AEAP). This has been divided into several abstractions: 1. transport - base communication layer (currently websocket only) 2. message - AEAP description and data (currently JSON only) 3. transaction - links/binds requests and responses 4. aeap - transport, message, and transaction handler/manager This patch also adds an AEAP implementation for speech to text. Existing speech API callbacks for speech to text have been completed making it possible for Asterisk to connect to a configured external translator service and provide audio for STT. Results can also be received from the external translator, and made available as speech results in Asterisk. Unit tests have also been created that test the AEAP framework, and also the speech to text implementation. ASTERISK-29726 #close Change-Id: Iaa4b259f84aa63501e5fd2a6fb107f900b4d4ed2
502 lines
12 KiB
C
502 lines
12 KiB
C
/*
|
|
* Asterisk -- An open source telephony toolkit.
|
|
*
|
|
* Copyright (C) 2021, Sangoma Technologies Corporation
|
|
*
|
|
* Kevin Harwell <kharwell@sangoma.com>
|
|
*
|
|
* See http://www.asterisk.org for more information about
|
|
* the Asterisk project. Please do not directly contact
|
|
* any of the maintainers of this project for assistance;
|
|
* the project provides a web site, mailing lists and IRC
|
|
* channels for your use.
|
|
*
|
|
* This program is free software, distributed under the terms of
|
|
* the GNU General Public License Version 2. See the LICENSE file
|
|
* at the top of the source tree.
|
|
*/
|
|
|
|
#include "asterisk.h"
|
|
|
|
#include <pthread.h>
|
|
|
|
#include "asterisk/astobj2.h"
|
|
#include "asterisk/strings.h"
|
|
|
|
#include "asterisk/res_aeap.h"
|
|
#include "asterisk/res_aeap_message.h"
|
|
|
|
#include "logger.h"
|
|
#include "transaction.h"
|
|
#include "transport.h"
|
|
|
|
#define AEAP_RECV_SIZE 32768
|
|
|
|
struct aeap_user_data {
|
|
/*! The user data object */
|
|
void *obj;
|
|
/*! A user data identifier */
|
|
char id[0];
|
|
};
|
|
|
|
AO2_STRING_FIELD_HASH_FN(aeap_user_data, id);
|
|
AO2_STRING_FIELD_CMP_FN(aeap_user_data, id);
|
|
|
|
#define USER_DATA_BUCKETS 11
|
|
|
|
struct ast_aeap {
|
|
/*! This object's configuration parameters */
|
|
const struct ast_aeap_params *params;
|
|
/*! Container for registered user data objects */
|
|
struct ao2_container *user_data;
|
|
/*! Transactions container */
|
|
struct ao2_container *transactions;
|
|
/*! Transport layer communicator */
|
|
struct aeap_transport *transport;
|
|
/*! Id of thread that reads data from the transport */
|
|
pthread_t read_thread_id;
|
|
};
|
|
|
|
static int tsx_end(void *obj, void *arg, int flags)
|
|
{
|
|
aeap_transaction_end(obj, -1);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void aeap_destructor(void *obj)
|
|
{
|
|
struct ast_aeap *aeap = obj;
|
|
|
|
/* Disconnect things first, which keeps transactions from further executing */
|
|
ast_aeap_disconnect(aeap);
|
|
|
|
aeap_transport_destroy(aeap->transport);
|
|
|
|
/*
|
|
* Each contained transaction holds a pointer back to this transactions container,
|
|
* which is removed upon transaction end. Thus by explicitly ending each transaction
|
|
* here we can ensure all references to the transactions container are removed.
|
|
*/
|
|
ao2_callback(aeap->transactions, OBJ_UNLINK | OBJ_NODATA | OBJ_MULTIPLE,
|
|
tsx_end, NULL);
|
|
ao2_cleanup(aeap->transactions);
|
|
|
|
ao2_cleanup(aeap->user_data);
|
|
}
|
|
|
|
struct ast_aeap *ast_aeap_create(const char *transport_type,
|
|
const struct ast_aeap_params *params)
|
|
{
|
|
struct ast_aeap *aeap;
|
|
|
|
aeap = ao2_alloc(sizeof(*aeap), aeap_destructor);
|
|
if (!aeap) {
|
|
ast_log(LOG_ERROR, "AEAP: unable to create");
|
|
return NULL;
|
|
}
|
|
|
|
aeap->params = params;
|
|
aeap->read_thread_id = AST_PTHREADT_NULL;
|
|
|
|
aeap->user_data = ao2_container_alloc_hash(AO2_ALLOC_OPT_LOCK_MUTEX, 0, USER_DATA_BUCKETS,
|
|
aeap_user_data_hash_fn, NULL, aeap_user_data_cmp_fn);
|
|
if (!aeap->user_data) {
|
|
aeap_error(aeap, NULL, "unable to create user data container");
|
|
ao2_ref(aeap, -1);
|
|
return NULL;
|
|
}
|
|
|
|
aeap->transactions = aeap_transactions_create();
|
|
if (!aeap->transactions) {
|
|
aeap_error(aeap, NULL, "unable to create transactions container");
|
|
ao2_ref(aeap, -1);
|
|
return NULL;
|
|
}
|
|
|
|
aeap->transport = aeap_transport_create(transport_type);
|
|
if (!aeap->transport) {
|
|
aeap_error(aeap, NULL, "unable to create transport");
|
|
ao2_ref(aeap, -1);
|
|
return NULL;
|
|
}
|
|
|
|
return aeap;
|
|
}
|
|
|
|
static struct aeap_user_data *aeap_user_data_create(const char *id, void *obj,
|
|
ast_aeap_user_obj_cleanup cleanup)
|
|
{
|
|
struct aeap_user_data *data;
|
|
|
|
ast_assert(id != NULL);
|
|
|
|
data = ao2_t_alloc_options(sizeof(*data) + strlen(id) + 1, cleanup,
|
|
AO2_ALLOC_OPT_LOCK_NOLOCK, "");
|
|
if (!data) {
|
|
if (cleanup) {
|
|
cleanup(obj);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
strcpy(data->id, id); /* safe */
|
|
data->obj = obj;
|
|
|
|
return data;
|
|
}
|
|
|
|
int ast_aeap_user_data_register(struct ast_aeap *aeap, const char *id, void *obj,
|
|
ast_aeap_user_obj_cleanup cleanup)
|
|
{
|
|
struct aeap_user_data *data;
|
|
|
|
data = aeap_user_data_create(id, obj, cleanup);
|
|
if (!data) {
|
|
return -1;
|
|
}
|
|
|
|
if (!ao2_link(aeap->user_data, data)) {
|
|
ao2_ref(data, -1);
|
|
return -1;
|
|
}
|
|
|
|
ao2_ref(data, -1);
|
|
return 0;
|
|
}
|
|
|
|
void ast_aeap_user_data_unregister(struct ast_aeap *aeap, const char *id)
|
|
{
|
|
ao2_find(aeap->user_data, id, OBJ_SEARCH_KEY | OBJ_UNLINK | OBJ_NODATA);
|
|
}
|
|
|
|
void *ast_aeap_user_data_object_by_id(struct ast_aeap *aeap, const char *id)
|
|
{
|
|
struct aeap_user_data *data;
|
|
void *obj;
|
|
|
|
data = ao2_find(aeap->user_data, id, OBJ_SEARCH_KEY);
|
|
if (!data) {
|
|
return NULL;
|
|
}
|
|
|
|
obj = data->obj;
|
|
ao2_ref(data, -1);
|
|
|
|
/*
|
|
* Returned object's lifetime is based on how it was registered.
|
|
* See public function docs for more info
|
|
*/
|
|
return obj;
|
|
}
|
|
|
|
static int raise_msg_handler(struct ast_aeap *aeap, const struct ast_aeap_message_handler *handlers,
|
|
size_t size, struct ast_aeap_message *msg, void *data)
|
|
{
|
|
ast_aeap_on_message on_message = NULL;
|
|
size_t i;
|
|
|
|
if (!aeap->params->emit_error) {
|
|
const char *error_msg = ast_aeap_message_error_msg(msg);
|
|
|
|
if (error_msg) {
|
|
aeap_error(aeap, NULL, "%s", error_msg);
|
|
return -1;
|
|
}
|
|
|
|
/* If no error_msg then it's assumed this is not an error message */
|
|
}
|
|
|
|
for (i = 0; i < size; ++i) {
|
|
if (ast_strlen_zero(handlers[i].name)) {
|
|
/* A default handler is specified. Use it if no other match is found */
|
|
on_message = handlers[i].on_message;
|
|
continue;
|
|
}
|
|
|
|
if (ast_aeap_message_is_named(msg, handlers[i].name)) {
|
|
on_message = handlers[i].on_message;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (on_message) {
|
|
return on_message(aeap, msg, data);
|
|
}
|
|
|
|
/* Respond with un-handled error */
|
|
ast_aeap_send_msg(aeap, ast_aeap_message_create_error(aeap->params->msg_type,
|
|
ast_aeap_message_name(msg), ast_aeap_message_id(msg),
|
|
"Unsupported and/or un-handled message"));
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void raise_msg(struct ast_aeap *aeap, const void *buf, intmax_t size,
|
|
enum AST_AEAP_DATA_TYPE serial_type)
|
|
{
|
|
struct ast_aeap_message *msg;
|
|
struct aeap_transaction *tsx;
|
|
int res = 0;
|
|
|
|
if (!aeap->params || !aeap->params->msg_type ||
|
|
ast_aeap_message_serial_type(aeap->params->msg_type) != serial_type ||
|
|
!(msg = ast_aeap_message_deserialize(aeap->params->msg_type, buf, size))) {
|
|
return;
|
|
}
|
|
|
|
/* See if this msg is involved in a transaction */
|
|
tsx = aeap_transaction_get(aeap->transactions, ast_aeap_message_id(msg));
|
|
|
|
/* If so go ahead and cancel the timeout timer */
|
|
aeap_transaction_cancel_timer(tsx);
|
|
|
|
if (aeap->params->request_handlers && ast_aeap_message_is_request(msg)) {
|
|
res = raise_msg_handler(aeap, aeap->params->request_handlers, aeap->params->request_handlers_size,
|
|
msg, tsx ? aeap_transaction_user_obj(tsx) : NULL);
|
|
} else if (aeap->params->response_handlers && ast_aeap_message_is_response(msg)) {
|
|
res = raise_msg_handler(aeap, aeap->params->response_handlers, aeap->params->response_handlers_size,
|
|
msg, tsx ? aeap_transaction_user_obj(tsx) : NULL);
|
|
}
|
|
|
|
/* Complete transaction (Note, removes tsx ref) */
|
|
aeap_transaction_end(tsx, res);
|
|
|
|
ao2_ref(msg, -1);
|
|
}
|
|
|
|
static void *aeap_receive(void *data)
|
|
{
|
|
struct ast_aeap *aeap = data;
|
|
void *buf;
|
|
|
|
buf = ast_calloc(1, AEAP_RECV_SIZE);
|
|
if (!buf) {
|
|
aeap_error(aeap, NULL, "unable to create read buffer");
|
|
goto aeap_receive_error;
|
|
}
|
|
|
|
while (aeap_transport_is_connected(aeap->transport)) {
|
|
enum AST_AEAP_DATA_TYPE rtype;
|
|
intmax_t size;
|
|
|
|
size = aeap_transport_read(aeap->transport, buf, AEAP_RECV_SIZE, &rtype);
|
|
if (size < 0) {
|
|
goto aeap_receive_error;
|
|
}
|
|
|
|
if (!size) {
|
|
continue;
|
|
}
|
|
|
|
switch (rtype) {
|
|
case AST_AEAP_DATA_TYPE_BINARY:
|
|
if (aeap->params && aeap->params->on_binary) {
|
|
aeap->params->on_binary(aeap, buf, size);
|
|
}
|
|
break;
|
|
case AST_AEAP_DATA_TYPE_STRING:
|
|
ast_debug(3, "AEAP: received message: %s\n", (char *)buf);
|
|
if (aeap->params && aeap->params->on_string) {
|
|
aeap->params->on_string(aeap, (const char *)buf, size - 1);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
raise_msg(aeap, buf, size, rtype);
|
|
};
|
|
|
|
ast_free(buf);
|
|
return NULL;
|
|
|
|
aeap_receive_error:
|
|
/*
|
|
* An unrecoverable error occurred so ensure the aeap and transport reset
|
|
* to a disconnected state. We don't want this thread to "join" itself so set
|
|
* its id to NULL prior to disconnecting.
|
|
*/
|
|
aeap_error(aeap, NULL, "unrecoverable read error, disconnecting");
|
|
|
|
ao2_lock(aeap);
|
|
aeap->read_thread_id = AST_PTHREADT_NULL;
|
|
ao2_unlock(aeap);
|
|
|
|
ast_aeap_disconnect(aeap);
|
|
|
|
ast_free(buf);
|
|
|
|
if (aeap->params && aeap->params->on_error) {
|
|
aeap->params->on_error(aeap);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
int ast_aeap_connect(struct ast_aeap *aeap, const char *url, const char *protocol, int timeout)
|
|
{
|
|
SCOPED_AO2LOCK(lock, aeap);
|
|
|
|
if (aeap_transport_is_connected(aeap->transport)) {
|
|
/* Should already be connected, so nothing to do */
|
|
return 0;
|
|
}
|
|
|
|
if (aeap_transport_connect(aeap->transport, url, protocol, timeout)) {
|
|
aeap_error(aeap, NULL, "unable to connect transport");
|
|
return -1;
|
|
}
|
|
|
|
if (ast_pthread_create_background(&aeap->read_thread_id, NULL,
|
|
aeap_receive, aeap)) {
|
|
aeap_error(aeap, NULL, "unable to start read thread: %s",
|
|
strerror(errno));
|
|
ast_aeap_disconnect(aeap);
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
struct ast_aeap *ast_aeap_create_and_connect(const char *type,
|
|
const struct ast_aeap_params *params, const char *url, const char *protocol, int timeout)
|
|
{
|
|
struct ast_aeap *aeap;
|
|
|
|
aeap = ast_aeap_create(type, params);
|
|
if (!aeap) {
|
|
return NULL;
|
|
}
|
|
|
|
if (ast_aeap_connect(aeap, url, protocol, timeout)) {
|
|
ao2_ref(aeap, -1);
|
|
return NULL;
|
|
}
|
|
|
|
return aeap;
|
|
}
|
|
|
|
int ast_aeap_disconnect(struct ast_aeap *aeap)
|
|
{
|
|
ao2_lock(aeap);
|
|
|
|
aeap_transport_disconnect(aeap->transport);
|
|
|
|
if (aeap->read_thread_id != AST_PTHREADT_NULL) {
|
|
/*
|
|
* The read thread calls disconnect if an error occurs, so
|
|
* unlock the aeap before "joining" to avoid a deadlock.
|
|
*/
|
|
ao2_unlock(aeap);
|
|
pthread_join(aeap->read_thread_id, NULL);
|
|
ao2_lock(aeap);
|
|
|
|
aeap->read_thread_id = AST_PTHREADT_NULL;
|
|
}
|
|
|
|
ao2_unlock(aeap);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int aeap_send(struct ast_aeap *aeap, const void *buf, uintmax_t size,
|
|
enum AST_AEAP_DATA_TYPE type)
|
|
{
|
|
intmax_t num;
|
|
|
|
num = aeap_transport_write(aeap->transport, buf, size, type);
|
|
|
|
if (num == 0) {
|
|
/* Nothing written, could be disconnected */
|
|
return 0;
|
|
}
|
|
|
|
if (num < 0) {
|
|
aeap_error(aeap, NULL, "error sending data");
|
|
return -1;
|
|
}
|
|
|
|
if (num < size) {
|
|
aeap_error(aeap, NULL, "not all data sent");
|
|
return -1;
|
|
}
|
|
|
|
if (num > size) {
|
|
aeap_error(aeap, NULL, "sent data truncated");
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int ast_aeap_send_binary(struct ast_aeap *aeap, const void *buf, uintmax_t size)
|
|
{
|
|
return aeap_send(aeap, buf, size, AST_AEAP_DATA_TYPE_BINARY);
|
|
}
|
|
|
|
int ast_aeap_send_msg(struct ast_aeap *aeap, struct ast_aeap_message *msg)
|
|
{
|
|
void *buf;
|
|
intmax_t size;
|
|
int res;
|
|
|
|
if (!msg) {
|
|
aeap_error(aeap, NULL, "no message to send");
|
|
return -1;
|
|
}
|
|
|
|
if (ast_aeap_message_serialize(msg, &buf, &size)) {
|
|
aeap_error(aeap, NULL, "unable to serialize outgoing message");
|
|
ao2_ref(msg, -1);
|
|
return -1;
|
|
}
|
|
|
|
res = aeap_send(aeap, buf, size, msg->type->serial_type);
|
|
|
|
ast_free(buf);
|
|
ao2_ref(msg, -1);
|
|
|
|
return res;
|
|
}
|
|
|
|
int ast_aeap_send_msg_tsx(struct ast_aeap *aeap, struct ast_aeap_tsx_params *params)
|
|
{
|
|
struct aeap_transaction *tsx = NULL;
|
|
int res = 0;
|
|
|
|
if (!params) {
|
|
return -1;
|
|
}
|
|
|
|
if (!params->msg) {
|
|
aeap_transaction_params_cleanup(params);
|
|
aeap_error(aeap, NULL, "no message to send");
|
|
return -1;
|
|
}
|
|
|
|
/* The transaction will take over params cleanup, which includes the msg reference */
|
|
tsx = aeap_transaction_create_and_add(aeap->transactions,
|
|
ast_aeap_message_id(params->msg), params, aeap);
|
|
if (!tsx) {
|
|
return -1;
|
|
}
|
|
|
|
if (ast_aeap_send_msg(aeap, ao2_bump(params->msg))) {
|
|
aeap_transaction_end(tsx, -1); /* Removes container, and tsx ref */
|
|
return -1;
|
|
}
|
|
|
|
if (aeap_transaction_start(tsx)) {
|
|
aeap_transaction_end(tsx, -1); /* Removes container, and tsx ref */
|
|
return -1;
|
|
}
|
|
|
|
res = aeap_transaction_result(tsx);
|
|
|
|
ao2_ref(tsx, -1);
|
|
|
|
return res;
|
|
}
|