From 2ac9b769c84ece9467b59ef5b1494e0f3fe76499 Mon Sep 17 00:00:00 2001 From: Christian Deacon Date: Mon, 10 Jun 2024 22:19:12 -0400 Subject: [PATCH] Add support for parsing output from commands and using counters from there. --- README.md | 54 +++++++++++++++++++++++++++-- src/cmdline.c | 42 ++++++++++++++++------- src/cmdline.h | 8 +++-- src/common.c | 93 +++++++++++++++++++++++++++++++++++++++++++++++++ src/common.h | 4 ++- src/stat.c | 95 +++++++++++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 277 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 364812b..913e492 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ [![Stat Build Workflow](https://github.com/gamemann/Stat/actions/workflows/build.yml/badge.svg)](https://github.com/gamemann/Stat/actions/workflows/build.yml) ## Description -A small project that allows you to gather statistics (integers/counts) from files on the file system. This was designed for Linux. +A small project that allows you to gather statistics (counters) from files on the file system or command outputs. This was designed for Linux. -This is useful for retrieving the incoming/outgoing packets per second or incoming/outgoing bytes per second on a network interface. +This is useful for retrieving the incoming/outgoing packets or bytes per second on a network interface from the file system or from the output of a command such as `ethtool`. ## Building Program You can simply use `make` to build this program. The `Makefile` uses `clang` to compile the program. @@ -33,6 +33,9 @@ gstat [-i --pps --bps --path -c <"kbps" or "mbps" or "gbps"> --interval => Use this interval (in microseconds) instead of one second. --count -n => Maximum amount of times to request the counter before stopping program (0 = no limit). --time -t => Time limit (in seconds) before stopping program (0 = no limit). +--cmd => The command to execute and retrieve output from. +--sep => The separator to apply to the command's output. +--key => The key to search for when separating the command output. ``` **Note** - If you want to receive another counter such as outgoing (TX) packets, you can set the file to pull the count from with the `-p` (or `--path`) flag. For example: @@ -41,5 +44,52 @@ gstat [-i --pps --bps --path -c <"kbps" or "mbps" or "gbps"> gstat --path /sys/class/net/ens18/statistics/tx_packets ``` +### Command Argument +You may use a combination of the `--cmd`, `--sep`, and `--key` arguments to retrieve a counter from a command's output such as `ethtool`. + +For example, take a look at the following output from `ethtool`. + +```bash +$ sudo ethtool -S enp1s0 + +NIC statistics: + rx_queue_0_packets: 901268 + rx_queue_0_bytes: 211930005 + rx_queue_0_drops: 0 + rx_queue_0_xdp_packets: 0 + rx_queue_0_xdp_tx: 0 + rx_queue_0_xdp_redirects: 0 + rx_queue_0_xdp_drops: 0 + rx_queue_0_kicks: 14 + rx_queue_1_packets: 1237084 + rx_queue_1_bytes: 469671713 + rx_queue_1_drops: 0 + rx_queue_1_xdp_packets: 0 + rx_queue_1_xdp_tx: 0 + rx_queue_1_xdp_redirects: 0 + rx_queue_1_xdp_drops: 0 + rx_queue_1_kicks: 19 + tx_queue_0_packets: 16508781 + tx_queue_0_bytes: 1528281628 + tx_queue_0_xdp_tx: 0 + tx_queue_0_xdp_tx_drops: 0 + tx_queue_0_kicks: 6598201 + tx_queue_0_tx_timeouts: 0 + tx_queue_1_packets: 16154602 + tx_queue_1_bytes: 1348764220 + tx_queue_1_xdp_tx: 0 + tx_queue_1_xdp_tx_drops: 0 + tx_queue_1_kicks: 6051221 + tx_queue_1_tx_timeouts: 0 +``` + +If I want the `rx_queue_0_packets` value used as the counter (starting at `901268`), you would execute the following command. + +```bash +sudo gstat --cmd "ethtool -S enp1s0" --sep ":" --key "rx_queue_0_packets" +``` + +Each line from the command's output is trimmed of white-spaces. + ## Credits * [Christian Deacon](https://github.com/gamemann) diff --git a/src/cmdline.c b/src/cmdline.c index da90fc1..263fe35 100644 --- a/src/cmdline.c +++ b/src/cmdline.c @@ -8,17 +8,20 @@ const struct option opts[] = { - {"dev", required_argument, NULL, 'i'}, - {"path", required_argument, NULL, 'p'}, - {"convert", required_argument, NULL, 'c'}, - {"custom", required_argument, NULL, 1}, - {"interval", required_argument, NULL, 2}, - {"count", required_argument, NULL, 'n'}, - {"time", required_argument, NULL, 't'}, - {"pps", no_argument, NULL, 3}, - {"bps", no_argument, NULL, 4}, - {"help", no_argument, NULL, 'h'}, - {NULL, 0, NULL, 0} + { "dev", required_argument, NULL, 'i' }, + { "path", required_argument, NULL, 'p' }, + { "convert", required_argument, NULL, 'c' }, + { "custom", required_argument, NULL, 1 }, + { "interval", required_argument, NULL, 2 }, + { "count", required_argument, NULL, 'n' }, + { "time", required_argument, NULL, 't' }, + { "pps", no_argument, NULL, 3 }, + { "bps", no_argument, NULL, 4 }, + { "cmd", required_argument, NULL, 5 }, + { "sep", required_argument, NULL, 6 }, + { "key", required_argument, NULL, 7 }, + { "help", no_argument, NULL, 'h' }, + { NULL, 0, NULL, 0 } }; /** @@ -29,7 +32,7 @@ const struct option opts[] = * * @return void */ -void parsecmdline(int argc, char *argv[], struct cmdline *cmd) +void parsecmdline(int argc, char *argv[], cmdline_t *cmd) { int c = -1; @@ -74,6 +77,21 @@ void parsecmdline(int argc, char *argv[], struct cmdline *cmd) cmd->bps = 1; break; + + case 5: + cmd->cmd = optarg; + + break; + + case 6: + cmd->sep = optarg; + + break; + + case 7: + cmd->key = optarg; + + break; case 'n': cmd->countmax = (uint64_t) strtoull((const char *)optarg, (char **)optarg, 0); diff --git a/src/cmdline.h b/src/cmdline.h index f61fc48..510104d 100644 --- a/src/cmdline.h +++ b/src/cmdline.h @@ -2,7 +2,7 @@ #include -struct cmdline +typedef struct cmdline { char *interface; char *path; @@ -18,6 +18,10 @@ struct cmdline unsigned int pps : 1; unsigned int bps : 1; -}; + + char *cmd; + char *sep; + char *key; +} cmdline_t; void parsecmdline(int argc, char *argv[], struct cmdline *cmd); \ No newline at end of file diff --git a/src/common.c b/src/common.c index 7864e5f..33c2d30 100644 --- a/src/common.c +++ b/src/common.c @@ -47,4 +47,97 @@ uint64_t getstat(const char *path) fclose(fp); return (uint64_t) strtoull((const char *)buff, (char **)buff, 0); +} + +/** + * Trims a string from whitespaces. + * + * @param str A pointer to the string you'd like to trim. + * + * @return void +*/ +char *trim(char* str) +{ + char* end; + + while (isspace((unsigned char)*str)) + { + str++; + } + + if (*str == 0) + { + return str; + } + + end = str + strlen(str) - 1; + + while (end > str && isspace((unsigned char)*end)) + { + end--; + } + + // Write new null terminator + *(end + 1) = '\0'; + + return str; +} + +/** + * Executes a command and returns the result. + * + * @param cmd The command to execute. + * + * @return A string. +*/ +char *execcmd(const char *cmd) +{ + FILE* fp; + char buffer[128]; + size_t size = 0; + size_t buffer_size = 128; + char* result = malloc(buffer_size); + + if (result == NULL) + { + fprintf(stderr, "Memory allocation failed\n"); + + exit(1); + } + + result[0] = '\0'; + + fp = popen(cmd, "r"); + + if (fp == NULL) + { + fprintf(stderr, "Failed to run command\n"); + + exit(1); + } + + while (fgets(buffer, sizeof(buffer), fp) != NULL) + { + size_t buffer_len = strlen(buffer); + + if (size + buffer_len + 1 > buffer_size) + { + buffer_size *= 2; + result = realloc(result, buffer_size); + + if (result == NULL) + { + fprintf(stderr, "Memory reallocation failed\n"); + + exit(1); + } + } + + strcat(result, buffer); + size += buffer_len; + } + + pclose(fp); + + return result; } \ No newline at end of file diff --git a/src/common.h b/src/common.h index e92662b..9496c94 100644 --- a/src/common.h +++ b/src/common.h @@ -3,4 +3,6 @@ #include char *lowerstr(char *str); -uint64_t getstat(const char *path); \ No newline at end of file +uint64_t getstat(const char *path); +char *trim(char* str); +char *execcmd(const char *cmd); \ No newline at end of file diff --git a/src/stat.c b/src/stat.c index 757509f..198ee43 100644 --- a/src/stat.c +++ b/src/stat.c @@ -17,6 +17,94 @@ void sighdl(int tmp) cont = 0; } +__uint64_t get_current(cmdline_t *cmd, const char *path) +{ + uint64_t cur = 0; + + if (cmd->cmd != NULL) + { + // Make sure key is available. + if (cmd->key == NULL) + { + fprintf(stderr, "Command specified, but no key for separator included. Please use --key.\n"); + + return cur; + } + + char *sep = " "; + + if (cmd->sep != NULL) + { + sep = cmd->sep; + } + + char *output = execcmd(cmd->cmd); + + // Make sure we have output. + if (output == NULL) + { + fprintf(stderr, "Command '%s' output is NULL.\n", cmd->cmd); + + return 0; + } + + // We need to copy output memory address since strsep() modifies output below. + char *output_copy = output; + + // Loop through each line (\n separator). + char *line = NULL; + + while ((line = strsep(&output, "\n"))) + { + // Make sure we have at least one byte. + if (strlen(line) < 1) + { + continue; + } + + // Trim the string so it doesn't mess anything up. + char *trimmed = trim(line); + + // We want to now split by our input separator (key). + char *token = NULL; + + // Indicate the position we're at. + int i = 1; + + while ((token = strsep(&trimmed, sep))) + { + // Key check. + if (i == 1 && strcmp(cmd->key, token) == 0) + { + i++; + + continue; + } + + // If the key matches, i will be 2. Therefore, use current token as value and convert to integer. + if (i == 2) + { + cur = (__uint64_t) strtoull((const char *)token, (char **)token, 0); + } + + break; + } + } + + // Free output string. + if (output_copy != NULL) + { + free(output_copy); + } + } + else + { + cur = getstat(path); + } + + return cur; +} + int main(int argc, char *argv[]) { // Start off by parsing our command line. @@ -37,6 +125,9 @@ int main(int argc, char *argv[]) "--interval => Use this interval (in microseconds) instead of one second.\n" \ "--count -n => Maximum amount of times to request the counter before stopping program (0 = no limit).\n" \ "--time -t => Time limit (in seconds) before stopping program (0 = no limit).\n" \ + "--cmd => The command to execute and retrieve output from.\n" \ + "--sep => The separator to apply to the command's output.\n" \ + "--key => The key to search for when separating the command output.\n" \ ); return 0; @@ -72,7 +163,7 @@ int main(int argc, char *argv[]) signal(SIGINT, sighdl); // Get current counter and create a loop. - uint64_t old = getstat(path); + uint64_t old = get_current(&cmd, path); uint64_t totcount = 0; uint64_t count = 0; @@ -105,7 +196,7 @@ int main(int argc, char *argv[]) sleep(1); } - uint64_t cur = getstat(path); + uint64_t cur = get_current(&cmd, path); uint64_t new = cur - old; // Save old counter.