lib: add time formatting printfrr exts

Refer to docs in doc/developer for details.

Signed-off-by: David Lamparter <equinox@opensourcerouting.org>
This commit is contained in:
David Lamparter 2022-01-13 07:51:54 +01:00
parent 2c5b4d80ef
commit 2c76ba433f
5 changed files with 630 additions and 0 deletions

View File

@ -219,6 +219,122 @@ Networking data types
:frrfmtout:`SOCK_STREAM`
Time/interval formats
^^^^^^^^^^^^^^^^^^^^^
.. frrfmt:: %pTS (struct timespec *)
.. frrfmt:: %pTV (struct timeval *)
.. frrfmt:: %pTT (time_t *)
Above 3 options internally result in the same code being called, support
the same flags and produce equal output with one exception: ``%pTT``
has no sub-second precision and the formatter will never print a
(nonsensical) ``.000``.
Exactly one of ``I``, ``M`` or ``R`` must immediately follow after
``TS``/``TV``/``TT`` to specify whether the input is an interval, monotonic
timestamp or realtime timestamp:
``%pTVI``: input is an interval, not a timestamp. Print interval.
``%pTVIs``: input is an interval, convert to wallclock by subtracting it
from current time (i.e. interval has passed **s**\ ince.)
``%pTVIu``: input is an interval, convert to wallclock by adding it to
current time (i.e. **u**\ ntil interval has passed.)
``%pTVM`` - input is a timestamp on CLOCK_MONOTONIC, convert to wallclock
time (by grabbing current CLOCK_MONOTONIC and CLOCK_REALTIME and doing the
math) and print calendaric date.
``%pTVMs`` - input is a timestamp on CLOCK_MONOTONIC, print interval
**s**\ ince that timestamp (elapsed.)
``%pTVMu`` - input is a timestamp on CLOCK_MONOTONIC, print interval
**u**\ ntil that timestamp (deadline.)
``%pTVR`` - input is a timestamp on CLOCK_REALTIME, print calendaric date.
``%pTVRs`` - input is a timestamp on CLOCK_REALTIME, print interval
**s**\ ince that timestamp.
``%pTVRu`` - input is a timestamp on CLOCK_REALTIME, print interval
**u**\ ntil that timestamp.
``%pTVA`` - reserved for CLOCK_TAI in case a PTP implementation is
interfaced to FRR. Not currently implemented.
.. note::
If ``%pTVRs`` or ``%pTVRu`` are used, this is generally an indication
that a CLOCK_MONOTONIC timestamp should be used instead (or added in
parallel.) CLOCK_REALTIME might be adjusted by NTP, PTP or similar
procedures, causing bogus intervals to be printed.
``%pTVM`` on first look might be assumed to have the same problem, but
on closer thought the assumption is always that current system time is
correct. And since a CLOCK_MONOTONIC interval is also quite safe to
assume to be correct, the (past) absolute timestamp to be printed from
this can likely be correct even if it doesn't match what CLOCK_REALTIME
would have indicated at that point in the past. This logic does,
however, not quite work for *future* times.
Generally speaking, almost all use cases in FRR should (and do) use
CLOCK_MONOTONIC (through :c:func:`monotime()`.)
Flags common to printing calendar times and intervals:
``p``: include spaces in appropriate places (depends on selected format.)
``%p.3TV...``: specify sub-second resolution (use with ``FMT_NSTD`` to
suppress gcc warning.) As noted above, ``%pTT`` will never print sub-second
digits since there are none. Only some formats support printing sub-second
digits and the default may vary.
The following flags are available for printing calendar times/dates:
(no flag): :frrfmtout:`Sat Jan 1 00:00:00 2022` - print output from
``ctime()``, in local time zone. Since FRR does not currently use/enable
locale support, this is always the C locale. (Locale support getting added
is unlikely for the time being and would likely break other things worse
than this.)
``i``: :frrfmtout:`2022-01-01T00:00:00.123` - ISO8601 timestamp in local
time zone (note there is no ``Z`` or ``+00:00`` suffix.) Defaults to
millisecond precision.
``ip``: :frrfmtout:`2022-01-01 00:00:00.123` - use readable form of ISO8601
with space instead of ``T`` separator.
The following flags are available for printing intervals:
(no flag): :frrfmtout:`9w9d09:09:09.123` - does not match any
preexisting format; added because it does not lose precision (like ``t``)
for longer intervals without printing huge numbers (like ``h``/``m``).
Defaults to millisecond precision. The week/day fields are left off if
they're zero, ``p`` adds a space after the respective letter.
``t``: :frrfmtout:`9w9d09h`, :frrfmtout:`9d09h09m`, :frrfmtout:`09:09:09` -
this replaces :c:func:`frrtime_to_interval()`. ``p`` adds spaces after
week/day/hour letters.
``d``: print decimal number of seconds. Defaults to millisecond precision.
``x`` / ``tx`` / ``dx``: Like no flag / ``t`` / ``d``, but print
:frrfmtout:`-` for zero or negative intervals (for use with unset timers.)
``h``: :frrfmtout:`09:09:09`
``hx``: :frrfmtout:`09:09:09`, :frrfmtout:`--:--:--` - this replaces
:c:func:`pim_time_timer_to_hhmmss()`.
``m``: :frrfmtout:`09:09`
``mx``: :frrfmtout:`09:09`, :frrfmtout:`--:--` - this replaces
:c:func:`pim_time_timer_to_mmss()`.
General utility formats
^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -25,6 +25,9 @@
extern "C" {
#endif
struct fbuf;
struct printfrr_eargs;
#ifndef TIMESPEC_TO_TIMEVAL
/* should be in sys/time.h on BSD & Linux libcs */
#define TIMESPEC_TO_TIMEVAL(tv, ts) \
@ -42,6 +45,31 @@ extern "C" {
} while (0)
#endif
/* Linux/glibc is sadly missing these timespec helpers */
#ifndef timespecadd
#define timespecadd(tsp, usp, vsp) \
do { \
(vsp)->tv_sec = (tsp)->tv_sec + (usp)->tv_sec; \
(vsp)->tv_nsec = (tsp)->tv_nsec + (usp)->tv_nsec; \
if ((vsp)->tv_nsec >= 1000000000L) { \
(vsp)->tv_sec++; \
(vsp)->tv_nsec -= 1000000000L; \
} \
} while (0)
#endif
#ifndef timespecsub
#define timespecsub(tsp, usp, vsp) \
do { \
(vsp)->tv_sec = (tsp)->tv_sec - (usp)->tv_sec; \
(vsp)->tv_nsec = (tsp)->tv_nsec - (usp)->tv_nsec; \
if ((vsp)->tv_nsec < 0) { \
(vsp)->tv_sec--; \
(vsp)->tv_nsec += 1000000000L; \
} \
} while (0)
#endif
static inline time_t monotime(struct timeval *tvo)
{
struct timespec ts;
@ -132,6 +160,53 @@ static inline const char *frrtime_to_interval(time_t t, char *buf,
return buf;
}
enum {
/* n/a - input was seconds precision, don't print any fractional */
TIMEFMT_SECONDS = (1 << 0),
/* caller is directly invoking printfrr_time and has pre-specified
* I/Iu/Is/M/Mu/Ms/R/Ru/Rs (for printing timers)
*/
TIMEFMT_PRESELECT = (1 << 1),
/* don't print any output - this is needed for invoking printfrr_time
* from another printfrr extensions to skip over flag characters
*/
TIMEFMT_SKIP = (1 << 2),
/* use spaces in appropriate places */
TIMEFMT_SPACE = (1 << 3),
/* input interpretations: */
TIMEFMT_REALTIME = (1 << 8),
TIMEFMT_MONOTONIC = (1 << 9),
TIMEFMT_SINCE = (1 << 10),
TIMEFMT_UNTIL = (1 << 11),
TIMEFMT_ABSOLUTE = TIMEFMT_REALTIME | TIMEFMT_MONOTONIC,
TIMEFMT_ANCHORS = TIMEFMT_SINCE | TIMEFMT_UNTIL,
/* calendaric formats: */
TIMEFMT_ISO8601 = (1 << 16),
/* interval formats: */
/* 't' - use [t]raditional 3-block format */
TIMEFMT_BASIC = (1 << 24),
/* 'm' - select mm:ss */
TIMEFMT_MMSS = (1 << 25),
/* 'h' - select hh:mm:ss */
TIMEFMT_HHMMSS = (1 << 26),
/* 'd' - print as decimal number of seconds */
TIMEFMT_DECIMAL = (1 << 27),
/* 'mx'/'hx' - replace zero value with "--:--" or "--:--:--" */
TIMEFMT_DASHES = (1 << 31),
/* helpers for reference */
TIMEFMT_TIMER_DEADLINE =
TIMEFMT_PRESELECT | TIMEFMT_MONOTONIC | TIMEFMT_UNTIL,
TIMEFMT_TIMER_INTERVAL = TIMEFMT_PRESELECT,
};
extern ssize_t printfrr_time(struct fbuf *buf, struct printfrr_eargs *ea,
const struct timespec *ts, unsigned int flags);
#ifdef __cplusplus
}
#endif

View File

@ -286,6 +286,10 @@ struct va_format {
#pragma FRR printfrr_ext "%pSE" (char *)
#pragma FRR printfrr_ext "%pSQ" (char *)
#pragma FRR printfrr_ext "%pTS" (struct timespec *)
#pragma FRR printfrr_ext "%pTV" (struct timeval *)
#pragma FRR printfrr_ext "%pTT" (time_t *)
#endif
/* when using non-ISO-C compatible extension specifiers... */

View File

@ -22,8 +22,10 @@
#include <string.h>
#include <ctype.h>
#include <time.h>
#include "printfrr.h"
#include "monotime.h"
printfrr_ext_autoreg_p("HX", printfrr_hexdump)
static ssize_t printfrr_hexdump(struct fbuf *buf, struct printfrr_eargs *ea,
@ -270,3 +272,326 @@ static ssize_t printfrr_quote(struct fbuf *buf, struct printfrr_eargs *ea,
ret += bputch(buf, '"');
return ret;
}
static ssize_t printfrr_abstime(struct fbuf *buf, struct printfrr_eargs *ea,
const struct timespec *ts, unsigned int flags);
static ssize_t printfrr_reltime(struct fbuf *buf, struct printfrr_eargs *ea,
const struct timespec *ts, unsigned int flags);
ssize_t printfrr_time(struct fbuf *buf, struct printfrr_eargs *ea,
const struct timespec *ts, unsigned int flags)
{
bool have_abs, have_anchor;
if (!(flags & TIMEFMT_PRESELECT)) {
switch (ea->fmt[0]) {
case 'I':
/* no bit set */
break;
case 'M':
flags |= TIMEFMT_MONOTONIC;
break;
case 'R':
flags |= TIMEFMT_REALTIME;
break;
default:
return bputs(buf,
"{invalid time format input specifier}");
}
ea->fmt++;
if (ea->fmt[0] == 's') {
flags |= TIMEFMT_SINCE;
ea->fmt++;
} else if (ea->fmt[0] == 'u') {
flags |= TIMEFMT_UNTIL;
ea->fmt++;
}
}
have_abs = !!(flags & TIMEFMT_ABSOLUTE);
have_anchor = !!(flags & TIMEFMT_ANCHORS);
if (have_abs ^ have_anchor)
return printfrr_abstime(buf, ea, ts, flags);
else
return printfrr_reltime(buf, ea, ts, flags);
}
static ssize_t do_subsec(struct fbuf *buf, const struct timespec *ts,
int precision, unsigned int flags)
{
unsigned long long frac;
if (precision <= 0 || (flags & TIMEFMT_SECONDS))
return 0;
frac = ts->tv_nsec;
if (precision > 9)
precision = 9;
for (int i = precision; i < 9; i++)
frac /= 10;
return bprintfrr(buf, ".%0*llu", precision, frac);
}
static ssize_t printfrr_abstime(struct fbuf *buf, struct printfrr_eargs *ea,
const struct timespec *ts, unsigned int flags)
{
struct timespec real_ts[1];
struct tm tm;
char cbuf[32] = ""; /* manpage says 26 for ctime_r */
ssize_t ret = 0;
int precision = ea->precision;
while (ea->fmt[0]) {
char ch = *ea->fmt++;
switch (ch) {
case 'p':
flags |= TIMEFMT_SPACE;
continue;
case 'i':
flags |= TIMEFMT_ISO8601;
continue;
}
ea->fmt--;
break;
}
if (flags & TIMEFMT_SKIP)
return 0;
if (flags & TIMEFMT_REALTIME)
*real_ts = *ts;
else if (flags & TIMEFMT_MONOTONIC) {
struct timespec mono_now[1];
clock_gettime(CLOCK_REALTIME, real_ts);
clock_gettime(CLOCK_MONOTONIC, mono_now);
timespecsub(real_ts, mono_now, real_ts);
timespecadd(real_ts, ts, real_ts);
} else {
clock_gettime(CLOCK_REALTIME, real_ts);
if (flags & TIMEFMT_SINCE)
timespecsub(real_ts, ts, real_ts);
else /* flags & TIMEFMT_UNTIL */
timespecadd(real_ts, ts, real_ts);
}
localtime_r(&real_ts->tv_sec, &tm);
if (flags & TIMEFMT_ISO8601) {
if (flags & TIMEFMT_SPACE)
strftime(cbuf, sizeof(cbuf), "%Y-%m-%d %H:%M:%S", &tm);
else
strftime(cbuf, sizeof(cbuf), "%Y-%m-%dT%H:%M:%S", &tm);
ret += bputs(buf, cbuf);
if (precision == -1)
precision = 3;
ret += do_subsec(buf, real_ts, precision, flags);
} else {
size_t len;
asctime_r(&tm, cbuf);
len = strlen(cbuf);
if (!len)
/* WTF. */
return 0;
if (cbuf[len - 1] == '\n')
cbuf[len - 1] = '\0';
ret += bputs(buf, cbuf);
}
return ret;
}
static ssize_t printfrr_reltime(struct fbuf *buf, struct printfrr_eargs *ea,
const struct timespec *ts, unsigned int flags)
{
struct timespec real_ts[1];
ssize_t ret = 0;
const char *space = "";
const char *dashes = "-";
int precision = ea->precision;
while (ea->fmt[0]) {
char ch = *ea->fmt++;
switch (ch) {
case 'p':
flags |= TIMEFMT_SPACE;
space = " ";
continue;
case 't':
flags |= TIMEFMT_BASIC;
continue;
case 'd':
flags |= TIMEFMT_DECIMAL;
continue;
case 'm':
flags |= TIMEFMT_MMSS;
dashes = "--:--";
continue;
case 'h':
flags |= TIMEFMT_HHMMSS;
dashes = "--:--:--";
continue;
case 'x':
flags |= TIMEFMT_DASHES;
continue;
}
ea->fmt--;
break;
}
if (flags & TIMEFMT_SKIP)
return 0;
if (flags & TIMEFMT_ABSOLUTE) {
struct timespec anchor[1];
if (flags & TIMEFMT_REALTIME)
clock_gettime(CLOCK_REALTIME, anchor);
else
clock_gettime(CLOCK_MONOTONIC, anchor);
if (flags & TIMEFMT_UNTIL)
timespecsub(ts, anchor, real_ts);
else /* flags & TIMEFMT_SINCE */
timespecsub(anchor, ts, real_ts);
} else
*real_ts = *ts;
if (real_ts->tv_sec == 0 && real_ts->tv_nsec == 0 &&
(flags & TIMEFMT_DASHES))
return bputs(buf, dashes);
if (real_ts->tv_sec < 0) {
if (flags & TIMEFMT_DASHES)
return bputs(buf, dashes);
/* -0.3s is { -1s + 700ms } */
real_ts->tv_sec = -real_ts->tv_sec - 1;
real_ts->tv_nsec = 1000000000L - real_ts->tv_nsec;
if (real_ts->tv_nsec >= 1000000000L) {
real_ts->tv_sec++;
real_ts->tv_nsec -= 1000000000L;
}
/* all formats have a - make sense in front */
ret += bputch(buf, '-');
}
if (flags & TIMEFMT_DECIMAL) {
ret += bprintfrr(buf, "%lld", (long long)real_ts->tv_sec);
if (precision == -1)
precision = 3;
ret += do_subsec(buf, real_ts, precision, flags);
return ret;
}
/* these divisions may be slow on embedded boxes, hence only do the
* ones we need, plus the ?: zero check to hopefully skip zeros fast
*/
lldiv_t min_sec = lldiv(real_ts->tv_sec, 60);
if (flags & TIMEFMT_MMSS) {
ret += bprintfrr(buf, "%02lld:%02lld", min_sec.quot,
min_sec.rem);
ret += do_subsec(buf, real_ts, precision, flags);
return ret;
}
lldiv_t hour_min = min_sec.quot ? lldiv(min_sec.quot, 60) : (lldiv_t){};
if (flags & TIMEFMT_HHMMSS) {
ret += bprintfrr(buf, "%02lld:%02lld:%02lld", hour_min.quot,
hour_min.rem, min_sec.rem);
ret += do_subsec(buf, real_ts, precision, flags);
return ret;
}
lldiv_t day_hour =
hour_min.quot ? lldiv(hour_min.quot, 24) : (lldiv_t){};
lldiv_t week_day =
day_hour.quot ? lldiv(day_hour.quot, 7) : (lldiv_t){};
/* if sub-second precision is not supported, return */
if (flags & TIMEFMT_BASIC) {
/* match frrtime_to_interval (without space flag) */
if (week_day.quot)
ret += bprintfrr(buf, "%lldw%s%lldd%s%02lldh",
week_day.quot, space, week_day.rem,
space, day_hour.rem);
else if (day_hour.quot)
ret += bprintfrr(buf, "%lldd%s%02lldh%s%02lldm",
day_hour.quot, space, day_hour.rem,
space, hour_min.rem);
else
ret += bprintfrr(buf, "%02lld:%02lld:%02lld",
hour_min.quot, hour_min.rem,
min_sec.rem);
/* no sub-seconds here */
return ret;
}
/* default format */
if (week_day.quot)
ret += bprintfrr(buf, "%lldw%s", week_day.quot, space);
if (week_day.rem || week_day.quot)
ret += bprintfrr(buf, "%lldd%s", week_day.rem, space);
ret += bprintfrr(buf, "%02lld:%02lld:%02lld", day_hour.rem,
hour_min.rem, min_sec.rem);
if (precision == -1)
precision = 3;
ret += do_subsec(buf, real_ts, precision, flags);
return ret;
}
printfrr_ext_autoreg_p("TS", printfrr_ts)
static ssize_t printfrr_ts(struct fbuf *buf, struct printfrr_eargs *ea,
const void *vptr)
{
const struct timespec *ts = vptr;
if (!ts)
return bputs(buf, "(null)");
return printfrr_time(buf, ea, ts, 0);
}
printfrr_ext_autoreg_p("TV", printfrr_tv)
static ssize_t printfrr_tv(struct fbuf *buf, struct printfrr_eargs *ea,
const void *vptr)
{
const struct timeval *tv = vptr;
struct timespec ts;
if (!tv)
return bputs(buf, "(null)");
ts.tv_sec = tv->tv_sec;
ts.tv_nsec = tv->tv_usec * 1000;
return printfrr_time(buf, ea, &ts, 0);
}
printfrr_ext_autoreg_p("TT", printfrr_tt)
static ssize_t printfrr_tt(struct fbuf *buf, struct printfrr_eargs *ea,
const void *vptr)
{
const time_t *tt = vptr;
struct timespec ts;
if (!tt)
return bputs(buf, "(null)");
ts.tv_sec = *tt;
ts.tv_nsec = 0;
return printfrr_time(buf, ea, &ts, TIMEFMT_SECONDS);
}

View File

@ -278,5 +278,115 @@ int main(int argc, char **argv)
inet_pton(AF_INET6, "fe2c::34", &nh.gate.ipv6);
printchk("fe2c::34", "%pNHcg", &nh);
/* time printing */
/* need a non-UTC timezone for testing */
setenv("TZ", "TEST-01:00", 1);
tzset();
struct timespec ts;
struct timeval tv;
time_t tt;
ts.tv_sec = tv.tv_sec = tt = 1642015880;
ts.tv_nsec = 123456789;
tv.tv_usec = 234567;
printchk("Wed Jan 12 20:31:20 2022", "%pTSR", &ts);
printchk("Wed Jan 12 20:31:20 2022", "%pTVR", &tv);
printchk("Wed Jan 12 20:31:20 2022", "%pTTR", &tt);
FMT_NSTD(printchk("Wed Jan 12 20:31:20 2022", "%.3pTSR", &ts));
printchk("2022-01-12T20:31:20.123", "%pTSRi", &ts);
printchk("2022-01-12 20:31:20.123", "%pTSRip", &ts);
printchk("2022-01-12 20:31:20.123", "%pTSRpi", &ts);
FMT_NSTD(printchk("2022-01-12T20:31:20", "%.0pTSRi", &ts));
FMT_NSTD(printchk("2022-01-12T20:31:20.123456789", "%.9pTSRi", &ts));
FMT_NSTD(printchk("2022-01-12T20:31:20", "%.3pTTRi", &tt));
ts.tv_sec = tv.tv_sec = tt = 9 * 86400 + 12345;
printchk("1w 2d 03:25:45.123", "%pTSIp", &ts);
printchk("1w2d03:25:45.123", "%pTSI", &ts);
printchk("1w2d03:25:45.234", "%pTVI", &tv);
printchk("1w2d03:25:45", "%pTTI", &tt);
printchk("1w 2d 03h", "%pTVItp", &tv);
printchk("1w2d03h", "%pTSIt", &ts);
printchk("219:25:45", "%pTVIh", &tv);
printchk("13165:45", "%pTVIm", &tv);
ts.tv_sec = tv.tv_sec = tt = 1 * 86400 + 12345;
printchk("1d 03:25:45.123", "%pTSIp", &ts);
printchk("1d03:25:45.234", "%pTVI", &tv);
printchk("1d 03h 25m", "%pTVItp", &tv);
printchk("1d03h25m", "%pTSIt", &ts);
printchk("98745.234", "%pTVId", &tv);
printchk("27:25:45", "%pTVIh", &tv);
printchk("1645:45", "%pTVIm", &tv);
ts.tv_sec = tv.tv_sec = tt = 12345;
printchk("03:25:45.123", "%pTSIp", &ts);
printchk("03:25:45.123", "%pTSI", &ts);
printchk("03:25:45.234", "%pTVI", &tv);
printchk("03:25:45", "%pTTI", &tt);
printchk("03:25:45", "%pTSItp", &ts);
printchk("03:25:45", "%pTVIt", &tv);
printchk("12345.234", "%pTVId", &tv);
printchk("03:25:45", "%pTVIh", &tv);
printchk("205:45", "%pTVIm", &tv);
ts.tv_sec = tv.tv_sec = tt = 0;
printchk("00:00:00.123", "%pTSIp", &ts);
printchk("00:00:00.123", "%pTSI", &ts);
printchk("00:00:00.234", "%pTVI", &tv);
printchk("00:00:00", "%pTTI", &tt);
printchk("00:00:00", "%pTVItp", &tv);
printchk("00:00:00", "%pTSIt", &ts);
printchk("0.234", "%pTVId", &tv);
printchk("0.234", "%pTVIdx", &tv);
printchk("-", "%pTTIdx", &tt);
printchk("00:00:00", "%pTVIhx", &tv);
printchk("--:--:--", "%pTTIhx", &tt);
printchk("00:00", "%pTVImx", &tv);
printchk("--:--", "%pTTImx", &tt);
ts.tv_sec = tv.tv_sec = tt = -10;
printchk("-00:00:09.876", "%pTSIp", &ts);
printchk("-00:00:09.876", "%pTSI", &ts);
printchk("-00:00:09.765", "%pTVI", &tv);
printchk("-00:00:10", "%pTTI", &tt);
printchk("-00:00:09", "%pTSItp", &ts);
printchk("-00:00:09", "%pTSIt", &ts);
printchk("-00:00:09", "%pTVIt", &tv);
printchk("-00:00:10", "%pTTIt", &tt);
printchk("-9.765", "%pTVId", &tv);
printchk("-", "%pTVIdx", &tv);
printchk("-00:00:09", "%pTSIh", &ts);
printchk("--:--:--", "%pTVIhx", &tv);
printchk("--:--:--", "%pTTIhx", &tt);
printchk("-00:09", "%pTSIm", &ts);
printchk("--:--", "%pTVImx", &tv);
printchk("--:--", "%pTTImx", &tt);
return !!errors;
}