xref: /freebsd/usr.bin/dpv/dpv.c (revision 732a02b4e77866604a120a275c082bb6221bd2ff)
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$");
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 */
50 static uint8_t debug = FALSE;
51 
52 /* Data to process */
53 static struct dpv_file_node *file_list = NULL;
54 static unsigned int nfiles = 0;
55 
56 /* Data processing */
57 static uint8_t line_mode = FALSE;
58 static uint8_t no_overrun = FALSE;
59 static char *buf = NULL;
60 static int fd = -1;
61 static int output_type = DPV_OUTPUT_NONE;
62 static size_t bsize;
63 static char rpath[PATH_MAX];
64 
65 /* Extra display information */
66 static uint8_t multiple = FALSE; /* `-m' */
67 static char *pgm; /* set to argv[0] by main() */
68 
69 /* Function prototypes */
70 static void	sig_int(int sig);
71 static void	usage(void);
72 int		main(int argc, char *argv[]);
73 static int	operate_common(struct dpv_file_node *file, int out);
74 static int	operate_on_bytes(struct dpv_file_node *file, int out);
75 static int	operate_on_lines(struct dpv_file_node *file, int out);
76 
77 static int
78 operate_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 
153 static int
154 operate_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 
197 static int
198 operate_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  */
251 int
252 main(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  */
483 static void
484 sig_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  */
492 static void
493 usage(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