Skip to content

Commit

Permalink
node: Allow nodes to be booted from doors.
Browse files Browse the repository at this point in the history
Use the node interrupt API to allow nodes to be
kicked from doors (but not the BBS as a whole)
to allow modules to be unloaded more seamlessly
if needed, including recursively.
  • Loading branch information
InterLinked1 committed Feb 2, 2024
1 parent 45eb6ab commit 4a0c831
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 53 deletions.
5 changes: 5 additions & 0 deletions bbs/door.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include <string.h>

#include "include/door.h"
#include "include/node.h"
#include "include/linkedlists.h"
#include "include/module.h"
#include "include/cli.h"
Expand Down Expand Up @@ -110,6 +111,7 @@ int bbs_door_exec(struct bbs_node *node, const char *name, const char *args)
{
int res;
struct bbs_door *door;
struct bbs_module *prevmod;

RWLIST_RDLOCK(&doors);
door = find_door(name);
Expand All @@ -123,7 +125,10 @@ int bbs_door_exec(struct bbs_node *node, const char *name, const char *args)
/* Ref module before unlocking */
bbs_module_ref(door->module, 1);
RWLIST_UNLOCK(&doors);
prevmod = node->doormod; /* Unless we're nesting door calls, node->doormod should be NULL at this point, but save it just in case */
node->doormod = door->module;
res = door->execute(node, args);
node->doormod = prevmod; /* Restore */
bbs_module_unref(door->module, 1);
return res;
}
Expand Down
20 changes: 10 additions & 10 deletions bbs/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ struct bbs_module_ref {
RWLIST_HEAD(module_list, bbs_module_ref);

/*! \note modules list must be locked when calling */
static int unload_dependencies(struct bbs_module *mod, struct stringlist *restrict removed)
static int unload_dependencies(struct bbs_module *mod, int force, struct stringlist *restrict removed)
{
struct bbs_module *m;
int res = 0;
Expand Down Expand Up @@ -726,7 +726,7 @@ static int unload_dependencies(struct bbs_module *mod, struct stringlist *restri
m = r->m;
free(r);
bbs_verb(5, "Unloading %s, since it depends on %s\n", m->name, mod->name);
if (!unload_resource_nolock(m, 0, &usecount, removed)) {
if (!unload_resource_nolock(m, force, &usecount, removed)) {
res = -1;
bbs_warning("Failed to unload module %s, which depends on %s\n", m->name, mod->name);
continue;
Expand Down Expand Up @@ -793,7 +793,7 @@ static struct bbs_module *unload_resource_nolock(struct bbs_module *mod, int for
{
int res;

bbs_debug(2, "Module %s has use count %d\n", mod->name, mod->usecount);
bbs_debug(2, "Module %s has use count %d (force: %d)\n", mod->name, mod->usecount, force);
*usecount = mod->usecount;

/* Automatically unload any other modules that may depend on this module. */
Expand All @@ -806,21 +806,21 @@ static struct bbs_module *unload_resource_nolock(struct bbs_module *mod, int for
* If a node is executing a door, for example, that won't apply: the module will have
* a refcount due to the usage, but we won't be able to kick the node in this manner here. */
if (nodes_usecount > 0) {
unsigned int kicked = bbs_node_shutdown_mod(mod); /* Kick all the nodes created by this module. */
if (kicked != nodes_usecount) {
bbs_warning("Wanted to kick %u nodes but only kicked %u?\n", nodes_usecount, kicked);
} else if (kicked) {
unsigned int removed_nodes = bbs_node_shutdown_mod(mod); /* Kick all the nodes created by this module. */
if (removed_nodes != nodes_usecount) {
bbs_warning("Wanted to kick %u nodes but only removed %u?\n", nodes_usecount, removed_nodes);
} else if (removed_nodes) {
usleep(10000); /* Wait for actual node exits to complete, to increase chance of success */
bbs_debug(3, "Kicked %d node%s\n", kicked, ESS(kicked));
bbs_debug(3, "Removed %d node%s from module %s\n", removed_nodes, ESS(removed_nodes), bbs_module_name(mod));
}
}
}
unload_dependencies(mod, removed);
unload_dependencies(mod, force, removed);
}

if (mod->usecount > 0) {
if (force > 1) {
bbs_warning("Warning: Forcing removal of module '%s' with use count %d\n", mod->name, mod->usecount);
bbs_warning("Warning: Forcing removal of module '%s' with use count %d\n", mod->name, mod->usecount);
} else {
if (RWLIST_EMPTY(&mod->refs)) {
/* The integer count is positive, but our list is empty?
Expand Down
123 changes: 84 additions & 39 deletions bbs/node.c
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ unsigned int bbs_node_mod_count(void *mod)

RWLIST_RDLOCK(&nodes);
RWLIST_TRAVERSE(&nodes, node, entry) {
if (node->module == mod) {
if (node->module == mod || node->doormod == mod) {
count++;
}
}
Expand Down Expand Up @@ -650,21 +650,75 @@ int bbs_node_shutdown_node(unsigned int nodenum)
return n ? 0 : -1;
}

static int interrupt_node(struct bbs_node *node)
{
int res = -1;
if (!node->thread) {
bbs_debug(1, "Node %u is not owned by a thread, and cannot be interrupted\n", node->id);
} else if (!node->slavefd) {
/* If there's no PTY, bbs_node_poll can't be used anyways.
* And if there's no PTY, it's a network protocol that doesn't make sense to interrupt.
* Only terminal protocols should be interrupted. */
bbs_debug(1, "Node %u has no PTY\n", node->id);
} else {
int err;
/* The node thread should never interrupt itself, this is only for other threads to
* interrupt a blocking I/O call. */
bbs_assert(node->thread != pthread_self());
node->interruptack = 0;
node->interrupt = 1; /* Indicate that interrupt was requested */

bbs_node_kill_child(node); /* If executing an external program, kill it */

/* Make the I/O function (probably poll(2)) exit with EINTR.
* Less overhead than always polling another alertpipe just for getting out of band alerts like this,
* since we can easily enough check the interrupt status in the necessary places on EINTR. */
err = pthread_kill(node->thread, SIGUSR1); /* Uncaught signal, so the blocking I/O call will get interrupted */
if (err) {
bbs_warning("pthread_kill(%lu) failed: %s\n", (unsigned long) node->thread, strerror(err));
bbs_node_unlock(node);
return 1;
}

bbs_verb(5, "Interrupted node %u\n", node->id);
res = 0;
}
return res;
}

unsigned int bbs_node_shutdown_mod(void *mod)
{
struct bbs_node *n;
unsigned int count = 0;

RWLIST_WRLOCK(&nodes);
RWLIST_TRAVERSE_SAFE_BEGIN(&nodes, n, entry) {
if (n->module != mod) {
continue;
if (n->doormod == mod) {
int res;
/* "Dump" any nodes executing door modules from their current door.
* We don't need to kick these nodes, just interrupt them. */
bbs_verb(5, "Interrupting node %u to allow %s to unload\n", n->id, bbs_module_name(mod));
/* Can't use bbs_interrupt_node since that will call bbs_node_get,
* which invokes a RDLOCK on the node list.
* Since we already hold a WRLOCK, that would cause a deadlock.
* Instead, use interrupt_node directly. */
bbs_node_lock(n);
res = interrupt_node(n);
bbs_node_unlock(n);
/* Wait for this node to exit the door */
if (!res && bbs_node_interrupt_wait(n, 250)) { /* Don't wait more than 250 ms for the node to exit the door */
count++;
}
} else if (n->module == mod) {
/* Kill any nodes that might be using a particular network module;
* since they created the node and "own it", we can't unload them
* without killing all their nodes. */
RWLIST_REMOVE_CURRENT(entry);
/* Wait for shutdown of node to finish. */
bbs_verb(5, "Kicking node %u to allow %s to unload\n", n->id, bbs_module_name(mod));
node_shutdown(n, 0);
count++;
}
RWLIST_REMOVE_CURRENT(entry);
/* Wait for shutdown of node to finish. */
node_shutdown(n, 0);
count++;
break;
}
RWLIST_TRAVERSE_SAFE_END;
RWLIST_UNLOCK(&nodes);
Expand Down Expand Up @@ -721,43 +775,14 @@ static int cli_nodes(struct bbs_cli_args *a)

int bbs_interrupt_node(unsigned int nodenum)
{
int res = -1;
int res;
struct bbs_node *node = bbs_node_get(nodenum);

if (!node) {
return -1;
}

if (!node->thread) {
bbs_debug(1, "Node %u is not owned by a thread, and cannot be interrupted\n", nodenum);
} else if (!node->slavefd) {
/* If there's no PTY, bbs_node_poll can't be used anyways.
* And if there's no PTY, it's a network protocol that doesn't make sense to interrupt.
* Only terminal protocols should be interrupted. */
bbs_debug(1, "Node %u has no PTY\n", nodenum);
} else {
int err;
/* The node thread should never interrupt itself, this is only for other threads to
* interrupt a blocking I/O call. */
bbs_assert(node->thread != pthread_self());
node->interruptack = 0;
node->interrupt = 1; /* Indicate that interrupt was requested */

bbs_node_kill_child(node); /* If executing an external program, kill it */

/* Make the I/O function (probably poll(2)) exit with EINTR.
* Less overhead than always polling another alertpipe just for getting out of band alerts like this,
* since we can easily enough check the interrupt status in the necessary places on EINTR. */
err = pthread_kill(node->thread, SIGUSR1); /* Uncaught signal, so the blocking I/O call will get interrupted */
if (err) {
bbs_warning("pthread_kill(%lu) failed: %s\n", (unsigned long) node->thread, strerror(err));
bbs_node_unlock(node);
return 1;
}

bbs_verb(5, "Interrupted node %u\n", nodenum);
res = 0;
}
res = interrupt_node(node);

bbs_node_unlock(node);
return res;
Expand Down Expand Up @@ -786,6 +811,26 @@ int bbs_node_interrupted(struct bbs_node *node)
return node->interrupt;
}

int bbs_node_interrupt_wait(struct bbs_node *node, int ms)
{
int elapsed = 0;

for (;;) {
if (!bbs_node_interrupted(node)) {
/* Interrupt was cleared */
return 1;
}
/* XXX Ideally, we would use an alertpipe or something
* of the sort, to avoid the need to poll for interrupt clear. */
usleep(10000); /* Wait 10 ms */
elapsed += 10;
if (ms > 0 && elapsed >= ms) {
return 0;
}
}
__builtin_unreachable();
}

static int cli_interrupt(struct bbs_cli_args *a)
{
int res, node = atoi(a->argv[1]);
Expand Down
21 changes: 17 additions & 4 deletions include/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ struct bbs_node {
pthread_t thread; /*!< Thread handling socket I/O */
pthread_t ptythread; /*!< Thread handling PTY master */
struct bbs_module *module; /*!< Module reference for socket/network driver module */
struct bbs_module *doormod; /*!< Module reference for current door being executed */
const char *protname; /*!< Socket driver protocol name */
struct bbs_user *user; /*!< Active user of a BBS node */
struct bbs_vars *vars; /*!< Variables */
Expand Down Expand Up @@ -107,9 +108,10 @@ int bbs_load_nodes(void);
unsigned int bbs_node_count(void);

/*!
* \brief Get number of allocated nodes created by a certain module
* \brief Get number of allocated nodes currently using a certain module
* \param mod Module reference
* \retval Number of current nodes created by mod
* \retval Number of current nodes created by or using mod
* \note Works for nets and doors only
*/
unsigned int bbs_node_mod_count(void *mod);

Expand Down Expand Up @@ -246,9 +248,10 @@ int bbs_node_unlink(struct bbs_node *node);
int bbs_node_shutdown_node(unsigned int nodenum);

/*!
* \brief Request a shut down of all nodes created using a particular module
* \brief Request any nodes using a particular module be kicked from that module
* \param mod Module reference
* \return Number of nodes kicked
* \note For nets, this will kick the module. For doors, it will interrupt the node to force it to exit the door.
* \return Number of nodes kicked from this module
*/
unsigned int bbs_node_shutdown_mod(void *mod);

Expand All @@ -275,6 +278,16 @@ int bbs_interrupt_node(unsigned int nodenum);
*/
int bbs_node_interrupted(struct bbs_node *node);

/*!
* \brief Wait for an interrupted node to acknowledge and clear the interrupt
* \param node
* \param ms If positive, maximum number of milliseconds to wait
* \retval 1 if interrupt cleared, 0 if timer expired (like poll)
* \note May be called in any thread, as long as node is guaranteed to remain a valid reference
* for the duration of this function (e.g. owning thread owns the node or holds a global node list lock)
*/
int bbs_node_interrupt_wait(struct bbs_node *node, int ms);

/*!
* \brief Clear the interrupt status for a node
* \param node
Expand Down

0 comments on commit 4a0c831

Please sign in to comment.