[Asterisk-code-review] res pjsip history: Add a module that provides PJSIP history ... (asterisk[13])

Matt Jordan asteriskteam at digium.com
Wed Dec 23 12:06:22 CST 2015


Matt Jordan has uploaded a new change for review.

  https://gerrit.asterisk.org/1849

Change subject: res_pjsip_history: Add a module that provides PJSIP history for debugging
......................................................................

res_pjsip_history: Add a module that provides PJSIP history for debugging

This patch adds a new module, res_pjsip_history, that provides a slightly
better way of debugging SIP message traffic on a busy Asterisk system. The
existing mechanisms all rely on passively dumping a SIP message to the CLI.
While this is perfectly fine for logging purposes and well controlled
environments, on many installations, the amount of SIP messages Asterisk
receives will quickly swamp the CLI. This makes it difficult to view/capture
those messages that you want to diagnose in real time.

This patch provides another way of handling this. When enabled, the module
will store SIP message traffic in memory. This traffic can then be queried
at leisure.

In order to make the querying useful, a CLI command has been implemented,
'pjsip show history', that supports a basic expression syntax similar to
SQL or other query languages. A small number of useful fields have been
added in this initial patch; additional fields can easily be added in
later improvements. Those fields are:
 - number: The entry index in the history
 - timestamp: The time the message was recieved
 - addr: The source/destination address of the message
 - sip.msg.request.method: The request method
 - sip.msg.call-id: The Call-ID header

Note - this is a resurrection of the module initially proposed on Review Board
here: https://reviewboard.asterisk.org/r/4053/

Change-Id: I39bd74ce998e99ad5ebc0aab3e84df3a150f8e36
---
M CHANGES
A res/res_pjsip_history.c
2 files changed, 1,361 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.asterisk.org:29418/asterisk refs/changes/49/1849/4

diff --git a/CHANGES b/CHANGES
index d807149..18a1737 100644
--- a/CHANGES
+++ b/CHANGES
@@ -9,6 +9,31 @@
 ==============================================================================
 
 ------------------------------------------------------------------------------
+--- Functionality changes from Asterisk 13.7.0 to Asterisk 13.8.0 ------------
+------------------------------------------------------------------------------
+
+res_pjsip_history
+------------------
+ * A new module, res_pjsip_history, has been added that provides SIP history
+   viewing/filtering from the CLI. The module is intended to be used on systems
+   with busy SIP traffic, where existing forms of viewing SIP messages - such
+   as the res_pjsip_logger - may be inadequate. The module provides two new
+   CLI commands:
+   - 'pjsip set history {on|off|clear}' - this enables/disables SIP history
+     capturing, as well as clears an existing history capture. Note that SIP
+     packets captured are stored in memory until cleared. As a result, the
+     history capture should only be used for debugging/viewing purposes, and
+     should *NOT* be left permanently enabled on a system.
+   - 'pjsip show history' - displays the captured SIP history. When invoked
+     with no options, the entire captured history is displayed. Two options
+     are available:
+     -- 'entry <num>' - display a detailed view of a single SIP message in
+        the history
+     -- 'where ...' - filter the history based on some expression. For more
+        information on filtering, view the current CLI help for the
+        'pjsip show history' command.
+
+------------------------------------------------------------------------------
 --- Functionality changes from Asterisk 13.6.0 to Asterisk 13.7.0 ------------
 ------------------------------------------------------------------------------
 
