diff options
-rw-r--r-- | include/Makefile.am | 2 | ||||
-rw-r--r-- | include/osmocom/core/tdef.h | 172 | ||||
-rw-r--r-- | include/osmocom/vty/tdef_vty.h | 67 | ||||
-rw-r--r-- | src/Makefile.am | 3 | ||||
-rw-r--r-- | src/fsm.c | 4 | ||||
-rw-r--r-- | src/tdef.c | 282 | ||||
-rw-r--r-- | src/vty/Makefile.am | 3 | ||||
-rw-r--r-- | src/vty/tdef_vty.c | 372 | ||||
-rw-r--r-- | tests/Makefile.am | 36 | ||||
-rw-r--r-- | tests/tdef/tdef_test.c | 445 | ||||
-rw-r--r-- | tests/tdef/tdef_test.ok | 184 | ||||
-rw-r--r-- | tests/tdef/tdef_vty_test_config_root.c | 292 | ||||
-rw-r--r-- | tests/tdef/tdef_vty_test_config_root.vty | 292 | ||||
-rw-r--r-- | tests/tdef/tdef_vty_test_config_subnode.c | 288 | ||||
-rw-r--r-- | tests/tdef/tdef_vty_test_config_subnode.vty | 107 | ||||
-rw-r--r-- | tests/tdef/tdef_vty_test_dynamic.c | 362 | ||||
-rw-r--r-- | tests/tdef/tdef_vty_test_dynamic.vty | 83 | ||||
-rw-r--r-- | tests/testsuite.at | 6 |
18 files changed, 2998 insertions, 2 deletions
diff --git a/include/Makefile.am b/include/Makefile.am index 25a6d75f..17f7d1ce 100644 --- a/include/Makefile.am +++ b/include/Makefile.am @@ -47,6 +47,7 @@ nobase_include_HEADERS = \ osmocom/core/statistics.h \ osmocom/core/strrb.h \ osmocom/core/talloc.h \ + osmocom/core/tdef.h \ osmocom/core/timer.h \ osmocom/core/timer_compat.h \ osmocom/core/utils.h \ @@ -154,6 +155,7 @@ nobase_include_HEADERS += \ osmocom/vty/vector.h \ osmocom/vty/vty.h \ osmocom/vty/ports.h \ + osmocom/vty/tdef_vty.h \ osmocom/ctrl/control_vty.h endif diff --git a/include/osmocom/core/tdef.h b/include/osmocom/core/tdef.h new file mode 100644 index 00000000..92b71597 --- /dev/null +++ b/include/osmocom/core/tdef.h @@ -0,0 +1,172 @@ +/*! \file tdef.h + * API to define Tnnn timers globally and use for FSM state changes. + */ +/* + * (C) 2018-2019 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de> + * + * All Rights Reserved + * + * SPDX-License-Identifier: GPL-2.0+ + * + * Author: Neels Hofmeyr <neels@hofmeyr.de> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +#pragma once + +#include <stdint.h> +#include <osmocom/core/utils.h> + +struct osmo_fsm_inst; + +/*! \defgroup Tdef Tnnn timer configuration + * @{ + * \file tdef.h + */ + +enum osmo_tdef_unit { + OSMO_TDEF_S = 0, /*!< most T are in seconds, keep 0 as default. */ + OSMO_TDEF_MS, /*!< milliseconds */ + OSMO_TDEF_M, /*!< minutes */ + OSMO_TDEF_CUSTOM, /*!< unspecified unit, explained in osmo_tdef.desc. */ +}; + +extern const struct value_string osmo_tdef_unit_names[]; +/*! \return enum osmo_tdef_unit value as human readable unit letter, or "custom-unit". */ +static inline const char *osmo_tdef_unit_name(enum osmo_tdef_unit val) +{ return get_value_string(osmo_tdef_unit_names, val); } + +/*! Define a GSM timer of the form Tnnn, with unit, default value and doc string. + * Typically used as an array with the last entry being left zero-initialized, e.g.: + * + * struct osmo_tdef tdefs[] = { + * { .T=10, .default_val=6, .desc="RR Assignment" }, + * { .T=101, .default_val=10, .desc="inter-BSC Handover MT, HO Request to HO Accept" }, + * { .T=3101, .default_val=3, .desc="RR Immediate Assignment" }, + * {} + * }; + * + * Program initialization should call osmo_tdefs_reset() so that all timers return the default_val, until e.g. the VTY + * configuration sets user-defined values (see osmo_tdef_vty_init()). + */ +struct osmo_tdef { + /*! T1234 number; type corresponds to struct osmo_fsm_inst.T. Negative and zero T numbers are actually possible, + * but be aware that osmo_tdef_fsm_inst_state_chg() interprets T == 0 as "no timer". */ + const int T; + /*! Timeout duration (according to unit), default value; type corresponds to osmo_fsm_inst_state_chg()'s + * timeout_secs argument. Note that osmo_fsm_inst_state_chg() clamps the range. */ + const unsigned long default_val; + const enum osmo_tdef_unit unit; + /*! Human readable description. For unit == OSMO_TDEF_CUSTOM, this should include an explanation of the value's + * unit. Best keep this a short one-liner (e.g. for VTY output). */ + const char *desc; + /*! Currently active timeout value, e.g. set by user config. This is the only mutable member: a user may + * configure the timeout value, but neither unit nor any other field. */ + unsigned long val; +}; + +/*! Iterate an array of struct osmo_tdef, the last item should be fully zero, i.e. "{}". + * Example: + * + * struct osmo_tdef *t; + * osmo_tdef_for_each(t, tdefs) { + * printf("%lu %s %s\n", t->val, osmo_tdef_unit_name(t->unit), t->desc); + * } + * + * \param[inout] t A struct osmo_tdef *t used for iteration, will point at the current entry inside the loop scope. + * \param[in] tdefs Array of struct osmo_tdef to iterate, zero-terminated. + */ +#define osmo_tdef_for_each(t, tdefs) \ + for (t = tdefs; t && (t->T || t->default_val || t->desc); t++) + +void osmo_tdefs_reset(struct osmo_tdef *tdefs); +unsigned long osmo_tdef_get(const struct osmo_tdef *tdefs, int T, enum osmo_tdef_unit as_unit, + unsigned long val_if_not_present); +struct osmo_tdef *osmo_tdef_get_entry(struct osmo_tdef *tdefs, int T); + +/*! Using osmo_tdef for osmo_fsm_inst: array entry for a mapping of state numbers to timeout definitions. + * For a usage example, see osmo_tdef_get_state_timeout() and test_tdef_state_timeout() in tdef_test.c. */ +struct osmo_tdef_state_timeout { + /*! Timer number to match struct osmo_tdef.T, and to pass to osmo_fsm_inst_state_chg(). */ + int T; + /*! If true, call osmo_fsm_inst_state_chg_keep_timer(). + * If T == 0, keep previous T number, otherwise also set fi->T. */ + bool keep_timer; +}; + +const struct osmo_tdef_state_timeout *osmo_tdef_get_state_timeout(uint32_t state, + const struct osmo_tdef_state_timeout *timeouts_array); + +/*! Call osmo_fsm_inst_state_chg() or osmo_fsm_inst_state_chg_keep_timer(), depending on the timeouts_array, tdefs and + * default_timeout. + * + * A T timer configured in sub-second precision is rounded up to the next full second. A timer in unit = + * OSMO_TDEF_CUSTOM is applied as if the unit is in seconds (i.e. this macro does not make sense for custom units!). + * + * See osmo_tdef_get_state_timeout() and osmo_tdef_get(). + * + * If no T timer is defined for the given state (T == 0), invoke the state change without a timeout. + * + * Should a T number be defined in timeouts_array that is not defined in tdefs, use default_timeout (in seconds). If + * default_timeout is negative, a missing T definition in tdefs instead causes a program abort. + * + * This is best used by wrapping this function call in a macro suitable for a specific FSM implementation, which can + * become as short as: my_fsm_state_chg(fi, NEXT_STATE): + * + * #define my_fsm_state_chg(fi, NEXT_STATE) \ + * osmo_tdef_fsm_inst_state_chg(fi, NEXT_STATE, my_fsm_timeouts, global_T_defs, 5) + * + * my_fsm_state_chg(fi, MY_FSM_STATE_1); + * // -> No timeout configured, will enter state without timeout. + * + * my_fsm_state_chg(fi, MY_FSM_STATE_3); + * // T423 configured for this state, will look up T423 in tdefs, or use 5 seconds if unset. + * + * my_fsm_state_chg(fi, MY_FSM_STATE_8); + * // keep_timer == true for this state, will invoke osmo_fsm_inst_state_chg_keep_timer(). + * + * \param[inout] fi osmo_fsm_inst to transition to another state. + * \param[in] state State number to transition to. + * \param[in] timeouts_array Array of struct osmo_tdef_state_timeout[32] to look up state in. + * \param[in] tdefs Array of struct osmo_tdef (last entry zero initialized) to look up T in. + * \param[in] default_timeout If a T is set in timeouts_array, but no timeout value is configured for T, then use this + * default timeout value as fallback, or pass -1 to abort the program. + * \return Return value from osmo_fsm_inst_state_chg() or osmo_fsm_inst_state_chg_keep_timer(). + */ +#define osmo_tdef_fsm_inst_state_chg(fi, state, timeouts_array, tdefs, default_timeout) \ + _osmo_tdef_fsm_inst_state_chg(fi, state, timeouts_array, tdefs, default_timeout, \ + __FILE__, __LINE__) +int _osmo_tdef_fsm_inst_state_chg(struct osmo_fsm_inst *fi, uint32_t state, + const struct osmo_tdef_state_timeout *timeouts_array, + const struct osmo_tdef *tdefs, unsigned long default_timeout, + const char *file, int line); + +/*! Manage timer definitions in named groups. + * This should be defined as an array with the final element kept fully zero-initialized, + * to be compatible with osmo_tdef_vty* API. There must not be any tdefs == NULL entries except on the final + * zero-initialized entry. */ +struct osmo_tdef_group { + const char *name; + const char *desc; + struct osmo_tdef *tdefs; +}; + +/*! Iterate an array of struct osmo_tdef_group, the last item should be fully zero, i.e. "{}". + * \param[inout] g A struct osmo_tdef_group *g used for iteration, will point at the current entry inside the loop scope. + * \param[in] tdefs Array of struct osmo_tdef_group to iterate, zero-terminated. + */ +#define osmo_tdef_groups_for_each(g, tdef_groups) \ + for (g = tdef_groups; g && g->tdefs; g++) + +/*! @} */ diff --git a/include/osmocom/vty/tdef_vty.h b/include/osmocom/vty/tdef_vty.h new file mode 100644 index 00000000..f55239ad --- /dev/null +++ b/include/osmocom/vty/tdef_vty.h @@ -0,0 +1,67 @@ +/*! \file tdef_vty.h + * API to configure osmo_tdef Tnnn timers from VTY configuration. + */ +/* (C) 2018-2019 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de> + * + * Author: Neels Hofmeyr <neels@hofmeyr.de> + * + * All Rights Reserved + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +#pragma once + +#include <stdint.h> + +struct vty; + +/*! \defgroup Tdef_VTY Tnnn timer VTY configuration + * @{ + * \file tdef_vty.h + */ + +struct osmo_tdef; +struct osmo_tdef_group; + +#define OSMO_TDEF_VTY_ARG_T "TNNNN" +#define OSMO_TDEF_VTY_DOC_T "T-number, optionally preceded by 't' or 'T'.\n" +#define OSMO_TDEF_VTY_ARG_T_OPTIONAL "[" OSMO_TDEF_VTY_ARG_T "]" + +#define OSMO_TDEF_VTY_ARG_VAL "(<0-2147483647>|default)" +#define OSMO_TDEF_VTY_DOC_VAL "New timer value\n" "Set to default timer value\n" +#define OSMO_TDEF_VTY_ARG_VAL_OPTIONAL "[" OSMO_TDEF_VTY_ARG_VAL "]" + +#define OSMO_TDEF_VTY_ARG_SET OSMO_TDEF_VTY_ARG_T " " OSMO_TDEF_VTY_ARG_VAL +#define OSMO_TDEF_VTY_DOC_SET OSMO_TDEF_VTY_DOC_T OSMO_TDEF_VTY_DOC_VAL +#define OSMO_TDEF_VTY_ARG_SET_OPTIONAL OSMO_TDEF_VTY_ARG_T_OPTIONAL " " OSMO_TDEF_VTY_ARG_VAL_OPTIONAL + +int osmo_tdef_vty_set_cmd(struct vty *vty, struct osmo_tdef *tdefs, const char **args); +int osmo_tdef_vty_show_cmd(struct vty *vty, struct osmo_tdef *tdefs, const char *T_arg, + const char *prefix_fmt, ...); +void osmo_tdef_vty_write(struct vty *vty, struct osmo_tdef *tdefs, + const char *prefix_fmt, ...); + +void osmo_tdef_vty_out_one(struct vty *vty, struct osmo_tdef *t, const char *prefix_fmt, ...); +void osmo_tdef_vty_out_all(struct vty *vty, struct osmo_tdef *tdefs, const char *prefix_fmt, ...); + +void osmo_tdef_vty_out_one_va(struct vty *vty, struct osmo_tdef *t, const char *prefix_fmt, va_list va); +void osmo_tdef_vty_out_all_va(struct vty *vty, struct osmo_tdef *tdefs, const char *prefix_fmt, va_list va); + +struct osmo_tdef *osmo_tdef_vty_parse_T_arg(struct vty *vty, struct osmo_tdef *tdefs, const char *osmo_tdef_str); +unsigned long osmo_tdef_vty_parse_val_arg(const char *val_arg, unsigned long default_val); + +void osmo_tdef_vty_groups_init(enum node_type parent_node, struct osmo_tdef_group *groups); +void osmo_tdef_vty_groups_write(struct vty *vty, const char *indent); + +/*! @} */ diff --git a/src/Makefile.am b/src/Makefile.am index 6840f798..27ab7026 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -23,7 +23,8 @@ libosmocore_la_SOURCES = timer.c timer_gettimeofday.c timer_clockgettime.c \ loggingrb.c crc8gen.c crc16gen.c crc32gen.c crc64gen.c \ macaddr.c stat_item.c stats.c stats_statsd.c prim.c \ conv_acc.c conv_acc_generic.c sercomm.c prbs.c \ - isdnhdlc.c + isdnhdlc.c \ + tdef.c if HAVE_SSSE3 libosmocore_la_SOURCES += conv_acc_sse.c @@ -498,6 +498,10 @@ static int state_chg(struct osmo_fsm_inst *fi, uint32_t new_state, * timer_cb. If passing timeout_secs == 0, it is recommended to also pass T == * 0, so that fi->T is reset to 0 when no timeout is invoked. * + * See also osmo_tdef_fsm_inst_state_chg() from the osmo_tdef API, which + * provides a unified way to configure and apply GSM style Tnnnn timers to FSM + * state transitions. + * * Range: since time_t's maximum value is not well defined in a cross platform * way, clamp timeout_secs to the maximum of the signed 32bit range, or roughly * 68 years (float(0x7fffffff) / (60. * 60 * 24 * 365.25) = 68.0497). Thus diff --git a/src/tdef.c b/src/tdef.c new file mode 100644 index 00000000..7e79d680 --- /dev/null +++ b/src/tdef.c @@ -0,0 +1,282 @@ +/*! \file tdef.c + * Implementation to define Tnnn timers globally and use for FSM state changes. + */ +/* + * (C) 2018-2019 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de> + * + * All Rights Reserved + * + * SPDX-License-Identifier: GPL-2.0+ + * + * Author: Neels Hofmeyr <neels@hofmeyr.de> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <limits.h> + +#include <osmocom/core/fsm.h> +#include <osmocom/core/tdef.h> + +/*! \addtogroup Tdef + * + * Implementation to define Tnnn timers globally and use for FSM state changes. + * + * See also \ref Tdef_VTY + * + * osmo_tdef provides: + * + * - a list of Tnnnn (GSM) timers with description, unit and default value. + * - vty UI to allow users to configure non-default timeouts. + * - API to tie T timers to osmo_fsm states and set them on state transitions. + * + * - a few standard units (minute, second, millisecond) as well as a custom unit + * (which relies on the timer's human readable description to indicate the + * meaning of the value). + * - conversion for standard units: for example, some GSM timers are defined in + * minutes, while our FSM definitions need timeouts in seconds. Conversion is + * for convenience only and can be easily avoided via the custom unit. + * + * By keeping separate osmo_tdef arrays, several groups of timers can be kept + * separately. The VTY tests in tests/tdef/ showcase different schemes: + * + * - \ref tests/vty/tdef_vty_test_config_root.c: + * Keep several timer definitions in separately named groups: showcase the + * osmo_tdef_vty_groups*() API. Each timer group exists exactly once. + * + * - \ref tests/vty/tdef_vty_test_config_subnode.c: + * Keep a single list of timers without separate grouping. + * Put this list on a specific subnode below the CONFIG_NODE. + * There could be several separate subnodes with timers like this, i.e. + * continuing from this example, sets of timers could be separated by placing + * timers in specific config subnodes instead of using the global group name. + * + * - \ref tests/vty/tdef_vty_test_dynamic.c: + * Dynamically allocate timer definitions per each new created object. + * Thus there can be an arbitrary number of independent timer definitions, one + * per allocated object. + * + * osmo_tdef was introduced because: + * + * - without osmo_tdef, each invocation of osmo_fsm_inst_state_chg() needs to be + * programmed with the right timeout value, for all code paths that invoke this + * state change. It is a likely source of errors to get one of them wrong. By + * defining a T timer exactly for an FSM state, the caller can merely invoke the + * state change and trust on the original state definition to apply the correct + * timeout. + * + * - it is helpful to have a standardized config file UI to provide user + * configurable timeouts, instead of inventing new VTY commands for each + * separate application of T timer numbers. See \ref tdef_vty.h. + * + * @{ + * \file tdef.c + */ + +/*! a = return_val * b. \return 0 if factor is below 1. */ +static unsigned long osmo_tdef_factor(enum osmo_tdef_unit a, enum osmo_tdef_unit b) +{ + if (b == a + || b == OSMO_TDEF_CUSTOM || a == OSMO_TDEF_CUSTOM) + return 1; + + switch (b) { + case OSMO_TDEF_MS: + switch (a) { + case OSMO_TDEF_S: + return 1000; + case OSMO_TDEF_M: + return 60*1000; + default: + return 0; + } + case OSMO_TDEF_S: + switch (a) { + case OSMO_TDEF_M: + return 60; + default: + return 0; + } + default: + return 0; + } +} + +/*! \return val in unit to_unit, rounded up to the next integer value and clamped to ULONG_MAX, or 0 if val == 0. */ +static unsigned long osmo_tdef_round(unsigned long val, enum osmo_tdef_unit from_unit, enum osmo_tdef_unit to_unit) +{ + unsigned long f; + if (!val) + return 0; + + f = osmo_tdef_factor(from_unit, to_unit); + if (f == 1) + return val; + if (f < 1) { + f = osmo_tdef_factor(to_unit, from_unit); + return (val / f) + (val % f? 1 : 0); + } + /* range checking */ + if (f > (ULONG_MAX / val)) + return ULONG_MAX; + return val * f; +} + +/*! Set all osmo_tdef values to the default_val. + * It is convenient to define a tdefs array by setting only the default_val, and calling osmo_tdefs_reset() once for + * program startup. (See also osmo_tdef_vty_init()) + * \param[in] tdefs Array of timer definitions, last entry being fully zero. + */ +void osmo_tdefs_reset(struct osmo_tdef *tdefs) +{ + struct osmo_tdef *t; + osmo_tdef_for_each(t, tdefs) + t->val = t->default_val; +} + +/*! Return the value of a T timer from a list of osmo_tdef, in the given unit. + * If no such timer is defined, return the default value passed, or abort the program if default < 0. + * + * Round up any value match as_unit: 1100 ms as OSMO_TDEF_S becomes 2 seconds, as OSMO_TDEF_M becomes one minute. + * However, always return a value of zero as zero (0 ms as OSMO_TDEF_M still is 0 m). + * + * Range: even though the value range is unsigned long here, in practice, using ULONG_MAX as value for a timeout in + * seconds may actually wrap to negative or low timeout values (e.g. in struct timeval). It is recommended to stay below + * INT_MAX seconds. See also osmo_fsm_inst_state_chg(). + * + * Usage example: + * + * struct osmo_tdef global_T_defs[] = { + * { .T=7, .default_val=50, .desc="Water Boiling Timeout" }, // default is .unit=OSMO_TDEF_S == 0 + * { .T=8, .default_val=300, .desc="Tea brewing" }, + * { .T=9, .default_val=5, .unit=OSMO_TDEF_M, .desc="Let tea cool down before drinking" }, + * { .T=10, .default_val=20, .unit=OSMO_TDEF_M, .desc="Forgot to drink tea while it's warm" }, + * {} // <-- important! last entry shall be zero + * }; + * osmo_tdefs_reset(global_T_defs); // make all values the default + * osmo_tdef_vty_init(global_T_defs, CONFIG_NODE); + * + * val = osmo_tdef_get(global_T_defs, 7, OSMO_TDEF_S, -1); // -> 50 + * sleep(val); + * + * val = osmo_tdef_get(global_T_defs, 7, OSMO_TDEF_M, -1); // 50 seconds becomes 1 minute -> 1 + * sleep_minutes(val); + * + * val = osmo_tdef_get(global_T_defs, 99, OSMO_TDEF_S, 3); // not defined, returns 3 + * + * val = osmo_tdef_get(global_T_defs, 99, OSMO_TDEF_S, -1); // not defined, program aborts! + * + * \param[in] tdefs Array of timer definitions, last entry must be fully zero initialized. + * \param[in] T Timer number to get the value for. + * \param[in] as_unit Return timeout value in this unit. + * \param[in] val_if_not_present Fallback value to return if no timeout is defined. + * \return Timeout value in the unit given by as_unit, rounded up if necessary, or val_if_not_present. + */ +unsigned long osmo_tdef_get(const struct osmo_tdef *tdefs, int T, enum osmo_tdef_unit as_unit, unsigned long val_if_not_present) +{ + const struct osmo_tdef *t = osmo_tdef_get_entry((struct osmo_tdef*)tdefs, T); + if (!t) { + OSMO_ASSERT(val_if_not_present >= 0); + return val_if_not_present; + } + return osmo_tdef_round(t->val, t->unit, as_unit); +} + +/*! Find tdef entry matching T. + * This is useful for manipulation, which is usually limited to the VTY configuration. To retrieve a timeout value, + * most callers probably should use osmo_tdef_get() instead. + * \param[in] tdefs Array of timer definitions, last entry being fully zero. + * \param[in] T Timer number to get the entry for. + * \return osmo_tdef entry matching T in given array, or NULL if no match is found. + */ +struct osmo_tdef *osmo_tdef_get_entry(struct osmo_tdef *tdefs, int T) +{ + struct osmo_tdef *t; + osmo_tdef_for_each(t, tdefs) { + if (t->T == T) + return t; + } + return NULL; +} + +/*! Using osmo_tdef for osmo_fsm_inst: find a given state's osmo_tdef_state_timeout entry. + * + * The timeouts_array shall contain exactly 32 elements, regardless whether only some of them are actually populated + * with nonzero values. 32 corresponds to the number of states allowed by the osmo_fsm_* API. Lookup is by array index. + * Not populated entries imply a state change invocation without timeout. + * + * For example: + * + * struct osmo_tdef_state_timeout my_fsm_timeouts[32] = { + * [MY_FSM_STATE_3] = { .T = 423 }, // look up timeout configured for T423 + * [MY_FSM_STATE_7] = { .T = 235 }, + * [MY_FSM_STATE_8] = { .keep_timer = true }, // keep previous state's T number, continue timeout. + * // any state that is omitted will remain zero == no timeout + * }; + * osmo_tdef_get_state_timeout(MY_FSM_STATE_0, &my_fsm_timeouts) -> NULL, + * osmo_tdef_get_state_timeout(MY_FSM_STATE_7, &my_fsm_timeouts) -> { .T = 235 } + * + * The intention is then to obtain the timer like osmo_tdef_get(global_T_defs, T=235); see also + * fsm_inst_state_chg_T() below. + * + * \param[in] state State constant to look up. + * \param[in] timeouts_array Array[32] of struct osmo_tdef_state_timeout defining which timer number to use per state. + * \return A struct osmo_tdef_state_timeout entry, or NULL if that entry is zero initialized. + */ +const struct osmo_tdef_state_timeout *osmo_tdef_get_state_timeout(uint32_t state, const struct osmo_tdef_state_timeout *timeouts_array) +{ + const struct osmo_tdef_state_timeout *t; + OSMO_ASSERT(state < 32); + t = &timeouts_array[state]; + if (!t->keep_timer && !t->T) + return NULL; + return t; +} + +/*! See invocation macro osmo_tdef_fsm_inst_state_chg() instead. + * \param[in] file Source file name, like __FILE__. + * \param[in] line Source file line number, like __LINE__. + */ +int _osmo_tdef_fsm_inst_state_chg(struct osmo_fsm_inst *fi, uint32_t state, + const struct osmo_tdef_state_timeout *timeouts_array, + const struct osmo_tdef *tdefs, unsigned long default_timeout, + const char *file, int line) +{ + const struct osmo_tdef_state_timeout *t = osmo_tdef_get_state_timeout(state, timeouts_array); + unsigned long val; + + /* No timeout defined for this state? */ + if (!t) + return _osmo_fsm_inst_state_chg(fi, state, 0, 0, file, line); + + if (t->keep_timer) { + int rc = _osmo_fsm_inst_state_chg_keep_timer(fi, state, file, line); + if (t->T && !rc) + fi->T = t->T; + return rc; + } + + val = osmo_tdef_get(tdefs, t->T, OSMO_TDEF_S, default_timeout); + return _osmo_fsm_inst_state_chg(fi, state, val, t->T, file, line); +} + +const struct value_string osmo_tdef_unit_names[] = { + { OSMO_TDEF_S, "s" }, + { OSMO_TDEF_MS, "ms" }, + { OSMO_TDEF_M, "m" }, + { OSMO_TDEF_CUSTOM, "custom-unit" }, + {} +}; + +/*! @} */ diff --git a/src/vty/Makefile.am b/src/vty/Makefile.am index 2e494982..cdde0fa5 100644 --- a/src/vty/Makefile.am +++ b/src/vty/Makefile.am @@ -11,7 +11,8 @@ lib_LTLIBRARIES = libosmovty.la libosmovty_la_SOURCES = buffer.c command.c vty.c vector.c utils.c \ telnet_interface.c logging_vty.c stats_vty.c \ - fsm_vty.c talloc_ctx_vty.c + fsm_vty.c talloc_ctx_vty.c \ + tdef_vty.c libosmovty_la_LDFLAGS = -version-info $(LIBVERSION) -no-undefined libosmovty_la_LIBADD = $(top_builddir)/src/libosmocore.la $(TALLOC_LIBS) endif diff --git a/src/vty/tdef_vty.c b/src/vty/tdef_vty.c new file mode 100644 index 00000000..1c6af70a --- /dev/null +++ b/src/vty/tdef_vty.c @@ -0,0 +1,372 @@ +/*! \file tdef_vty.c + * Implementation to configure osmo_tdef Tnnn timers from VTY configuration. + */ +/* (C) 2018-2019 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de> + * + * Author: Neels Hofmeyr <neels@hofmeyr.de> + * + * All Rights Reserved + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <limits.h> + +#include <osmocom/vty/vty.h> +#include <osmocom/vty/command.h> +#include <osmocom/vty/tdef_vty.h> +#include <osmocom/core/tdef.h> + +/*! \addtogroup Tdef_VTY + * + * VTY API for \ref Tdef. + * + * @{ + * \file tdef_vty.c + */ + +/*! Parse an argument like "T1234", "t1234" or "1234", as from OSMO_TDEF_VTY_ARG_T. + * \param[in] vty VTY context for vty_out() of error messages. + * \param[in] tdefs Array of timer definitions to look up T timer. + * \param[in] T_str Argument string. It is not validated, expected to be checked by VTY input. + * \return the corresponding osmo_tdef entry from the tdefs array, or NULL if no such entry exists. + */ +struct osmo_tdef *osmo_tdef_vty_parse_T_arg(struct vty *vty, struct osmo_tdef *tdefs, const char *T_str) +{ + long l; + int T; + struct osmo_tdef *t; + char *endptr; + const char *T_nr_str; + + if (!tdefs) { + vty_out(vty, "%% Error: no timers found%s", VTY_NEWLINE); + return NULL; + } + + T_nr_str = T_str; + if (T_nr_str[0] == 't' || T_nr_str[0] == 'T') + T_nr_str++; + + errno = 0; + l = strtol(T_nr_str, &endptr, 10); + if (errno || *endptr || l > INT_MAX) { + vty_out(vty, "%% No such timer: '%s'%s", T_str, VTY_NEWLINE); + return NULL; + } + T = l; + + t = osmo_tdef_get_entry(tdefs, T); + if (!t) + vty_out(vty, "%% No such timer: T%d%s", T, VTY_NEWLINE); + return t; +} + +/*! Parse an argument of the form "(0-2147483647|default)", as from OSMO_TDEF_VTY_ARG_VAL. + * \param[in] val_arg Argument string (not format checked). + * \param[in] default_val Value to return in case of val_arg being "default". + * \return Parsed value or default_val. + */ +unsigned long osmo_tdef_vty_parse_val_arg(const char *val_arg, unsigned long default_val) +{ + if (!strcmp(val_arg, "default")) + return default_val; + return atoll(val_arg); +} + +/*! Apply a timer configuration from VTY argument strings. + * Employ both osmo_tdef_vty_parse_T_arg() and osmo_tdef_vty_parse_val_arg() to configure a T timer in an array of + * tdefs. Evaluate two arguments, a "T1234" argument and a "(0-2147483647|default)" argument, as from + * OSMO_TDEF_VTY_ARGS. If the T timer given in the first argument is found in tdefs, set it to the value given in the + * second argument. + * \param[in] vty VTY context for vty_out() of error messages. + * \param[in] tdefs Array of timer definitions to look up T timer. + * \param[in] args Array of string arguments like { "T1234", "23" }. + * \return CMD_SUCCESS, or CMD_WARNING if no such timer is found in tdefs. + */ +int osmo_tdef_vty_set_cmd(struct vty *vty, struct osmo_tdef *tdefs, const char **args) +{ + const char *T_arg = args[0]; + const char *val_arg = args[1]; + struct osmo_tdef *t = osmo_tdef_vty_parse_T_arg(vty, tdefs, T_arg); + if (!t) + return CMD_WARNING; + t->val = osmo_tdef_vty_parse_val_arg(val_arg, t->default_val); + return CMD_SUCCESS; +} + +/*! Output one or all timers to the VTY, as for a VTY command like 'show timer [TNNNN]'. + * If T_arg is NULL, print all timers in tdefs to the VTY. + * If T_arg is not NULL, employ osmo_tdef_vty_parse_T_arg() to select one timer from tdefs and print only that to the + * VTY. + * \param[in] vty VTY context for vty_out() of error messages. + * \param[in] tdefs Array of timer definitions. + * \param[in] T_arg Argument string like "T1234", or NULL. + * \param[in] prefix_fmt Arbitrary string to start each line with, with variable printf like arguments. + * \return CMD_SUCCESS, or CMD_WARNING if no such timer is found in tdefs. + */ +int osmo_tdef_vty_show_cmd(struct vty *vty, struct osmo_tdef *tdefs, const char *T_arg, + const char *prefix_fmt, ...) +{ + va_list va; + if (T_arg) { + struct osmo_tdef *t = osmo_tdef_vty_parse_T_arg(vty, tdefs, T_arg); + if (!t) + return CMD_WARNING; + va_start(va, prefix_fmt); + osmo_tdef_vty_out_one_va(vty, t, prefix_fmt, va); + va_end(va); + } else { + va_start(va, prefix_fmt); + osmo_tdef_vty_out_all_va(vty, tdefs, prefix_fmt, va); + va_end(va); + } + return CMD_SUCCESS; +} + +/*! Write to VTY the current status of one timer. + * \param[in] vty VTY context for vty_out(). + * \param[in] t The timer to print. + * \param[in] prefix_fmt Arbitrary string to start each line with, with variable vprintf like arguments. + * \param[in] va va_list instance. As always, call va_start() before, and va_end() after this call. + */ +void osmo_tdef_vty_out_one_va(struct vty *vty, struct osmo_tdef *t, const char *prefix_fmt, va_list va) +{ + if (!t) { + vty_out(vty, "%% Error: no such timer%s", VTY_NEWLINE); + return; + } + if (prefix_fmt) + vty_out_va(vty, prefix_fmt, va); + vty_out(vty, "T%d = %lu%s%s\t%s (default: %lu%s%s)%s", + t->T, t->val, + t->unit == OSMO_TDEF_CUSTOM ? "" : " ", t->unit == OSMO_TDEF_CUSTOM ? "" : osmo_tdef_unit_name(t->unit), + t->desc, t->default_val, + t->unit == OSMO_TDEF_CUSTOM ? "" : " ", t->unit == OSMO_TDEF_CUSTOM ? "" : osmo_tdef_unit_name(t->unit), + VTY_NEWLINE); +} + +/*! Write to VTY the current status of one timer. + * \param[in] vty VTY context for vty_out(). + * \param[in] t The timer to print. + * \param[in] prefix_fmt Arbitrary string to start each line with, with variable printf like arguments. + */ +void osmo_tdef_vty_out_one(struct vty *vty, struct osmo_tdef *t, const char *prefix_fmt, ...) +{ + va_list va; + va_start(va, prefix_fmt); + osmo_tdef_vty_out_one_va(vty, t, prefix_fmt, va); + va_end(va); +} + +/*! Write to VTY the current status of all given timers. + * \param[in] vty VTY context for vty_out(). + * \param[in] tdefs Array of timers to print, ended with a fully zero-initialized entry. + * \param[in] prefix_fmt Arbitrary string to start each line with, with variable vprintf like arguments. + * \param[in] va va_list instance. As always, call va_start() before, and va_end() after this call. + */ +void osmo_tdef_vty_out_all_va(struct vty *vty, struct osmo_tdef *tdefs, const char *prefix_fmt, va_list va) +{ + struct osmo_tdef *t; + if (!tdefs) { + vty_out(vty, "%% Error: no such timers%s", VTY_NEWLINE); + return; + } + osmo_tdef_for_each(t, tdefs) { + va_list va2; + va_copy(va2, va); + osmo_tdef_vty_out_one_va(vty, t, prefix_fmt, va); + va_end(va2); + } +} + +/*! Write to VTY the current status of all given timers. + * \param[in] vty VTY context for vty_out(). + * \param[in] tdefs Array of timers to print, ended with a fully zero-initialized entry. + * \param[in] prefix_fmt Arbitrary string to start each line with, with variable printf like arguments. + */ +void osmo_tdef_vty_out_all(struct vty *vty, struct osmo_tdef *tdefs, const char *prefix_fmt, ...) +{ + va_list va; + va_start(va, prefix_fmt); + osmo_tdef_vty_out_all_va(vty, tdefs, prefix_fmt, va); + va_end(va); +} + +/*! Write current timer configuration arguments to the vty. Skip all entries that reflect their default value. + * The passed prefix string must contain both necessary indent and the VTY command the specific implementation is using. + * See tdef_vty_test_config_subnode.c and tdef_vty_test_dynamic.c for examples. + * \param[in] vty VTY context. + * \param[in] tdefs Array of timers to print, ended with a fully zero-initialized entry. + * \param[in] prefix_fmt Arbitrary string to start each line with, with variable printf like arguments. + */ +void osmo_tdef_vty_write(struct vty *vty, struct osmo_tdef *tdefs, const char *prefix_fmt, ...) +{ + va_list va; + struct osmo_tdef *t; + osmo_tdef_for_each(t, tdefs) { + if (t->val == t->default_val) + continue; + if (prefix_fmt && *prefix_fmt) { + va_start(va, prefix_fmt); + vty_out_va(vty, prefix_fmt, va); |