From: Alan T. DeKok Date: Wed, 18 Aug 2021 14:06:55 +0000 (-0400) Subject: initial stab at generic state machine handler. X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=292d2a70e3473b4a6c50f4284a9b3517f3bff643;p=thirdparty%2Ffreeradius-server.git initial stab at generic state machine handler. --- diff --git a/src/lib/util/libfreeradius-util.mk b/src/lib/util/libfreeradius-util.mk index e0b9ab16d1d..16027020f68 100644 --- a/src/lib/util/libfreeradius-util.mk +++ b/src/lib/util/libfreeradius-util.mk @@ -42,6 +42,7 @@ SOURCES := \ isaac.c \ log.c \ lst.c \ + machine.c \ md4.c \ md5.c \ misc.c \ diff --git a/src/lib/util/machine.c b/src/lib/util/machine.c new file mode 100644 index 00000000000..4d929359187 --- /dev/null +++ b/src/lib/util/machine.c @@ -0,0 +1,504 @@ +/* + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/** State machine functions + * + * @file src/lib/util/machine.c + * + * @copyright 2021 Network RADIUS SAS (legal@networkradius.com) + */ +RCSID("$Id$") + +#include "machine.h" + +typedef struct { + fr_machine_state_t const *def; //!< static definition of names, callbacks for this particular state + fr_dlist_head_t enter[2]; //!< pre/post enter hooks + fr_dlist_head_t process[2]; //!< pre/post process hooks + fr_dlist_head_t exit[2]; //!< pre/post exit hooks +} fr_machine_state_inst_t; + +/** Hooks + * + */ +struct fr_machine_s { + fr_machine_def_t const *def; //!< static definition of states, names, callbacks for the state machine + void *uctx; //!< to pass to the various handlers + fr_machine_state_inst_t *current; //!< current state we are in + void const *in_handler; //!< block transitions if we're in a callback + + int deferred; //!< deferred transition if we're paused + int paused; //!< are transitions paused? + bool dead; + + fr_machine_state_inst_t state[0]; //!< all of the state transitions +}; + +typedef struct { + void *uctx; //!< to pass back to the function + fr_machine_hook_func_t func; //!< function to call for the hook + bool oneshot; //!< is this a one-shot callback? + fr_dlist_head_t *head; //!< where the hook is stored + fr_dlist_t dlist; //!< linked list of hooks +} fr_machine_hook_t; + +#undef PRE +#define PRE (0) +#undef POST +#define POST (1) + +/** Call the hook with the current state machine, and the hooks context + * + * Note that in most cases, the hook will have saved it's own state + * machine in uctx, and will not need the current state machine. + */ +static inline void call_hook(fr_machine_t *m, fr_dlist_head_t *head, int state1, int state2) +{ + fr_machine_hook_t *hook, *next; + + for (hook = fr_dlist_head(head); hook != NULL; hook = next) { + next = fr_dlist_next(head, hook); + + hook->func(m, state1, state2, hook->uctx); + if (hook->oneshot) talloc_free(hook); + } +} + +/** Transition from one state to another. + * + * Including calling pre/post hooks. + * + * None of the functions called from here are allowed to perform a + * state transition. + */ +static void state_transition(fr_machine_t *m, int state, void *in_handler) +{ + fr_machine_state_inst_t *current = m->current; + int old; + + fr_assert(current != NULL); + fr_assert(m->deferred == 0); + + fr_assert(!m->in_handler); + m->in_handler = in_handler; + + old = current->def->number; + + /* + * Exit the current state. + */ + call_hook(m, ¤t->exit[PRE], old, state); + if (current->def->exit) current->def->exit(m, m->uctx); + call_hook(m, ¤t->exit[POST], old, state); + + /* + * Reset "current", and enter the new state. + */ + current = m->current = &m->state[state]; + + call_hook(m, ¤t->enter[PRE], old, state); + if (current->def->enter) current->def->enter(m, m->uctx); + call_hook(m, ¤t->enter[POST], old, state); + + m->in_handler = NULL; +} + + +/** Free a state machine + * + * When a state machine is freed, it will first transition to the + * "free" state. That state is presumed to do all appropriate + * cleanups. + */ +static int _machine_free(fr_machine_t *m) +{ + fr_assert(m); + fr_assert(m->def); + fr_assert(m->def->free); + + fr_assert(m->state[m->def->free].enter != NULL); + + /* + * Exit the current state, and enter the free state. + */ + state_transition(m, m->def->free, _machine_free); + + /* + * Don't call "process" on the free state. Simply + * entering the free state _should_ clean everything up. + */ + + return 0; +} + +/** Instantiate a state machine + * + * @param ctx the talloc ctx + * @param def the definition of the state machine + * @param uctx the context passed to the callbacks + * @return + * - NULL on error + * - !NULL state machine which can be used. + */ +fr_machine_t *fr_machine_alloc(TALLOC_CTX *ctx, fr_machine_def_t const *def, void *uctx) +{ + int i, j, next; + fr_machine_t *m; + + /* + * We always reserve 0 for "invalid state". + * + * The "max_state" is the maximum allowed state, which is a valid state number. + */ + m = (fr_machine_t *) talloc_zero_array(ctx, uint8_t, sizeof(fr_machine_t) + sizeof(m->state[0]) * (def->max_state + 1)); + if (!m) return NULL; + + talloc_set_type(m, fr_machine_t); + + *m = (fr_machine_t) { + .uctx = uctx, + .def = def, + }; + + /* + * Initialize the instance structures for each of the + * states. + */ + for (i = 1; i <= def->max_state; i++) { + fr_machine_state_inst_t *state = &m->state[i]; + + state->def = &def->state[i]; + + for (j = 0; j < 2; j++) { + fr_dlist_init(&state->enter[j], fr_machine_hook_t, dlist); + fr_dlist_init(&state->process[j], fr_machine_hook_t, dlist); + fr_dlist_init(&state->exit[j], fr_machine_hook_t, dlist); + } + } + + /* + * Set the current state to "init". + */ + m->current = &m->state[def->init]; + + /* + * We don't transition into the "init" state, as there is + * no previous state. We just run the "process" + * function, which should transition us into a more + * permanent state. + * + * We don't run any pre/post hooks, as the state machine + * is new, and no hooks have been added. + * + * The result of the initialization routine can be + * another new state, or 0 for "stay in the current + * state". + */ + fr_assert(!m->current->def->enter); + fr_assert(!m->current->def->exit); + + next = m->current->def->process(m, uctx); + fr_assert(next >= 0); + + if (def->free) talloc_set_destructor(m, _machine_free); + + if (next) fr_machine_transition(m, next); + + return m; +} + + +/** Process the state machine + * + * @param m The state machine + * @return + * - 0 for "no transition has occured" + * - >0 for "we are in a new state". + * -<0 for "error, you should tear down the state machine". + * + * This function should be called by the user of the machine. + * + * In general, the caller doesn't really care about the return code + * of this function. The only real utility is >=0 for "continue + * calling the state machine as necessary", or <0 for "shut down the + * state machine". + */ +int fr_machine_process(fr_machine_t *m) +{ + int state, old; + fr_machine_state_inst_t *current = m->current; + + fr_assert(current != NULL); + fr_assert(!m->deferred); + fr_assert(!m->dead); + fr_assert(!m->paused); + + m->in_handler = current; + old = current->def->number; + + call_hook(m, ¤t->process[PRE], old, old); + state = current->def->process(m, m->uctx); + call_hook(m, ¤t->process[POST], old, old); + + m->in_handler = NULL; + + /* + * No changes. Tell the caller to wait for something + * else to signal a transition. + */ + if (state == 0) return 0; + + /* + * The called function requested that we transition to + * the "free" state. Don't do that, but instead return + * an error to the caller. The caller MUST do nothing + * other than free the state machine. + */ + if (state == m->def->free) { + m->dead = true; + return -1; + } + + /* + * Transition to the new state. + */ + fr_machine_transition(m, state); + + return state; +} + +/** Transition to a new state + * + * @param m The state machine + * @param state the state to transition to + * @return + * - <0 for error + * - 0 for the transition was made (or deferred) + * + * The transition MAY be deferred. Note that only one transition at + * a time can be deferred. + * + * This function MUST NOT be called from any "hook", or from any + * enter/exit/process function. It should ONLY be called from the + * "parent" of the state machine, when it decides that the state + * machine needs to change. + * + * i.e. from a timer, or an IO callback + */ +int fr_machine_transition(fr_machine_t *m, int state) +{ + fr_machine_state_inst_t *current = m->current; + + if (m->dead) return -1; + + /* + * Bad states are not allowed. + */ + if ((state < 0) || (state > m->def->max_state)) return -1; + + /* + * If we are not in a state, we cannot transition to + * anything else. + */ + if (!current) return -1; + + /* + * Transition to self is "do nothing". + */ + if (current->def->number == state) return 0; + +#if 0 + /* + * Asked to do an illegal transition, that's an error. + */ + if (!current->out[state]) { + fr_assert(0); + return; + } +#endif + + /* + * We cannot transition from inside of a particular + * state. Instead, the state MUST return a new state + * number, and fr_machine_process() will do the + * transition. + */ + fr_assert(!m->in_handler); + + /* + * The caller may be mucking with bits of the state + * machine and/or the code surrounding the state machine. + * In that case, the caller doesn't want transitions to + * occur until it's done those changes. Otherwise the + * state machine could disappear in the middle of a + * function, which is bad. + * + * However, the rest of the code doesn't know what the + * caller wants. So the caller "pauses" state + * transitions until it's done. We check for that here, + * and defer transitions until such time as the + * transitions are resumed. + */ + if (m->paused) { + fr_assert(!m->deferred); + m->deferred = state; + return 0; + } + + /* + * We're allowed to do the transition now, so exit the + * current state, and enter the new one. + */ + state_transition(m, state, fr_machine_transition); + + return 0; +} + +/** Get the current state + * + * @param m The state machine + * @return + * The current state, or 0 for "not in any state" + */ +int fr_machine_current(fr_machine_t *m) +{ + fr_assert(!m->dead); + + if (!m->current) return 0; + + return m->current->def->number; +} + +/** Get the name of a particular state + * + * @param m The state machine + * @param state The state to query + * @return + * the name + */ +char const *fr_machine_state_name(fr_machine_t *m, int state) +{ + fr_assert(!m->dead); + + if ((state < 0) || (state > m->def->max_state)) return "???"; + + if (!state) { + if (m->current) { + state = m->current->def->number; + + } else if (m->deferred) { + state = m->deferred; + + } else { + return "???"; + } + } + + return m->def->state[state].name; +} + +static int _machine_hook_free(fr_machine_hook_t *hook) +{ + (void) fr_dlist_remove(hook->head, &hook->dlist); + + return 0; +} + + +/** Add a hook to a state, with an optional talloc_ctx. + * + * The hook is removed when the talloc ctx is freed. + * + * You can also remove the hook by freeing the returned pointer. + */ +void *fr_machine_hook(fr_machine_t *m, TALLOC_CTX *ctx, int state_to_hook, fr_machine_hook_type_t type, fr_machine_hook_sense_t sense, + bool oneshot, fr_machine_hook_func_t func, void *uctx) +{ + fr_machine_hook_t *hook; + fr_dlist_head_t *head; + fr_machine_state_inst_t *state = &m->state[state_to_hook]; + + fr_assert(!m->dead); + + switch (type) { + case FR_MACHINE_ENTER: + head = &state->enter[sense]; + break; + + case FR_MACHINE_PROCESS: + head = &state->process[sense]; + break; + + case FR_MACHINE_EXIT: + head = &state->exit[sense]; + break; + + default: + return NULL; + } + + hook = talloc_zero(ctx, fr_machine_hook_t); + if (!hook) return NULL; + + *hook = (fr_machine_hook_t) { + .func = func, + .head = head, /* needed for updating num_elements on remove */ + .uctx = uctx, + .oneshot = oneshot, + }; + + fr_dlist_insert_tail(head, &hook->dlist); + + talloc_set_destructor(hook, _machine_hook_free); + + return hook; +} + +/** Pause any transitions. + * + */ +void fr_machine_pause(fr_machine_t *m) +{ + fr_assert(!m->dead); + + m->paused++; +} + +/** Resume transitions. + * + */ +void fr_machine_resume(fr_machine_t *m) +{ + int state; + + fr_assert(!m->dead); + + if (m->paused > 0) { + m->paused--; + if (m->paused > 0) return; + } + + if (!m->deferred) return; + + /* + * Clear the deferred transition before making any + * changes, as we're now doing the transition. + */ + state = m->deferred; + m->deferred = 0; + + state_transition(m, state, fr_machine_resume); +} diff --git a/src/lib/util/machine.h b/src/lib/util/machine.h new file mode 100644 index 00000000000..b5e2fe8280c --- /dev/null +++ b/src/lib/util/machine.h @@ -0,0 +1,92 @@ +#pragma once +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/** State machine functions + * + * @file src/lib/util/machine.h + * + * @copyright 2021 Network RADIUS SAS (legal@networkradius.com) + */ +RCSIDH(machine_h, "$Id$") + +#include + +typedef struct fr_machine_state_s fr_machine_state_t; + +typedef struct fr_machine_s fr_machine_t; + +typedef void (*fr_machine_func_t)(fr_machine_t *m, void *uctx); + +typedef int (*fr_machine_process_t)(fr_machine_t *m, void *uctx); + +typedef void (*fr_machine_hook_func_t)(fr_machine_t *m, int, int, void *uctx); + +struct fr_machine_state_s { + char const *name; //!< state name + int number; //!< automatically generated number + fr_machine_func_t enter; //!< run this when entering the state + fr_machine_process_t process; //!< run this to process the current state + fr_machine_func_t exit; //!< run this when exiting the state +// fr_machine_state_t out[0]; //!< allowed OUT transitions +}; + +typedef struct { + int max_state; //!< 1..max_number are permitted + int init; //!< state to run on init + int free; //!< state to run on free + fr_machine_state_t state[0]; +} fr_machine_def_t; + +fr_machine_t *fr_machine_alloc(TALLOC_CTX *ctx, fr_machine_def_t const *def, void *uctx); + +/** Process the state machine + * + * This funtion should be called from the "user" of the machine. + */ +int fr_machine_process(fr_machine_t *m); + +int fr_machine_transition(fr_machine_t *m, int state); + +int fr_machine_current(fr_machine_t *m); + +char const *fr_machine_state_name(fr_machine_t *m, int state); + +void fr_machine_pause(fr_machine_t *m); + +void fr_machine_resume(fr_machine_t *m); + +typedef enum { + FR_MACHINE_ENTER, + FR_MACHINE_PROCESS, + FR_MACHINE_EXIT, + FR_MACHINE_SIGNAL, +} fr_machine_hook_type_t; + +typedef enum { + FR_MACHINE_PRE = 0, + FR_MACHINE_POST, +} fr_machine_hook_sense_t; + +void *fr_machine_hook(fr_machine_t *m, TALLOC_CTX *ctx, int state, fr_machine_hook_type_t type, fr_machine_hook_sense_t sense, + bool oneshot, fr_machine_hook_func_t func, void *uctx); + +#define MACHINE radius + +#define ENTER(_x) static int MACHINE ## _enter ## _x(fr_machine_t *m, void *uctx) +#define EXIT(_x) static int MACHINE ## _exit ## _x(fr_machine_t *m, void *uctx) +#define PROCESS(_x) static int MACHINE ## _process ## _x(fr_machine_t *m, void *uctx) +