diff --git a/bash-completion/Makemodule.am b/bash-completion/Makemodule.am index ac8926f2d..2763895e8 100644 --- a/bash-completion/Makemodule.am +++ b/bash-completion/Makemodule.am @@ -347,5 +347,8 @@ endif if BUILD_ENOSYS dist_bashcompletion_DATA += bash-completion/enosys endif +if BUILD_LSCLOCKS +dist_bashcompletion_DATA += bash-completion/lsclocks +endif endif # BUILD_BASH_COMPLETION diff --git a/bash-completion/lsclocks b/bash-completion/lsclocks new file mode 100644 index 000000000..bd81d88bc --- /dev/null +++ b/bash-completion/lsclocks @@ -0,0 +1,48 @@ +_lsclocks_module() +{ + local cur prev OPTS + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + case $prev in + '-J'|'--json') + return 0 + ;; + '-n'|'--noheadings') + return 0 + ;; + '-o'|'--output') + return 0 + ;; + '-r'|'--raw') + return 0 + ;; + '-t'|'--time') + clocks="$(command "$1" --noheadings --raw --output NAME)" + COMPREPLY=( $(compgen -W "$clocks" -- "$cur") ) + return 0 + ;; + '-h'|'--help'|'-V'|'--version') + return 0 + ;; + esac + case $cur in + -*) + OPTS="--json + --noheadings + --output + --raw + --time + --help + --version" + COMPREPLY=( $(compgen -W "${OPTS[*]}" -- $cur) ) + return 0 + ;; + *) + return 0 + ;; + esac + return 0 +} +complete -F _lsclocks_module lsclocks diff --git a/configure.ac b/configure.ac index 8bd10ee69..8906e8e7f 100644 --- a/configure.ac +++ b/configure.ac @@ -1894,6 +1894,9 @@ AS_IF([test "x$build_enosys" = xyes], [ ]) AM_CONDITIONAL([BUILD_ENOSYS], [test "x$build_enosys" = xyes]) +UL_BUILD_INIT([lsclocks], [yes]) +AM_CONDITIONAL([BUILD_LSCLOCKS], [test "x$build_lsclocks" = xyes]) + UL_BUILD_INIT([getopt], [yes]) AM_CONDITIONAL([BUILD_GETOPT], [test "x$build_getopt" = xyes]) diff --git a/meson.build b/meson.build index 37141e7db..6c6dc527b 100644 --- a/meson.build +++ b/meson.build @@ -2908,6 +2908,19 @@ if cc.compiles(fs.read('include/audit-arch.h'), name : 'has AUDIT_ARCH_NATIVE') endif endif +exe = executable( + 'lsclocks', + lsclocks_sources, + include_directories : includes, + link_with : [lib_common, lib_smartcols], + install_dir : usrbin_exec_dir, + install : true) +if not is_disabler(exe) + exes += exe + manadocs += ['misc-utils/lsclocks.1.adoc'] + bashcompletions += ['lsclocks'] +endif + ############################################################ opt = not get_option('build-schedutils').disabled() diff --git a/misc-utils/Makemodule.am b/misc-utils/Makemodule.am index dcc733b3f..adee4111f 100644 --- a/misc-utils/Makemodule.am +++ b/misc-utils/Makemodule.am @@ -316,3 +316,11 @@ enosys_SOURCES = misc-utils/enosys.c enosys_LDADD = $(LDADD) libcommon.la enosys_CFLAGS = $(AM_CFLAGS) endif + +if BUILD_LSCLOCKS +usrbin_exec_PROGRAMS += lsclocks +MANPAGES += misc-utils/lsclocks.1 +lsclocks_SOURCES = misc-utils/lsclocks.c +lsclocks_LDADD = $(LDADD) libcommon.la libsmartcols.la +lsclocks_CFLAGS = $(AM_CFLAGS) -I$(ul_libsmartcols_incdir) +endif diff --git a/misc-utils/lsclocks.1.adoc b/misc-utils/lsclocks.1.adoc new file mode 100644 index 000000000..5c7f5eac4 --- /dev/null +++ b/misc-utils/lsclocks.1.adoc @@ -0,0 +1,81 @@ +//po4a: entry man manual += lsclocks(1) +:doctype: manpage +:man manual: User Commands +:man source: util-linux {release-version} +:page-layout: base +:command: lsclocks + +== NAME + +lsclocks - display system clocks + +== SYNOPSIS + +*lsclocks* [option] + +== DESCRIPTION + +*lsclocks* is a simple command to display system clocks. + +It allows to display information like current time and resolutionof clocks like +CLOCK_MONOTONIC, CLOCK_REALTIME and CLOCK_BOOTTIME. + +== OPTIONS + +*-J*, *--json*:: +Use JSON output format. + +*-n*, *--noheadings*:: +Don't print headings. + +*-o*, *--output* _list_:: +Specify which output columns to print. See the *OUTPUT COLUMNS* +section for details of available columns. + +*-r*, *--raw*:: +Use raw output format. + +*-r*, *--time* _clock_ +Show current time of one specific clocks. + +include::man-common/help-version.adoc[] + +== OUTPUT COLUMNS + +Each column has a type. Types are surround by < and >. + +ID <``number``>:: +Numeric clock ID. + +CLOCK <``string``>:: +Name in the form *CLOCK_* + +NAME <``string``>:: +Shorter, easier to read name. + +TIME <``number``>:: +Current clock timestamp as returned by *clock_gettime()*. + +ISO_TIME <``string``>:: +ISO8601 formatted version of *TIME*. + +RESOLUTION <``number``>:: +Clock resolution as returned by *clock_getres()*. + + +== AUTHORS + +mailto:thomas@t-8ch.de[Thomas Weißschuh] + +== SEE ALSO + +*clock_getres*(2) *clock_gettime*(2) + +include::man-common/bugreports.adoc[] + +include::man-common/footer.adoc[] + +ifdef::translation[] +include::man-common/translation.adoc[] +endif::[] diff --git a/misc-utils/lsclocks.c b/misc-utils/lsclocks.c new file mode 100644 index 000000000..8531e9fe3 --- /dev/null +++ b/misc-utils/lsclocks.c @@ -0,0 +1,366 @@ +/* + * lsclocks(1) - display system clocks + * + * Copyright (C) 2023 Thomas Weißschuh + * + * 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 would 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 +#include +#include +#include +#include + +#include + +#include "c.h" +#include "nls.h" +#include "strutils.h" +#include "timeutils.h" +#include "closestream.h" +#include "xalloc.h" + +#define CLOCKFD 3 +#define FD_TO_CLOCKID(fd) ((~(clockid_t) (fd) << 3) | CLOCKFD) + +#ifndef CLOCK_REALTIME +#define CLOCK_REALTIME 0 +#endif + +#ifndef CLOCK_MONOTONIC +#define CLOCK_MONOTONIC 1 +#endif + +#ifndef CLOCK_MONOTONIC_RAW +#define CLOCK_MONOTONIC_RAW 4 +#endif + +#ifndef CLOCK_REALTIME_COARSE +#define CLOCK_REALTIME_COARSE 5 +#endif + +#ifndef CLOCK_MONOTONIC_COARSE +#define CLOCK_MONOTONIC_COARSE 6 +#endif + +#ifndef CLOCK_BOOTTIME +#define CLOCK_BOOTTIME 7 +#endif + +#ifndef CLOCK_REALTIME_ALARM +#define CLOCK_REALTIME_ALARM 8 +#endif + +#ifndef CLOCK_BOOTTIME_ALARM +#define CLOCK_BOOTTIME_ALARM 9 +#endif + +#ifndef CLOCK_TAI +#define CLOCK_TAI 11 +#endif + +struct clockinfo { + clockid_t id; + const char * const id_name; + const char * const name; +}; + +static const struct clockinfo clocks[] = { + { CLOCK_REALTIME, "CLOCK_REALTIME", "realtime" }, + { CLOCK_MONOTONIC, "CLOCK_MONOTONIC", "monotonic" }, + { CLOCK_MONOTONIC_RAW, "CLOCK_MONOTONIC_RAW", "monotonic-raw" }, + { CLOCK_REALTIME_COARSE, "CLOCK_REALTIME_COARSE", "realtime-coarse" }, + { CLOCK_MONOTONIC_COARSE, "CLOCK_MONOTONIC_COARSE", "monotonic-coarse" }, + { CLOCK_BOOTTIME, "CLOCK_BOOTTIME", "boottime" }, + { CLOCK_REALTIME_ALARM, "CLOCK_REALTIME_ALARM", "realtime-alarm" }, + { CLOCK_BOOTTIME_ALARM, "CLOCK_BOOTTIME_ALARM", "boottime-alarm" }, + { CLOCK_TAI, "CLOCK_TAI", "tai" }, +}; + +/* column IDs */ +enum { + COL_ID, + COL_CLOCK, + COL_NAME, + COL_TIME, + COL_ISO_TIME, + COL_RESOLUTION, +}; + +/* column names */ +struct colinfo { + const char * const name; /* header */ + double whint; /* width hint (N < 1 is in percent of termwidth) */ + int flags; /* SCOLS_FL_* */ + int json_type; /* SCOLS_JSON_* */ + const char * const help; +}; + +/* columns descriptions */ +static const struct colinfo infos[] = { + [COL_ID] = { "ID", 1, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, N_("numeric id") }, + [COL_CLOCK] = { "CLOCK", 1, 0, SCOLS_JSON_STRING, N_("symbolic name") }, + [COL_NAME] = { "NAME", 1, 0, SCOLS_JSON_STRING, N_("readable name") }, + [COL_TIME] = { "TIME", 1, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, N_("numeric time") }, + [COL_ISO_TIME] = { "ISO_TIME", 1, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, N_("human readable ISO time") }, + [COL_RESOLUTION] = { "RESOLUTION", 1, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, N_("resolution") }, +}; + +static int column_name_to_id(const char *name, size_t namesz) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE(infos); i++) { + const char *cn = infos[i].name; + + if (!strncasecmp(name, cn, namesz) && !*(cn + namesz)) + return i; + } + warnx(_("unknown column: %s"), name); + + return -1; +} + +static void __attribute__((__noreturn__)) usage(void) +{ + FILE *out = stdout; + size_t i; + + fputs(USAGE_HEADER, out); + fprintf(out, _(" %s [options]\n"), program_invocation_short_name); + + fputs(USAGE_OPTIONS, out); + fputs(_(" -J, --json use JSON output format\n"), out); + fputs(_(" -n, --noheadings don't print headings\n"), out); + fputs(_(" -o, --output output columns\n"), out); + fputs(_(" -r, --raw use raw output format\n"), out); + fputs(_(" -t, --time show current time of single clock\n"), out); + + fputs(USAGE_SEPARATOR, out); + printf(USAGE_HELP_OPTIONS(25)); + + fprintf(out, USAGE_COLUMNS); + + for (i = 0; i < ARRAY_SIZE(infos); i++) + fprintf(out, " %16s %-10s%s\n", infos[i].name, + infos[i].json_type == SCOLS_JSON_STRING? "": + infos[i].json_type == SCOLS_JSON_ARRAY_STRING? "": + infos[i].json_type == SCOLS_JSON_ARRAY_NUMBER? "": + infos[i].json_type == SCOLS_JSON_NUMBER? "": + "", + _(infos[i].help)); + + printf(USAGE_MAN_TAIL("lslocks(1)")); + + exit(EXIT_SUCCESS); +} + +__attribute__ ((__format__ (__printf__, 3, 4))) +static void scols_line_asprintf(struct libscols_line *ln, size_t n, const char *format, ...) +{ + char *data; + va_list args; + + va_start(args, format); + xvasprintf(&data, format, args); + va_end(args); + + scols_line_refer_data(ln, n, data); +} + +static void scols_line_format_timespec(struct libscols_line *ln, size_t n, const struct timespec *ts) +{ + scols_line_asprintf(ln, n, "%ju.%09" PRId32, (uintmax_t) ts->tv_sec, (uint32_t) ts->tv_nsec); +} + +static clockid_t parse_clock(const char *name) +{ + size_t i; + uint32_t id = -1; + int rc; + + rc = ul_strtou32(name, &id, 10); + + for (i = 0; i < ARRAY_SIZE(clocks); i++) { + if (!strcmp(name, clocks[i].id_name) + || !strcmp(name, clocks[i].name)) + return clocks[i].id; + if (rc == 0 && (clockid_t) id == clocks[i].id) + return id; + } + + errx(EXIT_FAILURE, _("Unknown clock: %s"), name); +} + +int main(int argc, char **argv) +{ + size_t i, j; + char c; + int rc; + const struct colinfo *colinfo; + const struct clockinfo *clockinfo; + + struct libscols_table *tb; + struct libscols_line *ln; + struct libscols_column *col; + + bool noheadings = false, raw = false, json = false; + const char *outarg = NULL; + int columns[ARRAY_SIZE(infos) * 2]; + size_t ncolumns = 0; + clockid_t clock = -1; + + struct timespec resolution, now; + char buf[FORMAT_TIMESTAMP_MAX]; + + static const struct option longopts[] = { + { "noheadings", no_argument, NULL, 'n' }, + { "output", required_argument, NULL, 'o' }, + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, + { "json", no_argument, NULL, 'J' }, + { "raw", no_argument, NULL, 'r' }, + { "time", required_argument, NULL, 't' }, + { 0 } + }; + + setlocale(LC_ALL, ""); + bindtextdomain(PACKAGE, LOCALEDIR); + textdomain(PACKAGE); + close_stdout_atexit(); + + while ((c = getopt_long(argc, argv, "no:Jrt:Vh", longopts, NULL)) != -1) { + switch (c) { + case 'n': + noheadings = true; + break; + case 'o': + outarg = optarg; + break; + case 'J': + json = true; + break; + case 'r': + raw = true; + break; + case 't': + clock = parse_clock(optarg); + break; + case 'V': + print_version(EXIT_SUCCESS); + case 'h': + usage(); + default: + errtryhelp(EXIT_FAILURE); + } + } + + if (argv[optind]) + errtryhelp(EXIT_FAILURE); + + if (clock != -1) { + rc = clock_gettime(clock, &now); + if (rc) + err(EXIT_FAILURE, _("failed to get time")); + printf("%ju.%09"PRId32"\n", (uintmax_t) now.tv_sec, (uint32_t) now.tv_nsec); + return EXIT_SUCCESS; + } + + if (!ncolumns) { + columns[ncolumns++] = COL_ID; + columns[ncolumns++] = COL_CLOCK; + columns[ncolumns++] = COL_NAME; + columns[ncolumns++] = COL_TIME; + columns[ncolumns++] = COL_ISO_TIME; + columns[ncolumns++] = COL_RESOLUTION; + } + + if (outarg && string_add_to_idarray(outarg, columns, ARRAY_SIZE(columns), + &ncolumns, column_name_to_id) < 0) + return EXIT_FAILURE; + + scols_init_debug(0); + + tb = scols_new_table(); + if (!tb) + errx(EXIT_FAILURE, _("failed to allocate output table")); + scols_table_set_name(tb, "clocks"); + + for (i = 0; i < ncolumns; i++) { + colinfo = &infos[columns[i]]; + + col = scols_table_new_column(tb, colinfo->name, colinfo->whint, colinfo->flags); + if (!col) + errx(EXIT_FAILURE, _("failed to allocate output column")); + + scols_column_set_json_type(col, colinfo->json_type); + } + + for (i = 0; i < ARRAY_SIZE(clocks); i++) { + clockinfo = &clocks[i]; + + ln = scols_table_new_line(tb, NULL); + if (!ln) + errx(EXIT_FAILURE, _("failed to allocate output line")); + + /* outside the loop to guarantee consistency between COL_TIME and COL_ISO_TIME */ + rc = clock_gettime(clockinfo->id, &now); + if (rc) + now.tv_nsec = -1; + + for (j = 0; j < ncolumns; j++) { + switch (columns[j]) { + case COL_ID: + scols_line_asprintf(ln, j, "%ju", (uintmax_t) clockinfo->id); + break; + case COL_CLOCK: + scols_line_set_data(ln, j, clockinfo->id_name); + break; + case COL_NAME: + scols_line_set_data(ln, j, clockinfo->name); + break; + case COL_TIME: + if (now.tv_nsec == -1) + break; + + scols_line_format_timespec(ln, j, &now); + break; + case COL_ISO_TIME: + if (now.tv_nsec == -1) + break; + + rc = strtimespec_iso(&now, + ISO_GMTIME | ISO_DATE | ISO_TIME | ISO_T | ISO_DOTNSEC | ISO_TIMEZONE, + buf, sizeof(buf)); + if (rc) + errx(EXIT_FAILURE, _("failed to format iso time")); + scols_line_set_data(ln, j, buf); + break; + case COL_RESOLUTION: + rc = clock_getres(clockinfo->id, &resolution); + if (!rc) + scols_line_format_timespec(ln, j, &resolution); + break; + } + } + } + + scols_table_enable_json(tb, json); + scols_table_enable_raw(tb, raw); + scols_table_enable_noheadings(tb, noheadings); + scols_print_table(tb); + scols_unref_table(tb); +} diff --git a/misc-utils/meson.build b/misc-utils/meson.build index 7d21d02c1..a7cc4e4f3 100644 --- a/misc-utils/meson.build +++ b/misc-utils/meson.build @@ -165,3 +165,7 @@ fadvise_sources = files( waitpid_sources = files( 'waitpid.c', ) + +lsclocks_sources = files( + 'lsclocks.c', +) diff --git a/tests/commands.sh b/tests/commands.sh index 38b425405..41c0ee98c 100644 --- a/tests/commands.sh +++ b/tests/commands.sh @@ -87,6 +87,7 @@ TS_CMD_LINE=${TS_CMD_LINE-"${ts_commandsdir}line"} TS_CMD_LOOK=${TS_CMD_LOOK-"${ts_commandsdir}look"} TS_CMD_LOSETUP=${TS_CMD_LOSETUP:-"${ts_commandsdir}losetup"} TS_CMD_LSBLK=${TS_CMD_LSBLK-"${ts_commandsdir}lsblk"} +TS_CMD_LSCLOCKS=${TS_CMD_LSCPU-"${ts_commandsdir}lsclocks"} TS_CMD_LSCPU=${TS_CMD_LSCPU-"${ts_commandsdir}lscpu"} TS_CMD_LSFD=${TS_CMD_LSFD-"${ts_commandsdir}lsfd"} TS_CMD_LSMEM=${TS_CMD_LSMEM-"${ts_commandsdir}lsmem"} diff --git a/tests/expected/misc/lsclocks-basic b/tests/expected/misc/lsclocks-basic new file mode 100644 index 000000000..b3b25f4a6 --- /dev/null +++ b/tests/expected/misc/lsclocks-basic @@ -0,0 +1,10 @@ +ID CLOCK NAME + 0 CLOCK_REALTIME realtime + 1 CLOCK_MONOTONIC monotonic + 4 CLOCK_MONOTONIC_RAW monotonic-raw + 5 CLOCK_REALTIME_COARSE realtime-coarse + 6 CLOCK_MONOTONIC_COARSE monotonic-coarse + 7 CLOCK_BOOTTIME boottime + 8 CLOCK_REALTIME_ALARM realtime-alarm + 9 CLOCK_BOOTTIME_ALARM boottime-alarm +11 CLOCK_TAI tai diff --git a/tests/expected/misc/lsclocks-time b/tests/expected/misc/lsclocks-time new file mode 100644 index 000000000..adc652433 --- /dev/null +++ b/tests/expected/misc/lsclocks-time @@ -0,0 +1 @@ +X.X diff --git a/tests/ts/misc/lsclocks b/tests/ts/misc/lsclocks new file mode 100755 index 000000000..55ee4df40 --- /dev/null +++ b/tests/ts/misc/lsclocks @@ -0,0 +1,42 @@ +#!/bin/bash + +# Copyright (C) 2023 Thomas Weißschuh +# +# This file is part of util-linux. +# +# This file 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 file 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. + +TS_TOPDIR="${0%/*}/../.." +TS_DESC="lsclocks" + +. "$TS_TOPDIR"/functions.sh +ts_init "$*" + +ts_check_test_command "$TS_CMD_LSCLOCKS" +ts_check_prog sed + +mask_timestamps() { + sed 's/[0-9]\+\.[0-9]\+/X.X/g' +} + +ts_init_subtest basic + +"$TS_CMD_LSCLOCKS" -o ID,CLOCK,NAME > "$TS_OUTPUT" 2>> "$TS_ERRLOG" + +ts_finalize_subtest + +ts_init_subtest time + +"$TS_CMD_LSCLOCKS" --time monotonic | mask_timestamps > "$TS_OUTPUT" 2>> "$TS_ERRLOG" + +ts_finalize_subtest + +ts_finalize