freebsd-dev/usr.bin/dpv/dpv.c
Devin Teske 041394f38a Add new libraries/utilities for data throughput visualization.
dpv(3): dialog progress view library
dpv(1): stream data from stdin or multiple paths with dialog progress view
figpar(3): configuration file parsing library

Reviews:	D714
Reviewed by:	jelischer, shurd
Discussed at:	MeetBSD California 2014 Vendor/Dev Summit
Discussed on:	-current
MFC after:	21 days
X-MFC-to:	stable/10 stable/9
2014-11-04 23:46:01 +00:00

542 lines
15 KiB
C

/*-
* Copyright (c) 2013-2014 Devin Teske <dteske@FreeBSD.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#include <sys/cdefs.h>
__FBSDID("$FreeBSD$");
#include <sys/stat.h>
#include <sys/types.h>
#define _BSD_SOURCE /* to get dprintf() prototype in stdio.h below */
#include <dialog.h>
#include <dpv.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string_m.h>
#include <unistd.h>
#include "dpv_util.h"
/* Debugging */
static uint8_t debug = FALSE;
/* Data to process */
static struct dpv_file_node *file_list = NULL;
static unsigned int nfiles = 0;
/* Data processing */
static uint8_t line_mode = FALSE;
static uint8_t no_overrun = FALSE;
static char *buf = NULL;
static int fd = -1;
static int output_type = DPV_OUTPUT_NONE;
static size_t bsize;
static char rpath[PATH_MAX];
/* Extra display information */
static uint8_t multiple = FALSE; /* `-m' */
static char *pgm; /* set to argv[0] by main() */
/* Function prototypes */
static void sig_int(int sig);
static void usage(void);
int main(int argc, char *argv[]);
static int operate_common(struct dpv_file_node *file, int out);
static int operate_on_bytes(struct dpv_file_node *file, int out);
static int operate_on_lines(struct dpv_file_node *file, int out);
static int
operate_common(struct dpv_file_node *file, int out)
{
struct stat sb;
/* Open the file if necessary */
if (fd < 0) {
if (multiple) {
/* Resolve the file path and attempt to open it */
if (realpath(file->path, rpath) == 0 ||
(fd = open(rpath, O_RDONLY)) < 0) {
warn("%s", file->path);
file->status = DPV_STATUS_FAILED;
return (-1);
}
} else {
/* Assume stdin, but if that's a TTY instead use the
* highest numbered file descriptor (obtained by
* generating new fd and then decrementing).
*
* NB: /dev/stdin should always be open(2)'able
*/
fd = STDIN_FILENO;
if (isatty(fd)) {
fd = open("/dev/stdin", O_RDONLY);
close(fd--);
}
/* This answer might be wrong, if dpv(3) has (by
* request) opened an output file or pipe. If we
* told dpv(3) to open a file, subtract one from
* previous answer. If instead we told dpv(3) to
* prepare a pipe output, subtract two.
*/
switch(output_type) {
case DPV_OUTPUT_FILE:
fd -= 1;
break;
case DPV_OUTPUT_SHELL:
fd -= 2;
break;
}
}
}
/* Allocate buffer if necessary */
if (buf == NULL) {
/* Use output block size as buffer size if available */
if (out >= 0) {
if (fstat(out, &sb) != 0) {
warn("%i", out);
file->status = DPV_STATUS_FAILED;
return (-1);
}
if (S_ISREG(sb.st_mode)) {
if (sysconf(_SC_PHYS_PAGES) >
PHYSPAGES_THRESHOLD)
bsize = MIN(BUFSIZE_MAX, MAXPHYS * 8);
else
bsize = BUFSIZE_SMALL;
} else
bsize = MAX(sb.st_blksize,
(blksize_t)sysconf(_SC_PAGESIZE));
} else
bsize = MIN(BUFSIZE_MAX, MAXPHYS * 8);
/* Attempt to allocate */
if ((buf = malloc(bsize+1)) == NULL) {
end_dialog();
err(EXIT_FAILURE, "Out of memory?!");
}
}
return (0);
}
static int
operate_on_bytes(struct dpv_file_node *file, int out)
{
int progress;
ssize_t r, w;
if (operate_common(file, out) < 0)
return (-1);
/* [Re-]Fill the buffer */
if ((r = read(fd, buf, bsize)) <= 0) {
if (fd != STDIN_FILENO)
close(fd);
fd = -1;
file->status = DPV_STATUS_DONE;
return (100);
}
/* [Re-]Dump the buffer */
if (out >= 0) {
if ((w = write(out, buf, r)) < 0) {
end_dialog();
err(EXIT_FAILURE, "output");
}
fsync(out);
}
overall_read += r;
file->read += r;
/* Calculate percentage of completion (if possible) */
if (file->length >= 0) {
progress = (file->read * 100 / (file->length > 0 ?
file->length : 1));
/* If no_overrun, do not return 100% until read >= length */
if (no_overrun && progress == 100 && file->read < file->length)
progress--;
return (progress);
} else
return (-1);
}
static int
operate_on_lines(struct dpv_file_node *file, int out)
{
char *p;
int progress;
ssize_t r, w;
if (operate_common(file, out) < 0)
return (-1);
/* [Re-]Fill the buffer */
if ((r = read(fd, buf, bsize)) <= 0) {
if (fd != STDIN_FILENO)
close(fd);
fd = -1;
file->status = DPV_STATUS_DONE;
return (100);
}
buf[r] = '\0';
/* [Re-]Dump the buffer */
if (out >= 0) {
if ((w = write(out, buf, r)) < 0) {
end_dialog();
err(EXIT_FAILURE, "output");
}
fsync(out);
}
/* Process the buffer for number of lines */
for (p = buf; p != NULL && *p != '\0';)
if ((p = strchr(p, '\n')) != NULL)
overall_read++, p++, file->read++;
/* Calculate percentage of completion (if possible) */
if (file->length >= 0) {
progress = (file->read * 100 / file->length);
/* If no_overrun, do not return 100% until read >= length */
if (no_overrun && progress == 100 && file->read < file->length)
progress--;
return (progress);
} else
return (-1);
}
/*
* Takes a list of names that are to correspond to input streams coming from
* stdin or fifos and produces necessary config to drive dpv(3) `--gauge'
* widget. If the `-d' flag is used, output is instead send to terminal
* standard output (and the output can then be saved to a file, piped into
* custom [X]dialog(1) invocation, or whatever.
*/
int
main(int argc, char *argv[])
{
char dummy;
int ch;
int n = 0;
size_t config_size = sizeof(struct dpv_config);
size_t file_node_size = sizeof(struct dpv_file_node);
struct dpv_config *config;
struct dpv_file_node *curfile;
struct sigaction act;
pgm = argv[0]; /* store a copy of invocation name */
/* Allocate config structure */
if ((config = malloc(config_size)) == NULL)
errx(EXIT_FAILURE, "Out of memory?!");
memset((void *)(config), '\0', config_size);
/*
* Process command-line options
*/
while ((ch = getopt(argc, argv,
"a:b:dDhi:I:lL:mn:No:p:P:t:TU:wx:X")) != -1) {
switch(ch) {
case 'a': /* additional message text to append */
if (config->aprompt == NULL) {
config->aprompt = malloc(DPV_APROMPT_MAX);
if (config->aprompt == NULL)
errx(EXIT_FAILURE, "Out of memory?!");
}
snprintf(config->aprompt, DPV_APROMPT_MAX, "%s",
optarg);
break;
case 'b': /* [X]dialog(1) backtitle */
if (config->backtitle != NULL)
free((char *)config->backtitle);
config->backtitle = malloc(strlen(optarg) + 1);
if (config->backtitle == NULL)
errx(EXIT_FAILURE, "Out of memory?!");
*(config->backtitle) = '\0';
strcat(config->backtitle, optarg);
break;
case 'd': /* debugging */
debug = TRUE;
config->debug = debug;
break;
case 'D': /* use dialog(1) instead of libdialog */
config->display_type = DPV_DISPLAY_DIALOG;
break;
case 'h': /* help/usage */
usage();
break; /* NOTREACHED */
case 'i': /* status line format string for single-file */
config->status_solo = optarg;
break;
case 'I': /* status line format string for many-files */
config->status_many = optarg;
break;
case 'l': /* Line mode */
line_mode = TRUE;
break;
case 'L': /* custom label size */
config->label_size =
(int)strtol(optarg, (char **)NULL, 10);
if (config->label_size == 0 && errno == EINVAL)
errx(EXIT_FAILURE,
"`-L' argument must be numeric");
else if (config->label_size < -1)
config->label_size = -1;
break;
case 'm': /* enable multiple file arguments */
multiple = TRUE;
break;
case 'o': /* `-o path' for sending data-read to file */
output_type = DPV_OUTPUT_FILE;
config->output_type = DPV_OUTPUT_FILE;
config->output = optarg;
break;
case 'n': /* custom number of files per `page' */
config->display_limit =
(int)strtol(optarg, (char **)NULL, 10);
if (config->display_limit == 0 && errno == EINVAL)
errx(EXIT_FAILURE,
"`-n' argument must be numeric");
else if (config->display_limit < 0)
config->display_limit = -1;
break;
case 'N': /* No overrun (truncate reads of known-length) */
no_overrun = TRUE;
config->options |= DPV_NO_OVERRUN;
break;
case 'p': /* additional message text to use as prefix */
if (config->pprompt == NULL) {
config->pprompt = malloc(DPV_PPROMPT_MAX + 2);
if (config->pprompt == NULL)
errx(EXIT_FAILURE, "Out of memory?!");
/* +2 is for implicit "\n" appended later */
}
snprintf(config->pprompt, DPV_PPROMPT_MAX, "%s",
optarg);
break;
case 'P': /* custom size for mini-progressbar */
config->pbar_size =
(int)strtol(optarg, (char **)NULL, 10);
if (config->pbar_size == 0 && errno == EINVAL)
errx(EXIT_FAILURE,
"`-P' argument must be numeric");
else if (config->pbar_size < -1)
config->pbar_size = -1;
break;
case 't': /* [X]dialog(1) title */
if (config->title != NULL)
free(config->title);
config->title = malloc(strlen(optarg) + 1);
if (config->title == NULL)
errx(EXIT_FAILURE, "Out of memory?!");
*(config->title) = '\0';
strcat(config->title, optarg);
break;
case 'T': /* test mode (don't read data, fake it) */
config->options |= DPV_TEST_MODE;
break;
case 'U': /* updates per second */
config->status_updates_per_second =
(int)strtol(optarg, (char **)NULL, 10);
if (config->status_updates_per_second == 0 &&
errno == EINVAL)
errx(EXIT_FAILURE,
"`-U' argument must be numeric");
break;
case 'w': /* `-p' and `-a' widths bump [X]dialog(1) width */
config->options |= DPV_WIDE_MODE;
break;
case 'x': /* `-x cmd' for sending data-read to sh(1) code */
output_type = DPV_OUTPUT_SHELL;
config->output_type = DPV_OUTPUT_SHELL;
config->output = optarg;
break;
case 'X': /* X11 support through x11/xdialog */
config->display_type = DPV_DISPLAY_XDIALOG;
break;
case '?': /* unknown argument (based on optstring) */
/* FALLTHROUGH */
default: /* unhandled argument (based on switch) */
usage();
/* NOTREACHED */
}
}
argc -= optind;
argv += optind;
/* Process remaining arguments as list of names to display */
for (curfile = file_list; n < argc; n++) {
nfiles++;
/* Allocate a new struct for the file argument */
if (curfile == NULL) {
if ((curfile = malloc(file_node_size)) == NULL)
errx(EXIT_FAILURE, "Out of memory?!");
memset((void *)(curfile), '\0', file_node_size);
file_list = curfile;
} else {
if ((curfile->next = malloc(file_node_size)) == NULL)
errx(EXIT_FAILURE, "Out of memory?!");
memset((void *)(curfile->next), '\0', file_node_size);
curfile = curfile->next;
}
curfile->name = argv[n];
/* Read possible `lines:' prefix from label syntax */
if (sscanf(curfile->name, "%lli:%c", &(curfile->length),
&dummy) == 2)
curfile->name = strchr(curfile->name, ':') + 1;
else
curfile->length = -1;
/* Read path argument if enabled */
if (multiple) {
if (++n >= argc)
errx(EXIT_FAILURE, "Missing path argument "
"for label number %i", nfiles);
curfile->path = argv[n];
} else
break;
}
/* Display usage and exit if not given at least one name */
if (nfiles == 0) {
warnx("no labels provided");
usage();
/* NOTREACHED */
}
/*
* Set cleanup routine for Ctrl-C action
*/
if (config->display_type == DPV_DISPLAY_LIBDIALOG) {
act.sa_handler = sig_int;
sigaction(SIGINT, &act, 0);
}
/* Set status formats and action */
if (line_mode) {
config->status_solo = LINE_STATUS_SOLO;
config->status_many = LINE_STATUS_SOLO;
config->action = operate_on_lines;
} else {
config->status_solo = BYTE_STATUS_SOLO;
config->status_many = BYTE_STATUS_SOLO;
config->action = operate_on_bytes;
}
/*
* Hand off to dpv(3)...
*/
if (dpv(config, file_list) != 0 && debug)
warnx("dpv(3) returned error!?");
end_dialog();
dpv_free();
exit(EXIT_SUCCESS);
}
/*
* Interrupt handler to indicate we received a Ctrl-C interrupt.
*/
static void
sig_int(int sig __unused)
{
dpv_interrupt = TRUE;
}
/*
* Print short usage statement to stderr and exit with error status.
*/
static void
usage(void)
{
if (debug) /* No need for usage */
exit(EXIT_FAILURE);
fprintf(stderr, "Usage: %s [options] bytes:label\n", pgm);
fprintf(stderr, " %s [options] -m bytes1:label1 path1 "
"[bytes2:label2 path2 ...]\n", pgm);
fprintf(stderr, "OPTIONS:\n");
#define OPTFMT "\t%-14s %s\n"
fprintf(stderr, OPTFMT, "-a text",
"Append text. Displayed below file progress indicators.");
fprintf(stderr, OPTFMT, "-b backtitle",
"String to be displayed on the backdrop, at top-left.");
fprintf(stderr, OPTFMT, "-d",
"Debug. Write to standard output instead of dialog.");
fprintf(stderr, OPTFMT, "-D",
"Use dialog(1) instead of dialog(3) [default].");
fprintf(stderr, OPTFMT, "-h",
"Produce this output on standard error and exit.");
fprintf(stderr, OPTFMT, "-i format",
"Customize status line format. See fdpv(1) for details.");
fprintf(stderr, OPTFMT, "-I format",
"Customize status line format. See fdpv(1) for details.");
fprintf(stderr, OPTFMT, "-L size",
"Label size. Must be a number greater than 0, or -1.");
fprintf(stderr, OPTFMT, "-m",
"Enable processing of multiple file argiments.");
fprintf(stderr, OPTFMT, "-n num",
"Display at-most num files per screen. Default is -1.");
fprintf(stderr, OPTFMT, "-N",
"No overrun. Stop reading input at stated length, if any.");
fprintf(stderr, OPTFMT, "-o file",
"Output data to file. First %s replaced with label text.");
fprintf(stderr, OPTFMT, "-p text",
"Prefix text. Displayed above file progress indicators.");
fprintf(stderr, OPTFMT, "-P size",
"Mini-progressbar size. Must be a number greater than 3.");
fprintf(stderr, OPTFMT, "-t title",
"Title string to be displayed at top of dialog(1) box.");
fprintf(stderr, OPTFMT, "-T",
"Test mode. Don't actually read any data, but fake it.");
fprintf(stderr, OPTFMT, "-U num",
"Update status line num times per-second. Default is 2.");
fprintf(stderr, OPTFMT, "-w",
"Wide. Width of `-p' and `-a' text bump dialog(1) width.");
fprintf(stderr, OPTFMT, "-x cmd",
"Send data to executed cmd. First %s replaced with label.");
fprintf(stderr, OPTFMT, "-X",
"X11. Use Xdialog(1) instead of dialog(1).");
exit(EXIT_FAILURE);
}