static void send_subscription_unsubscribe(struct stasis_topic *topic, struct stasis_subscription *sub) { struct stasis_subscription_change *change; struct stasis_message *msg; /* This assumes that we have already unsubscribed */ ast_assert(!stasis_subscription_is_subscribed(sub)); if (!stasis_subscription_change_type()) { return; } change = subscription_change_alloc(topic, sub->uniqueid, "Unsubscribe"); if (!change) { return; } msg = stasis_message_create(stasis_subscription_change_type(), change); if (!msg) { ao2_cleanup(change); return; } stasis_publish(topic, msg); /* Now we have to dispatch to the subscription itself */ dispatch_message(sub, msg, 0); ao2_cleanup(msg); ao2_cleanup(change); }
int stasis_subscription_final_message(struct stasis_subscription *sub, struct stasis_message *msg) { struct stasis_subscription_change *change; if (stasis_message_type(msg) != stasis_subscription_change_type()) { return 0; } change = stasis_message_data(msg); if (strcmp("Unsubscribe", change->description)) { return 0; } if (strcmp(stasis_subscription_uniqueid(sub), change->uniqueid)) { return 0; } return 1; }
struct stasis_app *app_create(const char *name, stasis_app_cb handler, void *data, enum stasis_app_subscription_model subscription_model) { RAII_VAR(struct stasis_app *, app, NULL, ao2_cleanup); size_t size; int res = 0; size_t context_size = strlen("stasis-") + strlen(name) + 1; char context_name[context_size]; ast_assert(name != NULL); ast_assert(handler != NULL); ast_verb(1, "Creating Stasis app '%s'\n", name); size = sizeof(*app) + strlen(name) + 1; app = ao2_alloc_options(size, app_dtor, AO2_ALLOC_OPT_LOCK_MUTEX); if (!app) { return NULL; } app->subscription_model = subscription_model; app->forwards = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_MUTEX, AO2_CONTAINER_ALLOC_OPT_DUPS_OBJ_REJECT, forwards_sort, NULL); if (!app->forwards) { return NULL; } app->topic = stasis_topic_create(name); if (!app->topic) { return NULL; } app->bridge_router = stasis_message_router_create(ast_bridge_topic_all()); if (!app->bridge_router) { return NULL; } res |= stasis_message_router_add(app->bridge_router, ast_bridge_merge_message_type(), bridge_merge_handler, app); res |= stasis_message_router_add(app->bridge_router, ast_blind_transfer_type(), bridge_blind_transfer_handler, app); res |= stasis_message_router_add(app->bridge_router, ast_attended_transfer_type(), bridge_attended_transfer_handler, app); res |= stasis_message_router_add(app->bridge_router, stasis_subscription_change_type(), bridge_subscription_change_handler, app); if (res != 0) { return NULL; } /* Bridge router holds a reference */ ao2_ref(app, +1); app->router = stasis_message_router_create(app->topic); if (!app->router) { return NULL; } res |= stasis_message_router_add(app->router, ast_bridge_snapshot_type(), sub_bridge_update_handler, app); res |= stasis_message_router_add(app->router, ast_channel_snapshot_type(), sub_channel_update_handler, app); res |= stasis_message_router_add_cache_update(app->router, ast_endpoint_snapshot_type(), sub_endpoint_update_handler, app); res |= stasis_message_router_add(app->router, stasis_subscription_change_type(), sub_subscription_change_handler, app); stasis_message_router_set_formatters_default(app->router, sub_default_handler, app, STASIS_SUBSCRIPTION_FORMATTER_JSON); if (res != 0) { return NULL; } /* Router holds a reference */ ao2_ref(app, +1); strncpy(app->name, name, size - sizeof(*app)); app->handler = handler; app->data = ao2_bump(data); /* Create a context, a match-all extension, and a 'h' extension for this application. Note that * this should only be done if a context does not already exist. */ strcpy(context_name, "stasis-"); strcat(context_name, name); if (!ast_context_find(context_name)) { if (!ast_context_find_or_create(NULL, NULL, context_name, "res_stasis")) { ast_log(LOG_WARNING, "Could not create context '%s' for Stasis application '%s'\n", context_name, name); } else { ast_add_extension(context_name, 0, "_.", 1, NULL, NULL, "Stasis", ast_strdup(name), ast_free_ptr, "res_stasis"); ast_add_extension(context_name, 0, "h", 1, NULL, NULL, "NoOp", NULL, NULL, "res_stasis"); } } else { ast_log(LOG_WARNING, "Not creating context '%s' for Stasis application '%s' because it already exists\n", context_name, name); } ao2_ref(app, +1); return app; }
static struct timespec make_deadline(int timeout_millis) { struct timeval start = ast_tvnow(); struct timeval delta = { .tv_sec = timeout_millis / 1000, .tv_usec = (timeout_millis % 1000) * 1000, }; struct timeval deadline_tv = ast_tvadd(start, delta); struct timespec deadline = { .tv_sec = deadline_tv.tv_sec, .tv_nsec = 1000 * deadline_tv.tv_usec, }; return deadline; } struct stasis_message_sink *stasis_message_sink_create(void) { RAII_VAR(struct stasis_message_sink *, sink, NULL, ao2_cleanup); sink = ao2_alloc(sizeof(*sink), stasis_message_sink_dtor); if (!sink) { return NULL; } ast_mutex_init(&sink->lock); ast_cond_init(&sink->cond, NULL); sink->max_messages = 4; sink->messages = ast_malloc(sizeof(*sink->messages) * sink->max_messages); if (!sink->messages) { return NULL; } ao2_ref(sink, +1); return sink; } /*! * \brief Implementation of the stasis_message_sink_cb() callback. * * Why the roundabout way of exposing this via stasis_message_sink_cb()? Well, * it has to do with how we load modules. * * Modules have their own metadata compiled into them in the module info block * at the end of the file. This includes dependency information in the * \c nonoptreq field. * * Asterisk loads the module, inspects the field, then loads any needed * dependencies. This works because Asterisk passes \c RTLD_LAZY to the initial * dlopen(), which defers binding function references until they are called. * * But when you take the address of a function, that function needs to be * available at load time. So if some module used the address of * message_sink_cb() directly, and \c res_stasis_test.so wasn't loaded yet, then * that module would fail to load. * * The stasis_message_sink_cb() function gives us a layer of indirection so that * the initial lazy binding will still work as expected. */ static void message_sink_cb(void *data, struct stasis_subscription *sub, struct stasis_message *message) { struct stasis_message_sink *sink = data; SCOPED_MUTEX(lock, &sink->lock); if (stasis_subscription_final_message(sub, message)) { sink->is_done = 1; ast_cond_signal(&sink->cond); return; } if (stasis_subscription_change_type() == stasis_message_type(message)) { /* Ignore subscription changes */ return; } if (sink->num_messages == sink->max_messages) { size_t new_max_messages = sink->max_messages * 2; struct stasis_message **new_messages = ast_realloc( sink->messages, sizeof(*new_messages) * new_max_messages); if (!new_messages) { return; } sink->max_messages = new_max_messages; sink->messages = new_messages; } ao2_ref(message, +1); sink->messages[sink->num_messages++] = message; ast_cond_signal(&sink->cond); } stasis_subscription_cb stasis_message_sink_cb(void) { return message_sink_cb; } int stasis_message_sink_wait_for_count(struct stasis_message_sink *sink, int num_messages, int timeout_millis) { struct timespec deadline = make_deadline(timeout_millis); SCOPED_MUTEX(lock, &sink->lock); while (sink->num_messages < num_messages) { int r = ast_cond_timedwait(&sink->cond, &sink->lock, &deadline); if (r == ETIMEDOUT) { break; } if (r != 0) { ast_log(LOG_ERROR, "Unexpected condition error: %s\n", strerror(r)); break; } } return sink->num_messages; } int stasis_message_sink_should_stay(struct stasis_message_sink *sink, int num_messages, int timeout_millis) { struct timespec deadline = make_deadline(timeout_millis); SCOPED_MUTEX(lock, &sink->lock); while (sink->num_messages == num_messages) { int r = ast_cond_timedwait(&sink->cond, &sink->lock, &deadline); if (r == ETIMEDOUT) { break; } if (r != 0) { ast_log(LOG_ERROR, "Unexpected condition error: %s\n", strerror(r)); break; } } return sink->num_messages; } int stasis_message_sink_wait_for(struct stasis_message_sink *sink, int start, stasis_wait_cb cmp_cb, const void *data, int timeout_millis) { struct timespec deadline = make_deadline(timeout_millis); SCOPED_MUTEX(lock, &sink->lock); /* wait for the start */ while (sink->num_messages < start + 1) { int r = ast_cond_timedwait(&sink->cond, &sink->lock, &deadline); if (r == ETIMEDOUT) { /* Timed out waiting for the start */ return -1; } if (r != 0) { ast_log(LOG_ERROR, "Unexpected condition error: %s\n", strerror(r)); return -2; } } while (!cmp_cb(sink->messages[start], data)) { ++start; while (sink->num_messages < start + 1) { int r = ast_cond_timedwait(&sink->cond, &sink->lock, &deadline); if (r == ETIMEDOUT) { return -1; } if (r != 0) { ast_log(LOG_ERROR, "Unexpected condition error: %s\n", strerror(r)); return -2; } } } return start; } struct stasis_message *stasis_test_message_create(void) { RAII_VAR(void *, data, NULL, ao2_cleanup); if (!stasis_test_message_type()) { return NULL; } /* We just need the unique pointer; don't care what's in it */ data = ao2_alloc(1, NULL); if (!data) { return NULL; } return stasis_message_create(stasis_test_message_type(), data); }
static void consumer_wait_for(struct consumer *consumer) { int res; struct timeval start = ast_tvnow(); struct timespec end = { .tv_sec = start.tv_sec + 10, .tv_nsec = start.tv_usec * 1000 }; SCOPED_AO2LOCK(lock, consumer); while (!consumer->already_out) { res = ast_cond_timedwait(&consumer->out, ao2_object_get_lockaddr(consumer), &end); if (!res || res == ETIMEDOUT) { break; } } } static int remove_device_states_cb(void *obj, void *arg, int flags) { struct stasis_message *msg = obj; struct ast_device_state_message *device_state = stasis_message_data(msg); if (strcmp(UNIT_TEST_DEVICE_IDENTIFIER, device_state->device)) { /* Not a unit test device */ return 0; } msg = stasis_cache_clear_create(msg); if (msg) { /* topic guaranteed to have been created by this point */ stasis_publish(ast_device_state_topic(device_state->device), msg); } ao2_cleanup(msg); return 0; } static void cache_cleanup(int unused) { struct ao2_container *cache_dump; /* remove all device states created during this test */ cache_dump = stasis_cache_dump_all(ast_device_state_cache(), NULL); if (!cache_dump) { return; } ao2_callback(cache_dump, 0, remove_device_states_cb, NULL); ao2_cleanup(cache_dump); } AST_TEST_DEFINE(device_state_aggregation_test) { RAII_VAR(struct consumer *, consumer, NULL, ao2_cleanup); RAII_VAR(struct stasis_message_router *, device_msg_router, NULL, stasis_message_router_unsubscribe); RAII_VAR(struct ast_eid *, foreign_eid, NULL, ast_free); RAII_VAR(int, cleanup_cache, 0, cache_cleanup); RAII_VAR(struct stasis_message *, msg, NULL, ao2_cleanup); int res; struct ast_device_state_message *device_state; switch (cmd) { case TEST_INIT: info->name = "device_state_aggregation_test"; info->category = "/main/devicestate/"; info->summary = "Tests message routing and aggregation through the Stasis device state system."; info->description = "Verifies that the device state system passes " "messages appropriately, that the aggregator is " "working properly, that the aggregate results match " "the expected combined devstate, and that the cached " "aggregate devstate is correct."; return AST_TEST_NOT_RUN; case TEST_EXECUTE: break; } foreign_eid = ast_malloc(sizeof(*foreign_eid)); ast_test_validate(test, NULL != foreign_eid); memset(foreign_eid, 0xFF, sizeof(*foreign_eid)); consumer = consumer_create(); ast_test_validate(test, NULL != consumer); device_msg_router = stasis_message_router_create(ast_device_state_topic_cached()); ast_test_validate(test, NULL != device_msg_router); ao2_ref(consumer, +1); res = stasis_message_router_add(device_msg_router, stasis_cache_update_type(), consumer_exec, consumer); ast_test_validate(test, !res); res = stasis_message_router_add(device_msg_router, stasis_subscription_change_type(), consumer_finalize, consumer); ast_test_validate(test, !res); /* push local state */ ast_publish_device_state(UNIT_TEST_DEVICE_IDENTIFIER, AST_DEVICE_NOT_INUSE, AST_DEVSTATE_CACHABLE); /* Check cache aggregate state immediately */ ao2_cleanup(msg); msg = stasis_cache_get_by_eid(ast_device_state_cache(), ast_device_state_message_type(), UNIT_TEST_DEVICE_IDENTIFIER, NULL); device_state = stasis_message_data(msg); ast_test_validate(test, AST_DEVICE_NOT_INUSE == device_state->state); consumer_wait_for(consumer); ast_test_validate(test, AST_DEVICE_NOT_INUSE == consumer->state); ast_test_validate(test, AST_DEVICE_NOT_INUSE == consumer->aggregate_state); ast_test_validate(test, 2 == consumer->event_count); consumer_reset(consumer); /* push remote state */ /* this will not produce a new aggregate state message since the aggregate state does not change */ consumer->sig_on_non_aggregate_state = 1; ast_publish_device_state_full(UNIT_TEST_DEVICE_IDENTIFIER, AST_DEVICE_NOT_INUSE, AST_DEVSTATE_CACHABLE, foreign_eid); /* Check cache aggregate state immediately */ ao2_cleanup(msg); msg = stasis_cache_get_by_eid(ast_device_state_cache(), ast_device_state_message_type(), UNIT_TEST_DEVICE_IDENTIFIER, NULL); device_state = stasis_message_data(msg); ast_test_validate(test, AST_DEVICE_NOT_INUSE == device_state->state); /* Check for expected events. */ consumer_wait_for(consumer); ast_test_validate(test, AST_DEVICE_NOT_INUSE == consumer->state); ast_test_validate(test, AST_DEVICE_TOTAL == consumer->aggregate_state); ast_test_validate(test, 1 == consumer->event_count); consumer_reset(consumer); /* push remote state different from local state */ ast_publish_device_state_full(UNIT_TEST_DEVICE_IDENTIFIER, AST_DEVICE_INUSE, AST_DEVSTATE_CACHABLE, foreign_eid); /* Check cache aggregate state immediately */ ao2_cleanup(msg); msg = stasis_cache_get_by_eid(ast_device_state_cache(), ast_device_state_message_type(), UNIT_TEST_DEVICE_IDENTIFIER, NULL); device_state = stasis_message_data(msg); ast_test_validate(test, AST_DEVICE_INUSE == device_state->state); /* Check for expected events. */ consumer_wait_for(consumer); ast_test_validate(test, AST_DEVICE_INUSE == consumer->state); ast_test_validate(test, AST_DEVICE_INUSE == consumer->aggregate_state); ast_test_validate(test, 2 == consumer->event_count); consumer_reset(consumer); /* push local state that will cause aggregated state different from local non-aggregate state */ ast_publish_device_state(UNIT_TEST_DEVICE_IDENTIFIER, AST_DEVICE_RINGING, AST_DEVSTATE_CACHABLE); /* Check cache aggregate state immediately */ ao2_cleanup(msg); msg = stasis_cache_get_by_eid(ast_device_state_cache(), ast_device_state_message_type(), UNIT_TEST_DEVICE_IDENTIFIER, NULL); device_state = stasis_message_data(msg); ast_test_validate(test, AST_DEVICE_RINGINUSE == device_state->state); /* Check for expected events. */ consumer_wait_for(consumer); ast_test_validate(test, AST_DEVICE_RINGING == consumer->state); ast_test_validate(test, AST_DEVICE_RINGINUSE == consumer->aggregate_state); ast_test_validate(test, 2 == consumer->event_count); consumer_reset(consumer); return AST_TEST_PASS; } static int unload_module(void) { AST_TEST_UNREGISTER(device2extenstate_test); AST_TEST_UNREGISTER(device_state_aggregation_test); return 0; }