xref: /freebsd/usr.bin/dpv/dpv.c (revision 0f7f3352c8bc463607912e2463d13e52d44a4cae)
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