diff --git a/res/res_pjsip_history.c b/res/res_pjsip_history.c
new file mode 100644
index 0000000..a91dfba
--- /dev/null
+++ b/res/res_pjsip_history.c
@@ -0,0 +1,1336 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 1999 - 2015, Digium, Inc.
+ *
+ * Matt Jordan <mjordan at digium.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.
+ */
+
+/*!
+ * \file
+ * \brief PJSIP History
+ *
+ * \author Matt Jordan <mjordan at digium.com>
+ *
+ */
+
+/*** MODULEINFO
+	<depend>pjproject</depend>
+	<depend>res_pjsip</depend>
+	<support_level>extended</support_level>
+ ***/
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#include <pjsip.h>
+#include <regex.h>
+
+#include "asterisk/res_pjsip.h"
+#include "asterisk/module.h"
+#include "asterisk/logger.h"
+#include "asterisk/cli.h"
+#include "asterisk/netsock2.h"
+#include "asterisk/vector.h"
+#include "asterisk/lock.h"
+
+#define HISTORY_INITIAL_SIZE 256
+
+/*! \brief Pool factory used by pjlib to allocate memory. */
+static pj_caching_pool cachingpool;
+
+/*! \brief Whether or not we are storing history */
+static int enabled;
+
+/*! \brief Packet count */
+static int packet_number;
+
+/*! \brief An item in the history */
+struct pjsip_history_entry {
+	/*! \brief Packet number */
+	int number;
+	/*! \brief Whether or not we transmitted the packet */
+	int transmitted;
+	/*! \brief Time the packet was received */
+	struct timeval timestamp;
+	/*! \brief Source address */
+	pj_sockaddr_in src;
+	/*! \brief Destination address */
+	pj_sockaddr_in dst;
+	/*! \brief Memory pool used to allocate \c msg */
+	pj_pool_t *pool;
+	/*! \brief The actual SIP message */
+	pjsip_msg *msg;
+};
+
+/*! \brief Mutex that protects \ref vector_history */
+AST_MUTEX_DEFINE_STATIC(history_lock);
+
+/*! \brief The one and only history that we've captured */
+static AST_VECTOR(vector_history_t, struct pjsip_history_entry *) vector_history;
+
+struct expression_token;
+
+/*! \brief An operator that we understand in an expression */
+struct operator {
+	/*! \brief Our operator's symbol */
+	const char *symbol;
+	/*! \brief Precedence of the symbol */
+	int precedence;
+	/*! \brief Non-zero if the operator is evaluated right-to-left */
+	int right_to_left;
+	/*! \brief Number of operands the operator takes */
+	int operands;
+	/*!
+	 * \brief Evaluation function for unary operators
+	 *
+	 * \param op The operator being evaluated
+	 * \param type The type of value contained in \c operand
+	 * \param operand A pointer to the value to evaluate
+	 *
+	 * \retval -1 error
+	 * \retval 0 evaluation is False
+	 * \retval 1 evaluation is True
+	 */
+	int (* const evaluate_unary)(struct operator *op, enum aco_option_type type, void *operand);
+	/*!
+	 * \brief Evaluation function for binary operators
+	 *
+	 * \param op The operator being evaluated
+	 * \param type The type of value contained in \c op_left
+	 * \param op_left A pointer to the value to evaluate (a result or extracted from an entry)
+	 * \param op_right The expression token containing the other value (a result or user-provided)
+	 *
+	 * \retval -1 error
+	 * \retval 0 evaluation is False
+	 * \retval 1 evaluation is True
+	 */
+	int (* const evaluate)(struct operator *op, enum aco_option_type type, void *op_left, struct expression_token *op_right);
+};
+
+/*! \brief A field that we understand and can perform operations on */
+struct allowed_field {
+	/*! \brief The representation of the field */
+	const char *symbol;
+	/*! \brief The type /c get_field returns */
+	enum aco_option_type return_type;
+	/*!
+	 * \brief Function that returns the field from a pjsip_history_entry
+	 *
+	 * Note that the function must return a pointer to the location in
+	 * \c pjsip_history_entry - no memory should be allocated as the caller
+	 * will not dispose of any
+	 */
+	void *(* const get_field)(struct pjsip_history_entry *entry);
+};
+
+/*! \brief The type of token that has been parsed out of an expression */
+enum expression_token_type {
+	/*! The \c expression_token contains a field */
+	TOKEN_TYPE_FIELD,
+	/*! The \c expression_token contains an operator */
+	TOKEN_TYPE_OPERATOR,
+	/*! The \c expression_token contains a previous result */
+	TOKEN_TYPE_RESULT
+};
+
+/*! \brief A token in the expression or an evaluated part of the expression */
+struct expression_token {
+	/*! \brief The next expression token in the queue */
+	struct expression_token *next;
+	/*! \brief The type of value stored in the expression token */
+	enum expression_token_type token_type;
+	/*! \brief An operator that evaluates expressions */
+	struct operator *op;
+	/*! \brief The result of an evaluated expression */
+	int result;
+	/*! \brief The field in the expression */
+	char field[];
+};
+
+/*!
+ * \brief Operator callback for determining equality
+ */
+static int evaluate_equal(struct operator *op, enum aco_option_type type, void *op_left, struct expression_token *op_right)
+{
+	switch (type) {
+	case OPT_BOOL_T:
+	case OPT_BOOLFLAG_T:
+	case OPT_INT_T:
+	case OPT_UINT_T:
+	{
+		int right;
+
+		if (sscanf(op_right->field, "%30d", &right) != 1) {
+			ast_log(LOG_WARNING, "Unable to extract field '%s': not an integer\n", op_right->field);
+			return -1;
+		}
+		return (*(int *)op_left) == right;
+	}
+	case OPT_DOUBLE_T:
+	{
+		double right;
+
+		if (sscanf(op_right->field, "%lf", &right) != 1) {
+			ast_log(LOG_WARNING, "Unable to extract field '%s': not a double\n", op_right->field);
+			return -1;
+		}
+		return (*(double *)op_left) == right;
+	}
+	case OPT_CHAR_ARRAY_T:
+	case OPT_STRINGFIELD_T:
+		/* In our case, we operate on pj_str_t */
+		return pj_strcmp2(op_left, op_right->field) == 0;
+	case OPT_NOOP_T:
+	/* Used for timeval */
+	{
+		struct timeval right = { 0, };
+
+		if (sscanf(op_right->field, "%ld", &right.tv_sec) != 1) {
+			ast_log(LOG_WARNING, "Unable to extract field '%s': not a timestamp\n", op_right->field);
+			return -1;
+		}
+
+		return ast_tvcmp(*(struct timeval *)op_left, right) == 0;
+	}
+	case OPT_SOCKADDR_T:
+	/* In our case, we operate only on pj_sockaddr_t */
+	{
+		pj_sockaddr right;
+		pj_str_t str_right;
+
+		pj_cstr(&str_right, op_right->field);
+		if (pj_sockaddr_parse(pj_AF_UNSPEC(), 0, &str_right, &right) != PJ_SUCCESS) {
+			ast_log(LOG_WARNING, "Unable to convert field '%s': not an IPv4 or IPv6 address\n", op_right->field);
+			return -1;
+		}
+
+		return pj_sockaddr_cmp(op_left, &right) == 0;
+	}
+	default:
+		ast_log(LOG_WARNING, "Cannot evaluate field '%s': invalid type for operator '%s'\n",
+			op_right->field, op->symbol);
+	}
+
+	return -1;
+}
+
+/*!
+ * \brief Operator callback for determining inequality
+ */
+static int evaluate_not_equal(struct operator *op, enum aco_option_type type, void *op_left, struct expression_token *op_right)
+{
+	return !evaluate_equal(op, type, op_left, op_right);
+}
+
+/*
+ * \brief Operator callback for determining if one operand is less than another
+ */
+static int evaluate_less_than(struct operator *op, enum aco_option_type type, void *op_left, struct expression_token *op_right)
+{
+	switch (type) {
+	case OPT_BOOL_T:
+	case OPT_BOOLFLAG_T:
+	case OPT_INT_T:
+	case OPT_UINT_T:
+	{
+		int right;
+
+		if (sscanf(op_right->field, "%30d", &right) != 1) {
+			ast_log(LOG_WARNING, "Unable to extract field '%s': not an integer\n", op_right->field);
+			return -1;
+		}
+		return (*(int *)op_left) < right;
+	}
+	case OPT_DOUBLE_T:
+	{
+		double right;
+
+		if (sscanf(op_right->field, "%lf", &right) != 1) {
+			ast_log(LOG_WARNING, "Unable to extract field '%s': not a double\n", op_right->field);
+			return -1;
+		}
+		return (*(double *)op_left) < right;
+	}
+	case OPT_NOOP_T:
+	/* Used for timeval */
+	{
+		struct timeval right = { 0, };
+
+		if (sscanf(op_right->field, "%ld", &right.tv_sec) != 1) {
+			ast_log(LOG_WARNING, "Unable to extract field '%s': not a timestamp\n", op_right->field);
+			return -1;
+		}
+
+		return ast_tvcmp(*(struct timeval *)op_left, right) == -1;
+	}
+	default:
+		ast_log(LOG_WARNING, "Cannot evaluate field '%s': invalid type for operator '%s'\n",
+			op_right->field, op->symbol);
+	}
+
+	return -1;
+}
+
+/*
+ * \brief Operator callback for determining if one operand is greater than another
+ */
+static int evaluate_greater_than(struct operator *op, enum aco_option_type type, void *op_left, struct expression_token *op_right)
+{
+	switch (type) {
+	case OPT_BOOL_T:
+	case OPT_BOOLFLAG_T:
+	case OPT_INT_T:
+	case OPT_UINT_T:
+	{
+		int right;
+
+		if (sscanf(op_right->field, "%30d", &right) != 1) {
+			ast_log(LOG_WARNING, "Unable to extract field '%s': not an integer\n", op_right->field);
+			return -1;
+		}
+		return (*(int *)op_left) > right;
+	}
+	case OPT_DOUBLE_T:
+	{
+		double right;
+
+		if (sscanf(op_right->field, "%lf", &right) != 1) {
+			ast_log(LOG_WARNING, "Unable to extract field '%s': not a double\n", op_right->field);
+			return -1;
+		}
+		return (*(double *)op_left) > right;
+	}
+	case OPT_NOOP_T:
+	/* Used for timeval */
+	{
+		struct timeval right = { 0, };
+
+		if (sscanf(op_right->field, "%ld", &right.tv_sec) != 1) {
+			ast_log(LOG_WARNING, "Unable to extract field '%s': not a timestamp\n", op_right->field);
+			return -1;
+		}
+
+		return ast_tvcmp(*(struct timeval *)op_left, right) == 1;
+	}
+	default:
+		ast_log(LOG_WARNING, "Cannot evaluate field '%s': invalid type for operator '%s'\n",
+			op_right->field, op->symbol);
+	}
+
+	return -1;
+}
+
+/*
+ * \brief Operator callback for determining if one operand is less than or equal to another
+ */
+static int evaluate_less_than_or_equal(struct operator *op, enum aco_option_type type, void *op_left, struct expression_token *op_right)
+{
+	return evaluate_less_than(op, type, op_left, op_right) || evaluate_equal(op, type, op_left, op_right);
+}
+
+/*
+ * \brief Operator callback for determining if one operand is greater than or equal to another
+ */
+static int evaluate_greater_than_or_equal(struct operator *op, enum aco_option_type type, void *op_left, struct expression_token *op_right)
+{
+	return evaluate_greater_than(op, type, op_left, op_right) || evaluate_equal(op, type, op_left, op_right);
+}
+
+/*
+ * \brief Operator callback for determining logical NOT
+ */
+static int evaluate_not(struct operator *op, enum aco_option_type type, void *operand)
+{
+	switch (type) {
+	case OPT_BOOL_T:
+	case OPT_BOOLFLAG_T:
+	case OPT_INT_T:
+	case OPT_UINT_T:
+		return !(*(int *)operand);
+	default:
+		ast_log(LOG_WARNING, "Cannot evaluate: invalid operand type for operator '%s'\n", op->symbol);
+	}
+
+	return -1;
+}
+
+/*
+ * \brief Operator callback for determining logical AND
+ */
+static int evaluate_and(struct operator *op, enum aco_option_type type, void *op_left, struct expression_token *op_right)
+{
+	switch (type) {
+	case OPT_BOOL_T:
+	case OPT_BOOLFLAG_T:
+	case OPT_INT_T:
+	case OPT_UINT_T:
+		return (*(int *)op_left && op_right->result);
+	default:
+		ast_log(LOG_WARNING, "Cannot evaluate: invalid operand type for operator '%s'\n", op->symbol);
+	}
+
+	return -1;
+}
+
+/*
+ * \brief Operator callback for determining logical OR
+ */
+static int evaluate_or(struct operator *op, enum aco_option_type type, void *op_left, struct expression_token *op_right)
+{
+	switch (type) {
+	case OPT_BOOL_T:
+	case OPT_BOOLFLAG_T:
+	case OPT_INT_T:
+	case OPT_UINT_T:
+		return (*(int *)op_left || op_right->result);
+	default:
+		ast_log(LOG_WARNING, "Cannot evaluate: invalid operand type for operator '%s'\n", op->symbol);
+	}
+
+	return -1;
+}
+
+/*
+ * \brief Operator callback for regex 'like'
+ */
+static int evaluate_like(struct operator *op, enum aco_option_type type, void *op_left, struct expression_token *op_right)
+{
+	switch (type) {
+	case OPT_CHAR_ARRAY_T:
+	case OPT_STRINGFIELD_T:
+	/* In our case, we operate on pj_str_t */
+	{
+		int result;
+		regex_t regexbuf;
+		char buf[pj_strlen(op_left) + 1];
+
+		ast_copy_pj_str(buf, op_left, pj_strlen(op_left));
+		if (regcomp(&regexbuf, op_right->field, REG_EXTENDED | REG_NOSUB)) {
+			ast_log(LOG_WARNING, "Failed to compile '%s' into a regular expression\n", op_right->field);
+			return -1;
+		}
+
+		result = (regexec(&regexbuf, buf, 0, NULL, 0) == 0);
+		regfree(&regexbuf);
+
+		return result;
+	}
+	default:
+		ast_log(LOG_WARNING, "Cannot evaluate: invalid operand type for operator '%s'\n", op->symbol);
+	}
+
+	return -1;
+}
+
+/*!
+ * \brief Operator token for a left parenthesis.
+ *
+ * While this is used by the shunting-yard algorithm implementation,
+ * it should never appear in the resulting RPN queue of expression tokens
+ */
+static struct operator left_paren = {
+	.symbol = "(",
+	.precedence = 15
+};
+
+/*!
+ * \brief Our allowed operations
+ */
+static struct operator allowed_operators[] = {
+	{ .symbol = "=", .precedence = 7, .operands = 2, .evaluate = evaluate_equal, },
+	{ .symbol = "==", .precedence = 7, .operands = 2, .evaluate = evaluate_equal, },
+	{ .symbol = "!=", .precedence = 7, .operands = 2, .evaluate = evaluate_not_equal, },
+	{ .symbol = "<", .precedence = 6, .operands = 2, .evaluate = evaluate_less_than, },
+	{ .symbol = ">", .precedence = 6, .operands = 2, .evaluate = evaluate_greater_than, },
+	{ .symbol = "<=", .precedence = 6, .operands = 2, .evaluate = evaluate_less_than_or_equal, },
+	{ .symbol = ">=", .precedence = 6, .operands = 2, .evaluate = evaluate_greater_than_or_equal, },
+	{ .symbol = "!", .precedence = 2, .operands = 1, .right_to_left = 1, .evaluate_unary = evaluate_not, },
+	{ .symbol = "&&", .precedence = 11, .operands = 2, .evaluate = evaluate_and, },
+	{ .symbol = "||", .precedence = 12, .operands = 2, .evaluate = evaluate_or, },
+	{ .symbol = "like", .precedence = 7, .operands = 2, .evaluate = evaluate_like, },
+	{ .symbol = "and", .precedence = 11, .operands = 2, .evaluate = evaluate_and, },
+	{ .symbol = "or", .precedence = 11, .operands = 2, .evaluate = evaluate_or, },
+	{ .symbol = "not", .precedence = 2, .operands = 1, .right_to_left = 1, .evaluate_unary = evaluate_not, },
+};
+
+/*! \brief Callback to retrieve the entry index number */
+static void *entry_get_number(struct pjsip_history_entry *entry)
+{
+	return &entry->number;
+}
+
+/*! \brief Callback to retrieve the entry's timestamp */
+static void *entry_get_timestamp(struct pjsip_history_entry *entry)
+{
+	return &entry->timestamp;
+}
+
+/*! \brief Callback to retrieve the entry's destination address */
+static void *entry_get_addr(struct pjsip_history_entry *entry)
+{
+	if (entry->transmitted) {
+		return &entry->dst;
+	} else {
+		return &entry->src;
+	}
+}
+
+/*! \brief Callback to retrieve the entry's SIP request method type */
+static void *entry_get_sip_msg_request_method(struct pjsip_history_entry *entry)
+{
+	if (entry->msg->type != PJSIP_REQUEST_MSG) {
+		return NULL;
+	}
+
+	return &entry->msg->line.req.method.name;
+}
+
+/*! \brief Callback to retrieve the entry's SIP Call-ID header */
+static void *entry_get_sip_msg_call_id(struct pjsip_history_entry *entry)
+{
+	pjsip_cid_hdr *cid_hdr;
+
+	cid_hdr = PJSIP_MSG_CID_HDR(entry->msg);
+
+	return &cid_hdr->id;
+}
+
+/*! \brief The fields we allow */
+static struct allowed_field allowed_fields[] = {
+	{ .symbol = "number", .return_type = OPT_INT_T, .get_field = entry_get_number, },
+	/* We co-op the NOOP type here for timeval */
+	{ .symbol = "timestamp", .return_type = OPT_NOOP_T, .get_field = entry_get_timestamp, },
+	{ .symbol = "addr", .return_type = OPT_SOCKADDR_T, .get_field = entry_get_addr, },
+	{ .symbol = "sip.msg.request.method", .return_type = OPT_CHAR_ARRAY_T, .get_field = entry_get_sip_msg_request_method, },
+	{ .symbol = "sip.msg.call-id", .return_type = OPT_CHAR_ARRAY_T, .get_field = entry_get_sip_msg_call_id, },
+};
+
+/*! \brief Free an expression token and all others it references */
+static struct expression_token *expression_token_free(struct expression_token *token)
+{
+	struct expression_token *it_token;
+
+	it_token = token;
+	while (it_token) {
+		struct expression_token *prev = it_token;
+
+		it_token = it_token->next;
+		ast_free(prev);
+	}
+
+	return NULL;
+}
+
+/*!
+ * \brief Allocate an expression token
+ *
+ * \param token_type The type of token in the expression
+ * \param value The value/operator/result to pack into the token
+ *
+ * \retval NULL on failure
+ * \retval \c expression_token on success
+ */
+static struct expression_token *expression_token_alloc(enum expression_token_type token_type, void *value)
+{
+	struct expression_token *token;
+
+	switch (token_type) {
+	case TOKEN_TYPE_RESULT:
+	case TOKEN_TYPE_OPERATOR:
+		token = ast_calloc(1, sizeof(*token));
+		break;
+	case TOKEN_TYPE_FIELD:
+		token = ast_calloc(1, sizeof(*token) + strlen((const char *)value) + 1);
+		break;
+	default:
+		ast_assert(0);
+		return NULL;
+	}
+
+	if (!token) {
+		return NULL;
+	}
+	token->token_type = token_type;
+
+	switch (token_type) {
+	case TOKEN_TYPE_RESULT:
+		token->result = *(int *)value;
+		break;
+	case TOKEN_TYPE_OPERATOR:
+		token->op = value;
+		break;
+	case TOKEN_TYPE_FIELD:
+		strcpy(token->field, value); /* safe */
+		break;
+	default:
+		ast_assert(0);
+	}
+
+	return token;
+}
+
+/*! \brief Determine if the expression token matches a field in \c allowed_fields */
+static struct allowed_field *get_allowed_field(struct expression_token *token)
+{
+	int i;
+
+	ast_assert(token->token_type == TOKEN_TYPE_FIELD);
+
+	for (i = 0; i < ARRAY_LEN(allowed_fields); i++) {
+		if (strcasecmp(allowed_fields[i].symbol, token->field)) {
+			continue;
+		}
+
+		return &allowed_fields[i];
+	}
+
+	return NULL;
+}
+
+/*! \brief AO2 destructor for \c pjsip_history_entry */
+static void pjsip_history_entry_dtor(void *obj)
+{
+	struct pjsip_history_entry *entry = obj;
+
+	if (entry->pool) {
+		pj_pool_release(entry->pool);
+		entry->pool = NULL;
+	}
+}
+
+/*!
+ * \brief Create a \c pjsip_history_entry AO2 object
+ *
+ * \param msg The PJSIP message that this history entry wraps
+ *
+ * \retval An AO2 \c pjsip_history_entry object on success
+ * \retval NULL on failure
+ */
+static struct pjsip_history_entry *pjsip_history_entry_alloc(pjsip_msg *msg)
+{
+	struct pjsip_history_entry *entry;
+
+	entry = ao2_alloc_options(sizeof(*entry), pjsip_history_entry_dtor, AO2_ALLOC_OPT_LOCK_NOLOCK);
+	if (!entry) {
+		return NULL;
+	}
+	entry->number = ast_atomic_fetchadd_int(&packet_number, 1);
+	entry->timestamp = ast_tvnow();
+	entry->timestamp.tv_usec = 0;
+
+	entry->pool = pj_pool_create(&cachingpool.factory, NULL, PJSIP_POOL_RDATA_LEN,
+	                             PJSIP_POOL_RDATA_INC, NULL);
+	if (!entry->pool) {
+		ao2_ref(entry, -1);
+		return NULL;
+	}
+
+	entry->msg = pjsip_msg_clone(entry->pool, msg);
+	if (!entry->msg) {
+		ao2_ref(entry, -1);
+		return NULL;
+	}
+
+	return entry;
+}
+
+/*! \brief PJSIP callback when a SIP message is transmitted */
+static pj_status_t history_on_tx_msg(pjsip_tx_data *tdata)
+{
+	struct pjsip_history_entry *entry;
+
+	if (!enabled) {
+		return PJ_SUCCESS;
+	}
+
+	entry = pjsip_history_entry_alloc(tdata->msg);
+	if (!entry) {
+		return PJ_SUCCESS;
+	}
+	entry->transmitted = 1;
+	pj_sockaddr_cp(&entry->src, &tdata->tp_info.transport->local_addr);
+	pj_sockaddr_cp(&entry->dst, &tdata->tp_info.dst_addr);
+
+	ast_mutex_lock(&history_lock);
+	AST_VECTOR_APPEND(&vector_history, entry);
+	ast_mutex_unlock(&history_lock);
+
+	return PJ_SUCCESS;
+}
+
+/*! \brief PJSIP callback when a SIP message is received */
+static pj_bool_t history_on_rx_msg(pjsip_rx_data *rdata)
+{
+	struct pjsip_history_entry *entry;
+
+	if (!enabled) {
+		return PJ_FALSE;
+	}
+
+	if (!rdata->msg_info.msg) {
+		return PJ_FALSE;
+	}
+
+	entry = pjsip_history_entry_alloc(rdata->msg_info.msg);
+	if (!entry) {
+		return PJ_FALSE;
+	}
+
+	if (rdata->tp_info.transport->addr_len) {
+		pj_sockaddr_cp(&entry->dst, &rdata->tp_info.transport->local_addr);
+	}
+
+	if (rdata->pkt_info.src_addr_len) {
+		pj_sockaddr_cp(&entry->src, &rdata->pkt_info.src_addr);
+	}
+
+	ast_mutex_lock(&history_lock);
+	AST_VECTOR_APPEND(&vector_history, entry);
+	ast_mutex_unlock(&history_lock);
+
+	return PJ_FALSE;
+}
+
+/*! \brief Vector callback that releases the reference for the entry in a history vector */
+static void clear_history_entry_cb(struct pjsip_history_entry *entry)
+{
+	ao2_ref(entry, -1);
+}
+
+/*! \brief Remove all entries from \ref vector_history */
+static void clear_history_entries(void)
+{
+	ast_mutex_lock(&history_lock);
+	AST_VECTOR_RESET(&vector_history, clear_history_entry_cb);
+	packet_number = 0;
+	ast_mutex_unlock(&history_lock);
+}
+
+/*!
+ * \brief Build a reverse polish notation expression queue
+ *
+ * This function is an implementation of the Shunting-Yard Algorithm. It takes
+ * a user provided infix-notation expression and converts it into a reverse
+ * polish notation expression, which is a queue of tokens that can be easily
+ * parsed.
+ *
+ * \params a The CLI arguments provided by the User, containing the infix expression
+ *
+ * \retval NULL error
+ * \retval expression_token A 'queue' of expression tokens in RPN
+ */
+static struct expression_token *build_expression_queue(struct ast_cli_args *a)
+{
+	AST_VECTOR(, struct operator *) operators; /* A stack of saved operators */
+	struct expression_token *output = NULL;    /* The output queue */
+	struct expression_token *head = NULL;      /* Pointer to the head of /c output */
+	int i;
+
+#define APPEND_TO_OUTPUT(output, token) do { \
+	if ((output)) { \
+		(output)->next = (token); \
+		(output) = (token); \
+	} else { \
+		(output) = (token); \
+		head = (output); \
+	} \
+} while (0)
+
+	if (AST_VECTOR_INIT(&operators, 8)) {
+		return NULL;
+	}
+
+	for (i = 4; i < a->argc; i++) {
+		struct expression_token *out_token;
+		char *token = ast_strdupa(a->argv[i]);
+		int j;
+
+		/* Strip off and append any left parentheses */
+		if (token[0] == '(') {
+			AST_VECTOR_APPEND(&operators, &left_paren);
+			if (!token[1]) {
+				continue;
+			}
+			token = &token[1];
+		}
+
+		/* Handle the case where the token is an operator */
+		for (j = 0; j < ARRAY_LEN(allowed_operators); j++) {
+			int k;
+
+			if (strcasecmp(token, allowed_operators[j].symbol)) {
+				continue;
+			}
+
+			for (k = AST_VECTOR_SIZE(&operators) - 1; k >= 0; k--) {
+				struct operator *top = AST_VECTOR_GET(&operators, k);
+
+				/* Remove and push queued up operators, if they are of
+				 * less precedence than this operator
+				 */
+				if ((allowed_operators[j].right_to_left && allowed_operators[j].precedence >= top->precedence)
+					|| (!allowed_operators[j].right_to_left && allowed_operators[j].precedence > top->precedence)) {
+
+					if (!(out_token = expression_token_alloc(TOKEN_TYPE_OPERATOR, top))) {
+						goto error;
+					}
+					APPEND_TO_OUTPUT(output, out_token);
+					AST_VECTOR_REMOVE(&operators, k, 1);
+				}
+			}
+
+			AST_VECTOR_APPEND(&operators, &allowed_operators[j]);
+			token = NULL;
+			break;
+		}
+
+		/* Token was an operator; continue to next token */
+		if (!token) {
+			continue;
+		}
+
+		/* Handle a right parentheses either by itself or as part of the token.
+		 * If part of the token, push the token onto the output queue first
+		 */
+		if (token[0] == ')' || token[strlen(token) - 1] == ')') {
+
+			if (token[strlen(token) - 1] == ')') {
+				token[strlen(token) - 1] = '\0';
+
+				if (!(out_token = expression_token_alloc(TOKEN_TYPE_FIELD, token))) {
+					goto error;
+				}
+				APPEND_TO_OUTPUT(output, out_token);
+				token = NULL;
+			}
+
+			for (j = AST_VECTOR_SIZE(&operators) - 1; j >= 0; j--) {
+				struct operator *top = AST_VECTOR_GET(&operators, j);
+
+				AST_VECTOR_REMOVE(&operators, j, 1);
+				if (top == &left_paren) {
+					break;
+				}
+
+				if (!(out_token = expression_token_alloc(TOKEN_TYPE_OPERATOR, top))) {
+					goto error;
+				}
+				APPEND_TO_OUTPUT(output, out_token);
+			}
+		}
+
+		/* Just a plain token, push to the output queue */
+		if (token) {
+			if (!(out_token = expression_token_alloc(TOKEN_TYPE_FIELD, token))) {
+				goto error;
+			}
+			APPEND_TO_OUTPUT(output, out_token);
+		}
+	}
+
+	/* Remove any non-applied operators that remain, applying them
+	 * to the output queue
+	 */
+	for (i = AST_VECTOR_SIZE(&operators) - 1; i >= 0; i--) {
+		struct operator *top = AST_VECTOR_GET(&operators, i);
+		struct expression_token *out_token;
+
+		AST_VECTOR_REMOVE(&operators, i, 1);
+		if (top == &left_paren) {
+			ast_log(LOG_WARNING, "Unbalanced '(' parentheses in expression!\n");
+			continue;
+		}
+
+		if (!(out_token = expression_token_alloc(TOKEN_TYPE_OPERATOR, top))) {
+			goto error;
+		}
+		APPEND_TO_OUTPUT(output, out_token);
+	}
+
+	AST_VECTOR_FREE(&operators);
+	return head;
+
+error:
+	AST_VECTOR_FREE(&operators);
+	expression_token_free(output);
+	return NULL;
+}
+
+/*!
+ * \brief Evaluate a single entry in this history using a RPN expression
+ *
+ * \param entry The entry in the history to evaluate
+ * \param queue The RPN expression
+ *
+ * \retval 0 The expression evaluated FALSE on \c entry
+ * \retval 1 The expression evaluated TRUE on \c entry
+ * \retval -1 The expression errored
+ */
+static int evaluate_history_entry(struct pjsip_history_entry *entry, struct expression_token *queue)
+{
+	AST_VECTOR(, struct expression_token *) stack; /* Our stack of results and operands */
+	struct expression_token *it_queue;
+	struct expression_token *final;
+	int result;
+	int i;
+
+	if (AST_VECTOR_INIT(&stack, 16)) {
+		return -1;
+	}
+
+	for (it_queue = queue; it_queue; it_queue = it_queue->next) {
+		struct expression_token *op_one;
+		struct expression_token *op_two = NULL;
+		struct expression_token *result;
+		int res = 0;
+
+		/* If this is not an operator, push it to the stack */
+		if (!it_queue->op) {
+			AST_VECTOR_APPEND(&stack, it_queue);
+			continue;
+		}
+
+		if (AST_VECTOR_SIZE(&stack) < it_queue->op->operands) {
+			ast_log(LOG_WARNING, "Unable to evaluate expression operator '%s': not enough operands\n",
+				it_queue->op->symbol);
+			goto error;
+		}
+
+		if (it_queue->op->operands == 1) {
+			/* Unary operators currently consist only of 'not', which can only act
+			 * upon an evaluated condition result.
+			 */
+			ast_assert(it_queue->op->evaluate_unary != NULL);
+
+			op_one = AST_VECTOR_REMOVE(&stack, AST_VECTOR_SIZE(&stack) - 1, 1);
+			if (op_one->token_type != TOKEN_TYPE_RESULT) {
+				ast_log(LOG_WARNING, "Unable to evaluate '%s': operand is not the result of an operation\n",
+					it_queue->op->symbol);
+				goto error;
+			}
+
+			res = it_queue->op->evaluate_unary(it_queue->op, OPT_INT_T, &op_one->result) == 0 ? 0 : 1;
+		} else if (it_queue->op->operands == 2) {
+			struct allowed_field *field;
+			enum aco_option_type type;
+			void *value;
+
+			ast_assert(it_queue->op->evaluate != NULL);
+
+			op_one = AST_VECTOR_REMOVE(&stack, AST_VECTOR_SIZE(&stack) - 1, 1);
+			op_two = AST_VECTOR_REMOVE(&stack, AST_VECTOR_SIZE(&stack) - 1, 1);
+
+			/* If operand two is a field, then it must be a field we recognize. */
+			if (op_two->token_type == TOKEN_TYPE_FIELD) {
+				field = get_allowed_field(op_two);
+				if (!field) {
+					ast_log(LOG_WARNING, "Unknown or unrecognized field: %s\n", op_two->field);
+					goto error;
+				}
+
+				type = field->return_type;
+				value = field->get_field(entry);
+			} else if (op_two->token_type == TOKEN_TYPE_RESULT) {
+				type = OPT_INT_T;
+				value = &op_two->result;
+			} else {
+				ast_log(LOG_WARNING, "Attempting to evaluate an operator: %s\n", op_two->op->symbol);
+				goto error;
+			}
+
+			if (value) {
+				res = it_queue->op->evaluate(it_queue->op, type, value, op_one) == 0 ? 0 : 1;
+			} else {
+				res = 0;
+			}
+		} else {
+			ast_log(LOG_WARNING, "Operator '%s' has an invalid number of operands\n", it_queue->op->symbol);
+			ast_assert(0);
+			goto error;
+		}
+
+		/* Results are temporary; clean used ones up */
+		if (op_one && op_one->token_type == TOKEN_TYPE_RESULT) {
+			ast_free(op_one);
+		}
+		if (op_two && op_two->token_type == TOKEN_TYPE_RESULT) {
+			ast_free(op_two);
+		}
+
+		/* Push the result onto the stack */
+		result = expression_token_alloc(TOKEN_TYPE_RESULT, &res);
+		if (!result) {
+			goto error;
+		}
+		AST_VECTOR_APPEND(&stack, result);
+	}
+
+	/*
+	 * When the evaluation is complete, we must have:
+	 *  - A single result remaining on the stack
+	 *  - An actual result
+	 */
+	if (AST_VECTOR_SIZE(&stack) != 1) {
+		ast_log(LOG_WARNING, "Expression was unbalanced: %zu results remained after evaluation\n",
+			AST_VECTOR_SIZE(&stack));
+		goto error;
+	}
+
+	final = AST_VECTOR_GET(&stack, 0);
+	if (final->token_type != TOKEN_TYPE_RESULT) {
+		ast_log(LOG_WARNING, "Expression did not create a usable result\n");
+		goto error;
+	}
+	result = final->result;
+	ast_free(final);
+
+	return result;
+
+error:
+	/* Clean out any remaining result expression tokens */
+	for (i = 0; i < AST_VECTOR_SIZE(&stack); i++) {
+		struct expression_token *failed_token = AST_VECTOR_GET(&stack, i);
+
+		if (failed_token->token_type == TOKEN_TYPE_RESULT) {
+			ast_free(failed_token);
+		}
+	}
+	AST_VECTOR_FREE(&stack);
+	return -1;
+}
+
+/*!
+ * \brief Create a filtered history based on a user provided expression
+ *
+ * \param a The CLI arguments containing the expression
+ *
+ * \retval NULL on error
+ * \retval A vector containing the filtered history on success
+ */
+static struct vector_history_t *filter_history(struct ast_cli_args *a)
+{
+	struct vector_history_t *output;
+	struct expression_token *queue;
+	int i;
+
+	output = ast_malloc(sizeof(*output));
+	if (!output) {
+		return NULL;
+	}
+
+	if (AST_VECTOR_INIT(output, HISTORY_INITIAL_SIZE / 2)) {
+		ast_free(output);
+		return NULL;
+	}
+
+	queue = build_expression_queue(a);
+	if (!queue) {
+		return NULL;
+	}
+
+	ast_mutex_lock(&history_lock);
+	for (i = 0; i < AST_VECTOR_SIZE(&vector_history); i++) {
+		struct pjsip_history_entry *entry = AST_VECTOR_GET(&vector_history, i);
+		int res;
+
+		res = evaluate_history_entry(entry, queue);
+		if (res == -1) {
+			/* Error in expression evaluation; bail */
+			ast_mutex_unlock(&history_lock);
+			AST_VECTOR_RESET(output, clear_history_entry_cb);
+			AST_VECTOR_FREE(output);
+			ast_free(output);
+			expression_token_free(queue);
+			return NULL;
+		} else if (!res) {
+			continue;
+		} else {
+			AST_VECTOR_APPEND(output, ao2_bump(entry));
+		}
+	}
+	ast_mutex_unlock(&history_lock);
+
+	expression_token_free(queue);
+
+	return output;
+}
+
+/*! \brief Print a detailed view of a single entry in the history to the CLI */
+static void display_single_entry(struct ast_cli_args *a, struct pjsip_history_entry *entry)
+{
+	char addr[64];
+	char *buf;
+
+	buf = ast_calloc(1, PJSIP_MAX_PKT_LEN * sizeof(char));
+	if (!buf) {
+		return;
+	}
+
+	if (pjsip_msg_print(entry->msg, buf, PJSIP_MAX_PKT_LEN) == -1) {
+		ast_log(LOG_WARNING, "Unable to print SIP message %d: packet too large!\n", entry->number);
+		ast_free(buf);
+		return;
+	}
+
+	if (entry->transmitted) {
+		pj_sockaddr_print(&entry->dst, addr, sizeof(addr), 3);
+	} else {
+		pj_sockaddr_print(&entry->src, addr, sizeof(addr), 3);
+	}
+
+	ast_cli(a->fd, "<--- History Entry %d %s %s at %-10.10ld --->\n",
+		entry->number,
+		entry->transmitted ? "Sent to" : "Received from",
+		addr,
+		entry->timestamp.tv_sec);
+	ast_cli(a->fd, "%s\n", buf);
+
+	ast_free(buf);
+}
+
+/*! \brief Print a list of the entries to the CLI */
+static void display_entry_list(struct ast_cli_args *a, struct vector_history_t *vec)
+{
+	int i;
+
+	ast_cli(a->fd, "%-5.5s %-10.10s %-25.25s %-35.35s\n",
+		"No.",
+		"Timestamp",
+		"(Dir) Address",
+		"SIP Message");
+	ast_cli(a->fd, "===== ========== ========================= ===================================\n");
+
+	for (i = 0; i < AST_VECTOR_SIZE(vec); i++) {
+		struct pjsip_history_entry *entry;
+		char addr[64];
+		char line[256];
+
+		entry = AST_VECTOR_GET(vec, i);
+
+		if (entry->transmitted) {
+			pj_sockaddr_print(&entry->dst, addr, sizeof(addr), 3);
+		} else {
+			pj_sockaddr_print(&entry->src, addr, sizeof(addr), 3);
+		}
+
+		if (entry->msg->type == PJSIP_REQUEST_MSG) {
+			char uri[128];
+
+			pjsip_uri_print(PJSIP_URI_IN_REQ_URI, entry->msg->line.req.uri, uri, sizeof(uri));
+			snprintf(line, sizeof(line), "%.*s %s SIP/2.0",
+				(int)pj_strlen(&entry->msg->line.req.method.name),
+				pj_strbuf(&entry->msg->line.req.method.name),
+				uri);
+		} else {
+			snprintf(line, sizeof(line), "SIP/2.0 %u %.*s",
+				entry->msg->line.status.code,
+				(int)pj_strlen(&entry->msg->line.status.reason),
+				pj_strbuf(&entry->msg->line.status.reason));
+		}
+
+		ast_cli(a->fd, "%-5.5d %-10.10ld %-5.5s %-20.20s %s\n",
+			entry->number,
+			entry->timestamp.tv_sec,
+			entry->transmitted ? "* ==>" : "* <==",
+			addr,
+			line);
+	}
+}
+
+static char *pjsip_show_history(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+	struct vector_history_t *vec = &vector_history;
+	struct pjsip_history_entry *entry = NULL;
+
+	if (cmd == CLI_INIT) {
+		e->command = "pjsip show history";
+		e->usage =
+			"Usage: pjsip show history [entry <num>|where [...]]\n"
+			"       Displays the currently collected history or an\n"
+			"       entry within the history.\n\n"
+			"       * Running the command with no options will display\n"
+			"         the entire history.\n"
+			"       * Providing 'entry <num>' will display the full\n"
+			"         detail of a particular entry in this history.\n"
+			"       * Providing 'where ...' will allow for filtering\n"
+			"         the history. The history can be filtered using\n"
+			"         any of the following fields:\n"
+			"         - number: The history entry number\n"
+			"         - timestamp: The time associated with the history entry\n"
+			"         - addr: The source/destination address of the SIP message\n"
+			"         - sip.msg.request.method: The request method type\n"
+			"         - sip.msg.call-id: The Call-ID header of the SIP message\n"
+			"\n"
+			"         When filtering, standard Boolean operators can be used,\n"
+			"         as well as 'like' for regexs.\n"
+			"\n"
+			"         Example:\n"
+			"         'pjsip show history where number > 5 and (addr = \"192.168.0.3:5060\" or addr = \"192.168.0.5:5060\")'\n";
+		return NULL;
+	} else if (cmd == CLI_GENERATE) {
+		return NULL;
+	}
+
+	if (a->argc > 3) {
+		if (!strcasecmp(a->argv[3], "entry") && a->argc == 5) {
+			int num;
+
+			if (sscanf(a->argv[4], "%30d", &num) != 1) {
+				ast_cli(a->fd, "'%s' is not a valid entry number\n", a->argv[4]);
+				return CLI_FAILURE;
+			}
+
+			/* Get the entry at the provided position */
+			ast_mutex_lock(&history_lock);
+			if (num >= AST_VECTOR_SIZE(&vector_history) || num < 0) {
+				ast_cli(a->fd, "Entry '%d' does not exist\n", num);
+				ast_mutex_unlock(&history_lock);
+				return CLI_FAILURE;
+			}
+			entry = ao2_bump(AST_VECTOR_GET(&vector_history, num));
+			ast_mutex_unlock(&history_lock);
+		} else if (!strcasecmp(a->argv[3], "where")) {
+			vec = filter_history(a);
+			if (!vec) {
+				return CLI_FAILURE;
+			}
+		} else {
+			return CLI_SHOWUSAGE;
+		}
+	}
+
+	if (AST_VECTOR_SIZE(vec) == 1) {
+		if (vec == &vector_history) {
+			ast_mutex_lock(&history_lock);
+		}
+		entry = ao2_bump(AST_VECTOR_GET(vec, 0));
+		if (vec == &vector_history) {
+			ast_mutex_lock(&history_lock);
+		}
+	}
+
+	if (entry) {
+		display_single_entry(a, entry);
+	} else {
+		if (vec == &vector_history) {
+			ast_mutex_lock(&history_lock);
+		}
+
+		display_entry_list(a, vec);
+
+		if (vec == &vector_history) {
+			ast_mutex_unlock(&history_lock);
+		}
+	}
+
+	if (vec != &vector_history) {
+		AST_VECTOR_RESET(vec, clear_history_entry_cb);
+		AST_VECTOR_FREE(vec);
+		ast_free(vec);
+	}
+	ao2_cleanup(entry);
+
+	return CLI_SUCCESS;
+}
+
+static char *pjsip_set_history(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+	const char *what;
+
+	if (cmd == CLI_INIT) {
+		e->command = "pjsip set history {on|off|clear}";
+		e->usage =
+			"Usage: pjsip set history {on|off|clear}\n"
+			"       Enables/disables/clears the PJSIP history.\n\n"
+			"       Enabling the history will start recording transmitted/received\n"
+			"       packets. Disabling the history will stop recording, but keep\n"
+			"       the already received packets. Clearing the history will wipe\n"
+			"       the received packets from memory.\n\n"
+			"       As the PJSIP history is maintained in memory, and includes\n"
+			"       all received/transmitted requests and responses, it should\n"
+			"       only be enabled for debugging purposes, and cleared when done.\n";
+		return NULL;
+	} else if (cmd == CLI_GENERATE) {
+		return NULL;
+	}
+
+	what = a->argv[e->args - 1];	/* Guaranteed to exist */
+
+	if (a->argc == e->args) {
+		if (!strcasecmp(what, "on")) {
+			enabled = 1;
+			ast_cli(a->fd, "PJSIP History enabled\n");
+			return CLI_SUCCESS;
+		} else if (!strcasecmp(what, "off")) {
+			enabled = 0;
+			ast_cli(a->fd, "PJSIP History disabled\n");
+			return CLI_SUCCESS;
+		} else if (!strcasecmp(what, "clear")) {
+			clear_history_entries();
+			ast_cli(a->fd, "PJSIP History cleared\n");
+			return CLI_SUCCESS;
+		}
+	}
+
+	return CLI_SHOWUSAGE;
+}
+
+static pjsip_module logging_module = {
+	.name = { "History Module", 14 },
+	.priority = 0,
+	.on_rx_request = history_on_rx_msg,
+	.on_rx_response = history_on_rx_msg,
+	.on_tx_request = history_on_tx_msg,
+	.on_tx_response = history_on_tx_msg,
+};
+
+static struct ast_cli_entry cli_pjsip[] = {
+	AST_CLI_DEFINE(pjsip_set_history, "Enable/Disable PJSIP History"),
+	AST_CLI_DEFINE(pjsip_show_history, "Display PJSIP History"),
+};
+
+static int load_module(void)
+{
+	CHECK_PJSIP_MODULE_LOADED();
+
+	pj_caching_pool_init(&cachingpool, &pj_pool_factory_default_policy, 0);
+
+	AST_VECTOR_INIT(&vector_history, HISTORY_INITIAL_SIZE);
+
+	ast_sip_register_service(&logging_module);
+	ast_cli_register_multiple(cli_pjsip, ARRAY_LEN(cli_pjsip));
+
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
+static int unload_module(void)
+{
+	ast_cli_unregister_multiple(cli_pjsip, ARRAY_LEN(cli_pjsip));
+	ast_sip_unregister_service(&logging_module);
+
+	clear_history_entries();
+	AST_VECTOR_FREE(&vector_history);
+
+	pj_caching_pool_destroy(&cachingpool);
+
+	return 0;
+}
+
+AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "PJSIP History",
+		.support_level = AST_MODULE_SUPPORT_EXTENDED,
+		.load = load_module,
+		.unload = unload_module,
+		.load_pri = AST_MODPRI_APP_DEPEND,
+	);

-- 
To view, visit https://gerrit.asterisk.org/1849
To unsubscribe, visit https://gerrit.asterisk.org/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I39bd74ce998e99ad5ebc0aab3e84df3a150f8e36
Gerrit-PatchSet: 4
Gerrit-Project: asterisk
Gerrit-Branch: 13
Gerrit-Owner: Matt Jordan <mjordan at digium.com>



More information about the asterisk-code-review mailing list