aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--include/Makefile.am2
-rw-r--r--include/osmocom/core/tdef.h172
-rw-r--r--include/osmocom/vty/tdef_vty.h67
-rw-r--r--src/Makefile.am3
-rw-r--r--src/fsm.c4
-rw-r--r--src/tdef.c282
-rw-r--r--src/vty/Makefile.am3
-rw-r--r--src/vty/tdef_vty.c372
-rw-r--r--tests/Makefile.am36
-rw-r--r--tests/tdef/tdef_test.c445
-rw-r--r--tests/tdef/tdef_test.ok184
-rw-r--r--tests/tdef/tdef_vty_test_config_root.c292
-rw-r--r--tests/tdef/tdef_vty_test_config_root.vty292
-rw-r--r--tests/tdef/tdef_vty_test_config_subnode.c288
-rw-r--r--tests/tdef/tdef_vty_test_config_subnode.vty107
-rw-r--r--tests/tdef/tdef_vty_test_dynamic.c362
-rw-r--r--tests/tdef/tdef_vty_test_dynamic.vty83
-rw-r--r--tests/testsuite.at6
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
diff --git a/src/fsm.c b/src/fsm.c
index 0d31f872..6e15ab70 100644
--- a/src/fsm.c
+++ b/src/fsm.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);
+ va_end(va);
+ }
+ vty_out(vty, "T%d %lu%s", t->T, t->val, VTY_NEWLINE);
+ }
+}
+
+/*! Singleton Tnnn groups definition as set by osmo_tdef_vty_groups_init(). */
+static struct osmo_tdef_group *global_tdef_groups;
+
+/*! \return true iff the first characters of str fully match startswith_str or both are empty. */
+static bool startswith(const char *str, const char *startswith_str)
+{
+ if (!startswith_str)
+ return true;
+ if (!str)
+ return false;
+ return strncmp(str, startswith_str, strlen(startswith_str)) == 0;
+}
+
+DEFUN(show_timer, show_timer_cmd, "DYNAMIC", "DYNAMIC")
+ /* show timer [(alpha|beta|gamma)] [TNNNN] */
+{
+ const char *group_arg = argc > 0 ? argv[0] : NULL;
+ const char *T_arg = argc > 1 ? argv[1] : NULL;
+ struct osmo_tdef_group *g;
+
+ /* The argument should be either "tea" or "software", but the VTY also allows partial arguments
+ * like "softw" or "t" (which can also be ambiguous). */
+
+ osmo_tdef_groups_for_each(g, global_tdef_groups) {
+ if (!group_arg || startswith(g->name, group_arg))
+ osmo_tdef_vty_show_cmd(vty, g->tdefs, T_arg, "%s: ", g->name);
+ }
+ return CMD_SUCCESS;
+}
+
+DEFUN(cfg_timer, cfg_timer_cmd, "DYNAMIC", "DYNAMIC")
+ /* show timer [(alpha|beta|gamma)] [TNNNN] [(<0-2147483647>|default)] */
+{
+ const char *group_arg;
+ const char **timer_args;
+ struct osmo_tdef *tdefs = NULL;
+ struct osmo_tdef_group *g = NULL;
+
+ /* If any arguments are missing, redirect to 'show' */
+ if (argc < 3)
+ return show_timer(self, vty, argc, argv);
+
+ /* If all arguments are passed, this is configuring a timer. */
+ group_arg = argc > 0 ? argv[0] : NULL;
+ timer_args = argv + 1;
+ osmo_tdef_groups_for_each(g, global_tdef_groups) {
+ if (strcmp(g->name, group_arg))
+ continue;
+ if (tdefs) {
+ vty_out(vty, "%% Error: ambiguous timer group match%s", VTY_NEWLINE);
+ return CMD_WARNING;
+ }
+ tdefs = g->tdefs;
+ }
+
+ return osmo_tdef_vty_set_cmd(vty, tdefs, timer_args);
+}
+
+static char *add_group_args(void *talloc_ctx, char *dest)
+{
+ struct osmo_tdef_group *g;
+ osmo_talloc_asprintf(talloc_ctx, dest, "[(");
+ osmo_tdef_groups_for_each(g, global_tdef_groups) {
+ osmo_talloc_asprintf(talloc_ctx, dest, "%s%s",
+ (g == global_tdef_groups) ? "" : "|",
+ g->name);
+ }
+ osmo_talloc_asprintf(talloc_ctx, dest, ")]");
+ return dest;
+}
+
+static char *add_group_docs(void *talloc_ctx, char *dest)
+{
+ struct osmo_tdef_group *g;
+ osmo_tdef_groups_for_each(g, global_tdef_groups) {
+ osmo_talloc_asprintf(talloc_ctx, dest, "%s\n", g->desc);
+ }
+ return dest;
+}
+
+static char *timer_command_string(const char *prefix, const char *suffix)
+{
+ char *dest = NULL;
+ osmo_talloc_asprintf(tall_vty_cmd_ctx, dest, "%s ", prefix);
+ dest = add_group_args(tall_vty_cmd_ctx, dest);
+ osmo_talloc_asprintf(tall_vty_cmd_ctx, dest, " %s", suffix);
+ return dest;
+}
+
+static char *timer_doc_string(const char *prefix, const char *suffix)
+{
+ char *dest = NULL;
+ osmo_talloc_asprintf(tall_vty_cmd_ctx, dest, "%s ", prefix);
+ dest = add_group_docs(tall_vty_cmd_ctx, dest);
+ osmo_talloc_asprintf(tall_vty_cmd_ctx, dest, " %s", suffix);
+ return dest;
+}
+
+/*! Convenience implementation for keeping a fixed set of timer groups in a program.
+ * Install a 'timer [(group|names|...)] [TNNN] [(<val>|default)]' command under the given parent_node,
+ * and install a 'show timer...' command on VIEW_NODE and ENABLE_NODE.
+ * For a usage example, see \ref tdef_test_config_root.c.
+ * The given timer definitions group is stored in a global pointer, so this can be done only once per main() scope.
+ * It would also be possible to have distinct timer groups on separate VTY subnodes, with a "manual" implementation, but
+ * not with this API.
+ * \param[in] parent_node VTY node id at which to add the timer group commands, e.g. CONFIG_NODE.
+ * \param[in] groups Global timer groups definition.
+ */
+void osmo_tdef_vty_groups_init(enum node_type parent_node, struct osmo_tdef_group *groups)
+{
+ struct osmo_tdef_group *g;
+ OSMO_ASSERT(!global_tdef_groups);
+ global_tdef_groups = groups;
+
+ osmo_tdef_groups_for_each(g, global_tdef_groups)
+ osmo_tdefs_reset(g->tdefs);
+
+ show_timer_cmd.string = timer_command_string("show timer", OSMO_TDEF_VTY_ARG_T_OPTIONAL);
+ show_timer_cmd.doc = timer_doc_string(SHOW_STR "Show timers\n", OSMO_TDEF_VTY_DOC_T);
+
+ cfg_timer_cmd.string = timer_command_string("timer", OSMO_TDEF_VTY_ARG_SET_OPTIONAL);
+ cfg_timer_cmd.doc = timer_doc_string("Configure or show timers\n", OSMO_TDEF_VTY_DOC_SET);
+
+ install_element_ve(&show_timer_cmd);
+ install_element(parent_node, &cfg_timer_cmd);
+}
+
+/*! Write the global osmo_tdef_group configuration to VTY, as previously passed to osmo_tdef_vty_groups_init().
+ * \param[in] vty VTY context.
+ * \param[in] indent String to print before each line.
+ */
+void osmo_tdef_vty_groups_write(struct vty *vty, const char *indent)
+{
+ struct osmo_tdef_group *g;
+ osmo_tdef_groups_for_each(g, global_tdef_groups)
+ osmo_tdef_vty_write(vty, g->tdefs, "%stimer %s ", indent ? : "", g->name);
+}
+
+/*! @} */
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 91f042e8..54fb11f0 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -27,6 +27,9 @@ check_PROGRAMS = timer/timer_test sms/sms_test ussd/ussd_test \
oap/oap_client_test gsm29205/gsm29205_test \
logging/logging_vty_test \
vty/vty_transcript_test \
+ tdef/tdef_test tdef/tdef_vty_test_config_root \
+ tdef/tdef_vty_test_config_subnode \
+ tdef/tdef_vty_test_dynamic \
$(NULL)
if ENABLE_MSGFILE
@@ -221,6 +224,18 @@ prbs_prbs_test_SOURCES = prbs/prbs_test.c
gsm23003_gsm23003_test_SOURCES = gsm23003/gsm23003_test.c
gsm23003_gsm23003_test_LDADD = $(LDADD) $(top_builddir)/src/gsm/libosmogsm.la
+tdef_tdef_test_SOURCES = tdef/tdef_test.c
+tdef_tdef_test_LDADD = $(LDADD)
+
+tdef_tdef_vty_test_config_root_SOURCES = tdef/tdef_vty_test_config_root.c
+tdef_tdef_vty_test_config_root_LDADD = $(LDADD) $(top_builddir)/src/vty/libosmovty.la
+
+tdef_tdef_vty_test_config_subnode_SOURCES = tdef/tdef_vty_test_config_subnode.c
+tdef_tdef_vty_test_config_subnode_LDADD = $(LDADD) $(top_builddir)/src/vty/libosmovty.la
+
+tdef_tdef_vty_test_dynamic_SOURCES = tdef/tdef_vty_test_dynamic.c
+tdef_tdef_vty_test_dynamic_LDADD = $(LDADD) $(top_builddir)/src/vty/libosmovty.la
+
# The `:;' works around a Bash 3.2 bug when the output is not writeable.
$(srcdir)/package.m4: $(top_srcdir)/configure.ac
:;{ \
@@ -284,6 +299,10 @@ EXTRA_DIST = testsuite.at $(srcdir)/package.m4 $(TESTSUITE) \
timer/clk_override_test.ok \
oap/oap_client_test.ok oap/oap_client_test.err \
vty/vty_transcript_test.vty \
+ tdef/tdef_test.ok \
+ tdef/tdef_vty_test_config_root.vty \
+ tdef/tdef_vty_test_config_subnode.vty \
+ tdef/tdef_vty_test_dynamic.vty \
$(NULL)
DISTCLEANFILES = atconfig atlocal conv/gsm0503_test_vectors.c
@@ -328,6 +347,7 @@ endif
# To update the VTY script from current application behavior,
# pass -u to osmo_verify_transcript_vty.py by doing:
# make vty-test U=-u
+
vty-test-logging:
osmo_verify_transcript_vty.py -v \
-p 42042 \
@@ -340,9 +360,25 @@ vty-test-vty:
-r "$(top_builddir)/tests/vty/vty_transcript_test" \
$(U) $(srcdir)/vty/*.vty
+vty-test-tdef:
+ osmo_verify_transcript_vty.py -v \
+ -p 42042 \
+ -r "$(top_builddir)/tests/tdef/tdef_vty_test_config_root" \
+ $(U) $(srcdir)/tdef/tdef_vty_test_config_root.vty
+ osmo_verify_transcript_vty.py -v \
+ -p 42042 \
+ -r "$(top_builddir)/tests/tdef/tdef_vty_test_config_subnode" \
+ $(U) $(srcdir)/tdef/tdef_vty_test_config_subnode.vty
+ osmo_verify_transcript_vty.py -v \
+ -p 42042 \
+ -r "$(top_builddir)/tests/tdef/tdef_vty_test_dynamic" \
+ $(U) $(srcdir)/tdef/tdef_vty_test_dynamic.vty
+
+# don't run vty tests concurrently so that the ports don't conflict
vty-test:
$(MAKE) vty-test-logging
$(MAKE) vty-test-vty
+ $(MAKE) vty-test-tdef
ctrl-test:
echo "No CTRL tests exist currently"
diff --git a/tests/tdef/tdef_test.c b/tests/tdef/tdef_test.c
new file mode 100644
index 00000000..682c7ac7
--- /dev/null
+++ b/tests/tdef/tdef_test.c
@@ -0,0 +1,445 @@
+/* Test implementation for osmo_tdef API. */
+/*
+ * (C) 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 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <stdio.h>
+#include <errno.h>
+#include <limits.h>
+
+#include <osmocom/core/logging.h>
+#include <osmocom/core/application.h>
+#include <osmocom/core/fsm.h>
+
+#include <osmocom/core/tdef.h>
+
+static void *ctx = NULL;
+
+static struct osmo_tdef tdefs[] = {
+ { .T=1, .default_val=100, .desc="100s" },
+ { .T=2, .default_val=100, .unit=OSMO_TDEF_MS, .desc="100ms" },
+ { .T=3, .default_val=100, .unit=OSMO_TDEF_M, .desc="100m" },
+ { .T=4, .default_val=100, .unit=OSMO_TDEF_CUSTOM, .desc="100 potatoes" },
+
+ { .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" },
+
+ /* test conversions */
+ { .T=1000, .default_val=2*1000, .unit=OSMO_TDEF_MS, .desc="two seconds from ms" },
+ { .T=1001, .default_val=60*1000, .unit=OSMO_TDEF_MS, .desc="one minute from ms" },
+ { .T=1002, .default_val=(ULONG_MAX/60), .unit=OSMO_TDEF_M, .desc="almost too many seconds" },
+ { .T=1003, .default_val=ULONG_MAX, .unit=OSMO_TDEF_M, .desc="too many seconds" },
+ { .T=1004, .default_val=1, .unit=OSMO_TDEF_MS, .desc="one ms" },
+ { .T=1005, .default_val=0, .unit=OSMO_TDEF_MS, .desc="zero ms" },
+ { .T=1006, .default_val=0, .unit=OSMO_TDEF_S, .desc="zero s" },
+ { .T=1007, .default_val=0, .unit=OSMO_TDEF_M, .desc="zero m" },
+ { .T=1008, .default_val=0, .unit=OSMO_TDEF_CUSTOM, .desc="zero" },
+
+ /* test range */
+ { .T=INT_MAX, .default_val=ULONG_MAX, .unit=OSMO_TDEF_S, .desc="very large" },
+ { .T=INT_MAX-1, .default_val=ULONG_MAX-1, .unit=OSMO_TDEF_S, .desc="very large" },
+ { .T=INT_MAX-2, .default_val=LONG_MAX, .unit=OSMO_TDEF_S, .desc="very large" },
+ { .T=INT_MAX-3, .default_val=ULONG_MAX, .unit=OSMO_TDEF_M, .desc="very large in minutes" },
+ { .T=INT_MIN, .default_val=ULONG_MAX, .unit=OSMO_TDEF_S, .desc="negative" },
+
+ { .T=0, .default_val=1, .unit=OSMO_TDEF_CUSTOM, .desc="zero" },
+
+ /* no desc */
+ { .T=123, .default_val=1 },
+
+ {} // <-- important! last entry shall be zero
+};
+
+#define print_tdef_get(T, AS_UNIT) do { \
+ unsigned long val = osmo_tdef_get(tdefs, T, AS_UNIT, 999); \
+ printf("osmo_tdef_get(tdefs, %d, %s, 999)\t= %lu\n", T, osmo_tdef_unit_name(AS_UNIT), val); \
+ } while (0)
+
+#define print_tdef_get_short(T, AS_UNIT) do { \
+ unsigned long val = osmo_tdef_get(tdefs, T, AS_UNIT, 999); \
+ printf("osmo_tdef_get(%d, %s)\t= %lu\n", T, osmo_tdef_unit_name(AS_UNIT), val); \
+ } while (0)
+
+void print_tdef_info(unsigned int T)
+{
+ const struct osmo_tdef *t = osmo_tdef_get_entry(tdefs, T);
+ if (!t) {
+ printf("T%d=NULL", T);
+ return;
+ }
+ printf("T%d=%lu%s", T, t->val, osmo_tdef_unit_name(t->unit));
+ if (t->val != t->default_val)
+ printf("(def=%lu)", t->default_val);
+ printf("\n");
+}
+
+static void test_tdef_get()
+{
+ int i;
+ enum osmo_tdef_unit as_unit;
+
+ printf("\n%s()\n", __func__);
+
+ osmo_tdefs_reset(tdefs); // make all values the default
+
+ for (i = 0; i < ARRAY_SIZE(tdefs)-1; i++) {
+ unsigned int T = tdefs[i].T;
+ print_tdef_info(T);
+ for (as_unit = OSMO_TDEF_S; as_unit <= OSMO_TDEF_CUSTOM; as_unit++) {
+ print_tdef_get_short(T, as_unit);
+ }
+ }
+}
+
+static void test_tdef_get_nonexisting()
+{
+ printf("\n%s()\n", __func__);
+
+ print_tdef_get(5, OSMO_TDEF_S);
+ print_tdef_get(5, OSMO_TDEF_MS);
+ print_tdef_get(5, OSMO_TDEF_M);
+ print_tdef_get(5, OSMO_TDEF_CUSTOM);
+}
+
+static void test_tdef_set_and_get()
+{
+ struct osmo_tdef *t;
+ printf("\n%s()\n", __func__);
+
+ t = osmo_tdef_get_entry(tdefs, 7);
+ printf("setting 7 = 42\n");
+ t->val = 42;
+ print_tdef_info(7);
+ print_tdef_get_short(7, OSMO_TDEF_MS);
+ print_tdef_get_short(7, OSMO_TDEF_S);
+ print_tdef_get_short(7, OSMO_TDEF_M);
+ print_tdef_get_short(7, OSMO_TDEF_CUSTOM);
+
+ printf("setting 7 = 420\n");
+ t->val = 420;
+ print_tdef_info(7);
+ print_tdef_get_short(7, OSMO_TDEF_MS);
+ print_tdef_get_short(7, OSMO_TDEF_S);
+ print_tdef_get_short(7, OSMO_TDEF_M);
+ print_tdef_get_short(7, OSMO_TDEF_CUSTOM);
+
+ printf("resetting\n");
+ osmo_tdefs_reset(tdefs);
+ print_tdef_info(7);
+ print_tdef_get_short(7, OSMO_TDEF_S);
+}
+
+enum test_tdef_fsm_states {
+ S_A = 0,
+ S_B,
+ S_C,
+ S_D,
+ S_G,
+ S_H,
+ S_I,
+ S_J,
+ S_K,
+ S_L,
+ S_M,
+ S_N,
+ S_O,
+ S_X,
+ S_Y,
+ S_Z,
+};
+
+static const struct osmo_tdef_state_timeout test_tdef_state_timeouts[32] = {
+ [S_A] = { .T = 1 },
+ [S_B] = { .T = 2 },
+ [S_C] = { .T = 3 },
+ [S_D] = { .T = 4 },
+
+ [S_G] = { .T = 7 },
+ [S_H] = { .T = 8 },
+ [S_I] = { .T = 9 },
+ [S_J] = { .T = 10 },
+
+ /* keep_timer: adopt whichever T was running before and continue the timeout. */
+ [S_K] = { .keep_timer = true },
+ /* S_F defines an undefined T, but should continue previous state's timeout. */
+ [S_L] = { .T = 123, .keep_timer = true },
+
+ /* range */
+ [S_M] = { .T = INT_MAX },
+ [S_N] = { .T = INT_MIN },
+
+ /* T0 is not addressable from osmo_tdef_state_timeout, since it is indistinguishable from an unset entry. Even
+ * though a timeout value is set for T=0, the transition to state S_O will show "no timer configured". */
+ [S_O] = { .T = 0 },
+
+ /* S_X undefined on purpose */
+ /* S_Y defines a T that does not exist */
+ [S_Y] = { .T = 666 },
+ /* S_Z undefined on purpose */
+};
+
+#define S(x) (1 << (x))
+
+static const struct osmo_fsm_state test_tdef_fsm_states[] = {
+#define DEF_STATE(NAME) \
+ [S_##NAME] = { \
+ .name = #NAME, \
+ .out_state_mask = 0 \
+ | S(S_A) \
+ | S(S_B) \
+ | S(S_C) \
+ | S(S_D) \
+ | S(S_G) \
+ | S(S_H) \
+ | S(S_I) \
+ | S(S_J) \
+ | S(S_K) \
+ | S(S_L) \
+ | S(S_M) \
+ | S(S_N) \
+ | S(S_O) \
+ | S(S_X) \
+ | S(S_Y) \
+ | S(S_Z) \
+ , \
+ }
+
+ DEF_STATE(A),
+ DEF_STATE(B),
+ DEF_STATE(C),
+ DEF_STATE(D),
+
+ DEF_STATE(G),
+ DEF_STATE(H),
+ DEF_STATE(I),
+ DEF_STATE(J),
+
+ DEF_STATE(K),
+ DEF_STATE(L),
+
+ DEF_STATE(M),
+ DEF_STATE(N),
+ DEF_STATE(O),
+
+ DEF_STATE(X),
+ DEF_STATE(Y),
+ /* Z: test not being allowed to transition to other states. */
+ [S_Z] = {
+ .name = "Z",
+ .out_state_mask = 0
+ | S(S_A)
+ ,
+ },
+};
+
+static const struct value_string test_tdef_fsm_event_names[] = { {} };
+
+static struct osmo_fsm test_tdef_fsm = {
+ .name = "tdef_test",
+ .states = test_tdef_fsm_states,
+ .event_names = test_tdef_fsm_event_names,
+ .num_states = ARRAY_SIZE(test_tdef_fsm_states),
+ .log_subsys = DLGLOBAL,
+};
+
+const struct timeval fake_time_start_time = { 123, 456 };
+
+#define fake_time_passes(secs, usecs) do \
+{ \
+ struct timeval diff; \
+ osmo_gettimeofday_override_add(secs, usecs); \
+ osmo_clock_override_add(CLOCK_MONOTONIC, secs, usecs * 1000); \
+ timersub(&osmo_gettimeofday_override_time, &fake_time_start_time, &diff); \
+ printf("Total time passed: %ld.%06ld s\n", diff.tv_sec, diff.tv_usec); \
+ osmo_timers_prepare(); \
+ osmo_timers_update(); \
+} while (0)
+
+void fake_time_start()
+{
+ struct timespec *clock_override;
+
+ osmo_gettimeofday_override_time = fake_time_start_time;
+ osmo_gettimeofday_override = true;
+ clock_override = osmo_clock_override_gettimespec(CLOCK_MONOTONIC);
+ OSMO_ASSERT(clock_override);
+ clock_override->tv_sec = fake_time_start_time.tv_sec;
+ clock_override->tv_nsec = fake_time_start_time.tv_usec * 1000;
+ osmo_clock_override_enable(CLOCK_MONOTONIC, true);
+ fake_time_passes(0, 0);
+}
+
+static void print_fsm_state(struct osmo_fsm_inst *fi)
+{
+ struct timeval remaining;
+ printf("state=%s T=%d", osmo_fsm_inst_state_name(fi), fi->T);
+
+ if (!osmo_timer_pending(&fi->timer)) {
+ printf(", no timeout\n");
+ return;
+ }
+
+ osmo_timer_remaining(&fi->timer, &osmo_gettimeofday_override_time, &remaining);
+ printf(", %lu.%06lu s remaining\n", remaining.tv_sec, remaining.tv_usec);
+}
+
+
+#define test_tdef_fsm_state_chg(NEXT_STATE) do { \
+ const struct osmo_tdef_state_timeout *st = osmo_tdef_get_state_timeout(NEXT_STATE, \
+ test_tdef_state_timeouts); \
+ if (!st) { \
+ printf(" --> %s (no timer configured for this state)\n", \
+ osmo_fsm_state_name(&test_tdef_fsm, NEXT_STATE)); \
+ } else { \
+ struct osmo_tdef *t = osmo_tdef_get_entry(tdefs, st->T); \
+ int rc = osmo_tdef_fsm_inst_state_chg(fi, NEXT_STATE, test_tdef_state_timeouts, tdefs, 999); \
+ printf(" --> %s (configured as T%d%s %lu %s) rc=%d;\t", osmo_fsm_state_name(&test_tdef_fsm, \
+ NEXT_STATE), \
+ st->T, st->keep_timer ? "(keep_timer)" : "", \
+ t? t->val : -1, t? osmo_tdef_unit_name(t->unit) : "-", \
+ rc); \
+ print_fsm_state(fi); \
+ } \
+ } while(0)
+
+
+
+static void test_tdef_state_timeout(bool test_range)
+{
+ struct osmo_fsm_inst *fi;
+ struct osmo_tdef *m = osmo_tdef_get_entry(tdefs, INT_MAX);
+ unsigned long m_secs;
+ printf("\n%s()\n", __func__);
+
+ osmo_tdefs_reset(tdefs);
+
+ fake_time_start();
+
+ fi = osmo_fsm_inst_alloc(&test_tdef_fsm, ctx, NULL, LOGL_DEBUG, __func__);
+ OSMO_ASSERT(fi);
+ print_fsm_state(fi);
+
+ test_tdef_fsm_state_chg(S_A);
+ test_tdef_fsm_state_chg(S_B);
+ test_tdef_fsm_state_chg(S_C);
+ test_tdef_fsm_state_chg(S_D);
+
+ test_tdef_fsm_state_chg(S_G);
+ test_tdef_fsm_state_chg(S_H);
+ test_tdef_fsm_state_chg(S_I);
+ test_tdef_fsm_state_chg(S_J);
+
+ printf("- test keep_timer:\n");
+ fake_time_passes(123, 45678);
+ print_fsm_state(fi);
+ test_tdef_fsm_state_chg(S_K);
+ test_tdef_fsm_state_chg(S_A);
+ fake_time_passes(23, 45678);
+ print_fsm_state(fi);
+ test_tdef_fsm_state_chg(S_K);
+
+ test_tdef_fsm_state_chg(S_A);
+ fake_time_passes(23, 45678);
+ print_fsm_state(fi);
+ test_tdef_fsm_state_chg(S_L);
+
+ printf("- test large T:\n");
+ test_tdef_fsm_state_chg(S_M);
+
+ printf("- test T<0:\n");
+ test_tdef_fsm_state_chg(S_N);
+
+ printf("- test T=0:\n");
+ test_tdef_fsm_state_chg(S_O);
+
+ printf("- test no timer:\n");
+ test_tdef_fsm_state_chg(S_X);
+
+ printf("- test undefined timer, using default_val arg of osmo_tdef_fsm_inst_state_chg(), here passed as 999:\n");
+ test_tdef_fsm_state_chg(S_Y);
+
+ /* the range of unsigned long is architecture dependent. This test can be invoked manually to see whether
+ * clamping the timeout values works, but the output will be of varying lengths depending on the system's
+ * unsigned long range, and would cause differences in expected output. */
+ if (test_range) {
+ printf("- test range:\n");
+ test_tdef_fsm_state_chg(S_M);
+ /* sweep through all the bits, shifting in 0xfffff.. from the right. */
+ m_secs = 0;
+ do {
+ m_secs = (m_secs << 1) + 1;
+ switch (m_secs) {
+ case 0x7fff:
+ printf("--- int32_t max ---\n");
+ break;
+ case 0xffff:
+ printf("--- uint32_t max ---\n");
+ break;
+ case 0x7fffffff:
+ printf("--- int64_t max ---\n");
+ break;
+ case 0xffffffff:
+ printf("--- uint64_t max ---\n");
+ break;
+ default:
+ break;
+ }
+
+ m->val = m_secs - 1;
+ test_tdef_fsm_state_chg(S_M);
+ m->val = m_secs;
+ test_tdef_fsm_state_chg(S_M);
+ m->val = m_secs + 1;
+ test_tdef_fsm_state_chg(S_M);
+ } while (m_secs < ULONG_MAX);
+ }
+
+ printf("- test disallowed transition:\n");
+ test_tdef_fsm_state_chg(S_Z);
+ test_tdef_fsm_state_chg(S_B);
+ test_tdef_fsm_state_chg(S_C);
+ test_tdef_fsm_state_chg(S_D);
+}
+
+int main(int argc, char **argv)
+{
+ ctx = talloc_named_const(NULL, 0, "tdef_test.c");
+ osmo_init_logging2(ctx, NULL);
+
+ log_set_print_filename(osmo_stderr_target, 0);
+ log_set_print_category(osmo_stderr_target, 1);
+ log_set_use_color(osmo_stderr_target, 0);
+
+ osmo_fsm_register(&test_tdef_fsm);
+
+ test_tdef_get();
+ test_tdef_get_nonexisting();
+ test_tdef_set_and_get();
+ /* Run range test iff any argument is passed on the cmdline. For the rationale, see the comment in
+ * test_tdef_state_timeout(). */
+ test_tdef_state_timeout(argc > 1);
+
+ return EXIT_SUCCESS;
+}
diff --git a/tests/tdef/tdef_test.ok b/tests/tdef/tdef_test.ok
new file mode 100644
index 00000000..cf4b77f5
--- /dev/null
+++ b/tests/tdef/tdef_test.ok
@@ -0,0 +1,184 @@
+
+test_tdef_get()
+T1=100s
+osmo_tdef_get(1, s) = 100
+osmo_tdef_get(1, ms) = 100000
+osmo_tdef_get(1, m) = 2
+osmo_tdef_get(1, custom-unit) = 100
+T2=100ms
+osmo_tdef_get(2, s) = 1
+osmo_tdef_get(2, ms) = 100
+osmo_tdef_get(2, m) = 1
+osmo_tdef_get(2, custom-unit) = 100
+T3=100m
+osmo_tdef_get(3, s) = 6000
+osmo_tdef_get(3, ms) = 6000000
+osmo_tdef_get(3, m) = 100
+osmo_tdef_get(3, custom-unit) = 100
+T4=100custom-unit
+osmo_tdef_get(4, s) = 100
+osmo_tdef_get(4, ms) = 100
+osmo_tdef_get(4, m) = 100
+osmo_tdef_get(4, custom-unit) = 100
+T7=50s
+osmo_tdef_get(7, s) = 50
+osmo_tdef_get(7, ms) = 50000
+osmo_tdef_get(7, m) = 1
+osmo_tdef_get(7, custom-unit) = 50
+T8=300s
+osmo_tdef_get(8, s) = 300
+osmo_tdef_get(8, ms) = 300000
+osmo_tdef_get(8, m) = 5
+osmo_tdef_get(8, custom-unit) = 300
+T9=5m
+osmo_tdef_get(9, s) = 300
+osmo_tdef_get(9, ms) = 300000
+osmo_tdef_get(9, m) = 5
+osmo_tdef_get(9, custom-unit) = 5
+T10=20m
+osmo_tdef_get(10, s) = 1200
+osmo_tdef_get(10, ms) = 1200000
+osmo_tdef_get(10, m) = 20
+osmo_tdef_get(10, custom-unit) = 20
+T1000=2000ms
+osmo_tdef_get(1000, s) = 2
+osmo_tdef_get(1000, ms) = 2000
+osmo_tdef_get(1000, m) = 1
+osmo_tdef_get(1000, custom-unit) = 2000
+T1001=60000ms
+osmo_tdef_get(1001, s) = 60
+osmo_tdef_get(1001, ms) = 60000
+osmo_tdef_get(1001, m) = 1
+osmo_tdef_get(1001, custom-unit) = 60000
+T1002=307445734561825860m
+osmo_tdef_get(1002, s) = 18446744073709551600
+osmo_tdef_get(1002, ms) = 18446744073709551615
+osmo_tdef_get(1002, m) = 307445734561825860
+osmo_tdef_get(1002, custom-unit) = 307445734561825860
+T1003=18446744073709551615m
+osmo_tdef_get(1003, s) = 18446744073709551615
+osmo_tdef_get(1003, ms) = 18446744073709551615
+osmo_tdef_get(1003, m) = 18446744073709551615
+osmo_tdef_get(1003, custom-unit) = 18446744073709551615
+T1004=1ms
+osmo_tdef_get(1004, s) = 1
+osmo_tdef_get(1004, ms) = 1
+osmo_tdef_get(1004, m) = 1
+osmo_tdef_get(1004, custom-unit) = 1
+T1005=0ms
+osmo_tdef_get(1005, s) = 0
+osmo_tdef_get(1005, ms) = 0
+osmo_tdef_get(1005, m) = 0
+osmo_tdef_get(1005, custom-unit) = 0
+T1006=0s
+osmo_tdef_get(1006, s) = 0
+osmo_tdef_get(1006, ms) = 0
+osmo_tdef_get(1006, m) = 0
+osmo_tdef_get(1006, custom-unit) = 0
+T1007=0m
+osmo_tdef_get(1007, s) = 0
+osmo_tdef_get(1007, ms) = 0
+osmo_tdef_get(1007, m) = 0
+osmo_tdef_get(1007, custom-unit) = 0
+T1008=0custom-unit
+osmo_tdef_get(1008, s) = 0
+osmo_tdef_get(1008, ms) = 0
+osmo_tdef_get(1008, m) = 0
+osmo_tdef_get(1008, custom-unit) = 0
+T2147483647=18446744073709551615s
+osmo_tdef_get(2147483647, s) = 18446744073709551615
+osmo_tdef_get(2147483647, ms) = 18446744073709551615
+osmo_tdef_get(2147483647, m) = 307445734561825861
+osmo_tdef_get(2147483647, custom-unit) = 18446744073709551615
+T2147483646=18446744073709551614s
+osmo_tdef_get(2147483646, s) = 18446744073709551614
+osmo_tdef_get(2147483646, ms) = 18446744073709551615
+osmo_tdef_get(2147483646, m) = 307445734561825861
+osmo_tdef_get(2147483646, custom-unit) = 18446744073709551614
+T2147483645=9223372036854775807s
+osmo_tdef_get(2147483645, s) = 9223372036854775807
+osmo_tdef_get(2147483645, ms) = 18446744073709551615
+osmo_tdef_get(2147483645, m) = 153722867280912931
+osmo_tdef_get(2147483645, custom-unit) = 9223372036854775807
+T2147483644=18446744073709551615m
+osmo_tdef_get(2147483644, s) = 18446744073709551615
+osmo_tdef_get(2147483644, ms) = 18446744073709551615
+osmo_tdef_get(2147483644, m) = 18446744073709551615
+osmo_tdef_get(2147483644, custom-unit) = 18446744073709551615
+T-2147483648=18446744073709551615s
+osmo_tdef_get(-2147483648, s) = 18446744073709551615
+osmo_tdef_get(-2147483648, ms) = 18446744073709551615
+osmo_tdef_get(-2147483648, m) = 307445734561825861
+osmo_tdef_get(-2147483648, custom-unit) = 18446744073709551615
+T0=1custom-unit
+osmo_tdef_get(0, s) = 1
+osmo_tdef_get(0, ms) = 1
+osmo_tdef_get(0, m) = 1
+osmo_tdef_get(0, custom-unit) = 1
+T123=1s
+osmo_tdef_get(123, s) = 1
+osmo_tdef_get(123, ms) = 1000
+osmo_tdef_get(123, m) = 1
+osmo_tdef_get(123, custom-unit) = 1
+
+test_tdef_get_nonexisting()
+osmo_tdef_get(tdefs, 5, s, 999) = 999
+osmo_tdef_get(tdefs, 5, ms, 999) = 999
+osmo_tdef_get(tdefs, 5, m, 999) = 999
+osmo_tdef_get(tdefs, 5, custom-unit, 999) = 999
+
+test_tdef_set_and_get()
+setting 7 = 42
+T7=42s(def=50)
+osmo_tdef_get(7, ms) = 42000
+osmo_tdef_get(7, s) = 42
+osmo_tdef_get(7, m) = 1
+osmo_tdef_get(7, custom-unit) = 42
+setting 7 = 420
+T7=420s(def=50)
+osmo_tdef_get(7, ms) = 420000
+osmo_tdef_get(7, s) = 420
+osmo_tdef_get(7, m) = 7
+osmo_tdef_get(7, custom-unit) = 420
+resetting
+T7=50s
+osmo_tdef_get(7, s) = 50
+
+test_tdef_state_timeout()
+Total time passed: 0.000000 s
+state=A T=0, no timeout
+ --> A (configured as T1 100 s) rc=0; state=A T=1, 100.000000 s remaining
+ --> B (configured as T2 100 ms) rc=0; state=B T=2, 1.000000 s remaining
+ --> C (configured as T3 100 m) rc=0; state=C T=3, 6000.000000 s remaining
+ --> D (configured as T4 100 custom-unit) rc=0; state=D T=4, 100.000000 s remaining
+ --> G (configured as T7 50 s) rc=0; state=G T=7, 50.000000 s remaining
+ --> H (configured as T8 300 s) rc=0; state=H T=8, 300.000000 s remaining
+ --> I (configured as T9 5 m) rc=0; state=I T=9, 300.000000 s remaining
+ --> J (configured as T10 20 m) rc=0; state=J T=10, 1200.000000 s remaining
+- test keep_timer:
+Total time passed: 123.045678 s
+state=J T=10, 1076.954322 s remaining
+ --> K (configured as T0(keep_timer) 1 custom-unit) rc=0; state=K T=10, 1076.954322 s remaining
+ --> A (configured as T1 100 s) rc=0; state=A T=1, 100.000000 s remaining
+Total time passed: 146.091356 s
+state=A T=1, 76.954322 s remaining
+ --> K (configured as T0(keep_timer) 1 custom-unit) rc=0; state=K T=1, 76.954322 s remaining
+ --> A (configured as T1 100 s) rc=0; state=A T=1, 100.000000 s remaining
+Total time passed: 169.137034 s
+state=A T=1, 76.954322 s remaining
+ --> L (configured as T123(keep_timer) 1 s) rc=0; state=L T=123, 76.954322 s remaining
+- test large T:
+ --> M (configured as T2147483647 18446744073709551615 s) rc=0; state=M T=2147483647, 2147483647.000000 s remaining
+- test T<0:
+ --> N (configured as T-2147483648 18446744073709551615 s) rc=0; state=N T=-2147483648, 2147483647.000000 s remaining
+- test T=0:
+ --> O (no timer configured for this state)
+- test no timer:
+ --> X (no timer configured for this state)
+- test undefined timer, using default_val arg of osmo_tdef_fsm_inst_state_chg(), here passed as 999:
+ --> Y (configured as T666 18446744073709551615 -) rc=0; state=Y T=666, 999.000000 s remaining
+- test disallowed transition:
+ --> Z (no timer configured for this state)
+ --> B (configured as T2 100 ms) rc=0; state=B T=2, 1.000000 s remaining
+ --> C (configured as T3 100 m) rc=0; state=C T=3, 6000.000000 s remaining
+ --> D (configured as T4 100 custom-unit) rc=0; state=D T=4, 100.000000 s remaining
diff --git a/tests/tdef/tdef_vty_test_config_root.c b/tests/tdef/tdef_vty_test_config_root.c
new file mode 100644
index 00000000..138ac00a
--- /dev/null
+++ b/tests/tdef/tdef_vty_test_config_root.c
@@ -0,0 +1,292 @@
+/* Test implementation for osmo_tdef VTY configuration API. */
+/*
+ * (C) 2019 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * Author: Neels Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#define _GNU_SOURCE
+#include <getopt.h>
+#include <signal.h>
+#include <limits.h>
+#include <string.h>
+
+#include <osmocom/core/application.h>
+
+#include <osmocom/vty/command.h>
+#include <osmocom/vty/misc.h>
+#include <osmocom/vty/telnet_interface.h>
+
+#include <osmocom/core/tdef.h>
+#include <osmocom/vty/tdef_vty.h>
+
+#include <stdlib.h>
+
+#include "config.h"
+
+/* ------------------- HERE IS THE INTERESTING TDEF RELEVANT PART ------------------- */
+
+/* This example keeps several separate timer groups and offers 'timer' VTY commands at the root of the config node. See
+ * the tdef_vty_test_config_root.vty transcript test.
+ */
+
+static struct osmo_tdef tdefs_test[] = {
+ { .T=1, .default_val=100, .desc="Testing a hundred seconds" }, // default is .unit=OSMO_TDEF_S == 0
+ { .T=2, .default_val=100, .unit=OSMO_TDEF_MS, .desc="Testing a hundred milliseconds" },
+ { .T=3, .default_val=100, .unit=OSMO_TDEF_M, .desc="Testing a hundred minutes" },
+ { .T=4, .default_val=100, .unit=OSMO_TDEF_CUSTOM, .desc="Testing a hundred potatoes" },
+ { .T=INT_MAX, .default_val=ULONG_MAX, .unit=OSMO_TDEF_M, .desc="Very large" },
+ { .T=-23, .default_val=-15, .desc="Negative T number" },
+ {} // <-- important! last entry shall be zero
+};
+
+static struct osmo_tdef tdefs_tea[] = {
+ { .T=1, .default_val=50, .desc="Water Boiling Timeout" },
+ { .T=2, .default_val=300, .desc="Tea brewing" },
+ { .T=3, .default_val=5, .unit=OSMO_TDEF_M, .desc="Let tea cool down before drinking" },
+ { .T=4, .default_val=20, .unit=OSMO_TDEF_M, .desc="Forgot to drink tea while it's warm" },
+ {}
+};
+
+static struct osmo_tdef tdefs_software[] = {
+ { .T=1, .default_val=30, .unit=OSMO_TDEF_M, .desc="Write code" },
+ { .T=2, .default_val=20, .unit=OSMO_TDEF_MS, .desc="Hit segfault" },
+ { .T=3, .default_val=480, .unit=OSMO_TDEF_M, .desc="Fix bugs" },
+ {}
+};
+
+static struct osmo_tdef_group tdef_groups[] = {
+ {
+ .name = "tea",
+ .desc = "Tea time",
+ .tdefs = tdefs_tea,
+ },
+ {
+ .name = "test",
+ .desc = "Test timers",
+ .tdefs = tdefs_test,
+ },
+ {
+ .name = "software",
+ .desc = "Typical software development cycle",
+ .tdefs = tdefs_software,
+ },
+ {}
+};
+
+enum tdef_vty_test_nodes {
+ TIMER_NODE = _LAST_OSMOVTY_NODE + 1,
+};
+
+/* This example puts 'timer' configuration commands directly at the root of the CONFIG_NODE.
+ * This TIMER_NODE is merely needed as a hook for the vty_write() command, but becomes an empty node in the VTY docs.
+ * It is possible to cheat around needing this if you choose to config_write_timer() in another root nodes' write cb.
+ * Another example using a 'network' subnode is \ref tdef_vty_test_config_subnode.c */
+static struct cmd_node timer_node = {
+ TIMER_NODE,
+ "%s(config-timer)# ",
+ 1,
+};
+
+static int config_write_timer(struct vty *vty)
+{
+ osmo_tdef_vty_groups_write(vty, "");
+ return CMD_SUCCESS;
+}
+
+static void timer_init_vty()
+{
+ /* Again, this is merely to get a vty write hook, see above. */
+ install_node(&timer_node, config_write_timer);
+
+ osmo_tdef_vty_groups_init(CONFIG_NODE, tdef_groups);
+}
+
+/* ------------------- THE REST is just boilerplate osmo main() ------------------- */
+
+void *root_ctx = NULL;
+
+static void print_help()
+{
+ printf( "options:\n"
+ " -h --help this text\n"
+ " -d --debug MASK Enable debugging (e.g. -d DRSL:DOML:DLAPDM)\n"
+ " -D --daemonize For the process into a background daemon\n"
+ " -c --config-file Specify the filename of the config file\n"
+ " -s --disable-color Don't use colors in stderr log output\n"
+ " -T --timestamp Prefix every log line with a timestamp\n"
+ " -V --version Print version information and exit\n"
+ " -e --log-level Set a global log-level\n"
+ );
+}
+
+static struct {
+ const char *config_file;
+ int daemonize;
+} cmdline_config = {};
+
+static void handle_options(int argc, char **argv)
+{
+ while (1) {
+ int option_idx = 0, c;
+ static const struct option long_options[] = {
+ { "help", 0, 0, 'h' },
+ { "debug", 1, 0, 'd' },
+ { "daemonize", 0, 0, 'D' },
+ { "config-file", 1, 0, 'c' },
+ { "disable-color", 0, 0, 's' },
+ { "timestamp", 0, 0, 'T' },
+ { "version", 0, 0, 'V' },
+ { "log-level", 1, 0, 'e' },
+ {}
+ };
+
+ c = getopt_long(argc, argv, "hc:d:Dc:sTVe:",
+ long_options, &option_idx);
+ if (c == -1)
+ break;
+
+ switch (c) {
+ case 'h':
+ print_help();
+ exit(0);
+ case 's':
+ log_set_use_color(osmo_stderr_target, 0);
+ break;
+ case 'd':
+ log_parse_category_mask(osmo_stderr_target, optarg);
+ break;
+ case 'D':
+ cmdline_config.daemonize = 1;
+ break;
+ case 'c':
+ cmdline_config.config_file = optarg;
+ break;
+ case 'T':
+ log_set_print_timestamp(osmo_stderr_target, 1);
+ break;
+ case 'e':
+ log_set_log_level(osmo_stderr_target, atoi(optarg));
+ break;
+ case 'V':
+ print_version(1);
+ exit(0);
+ break;
+ default:
+ /* catch unknown options *as well as* missing arguments. */
+ fprintf(stderr, "Error in command line options. Exiting.\n");
+ exit(-1);
+ }
+ }
+}
+
+static int quit = 0;
+
+static void signal_handler(int signal)
+{
+ fprintf(stdout, "signal %u received\n", signal);
+
+ switch (signal) {
+ case SIGINT:
+ case SIGTERM:
+ quit++;
+ break;
+ case SIGABRT:
+ osmo_generate_backtrace();
+ /* in case of abort, we want to obtain a talloc report
+ * and then return to the caller, who will abort the process */
+ case SIGUSR1:
+ talloc_report(tall_vty_ctx, stderr);
+ talloc_report_full(root_ctx, stderr);
+ break;
+ case SIGUSR2:
+ talloc_report_full(tall_vty_ctx, stderr);
+ break;
+ default:
+ break;
+ }
+}
+
+static struct vty_app_info vty_info = {
+ .name = "tdef_vty_test",
+ .version = PACKAGE_VERSION,
+};
+
+static const struct log_info_cat default_categories[] = {};
+
+const struct log_info log_info = {
+ .cat = default_categories,
+ .num_cat = ARRAY_SIZE(default_categories),
+};
+
+int main(int argc, char **argv)
+{
+ int rc;
+
+ root_ctx = talloc_named_const(NULL, 0, "tdef_vty_test");
+
+ osmo_init_logging2(root_ctx, &log_info);
+
+ vty_info.tall_ctx = root_ctx;
+ vty_init(&vty_info);
+ osmo_talloc_vty_add_cmds();
+
+ timer_init_vty(); /* <---- the only tdef relevant init */
+
+ handle_options(argc, argv);
+
+ if (cmdline_config.config_file) {
+ rc = vty_read_config_file(cmdline_config.config_file, NULL);
+ if (rc < 0) {
+ fprintf(stderr, "Failed to parse the config file: '%s'\n", cmdline_config.config_file);
+ return 1;
+ }
+ }
+
+ rc = telnet_init_dynif(root_ctx, NULL, vty_get_bind_addr(), 42042);
+ if (rc < 0)
+ return 2;
+
+ signal(SIGINT, &signal_handler);
+ signal(SIGTERM, &signal_handler);
+ signal(SIGABRT, &signal_handler);
+ signal(SIGUSR1, &signal_handler);
+ signal(SIGUSR2, &signal_handler);
+ osmo_init_ignore_signals();
+
+ if (cmdline_config.daemonize) {
+ rc = osmo_daemonize();
+ if (rc < 0) {
+ perror("Error during daemonize");
+ return 6;
+ }
+ }
+
+ while (!quit) {
+ log_reset_context();
+ osmo_select_main(0);
+ }
+
+ talloc_free(root_ctx);
+ talloc_free(tall_vty_ctx);
+
+ return 0;
+}
diff --git a/tests/tdef/tdef_vty_test_config_root.vty b/tests/tdef/tdef_vty_test_config_root.vty
new file mode 100644
index 00000000..12876a68
--- /dev/null
+++ b/tests/tdef/tdef_vty_test_config_root.vty
@@ -0,0 +1,292 @@
+tdef_vty_test> list
+...
+ show timer [(tea|test|software)] [TNNNN]
+...
+
+tdef_vty_test> show timer ?
+ [tea] Tea time
+ [test] Test timers
+ [software] Typical software development cycle
+
+tdef_vty_test> show timer test ?
+ [TNNNN] T-number, optionally preceded by 't' or 'T'.
+
+tdef_vty_test> show timer
+tea: T1 = 50 s Water Boiling Timeout (default: 50 s)
+tea: T2 = 300 s Tea brewing (default: 300 s)
+tea: T3 = 5 m Let tea cool down before drinking (default: 5 m)
+tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m)
+test: T1 = 100 s Testing a hundred seconds (default: 100 s)
+test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms)
+test: T3 = 100 m Testing a hundred minutes (default: 100 m)
+test: T4 = 100 Testing a hundred potatoes (default: 100)
+test: T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m)
+test: T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s)
+software: T1 = 30 m Write code (default: 30 m)
+software: T2 = 20 ms Hit segfault (default: 20 ms)
+software: T3 = 480 m Fix bugs (default: 480 m)
+
+tdef_vty_test> enable
+tdef_vty_test# show timer
+tea: T1 = 50 s Water Boiling Timeout (default: 50 s)
+tea: T2 = 300 s Tea brewing (default: 300 s)
+tea: T3 = 5 m Let tea cool down before drinking (default: 5 m)
+tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m)
+test: T1 = 100 s Testing a hundred seconds (default: 100 s)
+test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms)
+test: T3 = 100 m Testing a hundred minutes (default: 100 m)
+test: T4 = 100 Testing a hundred potatoes (default: 100)
+test: T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m)
+test: T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s)
+software: T1 = 30 m Write code (default: 30 m)
+software: T2 = 20 ms Hit segfault (default: 20 ms)
+software: T3 = 480 m Fix bugs (default: 480 m)
+
+tdef_vty_test# configure terminal
+
+tdef_vty_test(config)# show running-config
+... !timer
+
+tdef_vty_test(config)# list
+...
+ timer [(tea|test|software)] [TNNNN] [(<0-2147483647>|default)]
+...
+
+tdef_vty_test(config)# timer ?
+ [tea] Tea time
+ [test] Test timers
+ [software] Typical software development cycle
+
+tdef_vty_test(config)# timer sof T123 ?
+ [<0-2147483647>] New timer value
+ [default] Set to default timer value
+
+tdef_vty_test(config)# timer sof T123 ?
+ [<0-2147483647>] New timer value
+ [default] Set to default timer value
+
+tdef_vty_test(config)# timer test ?
+ [TNNNN] T-number, optionally preceded by 't' or 'T'.
+
+tdef_vty_test(config)# timer test t2 ?
+ [<0-2147483647>] New timer value
+ [default] Set to default timer value
+
+tdef_vty_test(config)# do show timer
+tea: T1 = 50 s Water Boiling Timeout (default: 50 s)
+tea: T2 = 300 s Tea brewing (default: 300 s)
+tea: T3 = 5 m Let tea cool down before drinking (default: 5 m)
+tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m)
+test: T1 = 100 s Testing a hundred seconds (default: 100 s)
+test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms)
+test: T3 = 100 m Testing a hundred minutes (default: 100 m)
+test: T4 = 100 Testing a hundred potatoes (default: 100)
+test: T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m)
+test: T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s)
+software: T1 = 30 m Write code (default: 30 m)
+software: T2 = 20 ms Hit segfault (default: 20 ms)
+software: T3 = 480 m Fix bugs (default: 480 m)
+
+tdef_vty_test(config)# do show timer tea
+tea: T1 = 50 s Water Boiling Timeout (default: 50 s)
+tea: T2 = 300 s Tea brewing (default: 300 s)
+tea: T3 = 5 m Let tea cool down before drinking (default: 5 m)
+tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m)
+
+tdef_vty_test(config)# do show timer tea 2
+tea: T2 = 300 s Tea brewing (default: 300 s)
+
+tdef_vty_test(config)# do show timer tea t2
+tea: T2 = 300 s Tea brewing (default: 300 s)
+
+tdef_vty_test(config)# do show timer tea T2
+tea: T2 = 300 s Tea brewing (default: 300 s)
+
+tdef_vty_test(config)# do show timer tea T5
+% No such timer: T5
+
+tdef_vty_test(config)# do show timer tea T0
+% No such timer: T0
+
+tdef_vty_test(config)# do show timer tea T-123
+% No such timer: T-123
+
+tdef_vty_test(config)# do show timer t
+tea: T1 = 50 s Water Boiling Timeout (default: 50 s)
+tea: T2 = 300 s Tea brewing (default: 300 s)
+tea: T3 = 5 m Let tea cool down before drinking (default: 5 m)
+tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m)
+test: T1 = 100 s Testing a hundred seconds (default: 100 s)
+test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms)
+test: T3 = 100 m Testing a hundred minutes (default: 100 m)
+test: T4 = 100 Testing a hundred potatoes (default: 100)
+test: T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m)
+test: T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s)
+
+tdef_vty_test(config)# do show timer te
+tea: T1 = 50 s Water Boiling Timeout (default: 50 s)
+tea: T2 = 300 s Tea brewing (default: 300 s)
+tea: T3 = 5 m Let tea cool down before drinking (default: 5 m)
+tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m)
+test: T1 = 100 s Testing a hundred seconds (default: 100 s)
+test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms)
+test: T3 = 100 m Testing a hundred minutes (default: 100 m)
+test: T4 = 100 Testing a hundred potatoes (default: 100)
+test: T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m)
+test: T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s)
+
+tdef_vty_test(config)# do show timer te T2
+tea: T2 = 300 s Tea brewing (default: 300 s)
+test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms)
+
+
+tdef_vty_test(config)# timer tea 3 30
+tdef_vty_test(config)# timer tea T3
+tea: T3 = 30 m Let tea cool down before drinking (default: 5 m)
+
+tdef_vty_test(config)# timer tea t3 31
+tdef_vty_test(config)# timer tea T3
+tea: T3 = 31 m Let tea cool down before drinking (default: 5 m)
+
+tdef_vty_test(config)# timer tea T3 32
+tdef_vty_test(config)# timer tea T3
+tea: T3 = 32 m Let tea cool down before drinking (default: 5 m)
+
+tdef_vty_test(config)# timer tea T-123 99
+% No such timer: T-123
+
+tdef_vty_test(config)# timer tea T0 0
+% No such timer: T0
+
+tdef_vty_test(config)# timer tea T123 default
+% No such timer: T123
+
+tdef_vty_test(config)# timer tea
+tea: T1 = 50 s Water Boiling Timeout (default: 50 s)
+tea: T2 = 300 s Tea brewing (default: 300 s)
+tea: T3 = 32 m Let tea cool down before drinking (default: 5 m)
+tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m)
+
+tdef_vty_test(config)# timer t
+tea: T1 = 50 s Water Boiling Timeout (default: 50 s)
+tea: T2 = 300 s Tea brewing (default: 300 s)
+tea: T3 = 32 m Let tea cool down before drinking (default: 5 m)
+tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m)
+test: T1 = 100 s Testing a hundred seconds (default: 100 s)
+test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms)
+test: T3 = 100 m Testing a hundred minutes (default: 100 m)
+test: T4 = 100 Testing a hundred potatoes (default: 100)
+test: T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m)
+test: T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s)
+
+tdef_vty_test(config)# timer te T2
+tea: T2 = 300 s Tea brewing (default: 300 s)
+test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms)
+
+tdef_vty_test(config)# timer test T2 100
+
+tdef_vty_test(config)# timer tes T2 100
+% Error: no timers found
+
+tdef_vty_test(config)# timer te T2 100
+% Error: no timers found
+
+
+tdef_vty_test(config)# do show timer software
+software: T1 = 30 m Write code (default: 30 m)
+software: T2 = 20 ms Hit segfault (default: 20 ms)
+software: T3 = 480 m Fix bugs (default: 480 m)
+
+tdef_vty_test(config)# do show timer software 1
+software: T1 = 30 m Write code (default: 30 m)
+
+tdef_vty_test(config)# do show timer software t1
+software: T1 = 30 m Write code (default: 30 m)
+
+tdef_vty_test(config)# do show timer software T1
+software: T1 = 30 m Write code (default: 30 m)
+
+tdef_vty_test(config)# do show timer software T99
+% No such timer: T99
+
+tdef_vty_test(config)# do show timer software T-123123
+% No such timer: T-123123
+
+tdef_vty_test(config)# do show timer software T0
+% No such timer: T0
+
+tdef_vty_test(config)# timer software 1 11
+tdef_vty_test(config)# timer software T1
+software: T1 = 11 m Write code (default: 30 m)
+
+tdef_vty_test(config)# timer software t1 12
+tdef_vty_test(config)# timer software T1
+software: T1 = 12 m Write code (default: 30 m)
+
+tdef_vty_test(config)# timer software T1 13
+tdef_vty_test(config)# timer software T2 0
+tdef_vty_test(config)# timer software
+software: T1 = 13 m Write code (default: 30 m)
+software: T2 = 0 ms Hit segfault (default: 20 ms)
+software: T3 = 480 m Fix bugs (default: 480 m)
+
+tdef_vty_test(config)# timer softw
+software: T1 = 13 m Write code (default: 30 m)
+software: T2 = 0 ms Hit segfault (default: 20 ms)
+software: T3 = 480 m Fix bugs (default: 480 m)
+
+tdef_vty_test(config)# timer softw T3
+software: T3 = 480 m Fix bugs (default: 480 m)
+
+tdef_vty_test(config)# timer softw T3 23
+% Error: no timers found
+
+tdef_vty_test(config)# timer
+tea: T1 = 50 s Water Boiling Timeout (default: 50 s)
+tea: T2 = 300 s Tea brewing (default: 300 s)
+tea: T3 = 32 m Let tea cool down before drinking (default: 5 m)
+tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m)
+test: T1 = 100 s Testing a hundred seconds (default: 100 s)
+test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms)
+test: T3 = 100 m Testing a hundred minutes (default: 100 m)
+test: T4 = 100 Testing a hundred potatoes (default: 100)
+test: T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m)
+test: T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s)
+software: T1 = 13 m Write code (default: 30 m)
+software: T2 = 0 ms Hit segfault (default: 20 ms)
+software: T3 = 480 m Fix bugs (default: 480 m)
+
+tdef_vty_test(config)# do show timer
+tea: T1 = 50 s Water Boiling Timeout (default: 50 s)
+tea: T2 = 300 s Tea brewing (default: 300 s)
+tea: T3 = 32 m Let tea cool down before drinking (default: 5 m)
+tea: T4 = 20 m Forgot to drink tea while it's warm (default: 20 m)
+test: T1 = 100 s Testing a hundred seconds (default: 100 s)
+test: T2 = 100 ms Testing a hundred milliseconds (default: 100 ms)
+test: T3 = 100 m Testing a hundred minutes (default: 100 m)
+test: T4 = 100 Testing a hundred potatoes (default: 100)
+test: T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m)
+test: T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s)
+software: T1 = 13 m Write code (default: 30 m)
+software: T2 = 0 ms Hit segfault (default: 20 ms)
+software: T3 = 480 m Fix bugs (default: 480 m)
+
+tdef_vty_test(config)# show running-config
+... !timer
+timer tea T3 32
+timer software T1 13
+timer software T2 0
+... !timer
+
+tdef_vty_test(config)# timer tea T3 default
+tdef_vty_test(config)# timer software T1 default
+tdef_vty_test(config)# show running-config
+... !timer
+timer software T2 0
+... !timer
+
+tdef_vty_test(config)# timer softw 2 default
+% Error: no timers found
+tdef_vty_test(config)# timer software 2 default
+tdef_vty_test(config)# show running-config
+... !timer
diff --git a/tests/tdef/tdef_vty_test_config_subnode.c b/tests/tdef/tdef_vty_test_config_subnode.c
new file mode 100644
index 00000000..c371c8d6
--- /dev/null
+++ b/tests/tdef/tdef_vty_test_config_subnode.c
@@ -0,0 +1,288 @@
+/* Test implementation for osmo_tdef VTY configuration API. */
+/*
+ * (C) 2019 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * Author: Neels Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#define _GNU_SOURCE
+#include <getopt.h>
+#include <signal.h>
+#include <limits.h>
+#include <string.h>
+
+#include <osmocom/core/application.h>
+
+#include <osmocom/vty/command.h>
+#include <osmocom/vty/misc.h>
+#include <osmocom/vty/telnet_interface.h>
+
+#include <osmocom/core/tdef.h>
+#include <osmocom/vty/tdef_vty.h>
+
+#include <stdlib.h>
+
+#include "config.h"
+
+/* ------------------- HERE IS THE INTERESTING TDEF RELEVANT PART ------------------- */
+
+/* This example keeps a single global timer group and offers a custom 'timer' VTY command in a 'network' subnode below
+ * the CONFIG_NODE.
+ * the tdef_vty_test_config_subnode.vty transcript test.
+ */
+
+static struct osmo_tdef global_tdefs[] = {
+ { .T=1, .default_val=100, .desc="Testing a hundred seconds" }, // default is .unit=OSMO_TDEF_S == 0
+ { .T=2, .default_val=100, .unit=OSMO_TDEF_MS, .desc="Testing a hundred milliseconds" },
+ { .T=3, .default_val=100, .unit=OSMO_TDEF_M, .desc="Testing a hundred minutes" },
+ { .T=4, .default_val=100, .unit=OSMO_TDEF_CUSTOM, .desc="Testing a hundred potatoes" },
+ { .T=INT_MAX, .default_val=ULONG_MAX, .unit=OSMO_TDEF_M, .desc="Very large" },
+ { .T=-23, .default_val=-15, .desc="Negative T number" },
+ {} // <-- important! last entry shall be zero
+};
+
+enum tdef_vty_test_nodes {
+ GSMNET_NODE = _LAST_OSMOVTY_NODE + 1,
+};
+
+/* This example offers 'timer T123' commands within an "unrelated" already existing subnode. */
+static struct cmd_node gsmnet_node = {
+ GSMNET_NODE,
+ "%s(config-net)# ",
+ 1,
+};
+
+DEFUN(show_timer, show_timer_cmd,
+ "show timer " OSMO_TDEF_VTY_ARG_T_OPTIONAL,
+ SHOW_STR "Show timers\n"
+ OSMO_TDEF_VTY_DOC_T)
+{
+ const char *T_arg = argc > 0 ? argv[0] : NULL;
+ return osmo_tdef_vty_show_cmd(vty, global_tdefs, T_arg, NULL);
+}
+
+DEFUN(cfg_net_timer, cfg_net_timer_cmd,
+ "timer " OSMO_TDEF_VTY_ARG_SET_OPTIONAL,
+ "Configure or show timers\n"
+ OSMO_TDEF_VTY_DOC_SET)
+{
+ /* If any arguments are missing, redirect to 'show' */
+ if (argc < 2)
+ return show_timer(self, vty, argc, argv);
+ return osmo_tdef_vty_set_cmd(vty, global_tdefs, argv);
+}
+
+DEFUN(cfg_net, cfg_net_cmd,
+ "network", "Enter network node\n")
+{
+ vty->node = GSMNET_NODE;
+ return CMD_SUCCESS;
+}
+
+static int config_write_gsmnet(struct vty *vty)
+{
+ vty_out(vty, "net%s", VTY_NEWLINE);
+ /* usually, here would be the output of any other 'net' config items... */
+
+ osmo_tdef_vty_write(vty, global_tdefs, " timer ");
+ return CMD_SUCCESS;
+}
+
+static void gsmnet_init_vty()
+{
+ install_node(&gsmnet_node, config_write_gsmnet);
+ install_element(CONFIG_NODE, &cfg_net_cmd);
+
+ osmo_tdefs_reset(global_tdefs);
+ install_element_ve(&show_timer_cmd);
+ install_element(GSMNET_NODE, &cfg_net_timer_cmd);
+}
+
+/* ------------------- THE REST is just boilerplate osmo main() ------------------- */
+
+void *root_ctx = NULL;
+
+static void print_help()
+{
+ printf( "options:\n"
+ " -h --help this text\n"
+ " -d --debug MASK Enable debugging (e.g. -d DRSL:DOML:DLAPDM)\n"
+ " -D --daemonize For the process into a background daemon\n"
+ " -c --config-file Specify the filename of the config file\n"
+ " -s --disable-color Don't use colors in stderr log output\n"
+ " -T --timestamp Prefix every log line with a timestamp\n"
+ " -V --version Print version information and exit\n"
+ " -e --log-level Set a global log-level\n"
+ );
+}
+
+static struct {
+ const char *config_file;
+ int daemonize;
+} cmdline_config = {};
+
+static void handle_options(int argc, char **argv)
+{
+ while (1) {
+ int option_idx = 0, c;
+ static const struct option long_options[] = {
+ { "help", 0, 0, 'h' },
+ { "debug", 1, 0, 'd' },
+ { "daemonize", 0, 0, 'D' },
+ { "config-file", 1, 0, 'c' },
+ { "disable-color", 0, 0, 's' },
+ { "timestamp", 0, 0, 'T' },
+ { "version", 0, 0, 'V' },
+ { "log-level", 1, 0, 'e' },
+ {}
+ };
+
+ c = getopt_long(argc, argv, "hc:d:Dc:sTVe:",
+ long_options, &option_idx);
+ if (c == -1)
+ break;
+
+ switch (c) {
+ case 'h':
+ print_help();
+ exit(0);
+ case 's':
+ log_set_use_color(osmo_stderr_target, 0);
+ break;
+ case 'd':
+ log_parse_category_mask(osmo_stderr_target, optarg);
+ break;
+ case 'D':
+ cmdline_config.daemonize = 1;
+ break;
+ case 'c':
+ cmdline_config.config_file = optarg;
+ break;
+ case 'T':
+ log_set_print_timestamp(osmo_stderr_target, 1);
+ break;
+ case 'e':
+ log_set_log_level(osmo_stderr_target, atoi(optarg));
+ break;
+ case 'V':
+ print_version(1);
+ exit(0);
+ break;
+ default:
+ /* catch unknown options *as well as* missing arguments. */
+ fprintf(stderr, "Error in command line options. Exiting.\n");
+ exit(-1);
+ }
+ }
+}
+
+static int quit = 0;
+
+static void signal_handler(int signal)
+{
+ fprintf(stdout, "signal %u received\n", signal);
+
+ switch (signal) {
+ case SIGINT:
+ case SIGTERM:
+ quit++;
+ break;
+ case SIGABRT:
+ osmo_generate_backtrace();
+ /* in case of abort, we want to obtain a talloc report
+ * and then return to the caller, who will abort the process */
+ case SIGUSR1:
+ talloc_report(tall_vty_ctx, stderr);
+ talloc_report_full(root_ctx, stderr);
+ break;
+ case SIGUSR2:
+ talloc_report_full(tall_vty_ctx, stderr);
+ break;
+ default:
+ break;
+ }
+}
+
+static struct vty_app_info vty_info = {
+ .name = "tdef_vty_test",
+ .version = PACKAGE_VERSION,
+};
+
+static const struct log_info_cat default_categories[] = {};
+
+const struct log_info log_info = {
+ .cat = default_categories,
+ .num_cat = ARRAY_SIZE(default_categories),
+};
+
+int main(int argc, char **argv)
+{
+ int rc;
+
+ root_ctx = talloc_named_const(NULL, 0, "tdef_vty_test");
+
+ osmo_init_logging2(root_ctx, &log_info);
+
+ vty_info.tall_ctx = root_ctx;
+ vty_init(&vty_info);
+ osmo_talloc_vty_add_cmds();
+
+ gsmnet_init_vty(); /* <--- relevant init for this example */
+
+ handle_options(argc, argv);
+
+ if (cmdline_config.config_file) {
+ rc = vty_read_config_file(cmdline_config.config_file, NULL);
+ if (rc < 0) {
+ fprintf(stderr, "Failed to parse the config file: '%s'\n", cmdline_config.config_file);
+ return 1;
+ }
+ }
+
+ rc = telnet_init_dynif(root_ctx, NULL, vty_get_bind_addr(), 42042);
+ if (rc < 0)
+ return 2;
+
+ signal(SIGINT, &signal_handler);
+ signal(SIGTERM, &signal_handler);
+ signal(SIGABRT, &signal_handler);
+ signal(SIGUSR1, &signal_handler);
+ signal(SIGUSR2, &signal_handler);
+ osmo_init_ignore_signals();
+
+ if (cmdline_config.daemonize) {
+ rc = osmo_daemonize();
+ if (rc < 0) {
+ perror("Error during daemonize");
+ return 6;
+ }
+ }
+
+ while (!quit) {
+ log_reset_context();
+ osmo_select_main(0);
+ }
+
+ talloc_free(root_ctx);
+ talloc_free(tall_vty_ctx);
+
+ return 0;
+}
diff --git a/tests/tdef/tdef_vty_test_config_subnode.vty b/tests/tdef/tdef_vty_test_config_subnode.vty
new file mode 100644
index 00000000..6cfd3bf3
--- /dev/null
+++ b/tests/tdef/tdef_vty_test_config_subnode.vty
@@ -0,0 +1,107 @@
+tdef_vty_test> list
+... !timer
+ show timer [TNNNN]
+... !timer
+
+tdef_vty_test> show timer ?
+ [TNNNN] T-number, optionally preceded by 't' or 'T'.
+
+tdef_vty_test> show timer
+T1 = 100 s Testing a hundred seconds (default: 100 s)
+T2 = 100 ms Testing a hundred milliseconds (default: 100 ms)
+T3 = 100 m Testing a hundred minutes (default: 100 m)
+T4 = 100 Testing a hundred potatoes (default: 100)
+T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m)
+T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s)
+
+tdef_vty_test> enable
+tdef_vty_test# show timer
+T1 = 100 s Testing a hundred seconds (default: 100 s)
+T2 = 100 ms Testing a hundred milliseconds (default: 100 ms)
+T3 = 100 m Testing a hundred minutes (default: 100 m)
+T4 = 100 Testing a hundred potatoes (default: 100)
+T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m)
+T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s)
+
+tdef_vty_test# configure terminal
+tdef_vty_test(config)# show running-config
+... !timer
+
+tdef_vty_test(config)# network
+
+tdef_vty_test(config-net)# do show timer
+T1 = 100 s Testing a hundred seconds (default: 100 s)
+T2 = 100 ms Testing a hundred milliseconds (default: 100 ms)
+T3 = 100 m Testing a hundred minutes (default: 100 m)
+T4 = 100 Testing a hundred potatoes (default: 100)
+T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m)
+T-23 = 18446744073709551601 s Negative T number (default: 18446744073709551601 s)
+
+tdef_vty_test(config-net)# do show timer T3
+T3 = 100 m Testing a hundred minutes (default: 100 m)
+tdef_vty_test(config-net)# do show timer 3
+T3 = 100 m Testing a hundred minutes (default: 100 m)
+tdef_vty_test(config-net)# do show timer t3
+T3 = 100 m Testing a hundred minutes (default: 100 m)
+
+tdef_vty_test(config-net)# timer T1 5
+tdef_vty_test(config-net)# timer T1
+T1 = 5 s Testing a hundred seconds (default: 100 s)
+
+tdef_vty_test(config-net)# timer t1 678
+tdef_vty_test(config-net)# timer T1
+T1 = 678 s Testing a hundred seconds (default: 100 s)
+
+tdef_vty_test(config-net)# timer 1 9012345
+tdef_vty_test(config-net)# timer T1
+T1 = 9012345 s Testing a hundred seconds (default: 100 s)
+
+tdef_vty_test(config-net)# do show timer T666
+% No such timer: T666
+tdef_vty_test(config-net)# do show timer t666
+% No such timer: T666
+tdef_vty_test(config-net)# do show timer 666
+% No such timer: T666
+
+tdef_vty_test(config-net)# timer T666
+% No such timer: T666
+tdef_vty_test(config-net)# timer t666
+% No such timer: T666
+tdef_vty_test(config-net)# timer 666
+% No such timer: T666
+
+tdef_vty_test(config-net)# timer T666 5
+% No such timer: T666
+
+tdef_vty_test(config-net)# timer T-23 42
+tdef_vty_test(config-net)# timer T-23
+T-23 = 42 s Negative T number (default: 18446744073709551601 s)
+
+tdef_vty_test(config-net)# timer t-23 43
+tdef_vty_test(config-net)# timer T-23
+T-23 = 43 s Negative T number (default: 18446744073709551601 s)
+
+tdef_vty_test(config-net)# timer -23 44
+tdef_vty_test(config-net)# timer T-23
+T-23 = 44 s Negative T number (default: 18446744073709551601 s)
+
+tdef_vty_test(config-net)# do show timer
+T1 = 9012345 s Testing a hundred seconds (default: 100 s)
+T2 = 100 ms Testing a hundred milliseconds (default: 100 ms)
+T3 = 100 m Testing a hundred minutes (default: 100 m)
+T4 = 100 Testing a hundred potatoes (default: 100)
+T2147483647 = 18446744073709551615 m Very large (default: 18446744073709551615 m)
+T-23 = 44 s Negative T number (default: 18446744073709551601 s)
+
+tdef_vty_test(config-net)# show running-config
+... !timer
+net
+ timer T1 9012345
+ timer T-23 44
+... !timer
+
+tdef_vty_test(config-net)# timer T1 default
+tdef_vty_test(config-net)# timer T-23 default
+
+tdef_vty_test(config-net)# show running-config
+... !timer
diff --git a/tests/tdef/tdef_vty_test_dynamic.c b/tests/tdef/tdef_vty_test_dynamic.c
new file mode 100644
index 00000000..20dae535
--- /dev/null
+++ b/tests/tdef/tdef_vty_test_dynamic.c
@@ -0,0 +1,362 @@
+/* Test implementation for osmo_tdef VTY configuration API. */
+/*
+ * (C) 2019 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * Author: Neels Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#define _GNU_SOURCE
+#include <getopt.h>
+#include <signal.h>
+#include <limits.h>
+#include <string.h>
+
+#include <osmocom/core/application.h>
+
+#include <osmocom/vty/command.h>
+#include <osmocom/vty/misc.h>
+#include <osmocom/vty/telnet_interface.h>
+
+#include <osmocom/core/tdef.h>
+#include <osmocom/vty/tdef_vty.h>
+
+#include <stdlib.h>
+
+#include "config.h"
+
+void *root_ctx = NULL;
+
+/* ------------------- HERE IS THE INTERESTING TDEF RELEVANT PART ------------------- */
+
+/* This example keeps a separate list of timers for each instance of a dynamically allocated instance of a VTY node,
+ * for example of keeping separate timers for each BTS in a BSC.
+ */
+
+static const struct osmo_tdef bts_default_tdefs[] = {
+ { .T=1111, .default_val=2, .desc="Dynamic Duo" },
+ { .T=2222, .default_val=1, .desc="BATMAN" },
+ { .T=3333, .default_val=12, .desc="Dadadadadadadadadadadada" },
+ { .T=4444, .default_val=500, .unit=OSMO_TDEF_MS, .desc="POW!" },
+ {}
+};
+
+
+/* Boilerplate dynamic VTY node ... */
+
+enum tdef_vty_test_nodes {
+ MEMBER_NODE = _LAST_OSMOVTY_NODE + 1,
+};
+
+static struct cmd_node member_node = {
+ MEMBER_NODE,
+ "%s(config-member)# ",
+ 1,
+};
+
+struct member {
+ struct llist_head entry;
+ char name[23];
+ struct osmo_tdef *tdefs;
+};
+
+LLIST_HEAD(all_members);
+
+struct member *member_alloc(const char *name)
+{
+ struct member *m = talloc_zero(root_ctx, struct member);
+ osmo_strlcpy(m->name, name, sizeof(m->name));
+
+ /* DYNAMIC TDEF COPIES */
+ m->tdefs = (struct osmo_tdef*)talloc_size(m, sizeof(bts_default_tdefs));
+ memcpy((char*)m->tdefs, (char*)&bts_default_tdefs, sizeof(bts_default_tdefs));
+ osmo_tdefs_reset(m->tdefs);
+
+ llist_add_tail(&m->entry, &all_members);
+ return m;
+}
+
+struct member *member_find(const char *name)
+{
+ struct member *m;
+ llist_for_each_entry(m, &all_members, entry) {
+ if (!strcmp(m->name, name))
+ return m;
+ }
+ return NULL;
+}
+
+DEFUN(cfg_member, cfg_member_cmd,
+ "member NAME",
+ "Enter member node\n" "Existing or new member node name\n")
+{
+ const char *name = argv[0];
+ struct member *m = member_find(name);
+ if (!m)
+ m = member_alloc(name);
+ vty->index = m;
+ vty->node = MEMBER_NODE;
+ return CMD_SUCCESS;
+}
+
+
+/* TDEF SPECIFIC VTY */
+
+static bool startswith(const char *str, const char *startswith_str)
+{
+ if (!startswith_str)
+ return true;
+ if (!str)
+ return false;
+ return strncmp(str, startswith_str, strlen(startswith_str)) == 0;
+}
+
+DEFUN(show_timer, show_member_timer_cmd,
+ "show member-timer [NAME] " OSMO_TDEF_VTY_ARG_T_OPTIONAL,
+ SHOW_STR "Show timers for a specific member" "member name\n"
+ OSMO_TDEF_VTY_DOC_T)
+{
+ const char *name = argc > 0 ? argv[0] : NULL;
+ struct member *m;
+ const char *T_arg = argc > 1 ? argv[1] : NULL;
+ int shown = 0;
+
+ llist_for_each_entry(m, &all_members, entry) {
+ if (!name || startswith(m->name, name)) {
+ osmo_tdef_vty_show_cmd(vty, m->tdefs, T_arg, "%11s: ", m->name);
+ shown ++;
+ }
+ }
+ if (!shown) {
+ vty_out(vty, "%% No such member: %s%s", name ? : "(none)", VTY_NEWLINE);
+ return CMD_WARNING;
+ }
+ return CMD_SUCCESS;
+}
+
+DEFUN(cfg_member_timer, cfg_member_timer_cmd,
+ "timer " OSMO_TDEF_VTY_ARG_SET_OPTIONAL,
+ "Configure or show timers for this member\n"
+ OSMO_TDEF_VTY_DOC_SET)
+{
+ struct member *m = vty->index;
+
+ if (!m || !m->tdefs) {
+ vty_out(vty, "%% No timers here%s", VTY_NEWLINE);
+ return CMD_WARNING;
+ }
+
+ /* If any arguments are missing, redirect to 'show' */
+ if (argc < 2) {
+ const char *T_arg = argc > 0 ? argv[0] : NULL;
+ return osmo_tdef_vty_show_cmd(vty, m->tdefs, T_arg, "%11s: ", m->name);
+ }
+
+ return osmo_tdef_vty_set_cmd(vty, m->tdefs, argv);
+}
+
+static int config_write_member(struct vty *vty)
+{
+ struct member *m;
+ llist_for_each_entry(m, &all_members, entry) {
+ vty_out(vty, "member %s%s", m->name, VTY_NEWLINE);
+ osmo_tdef_vty_write(vty, m->tdefs, " timer ");
+ }
+
+ return CMD_SUCCESS;
+}
+
+static void member_init_vty()
+{
+ install_node(&member_node, config_write_member);
+ install_element(CONFIG_NODE, &cfg_member_cmd);
+
+ install_element_ve(&show_member_timer_cmd);
+ install_element(MEMBER_NODE, &cfg_member_timer_cmd);
+}
+
+/* ------------------- THE REST is just boilerplate osmo main() ------------------- */
+
+static void print_help()
+{
+ printf( "options:\n"
+ " -h --help this text\n"
+ " -d --debug MASK Enable debugging (e.g. -d DRSL:DOML:DLAPDM)\n"
+ " -D --daemonize For the process into a background daemon\n"
+ " -c --config-file Specify the filename of the config file\n"
+ " -s --disable-color Don't use colors in stderr log output\n"
+ " -T --timestamp Prefix every log line with a timestamp\n"
+ " -V --version Print version information and exit\n"
+ " -e --log-level Set a global log-level\n"
+ );
+}
+
+static struct {
+ const char *config_file;
+ int daemonize;
+} cmdline_config = {};
+
+static void handle_options(int argc, char **argv)
+{
+ while (1) {
+ int option_idx = 0, c;
+ static const struct option long_options[] = {
+ { "help", 0, 0, 'h' },
+ { "debug", 1, 0, 'd' },
+ { "daemonize", 0, 0, 'D' },
+ { "config-file", 1, 0, 'c' },
+ { "disable-color", 0, 0, 's' },
+ { "timestamp", 0, 0, 'T' },
+ { "version", 0, 0, 'V' },
+ { "log-level", 1, 0, 'e' },
+ {}
+ };
+
+ c = getopt_long(argc, argv, "hc:d:Dc:sTVe:",
+ long_options, &option_idx);
+ if (c == -1)
+ break;
+
+ switch (c) {
+ case 'h':
+ print_help();
+ exit(0);
+ case 's':
+ log_set_use_color(osmo_stderr_target, 0);
+ break;
+ case 'd':
+ log_parse_category_mask(osmo_stderr_target, optarg);
+ break;
+ case 'D':
+ cmdline_config.daemonize = 1;
+ break;
+ case 'c':
+ cmdline_config.config_file = optarg;
+ break;
+ case 'T':
+ log_set_print_timestamp(osmo_stderr_target, 1);
+ break;
+ case 'e':
+ log_set_log_level(osmo_stderr_target, atoi(optarg));
+ break;
+ case 'V':
+ print_version(1);
+ exit(0);
+ break;
+ default:
+ /* catch unknown options *as well as* missing arguments. */
+ fprintf(stderr, "Error in command line options. Exiting.\n");
+ exit(-1);
+ }
+ }
+}
+
+static int quit = 0;
+
+static void signal_handler(int signal)
+{
+ fprintf(stdout, "signal %u received\n", signal);
+
+ switch (signal) {
+ case SIGINT:
+ case SIGTERM:
+ quit++;
+ break;
+ case SIGABRT:
+ osmo_generate_backtrace();
+ /* in case of abort, we want to obtain a talloc report
+ * and then return to the caller, who will abort the process */
+ case SIGUSR1:
+ talloc_report(tall_vty_ctx, stderr);
+ talloc_report_full(root_ctx, stderr);
+ break;
+ case SIGUSR2:
+ talloc_report_full(tall_vty_ctx, stderr);
+ break;
+ default:
+ break;
+ }
+}
+
+static struct vty_app_info vty_info = {
+ .name = "tdef_vty_test",
+ .version = PACKAGE_VERSION,
+};
+
+static const struct log_info_cat default_categories[] = {};
+
+const struct log_info log_info = {
+ .cat = default_categories,
+ .num_cat = ARRAY_SIZE(default_categories),
+};
+
+int main(int argc, char **argv)
+{
+ int rc;
+
+ root_ctx = talloc_named_const(NULL, 0, "tdef_vty_test");
+
+ osmo_init_logging2(root_ctx, &log_info);
+
+ vty_info.tall_ctx = root_ctx;
+ vty_init(&vty_info);
+ osmo_talloc_vty_add_cmds();
+
+ member_init_vty(); /* <--- relevant init for this example */
+
+ handle_options(argc, argv);
+
+ if (cmdline_config.config_file) {
+ rc = vty_read_config_file(cmdline_config.config_file, NULL);
+ if (rc < 0) {
+ fprintf(stderr, "Failed to parse the config file: '%s'\n", cmdline_config.config_file);
+ return 1;
+ }
+ }
+
+ rc = telnet_init_dynif(root_ctx, NULL, vty_get_bind_addr(), 42042);
+ if (rc < 0)
+ return 2;
+
+ signal(SIGINT, &signal_handler);
+ signal(SIGTERM, &signal_handler);
+ signal(SIGABRT, &signal_handler);
+ signal(SIGUSR1, &signal_handler);
+ signal(SIGUSR2, &signal_handler);
+ osmo_init_ignore_signals();
+
+ if (cmdline_config.daemonize) {
+ rc = osmo_daemonize();
+ if (rc < 0) {
+ perror("Error during daemonize");
+ return 6;
+ }
+ }
+
+ while (!quit) {
+ log_reset_context();
+ osmo_select_main(0);
+ }
+
+ talloc_free(root_ctx);
+ talloc_free(tall_vty_ctx);
+
+ return 0;
+}
diff --git a/tests/tdef/tdef_vty_test_dynamic.vty b/tests/tdef/tdef_vty_test_dynamic.vty
new file mode 100644
index 00000000..6aae746b
--- /dev/null
+++ b/tests/tdef/tdef_vty_test_dynamic.vty
@@ -0,0 +1,83 @@
+tdef_vty_test> list
+...
+ show member-timer [NAME] [TNNNN]
+...
+
+tdef_vty_test> enable
+tdef_vty_test# configure terminal
+
+tdef_vty_test(config)# member robin
+tdef_vty_test(config-member)# timer
+ robin: T1111 = 2 s Dynamic Duo (default: 2 s)
+ robin: T2222 = 1 s BATMAN (default: 1 s)
+ robin: T3333 = 12 s Dadadadadadadadadadadada (default: 12 s)
+ robin: T4444 = 500 ms POW! (default: 500 ms)
+
+tdef_vty_test(config-member)# timer T2222 423
+tdef_vty_test(config-member)# timer T2222
+ robin: T2222 = 423 s BATMAN (default: 1 s)
+
+tdef_vty_test(config-member)# timer
+ robin: T1111 = 2 s Dynamic Duo (default: 2 s)
+ robin: T2222 = 423 s BATMAN (default: 1 s)
+ robin: T3333 = 12 s Dadadadadadadadadadadada (default: 12 s)
+ robin: T4444 = 500 ms POW! (default: 500 ms)
+
+tdef_vty_test(config-member)# do show member-timer
+ robin: T1111 = 2 s Dynamic Duo (default: 2 s)
+ robin: T2222 = 423 s BATMAN (default: 1 s)
+ robin: T3333 = 12 s Dadadadadadadadadadadada (default: 12 s)
+ robin: T4444 = 500 ms POW! (default: 500 ms)
+
+tdef_vty_test(config-member)# exit
+
+tdef_vty_test(config)# member batman
+tdef_vty_test(config-member)# timer 3333 17
+tdef_vty_test(config-member)# timer 3333
+ batman: T3333 = 17 s Dadadadadadadadadadadada (default: 12 s)
+
+tdef_vty_test(config-member)# show running-config
+
+Current configuration:
+...
+member robin
+ timer T2222 423
+member batman
+ timer T3333 17
+...
+
+tdef_vty_test(config-member)# timer 3333 default
+
+tdef_vty_test(config-member)# show running-config
+...
+member robin
+ timer T2222 423
+member batman
+... !timer
+
+tdef_vty_test(config-member)# exit
+tdef_vty_test(config)# exit
+tdef_vty_test# show member-timer
+ robin: T1111 = 2 s Dynamic Duo (default: 2 s)
+ robin: T2222 = 423 s BATMAN (default: 1 s)
+ robin: T3333 = 12 s Dadadadadadadadadadadada (default: 12 s)
+ robin: T4444 = 500 ms POW! (default: 500 ms)
+ batman: T1111 = 2 s Dynamic Duo (default: 2 s)
+ batman: T2222 = 1 s BATMAN (default: 1 s)
+ batman: T3333 = 12 s Dadadadadadadadadadadada (default: 12 s)
+ batman: T4444 = 500 ms POW! (default: 500 ms)
+
+tdef_vty_test# show member-timer batman
+ batman: T1111 = 2 s Dynamic Duo (default: 2 s)
+ batman: T2222 = 1 s BATMAN (default: 1 s)
+ batman: T3333 = 12 s Dadadadadadadadadadadada (default: 12 s)
+ batman: T4444 = 500 ms POW! (default: 500 ms)
+
+tdef_vty_test# show member-timer robin
+ robin: T1111 = 2 s Dynamic Duo (default: 2 s)
+ robin: T2222 = 423 s BATMAN (default: 1 s)
+ robin: T3333 = 12 s Dadadadadadadadadadadada (default: 12 s)
+ robin: T4444 = 500 ms POW! (default: 500 ms)
+
+tdef_vty_test# show member-timer joker
+% No such member: joker
diff --git a/tests/testsuite.at b/tests/testsuite.at
index 6aaaa787..0093403a 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -325,3 +325,9 @@ AT_KEYWORDS([gsm23003])
cat $abs_srcdir/gsm23003/gsm23003_test.ok > expout
AT_CHECK([$abs_top_builddir/tests/gsm23003/gsm23003_test], [0], [expout], [ignore])
AT_CLEANUP
+
+AT_SETUP([tdef])
+AT_KEYWORDS([tdef])
+cat $abs_srcdir/tdef/tdef_test.ok > expout
+AT_CHECK([$abs_top_builddir/tests/tdef/tdef_test], [0], [expout], [ignore])
+AT_CLEANUP