loader.c: Allow dependent modules to be unloaded recursively.

Because of the (often recursive) nature of module dependencies in
Asterisk, hot swapping a module on the fly is cumbersome if a module
is depended on by other modules. Currently, dependencies must be
popped manually by unloading dependents, unloading the module of
interest, and then loading modules again in reverse order.

To make this easier, the ability to do this recursively in certain
circumstances has been added, as an optional extension to the
"module refresh" command. If requested, Asterisk will check if a module
that has a positive usecount could be unloaded safely if anything
recursively dependent on it were unloaded. If so, it will go ahead
and unload all these modules and load them back again. This makes
hot swapping modules that provide dependencies much easier.

Resolves: #474

UserNote: Modules with dependency relations can have their dependents
automatically recursively unloaded and loaded again using the
"module refresh" CLI command or the ModuleLoad AMI command.
This commit is contained in:
Naveen Albert 2023-12-02 18:07:02 -05:00
parent 4f52ed660d
commit ace9d3431d
4 changed files with 208 additions and 19 deletions

View File

@ -149,6 +149,20 @@ enum ast_module_helper_type {
*/
enum ast_module_load_result ast_load_resource(const char *resource_name);
/*!
* \brief Unload and load a module again.
* \param resource_name The name of the module to unload.
* \param ast_module_unload_mode The force flag. This should be set using one of the AST_FORCE flags.
* \param recursive Attempt to recursively unload any dependents of this module
* if that will allow the module to unload, and load them back again afterwards.
*
*
* \retval 0 on success.
* \retval 1 on error unloading modules.
* \retval -1 on error loading modules back.
*/
int ast_refresh_resource(const char *resource_name, enum ast_module_unload_mode force, int recursive);
/*!
* \brief Unload a module.
* \param resource_name The name of the module to unload.

View File

@ -807,30 +807,35 @@ static char *handle_logger_mute(struct ast_cli_entry *e, int cmd, struct ast_cli
static char *handle_refresh(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
{
static const char * const completions[] = { "recursively", NULL };
int res;
/* "module refresh <mod>" */
switch (cmd) {
case CLI_INIT:
e->command = "module refresh";
e->usage =
"Usage: module refresh <module name>\n"
" Unloads and loads the specified module into Asterisk.\n";
"Usage: module refresh <module name> [recursively]\n"
" Unloads and loads the specified module into Asterisk.\n"
" 'recursively' will attempt to unload any modules with\n"
" dependencies on this module for you and load them again\n"
" afterwards.\n";
return NULL;
case CLI_GENERATE:
if (a->pos != e->args) {
return NULL;
if (a->pos == e->args) {
return ast_module_helper(a->line, a->word, a->pos, a->n, a->pos, AST_MODULE_HELPER_UNLOAD);
} else if (a->pos == e->args + 1) {
return ast_cli_complete(a->word, completions, a->n);
}
return ast_module_helper(a->line, a->word, a->pos, a->n, a->pos, AST_MODULE_HELPER_UNLOAD);
return NULL;
}
if (a->argc != e->args + 1) {
if (a->argc < 3 || a->argc > 4) {
return CLI_SHOWUSAGE;
}
if (ast_unload_resource(a->argv[e->args], AST_FORCE_SOFT)) {
ast_cli(a->fd, "Unable to unload resource %s\n", a->argv[e->args]);
return CLI_FAILURE;
}
if (ast_load_resource(a->argv[e->args])) {
ast_cli(a->fd, "Unable to load module %s\n", a->argv[e->args]);
res = ast_refresh_resource(a->argv[e->args], AST_FORCE_SOFT, a->argc == 4 && !strcasecmp(a->argv[3], "automatically"));
if (res) {
ast_cli(a->fd, "Unable to %s resource %s\n", res > 0 ? "unload" : "load", a->argv[e->args]);
return CLI_FAILURE;
}
ast_cli(a->fd, "Unloaded and loaded %s\n", a->argv[e->args]);

View File

@ -1216,7 +1216,73 @@ int modules_shutdown(void)
return !res;
}
int ast_unload_resource(const char *resource_name, enum ast_module_unload_mode force)
/*!
* \brief Whether or not this module should be able to be unloaded successfully,
* if we recursively unload any modules that are dependent on it.
* \note module_list should be locked when calling this
* \retval 0 if not, 1 if likely possible
*/
static int graceful_unload_possible(struct ast_module *target, struct ast_vector_const_string *dependents)
{
struct ast_module *mod;
int usecount = target->usecount;
/* Check the reffed_deps of each module to see if we're one of them. */
AST_DLLIST_TRAVERSE(&module_list, mod, entry) {
if (AST_VECTOR_GET_CMP(&mod->reffed_deps, target, AST_VECTOR_ELEM_DEFAULT_CMP)) {
const char *name;
/* This module is dependent on the target.
* If we can unload this module gracefully,
* then that would decrement our use count.
* If any single module could not be unloaded gracefully,
* then we don't proceed. */
int unloadable;
if (AST_VECTOR_GET_CMP(dependents, ast_module_name(mod), !strcasecmp)) {
/* Already in our list, we already checked this module,
* and we gave it the green light. */
ast_debug(3, "Skipping duplicate dependent %s\n", ast_module_name(mod));
if (!--usecount) {
break;
}
continue;
}
unloadable = graceful_unload_possible(mod, dependents);
if (!unloadable) {
ast_log(LOG_NOTICE, "Can't unload %s right now because %s is dependent on it\n", ast_module_name(target), ast_module_name(mod));
return 0;
}
/* Insert at beginning, so later if we're loading modules again automatically, we can do so in the same order. */
name = ast_strdup(ast_module_name(mod));
if (!name) {
return 0;
}
ast_debug(3, "Found new dependent %s\n", ast_module_name(mod));
if (AST_VECTOR_INSERT_AT(dependents, 0, name)) {
ast_log(LOG_ERROR, "Failed to add module '%s' to vector\n", ast_module_name(mod));
return 0;
}
if (!--usecount) {
break;
}
}
}
if (usecount) {
ast_log(LOG_NOTICE, "Module %s cannot be unloaded (would still have use count %d/%d after unloading dependents)\n", ast_module_name(target), usecount, target->usecount);
return 0;
}
return 1;
}
/*!
* \brief Unload a resource
* \param resource_name Module name
* \param force
* \param autounload Whether to attempt to automatically unload dependents of this module and load them again afterwards
* \param dependents. Can be NULL if autounload is 0.
* \retval 0 on success, -1 on failure
*/
static int auto_unload_resource(const char *resource_name, enum ast_module_unload_mode force, int autounload, struct ast_vector_const_string *dependents)
{
struct ast_module *mod;
int res = -1;
@ -1244,13 +1310,55 @@ int ast_unload_resource(const char *resource_name, enum ast_module_unload_mode f
goto exit; /* Skip all the intervening !error checks, only the last one is relevant. */
}
if (!error && mod->usecount > 0 && autounload) {
/* Try automatically unloading all modules dependent on the module we're trying to unload,
* and then, optionally, load them back again if we end up loading this module again.
* If any modules that have us as a dependency can't be unloaded, for whatever reason,
* then the entire unload operation will fail, so to try to make this an atomic operation
* and avoid leaving modules in a partial unload state, first check if we think we're going
* to be able to pull this off, and if not, abort.
*
* A race condition is technically still possible, if some depending module suddenly goes in use
* between this check and trying to unload it, but this takes care of the majority of
* easy-to-avoid cases that would fail eventually anyways.
*
* Note that we can encounter false negatives (e.g. unloading all the dependents would allow
* a module to unload, but graceful_unload_possible returns 0). This is because it's only
* checking direct module dependencies; other dependencies caused by a module registering
* a resource that cause its ref count to get bumped aren't accounted for here.
*/
if (graceful_unload_possible(mod, dependents)) {
int i, res = 0;
size_t num_deps = AST_VECTOR_SIZE(dependents);
ast_debug(1, "%lu module%s will need to be unloaded\n", AST_VECTOR_SIZE(dependents), ESS(AST_VECTOR_SIZE(dependents)));
/* Unload from the end, since the last module was the first one added, which means it isn't a dependency of anything else. */
for (i = AST_VECTOR_SIZE(dependents) - 1; i >= 0; i--) {
const char *depname = AST_VECTOR_GET(dependents, i);
res = ast_unload_resource(depname, force);
if (res) {
ast_log(LOG_WARNING, "Failed to unload %lu module%s automatically (%s could not be unloaded)\n", num_deps, ESS(num_deps), depname);
/* To be polite, load modules that we already unloaded,
* to try to leave things the way they were when we started. */
for (i++; i < num_deps; i++) {
const char *depname = AST_VECTOR_GET(dependents, i);
res = ast_load_resource(depname);
if (res) {
ast_log(LOG_WARNING, "Could not load module '%s' again automatically\n", depname);
}
}
break;
}
}
/* Either we failed, or we successfully unloaded everything.
* If we succeeded, we can now proceed and unload ourselves. */
}
}
if (!error && (mod->usecount > 0)) {
if (force)
ast_log(LOG_WARNING, "Warning: Forcing removal of module '%s' with use count %d\n",
resource_name, mod->usecount);
else {
ast_log(LOG_WARNING, "Soft unload failed, '%s' has use count %d\n", resource_name,
mod->usecount);
if (force) {
ast_log(LOG_WARNING, "Warning: Forcing removal of module '%s' with use count %d\n", resource_name, mod->usecount);
} else {
ast_log(LOG_WARNING, "Soft unload failed, '%s' has use count %d\n", resource_name, mod->usecount);
error = 1;
}
}
@ -1296,6 +1404,52 @@ exit:
return res;
}
int ast_refresh_resource(const char *resource_name, enum ast_module_unload_mode force, int recursive)
{
if (recursive) {
/* Recursively unload dependents of this module and then load them back again */
int res, i;
struct ast_vector_const_string dependents;
AST_VECTOR_INIT(&dependents, 0);
res = auto_unload_resource(resource_name, force, recursive, &dependents);
if (res) {
AST_VECTOR_FREE(&dependents);
return 1;
}
/* Start by loading the target again. */
if (ast_load_resource(resource_name)) {
ast_log(LOG_WARNING, "Failed to load module '%s' again automatically\n", resource_name);
AST_VECTOR_FREE(&dependents);
return -1;
}
res = 0;
/* Finally, load again any modules we had to unload in order to refresh the target.
* We must load modules in the reverse order that we unloaded them,
* to preserve dependency requirements. */
for (i = 0; i < AST_VECTOR_SIZE(&dependents); i++) {
const char *depname = AST_VECTOR_GET(&dependents, i);
int mres = ast_load_resource(depname);
if (mres) {
ast_log(LOG_WARNING, "Could not load module '%s' again automatically\n", depname);
}
res |= mres;
}
AST_VECTOR_FREE(&dependents);
return res ? -1 : 0;
}
/* Simple case: just unload and load the module again */
if (ast_unload_resource(resource_name, force)) {
return 1;
}
return ast_load_resource(resource_name);
}
int ast_unload_resource(const char *resource_name, enum ast_module_unload_mode force)
{
return auto_unload_resource(resource_name, force, 0, NULL);
}
static int module_matches_helper_type(struct ast_module *mod, enum ast_module_helper_type type)
{
switch (type) {

View File

@ -1112,10 +1112,18 @@
<enum name="load" />
<enum name="unload" />
<enum name="reload" />
<enum name="refresh">
<para>Completely unload and load again a specified module.</para>
</enum>
</enumlist>
<para>If no module is specified for a <literal>reload</literal> loadtype,
all modules are reloaded.</para>
</parameter>
<parameter name="Automatic" required="false">
<para>For <literal>refresh</literal> operations, attempt to automatically
unload any other modules that are dependent on this module, if that would
allow it to successfully unload, and load them again afterwards.</para>
</parameter>
</syntax>
<description>
<para>Loads, unloads or reloads an Asterisk module in a running system.</para>
@ -7135,6 +7143,7 @@ static int manager_moduleload(struct mansession *s, const struct message *m)
int res;
const char *module = astman_get_header(m, "Module");
const char *loadtype = astman_get_header(m, "LoadType");
const char *automatic = astman_get_header(m, "Automatic");
if (!loadtype || strlen(loadtype) == 0) {
astman_send_error(s, m, "Incomplete ModuleLoad action.");
@ -7157,6 +7166,13 @@ static int manager_moduleload(struct mansession *s, const struct message *m)
} else {
astman_send_ack(s, m, "Module unloaded.");
}
} else if (!strcasecmp(loadtype, "refresh")) {
res = ast_refresh_resource(module, AST_FORCE_SOFT, !ast_strlen_zero(automatic) && ast_true(automatic));
if (res) {
astman_send_error(s, m, "Could not refresh module.");
} else {
astman_send_ack(s, m, "Module unloaded and loaded.");
}
} else if (!strcasecmp(loadtype, "reload")) {
/* TODO: Unify the ack/error messages here with action_reload */
if (!ast_strlen_zero(module)) {