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