1/*-
2 * Copyright (c) 2013-2016 Devin Teske <dteske@FreeBSD.org>
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 *    notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 *    notice, this list of conditions and the following disclaimer in the
12 *    documentation and/or other materials provided with the distribution.
13 *
14 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
15 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
18 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24 * SUCH DAMAGE.
25 */
26
27#include <sys/cdefs.h>
28__FBSDID("$FreeBSD: releng/10.3/usr.bin/dpv/dpv.c 295110 2016-02-01 00:58:07Z dteske $");
29
30#include <sys/stat.h>
31#include <sys/types.h>
32
33#define _BSD_SOURCE /* to get dprintf() prototype in stdio.h below */
34#include <dialog.h>
35#include <dpv.h>
36#include <err.h>
37#include <errno.h>
38#include <fcntl.h>
39#include <limits.h>
40#include <signal.h>
41#include <stdio.h>
42#include <stdlib.h>
43#include <string.h>
44#include <string_m.h>
45#include <unistd.h>
46
47#include "dpv_util.h"
48
49/* Debugging */
50static uint8_t debug = FALSE;
51
52/* Data to process */
53static struct dpv_file_node *file_list = NULL;
54static unsigned int nfiles = 0;
55
56/* Data processing */
57static uint8_t line_mode = FALSE;
58static uint8_t no_overrun = FALSE;
59static char *buf = NULL;
60static int fd = -1;
61static int output_type = DPV_OUTPUT_NONE;
62static size_t bsize;
63static char rpath[PATH_MAX];
64
65/* Extra display information */
66static uint8_t multiple = FALSE; /* `-m' */
67static char *pgm; /* set to argv[0] by main() */
68
69/* Function prototypes */
70static void	sig_int(int sig);
71static void	usage(void);
72int		main(int argc, char *argv[]);
73static int	operate_common(struct dpv_file_node *file, int out);
74static int	operate_on_bytes(struct dpv_file_node *file, int out);
75static int	operate_on_lines(struct dpv_file_node *file, int out);
76
77static int
78operate_common(struct dpv_file_node *file, int out)
79{
80	struct stat sb;
81
82	/* Open the file if necessary */
83	if (fd < 0) {
84		if (multiple) {
85			/* Resolve the file path and attempt to open it */
86			if (realpath(file->path, rpath) == 0 ||
87			    (fd = open(rpath, O_RDONLY)) < 0) {
88				warn("%s", file->path);
89				file->status = DPV_STATUS_FAILED;
90				return (-1);
91			}
92		} else {
93			/* Assume stdin, but if that's a TTY instead use the
94			 * highest numbered file descriptor (obtained by
95			 * generating new fd and then decrementing).
96			 *
97			 * NB: /dev/stdin should always be open(2)'able
98			 */
99			fd = STDIN_FILENO;
100			if (isatty(fd)) {
101				fd = open("/dev/stdin", O_RDONLY);
102				close(fd--);
103			}
104
105			/* This answer might be wrong, if dpv(3) has (by
106			 * request) opened an output file or pipe. If we
107			 * told dpv(3) to open a file, subtract one from
108			 * previous answer. If instead we told dpv(3) to
109			 * prepare a pipe output, subtract two.
110			 */
111			switch(output_type) {
112			case DPV_OUTPUT_FILE:
113				fd -= 1;
114				break;
115			case DPV_OUTPUT_SHELL:
116				fd -= 2;
117				break;
118			}
119		}
120	}
121
122	/* Allocate buffer if necessary */
123	if (buf == NULL) {
124		/* Use output block size as buffer size if available */
125		if (out >= 0) {
126			if (fstat(out, &sb) != 0) {
127				warn("%i", out);
128				file->status = DPV_STATUS_FAILED;
129				return (-1);
130			}
131			if (S_ISREG(sb.st_mode)) {
132				if (sysconf(_SC_PHYS_PAGES) >
133				    PHYSPAGES_THRESHOLD)
134					bsize = MIN(BUFSIZE_MAX, MAXPHYS * 8);
135				else
136					bsize = BUFSIZE_SMALL;
137			} else
138				bsize = MAX(sb.st_blksize,
139				    (blksize_t)sysconf(_SC_PAGESIZE));
140		} else
141			bsize = MIN(BUFSIZE_MAX, MAXPHYS * 8);
142
143		/* Attempt to allocate */
144		if ((buf = malloc(bsize+1)) == NULL) {
145			end_dialog();
146			err(EXIT_FAILURE, "Out of memory?!");
147		}
148	}
149
150	return (0);
151}
152
153static int
154operate_on_bytes(struct dpv_file_node *file, int out)
155{
156	int progress;
157	ssize_t r, w;
158
159	if (operate_common(file, out) < 0)
160		return (-1);
161
162	/* [Re-]Fill the buffer */
163	if ((r = read(fd, buf, bsize)) <= 0) {
164		if (fd != STDIN_FILENO)
165			close(fd);
166		fd = -1;
167		file->status = DPV_STATUS_DONE;
168		return (100);
169	}
170
171	/* [Re-]Dump the buffer */
172	if (out >= 0) {
173		if ((w = write(out, buf, r)) < 0) {
174			end_dialog();
175			err(EXIT_FAILURE, "output");
176		}
177		fsync(out);
178	}
179
180	dpv_overall_read += r;
181	file->read += r;
182
183	/* Calculate percentage of completion (if possible) */
184	if (file->length >= 0) {
185		progress = (file->read * 100 / (file->length > 0 ?
186		    file->length : 1));
187
188		/* If no_overrun, do not return 100% until read >= length */
189		if (no_overrun && progress == 100 && file->read < file->length)
190			progress--;
191
192		return (progress);
193	} else
194		return (-1);
195}
196
197static int
198operate_on_lines(struct dpv_file_node *file, int out)
199{
200	char *p;
201	int progress;
202	ssize_t r, w;
203
204	if (operate_common(file, out) < 0)
205		return (-1);
206
207	/* [Re-]Fill the buffer */
208	if ((r = read(fd, buf, bsize)) <= 0) {
209		if (fd != STDIN_FILENO)
210			close(fd);
211		fd = -1;
212		file->status = DPV_STATUS_DONE;
213		return (100);
214	}
215	buf[r] = '\0';
216
217	/* [Re-]Dump the buffer */
218	if (out >= 0) {
219		if ((w = write(out, buf, r)) < 0) {
220			end_dialog();
221			err(EXIT_FAILURE, "output");
222		}
223		fsync(out);
224	}
225
226	/* Process the buffer for number of lines */
227	for (p = buf; p != NULL && *p != '\0';)
228		if ((p = strchr(p, '\n')) != NULL)
229			dpv_overall_read++, p++, file->read++;
230
231	/* Calculate percentage of completion (if possible) */
232	if (file->length >= 0) {
233		progress = (file->read * 100 / file->length);
234
235		/* If no_overrun, do not return 100% until read >= length */
236		if (no_overrun && progress == 100 && file->read < file->length)
237			progress--;
238
239		return (progress);
240	} else
241		return (-1);
242}
243
244/*
245 * Takes a list of names that are to correspond to input streams coming from
246 * stdin or fifos and produces necessary config to drive dpv(3) `--gauge'
247 * widget. If the `-d' flag is used, output is instead send to terminal
248 * standard output (and the output can then be saved to a file, piped into
249 * custom [X]dialog(1) invocation, or whatever.
250 */
251int
252main(int argc, char *argv[])
253{
254	char dummy;
255	int ch;
256	int n = 0;
257	size_t config_size = sizeof(struct dpv_config);
258	size_t file_node_size = sizeof(struct dpv_file_node);
259	struct dpv_config *config;
260	struct dpv_file_node *curfile;
261	struct sigaction act;
262
263	pgm = argv[0]; /* store a copy of invocation name */
264
265	/* Allocate config structure */
266	if ((config = malloc(config_size)) == NULL)
267		errx(EXIT_FAILURE, "Out of memory?!");
268	memset((void *)(config), '\0', config_size);
269
270	/*
271	 * Process command-line options
272	 */
273	while ((ch = getopt(argc, argv,
274	    "a:b:dDhi:I:klL:mn:No:p:P:t:TU:wx:X")) != -1) {
275		switch(ch) {
276		case 'a': /* additional message text to append */
277			if (config->aprompt == NULL) {
278				config->aprompt = malloc(DPV_APROMPT_MAX);
279				if (config->aprompt == NULL)
280					errx(EXIT_FAILURE, "Out of memory?!");
281			}
282			snprintf(config->aprompt, DPV_APROMPT_MAX, "%s",
283			    optarg);
284			break;
285		case 'b': /* [X]dialog(1) backtitle */
286			if (config->backtitle != NULL)
287				free((char *)config->backtitle);
288			config->backtitle = malloc(strlen(optarg) + 1);
289			if (config->backtitle == NULL)
290				errx(EXIT_FAILURE, "Out of memory?!");
291			*(config->backtitle) = '\0';
292			strcat(config->backtitle, optarg);
293			break;
294		case 'd': /* debugging */
295			debug = TRUE;
296			config->debug = debug;
297			break;
298		case 'D': /* use dialog(1) instead of libdialog */
299			config->display_type = DPV_DISPLAY_DIALOG;
300			break;
301		case 'h': /* help/usage */
302			usage();
303			break; /* NOTREACHED */
304		case 'i': /* status line format string for single-file */
305			config->status_solo = optarg;
306			break;
307		case 'I': /* status line format string for many-files */
308			config->status_many = optarg;
309			break;
310		case 'k': /* keep tite */
311			config->keep_tite = TRUE;
312			break;
313		case 'l': /* Line mode */
314			line_mode = TRUE;
315			break;
316		case 'L': /* custom label size */
317			config->label_size =
318			    (int)strtol(optarg, (char **)NULL, 10);
319			if (config->label_size == 0 && errno == EINVAL)
320				errx(EXIT_FAILURE,
321				    "`-L' argument must be numeric");
322			else if (config->label_size < -1)
323				config->label_size = -1;
324			break;
325		case 'm': /* enable multiple file arguments */
326			multiple = TRUE;
327			break;
328		case 'o': /* `-o path' for sending data-read to file */
329			output_type = DPV_OUTPUT_FILE;
330			config->output_type = DPV_OUTPUT_FILE;
331			config->output = optarg;
332			break;
333		case 'n': /* custom number of files per `page' */
334			config->display_limit =
335				(int)strtol(optarg, (char **)NULL, 10);
336			if (config->display_limit == 0 && errno == EINVAL)
337				errx(EXIT_FAILURE,
338				    "`-n' argument must be numeric");
339			else if (config->display_limit < 0)
340				config->display_limit = -1;
341			break;
342		case 'N': /* No overrun (truncate reads of known-length) */
343			no_overrun = TRUE;
344			config->options |= DPV_NO_OVERRUN;
345			break;
346		case 'p': /* additional message text to use as prefix */
347			if (config->pprompt == NULL) {
348				config->pprompt = malloc(DPV_PPROMPT_MAX + 2);
349				if (config->pprompt == NULL)
350					errx(EXIT_FAILURE, "Out of memory?!");
351				/* +2 is for implicit "\n" appended later */
352			}
353			snprintf(config->pprompt, DPV_PPROMPT_MAX, "%s",
354			    optarg);
355			break;
356		case 'P': /* custom size for mini-progressbar */
357			config->pbar_size =
358			    (int)strtol(optarg, (char **)NULL, 10);
359			if (config->pbar_size == 0 && errno == EINVAL)
360				errx(EXIT_FAILURE,
361				    "`-P' argument must be numeric");
362			else if (config->pbar_size < -1)
363				config->pbar_size = -1;
364			break;
365		case 't': /* [X]dialog(1) title */
366			if (config->title != NULL)
367				free(config->title);
368			config->title = malloc(strlen(optarg) + 1);
369			if (config->title == NULL)
370				errx(EXIT_FAILURE, "Out of memory?!");
371			*(config->title) = '\0';
372			strcat(config->title, optarg);
373			break;
374		case 'T': /* test mode (don't read data, fake it) */
375			config->options |= DPV_TEST_MODE;
376			break;
377		case 'U': /* updates per second */
378			config->status_updates_per_second =
379			    (int)strtol(optarg, (char **)NULL, 10);
380			if (config->status_updates_per_second == 0 &&
381			    errno == EINVAL)
382				errx(EXIT_FAILURE,
383				    "`-U' argument must be numeric");
384			break;
385		case 'w': /* `-p' and `-a' widths bump [X]dialog(1) width */
386			config->options |= DPV_WIDE_MODE;
387			break;
388		case 'x': /* `-x cmd' for sending data-read to sh(1) code */
389			output_type = DPV_OUTPUT_SHELL;
390			config->output_type = DPV_OUTPUT_SHELL;
391			config->output = optarg;
392			break;
393		case 'X': /* X11 support through x11/xdialog */
394			config->display_type = DPV_DISPLAY_XDIALOG;
395			break;
396		case '?': /* unknown argument (based on optstring) */
397			/* FALLTHROUGH */
398		default: /* unhandled argument (based on switch) */
399			usage();
400			/* NOTREACHED */
401		}
402	}
403	argc -= optind;
404	argv += optind;
405
406	/* Process remaining arguments as list of names to display */
407	for (curfile = file_list; n < argc; n++) {
408		nfiles++;
409
410		/* Allocate a new struct for the file argument */
411		if (curfile == NULL) {
412			if ((curfile = malloc(file_node_size)) == NULL)
413				errx(EXIT_FAILURE, "Out of memory?!");
414			memset((void *)(curfile), '\0', file_node_size);
415			file_list = curfile;
416		} else {
417			if ((curfile->next = malloc(file_node_size)) == NULL)
418				errx(EXIT_FAILURE, "Out of memory?!");
419			memset((void *)(curfile->next), '\0', file_node_size);
420			curfile = curfile->next;
421		}
422		curfile->name = argv[n];
423
424		/* Read possible `lines:' prefix from label syntax */
425		if (sscanf(curfile->name, "%lli:%c", &(curfile->length),
426		    &dummy) == 2)
427			curfile->name = strchr(curfile->name, ':') + 1;
428		else
429			curfile->length = -1;
430
431		/* Read path argument if enabled */
432		if (multiple) {
433			if (++n >= argc)
434				errx(EXIT_FAILURE, "Missing path argument "
435				    "for label number %i", nfiles);
436			curfile->path = argv[n];
437		} else
438			break;
439	}
440
441	/* Display usage and exit if not given at least one name */
442	if (nfiles == 0) {
443		warnx("no labels provided");
444		usage();
445		/* NOTREACHED */
446	}
447
448	/*
449	 * Set cleanup routine for Ctrl-C action
450	 */
451	if (config->display_type == DPV_DISPLAY_LIBDIALOG) {
452		act.sa_handler = sig_int;
453		sigaction(SIGINT, &act, 0);
454	}
455
456	/* Set status formats and action */
457	if (line_mode) {
458		config->status_solo = LINE_STATUS_SOLO;
459		config->status_many = LINE_STATUS_SOLO;
460		config->action = operate_on_lines;
461	} else {
462		config->status_solo = BYTE_STATUS_SOLO;
463		config->status_many = BYTE_STATUS_SOLO;
464		config->action = operate_on_bytes;
465	}
466
467	/*
468	 * Hand off to dpv(3)...
469	 */
470	if (dpv(config, file_list) != 0 && debug)
471		warnx("dpv(3) returned error!?");
472
473	if (!config->keep_tite)
474		end_dialog();
475	dpv_free();
476
477	exit(EXIT_SUCCESS);
478}
479
480/*
481 * Interrupt handler to indicate we received a Ctrl-C interrupt.
482 */
483static void
484sig_int(int sig __unused)
485{
486	dpv_interrupt = TRUE;
487}
488
489/*
490 * Print short usage statement to stderr and exit with error status.
491 */
492static void
493usage(void)
494{
495
496	if (debug) /* No need for usage */
497		exit(EXIT_FAILURE);
498
499	fprintf(stderr, "Usage: %s [options] bytes:label\n", pgm);
500	fprintf(stderr, "       %s [options] -m bytes1:label1 path1 "
501	    "[bytes2:label2 path2 ...]\n", pgm);
502	fprintf(stderr, "OPTIONS:\n");
503#define OPTFMT "\t%-14s %s\n"
504	fprintf(stderr, OPTFMT, "-a text",
505	    "Append text. Displayed below file progress indicators.");
506	fprintf(stderr, OPTFMT, "-b backtitle",
507	    "String to be displayed on the backdrop, at top-left.");
508	fprintf(stderr, OPTFMT, "-d",
509	    "Debug. Write to standard output instead of dialog.");
510	fprintf(stderr, OPTFMT, "-D",
511	    "Use dialog(1) instead of dialog(3) [default].");
512	fprintf(stderr, OPTFMT, "-h",
513	    "Produce this output on standard error and exit.");
514	fprintf(stderr, OPTFMT, "-i format",
515	    "Customize status line format. See fdpv(1) for details.");
516	fprintf(stderr, OPTFMT, "-I format",
517	    "Customize status line format. See fdpv(1) for details.");
518	fprintf(stderr, OPTFMT, "-L size",
519	    "Label size. Must be a number greater than 0, or -1.");
520	fprintf(stderr, OPTFMT, "-m",
521	    "Enable processing of multiple file argiments.");
522	fprintf(stderr, OPTFMT, "-n num",
523	    "Display at-most num files per screen. Default is -1.");
524	fprintf(stderr, OPTFMT, "-N",
525	    "No overrun. Stop reading input at stated length, if any.");
526	fprintf(stderr, OPTFMT, "-o file",
527	    "Output data to file. First %s replaced with label text.");
528	fprintf(stderr, OPTFMT, "-p text",
529	    "Prefix text. Displayed above file progress indicators.");
530	fprintf(stderr, OPTFMT, "-P size",
531	    "Mini-progressbar size. Must be a number greater than 3.");
532	fprintf(stderr, OPTFMT, "-t title",
533	    "Title string to be displayed at top of dialog(1) box.");
534	fprintf(stderr, OPTFMT, "-T",
535	    "Test mode. Don't actually read any data, but fake it.");
536	fprintf(stderr, OPTFMT, "-U num",
537	    "Update status line num times per-second. Default is 2.");
538	fprintf(stderr, OPTFMT, "-w",
539	    "Wide. Width of `-p' and `-a' text bump dialog(1) width.");
540	fprintf(stderr, OPTFMT, "-x cmd",
541	    "Send data to executed cmd. First %s replaced with label.");
542	fprintf(stderr, OPTFMT, "-X",
543	    "X11. Use Xdialog(1) instead of dialog(1).");
544	exit(EXIT_FAILURE);
545}
546