xref: /freebsd/lib/libdpv/dpv.c (revision 7899f917b1c0ea178f1d2be0cfb452086d079d23)
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/time.h>
30 #include <sys/types.h>
31 #include <sys/wait.h>
32 
33 #include <ctype.h>
34 #include <dialog.h>
35 #include <err.h>
36 #include <limits.h>
37 #include <locale.h>
38 #include <stdio.h>
39 #include <stdlib.h>
40 #include <string.h>
41 #include <string_m.h>
42 #include <unistd.h>
43 
44 #include "dialog_util.h"
45 #include "dialogrc.h"
46 #include "dprompt.h"
47 #include "dpv.h"
48 #include "dpv_private.h"
49 #include "status.h"
50 #include "util.h"
51 
52 /* Test Mechanics (Only used when dpv_config.options |= DPV_TEST_MODE) */
53 #define INCREMENT		1	/* Increment % per-pass test-mode */
54 #define XDIALOG_INCREMENT	15	/* different for slower Xdialog(1) */
55 static uint8_t increment = INCREMENT;
56 
57 /* Debugging */
58 uint8_t debug = FALSE;
59 
60 /* Data to process */
61 int dpv_interrupt = FALSE;
62 int dpv_abort = FALSE;
63 unsigned int dpv_nfiles = 0;
64 
65 /* Data processing */
66 long long dpv_overall_read = 0;
67 static char pathbuf[PATH_MAX];
68 
69 /* Extra display information */
70 uint8_t keep_tite = FALSE;	/* dpv_config.keep_tite */
71 uint8_t no_labels = FALSE;	/* dpv_config.options & DPV_NO_LABELS */
72 uint8_t wide = FALSE;		/* dpv_config.options & DPV_WIDE_MODE */
73 char *aprompt = NULL;		/* dpv_config.aprompt */
74 char *msg_done = NULL;		/* dpv_config.msg_done */
75 char *msg_fail = NULL;		/* dpv_config.msg_fail */
76 char *msg_pending = NULL;	/* dpv_config.msg_pending */
77 char *pprompt = NULL;		/* dpv_config.pprompt */
78 
79 /* Status-Line format for when using dialog(3) */
80 static const char *status_format_custom = NULL;
81 static char status_format_default[DPV_STATUS_FORMAT_MAX];
82 
83 /*
84  * Takes a pointer to a dpv_config structure containing layout details and
85  * pointer to initial element in a linked-list of dpv_file_node structures,
86  * each presenting a file to process. Executes the `action' function passed-in
87  * as a member to the `config' structure argument.
88  */
89 int
90 dpv(struct dpv_config *config, struct dpv_file_node *file_list)
91 {
92 	char c;
93 	uint8_t keep_going;
94 	uint8_t nls = FALSE; /* See dialog_prompt_nlstate() */
95 	uint8_t no_overrun = FALSE;
96 	uint8_t pprompt_nls = FALSE; /* See dialog_prompt_nlstate() */
97 	uint8_t shrink_label_size = FALSE;
98 	mode_t mask;
99 	uint16_t options;
100 	char *cp;
101 	char *fc;
102 	char *last;
103 	char *name;
104 	char *output;
105 	const char *status_fmt;
106 	const char *path_fmt;
107 	enum dpv_display display_type;
108 	enum dpv_output output_type;
109 	enum dpv_status status;
110 	int (*action)(struct dpv_file_node *file, int out);
111 	int backslash;
112 	int dialog_last_update = 0;
113 	int dialog_old_nthfile = 0;
114 	int dialog_old_seconds = -1;
115 	int dialog_out = STDOUT_FILENO;
116 	int dialog_update_usec = 0;
117 	int dialog_updates_per_second;
118 	int files_left;
119 	int max_cols;
120 	int nthfile = 0;
121 	int output_out;
122 	int overall = 0;
123 	int pct;
124 	int res;
125 	int seconds;
126 	int status_last_update = 0;
127 	int status_old_nthfile = 0;
128 	int status_old_seconds = -1;
129 	int status_update_usec = 0;
130 	int status_updates_per_second;
131 	pid_t output_pid;
132 	pid_t pid;
133 	size_t len;
134 	struct dpv_file_node *curfile;
135 	struct dpv_file_node *first_file;
136 	struct dpv_file_node *list_head;
137 	struct timeval now;
138 	struct timeval start;
139 	char init_prompt[PROMPT_MAX + 1] = "";
140 
141 	/* Initialize globals to default values */
142 	aprompt		= NULL;
143 	pprompt		= NULL;
144 	options		= 0;
145 	action		= NULL;
146 	backtitle	= NULL;
147 	debug		= FALSE;
148 	dialog_test	= FALSE;
149 	dialog_updates_per_second = DIALOG_UPDATES_PER_SEC;
150 	display_limit	= DISPLAY_LIMIT_DEFAULT;
151 	display_type	= DPV_DISPLAY_LIBDIALOG;
152 	keep_tite	= FALSE;
153 	label_size	= LABEL_SIZE_DEFAULT;
154 	msg_done	= NULL;
155 	msg_fail	= NULL;
156 	msg_pending	= NULL;
157 	no_labels	= FALSE;
158 	output		= NULL;
159 	output_type	= DPV_OUTPUT_NONE;
160 	pbar_size	= PBAR_SIZE_DEFAULT;
161 	status_format_custom = NULL;
162 	status_updates_per_second = STATUS_UPDATES_PER_SEC;
163 	title		= NULL;
164 	wide		= FALSE;
165 
166 	/* Process config options (overriding defaults) */
167 	if (config != NULL) {
168 		if (config->aprompt != NULL) {
169 			if (aprompt == NULL) {
170 				aprompt = malloc(DPV_APROMPT_MAX);
171 				if (aprompt == NULL)
172 					return (-1);
173 			}
174 			snprintf(aprompt, DPV_APROMPT_MAX, "%s",
175 			    config->aprompt);
176 		}
177 		if (config->pprompt != NULL) {
178 			if (pprompt == NULL) {
179 				pprompt = malloc(DPV_PPROMPT_MAX + 2);
180 				/* +2 is for implicit "\n" appended later */
181 				if (pprompt == NULL)
182 					return (-1);
183 			}
184 			snprintf(pprompt, DPV_APROMPT_MAX, "%s",
185 			    config->pprompt);
186 		}
187 
188 		options		= config->options;
189 		action		= config->action;
190 		backtitle	= config->backtitle;
191 		debug		= config->debug;
192 		dialog_test	= ((options & DPV_TEST_MODE) != 0);
193 		dialog_updates_per_second = config->dialog_updates_per_second;
194 		display_limit	= config->display_limit;
195 		display_type	= config->display_type;
196 		keep_tite	= config->keep_tite;
197 		label_size	= config->label_size;
198 		msg_done	= (char *)config->msg_done;
199 		msg_fail	= (char *)config->msg_fail;
200 		msg_pending	= (char *)config->msg_pending;
201 		no_labels	= ((options & DPV_NO_LABELS) != 0);
202 		no_overrun	= ((options & DPV_NO_OVERRUN) != 0);
203 		output          = config->output;
204 		output_type	= config->output_type;
205 		pbar_size	= config->pbar_size;
206 		status_updates_per_second = config->status_updates_per_second;
207 		title		= config->title;
208 		wide		= ((options & DPV_WIDE_MODE) != 0);
209 
210 		/* Enforce some minimums (pedantic) */
211 		if (display_limit < -1)
212 			display_limit = -1;
213 		if (label_size < -1)
214 			label_size = -1;
215 		if (pbar_size < -1)
216 			pbar_size = -1;
217 
218 		/* For the mini-pbar, -1 means hide, zero is invalid unless
219 		 * only one file is given */
220 		if (pbar_size == 0) {
221 			if (file_list == NULL || file_list->next == NULL)
222 				pbar_size = -1;
223 			else
224 				pbar_size = PBAR_SIZE_DEFAULT;
225 		}
226 
227 		/* For the label, -1 means auto-size, zero is invalid unless
228 		 * specifically requested through the use of options flag */
229 		if (label_size == 0 && no_labels == FALSE)
230 			label_size = LABEL_SIZE_DEFAULT;
231 
232 		/* Status update should not be zero */
233 		if (status_updates_per_second == 0)
234 			status_updates_per_second = STATUS_UPDATES_PER_SEC;
235 	} /* config != NULL */
236 
237 	/* Process the type of display we've been requested to produce */
238 	switch (display_type) {
239 	case DPV_DISPLAY_STDOUT:
240 		debug		= TRUE;
241 		use_color	= FALSE;
242 		use_dialog	= FALSE;
243 		use_libdialog	= FALSE;
244 		use_xdialog	= FALSE;
245 		break;
246 	case DPV_DISPLAY_DIALOG:
247 		use_color	= TRUE;
248 		use_dialog	= TRUE;
249 		use_libdialog	= FALSE;
250 		use_xdialog	= FALSE;
251 		break;
252 	case DPV_DISPLAY_XDIALOG:
253 		snprintf(dialog, PATH_MAX, XDIALOG);
254 		use_color	= FALSE;
255 		use_dialog	= FALSE;
256 		use_libdialog	= FALSE;
257 		use_xdialog	= TRUE;
258 		break;
259 	default:
260 		use_color	= TRUE;
261 		use_dialog	= FALSE;
262 		use_libdialog	= TRUE;
263 		use_xdialog	= FALSE;
264 		break;
265 	} /* display_type */
266 
267 	/* Enforce additional minimums that require knowing our display type */
268 	if (dialog_updates_per_second == 0)
269 		dialog_updates_per_second = use_xdialog ?
270 			XDIALOG_UPDATES_PER_SEC : DIALOG_UPDATES_PER_SEC;
271 
272 	/* Allow forceful override of use_color */
273 	if (config != NULL && (config->options & DPV_USE_COLOR) != 0)
274 		use_color = TRUE;
275 
276 	/* Count the number of files in provided list of dpv_file_node's */
277 	if (use_dialog && pprompt != NULL && *pprompt != '\0')
278 		pprompt_nls = dialog_prompt_nlstate(pprompt);
279 
280 	max_cols = dialog_maxcols();
281 	if (label_size == -1)
282 		shrink_label_size = TRUE;
283 
284 	/* Process file arguments */
285 	for (curfile = file_list; curfile != NULL; curfile = curfile->next) {
286 		dpv_nfiles++;
287 
288 		/* dialog(3) only expands literal newlines */
289 		if (use_libdialog) strexpandnl(curfile->name);
290 
291 		/* Optionally calculate label size for file */
292 		if (shrink_label_size) {
293 			nls = FALSE;
294 			name = curfile->name;
295 			if (curfile == file_list)
296 				nls = pprompt_nls;
297 			last = (char *)dialog_prompt_lastline(name, nls);
298 			if (use_dialog) {
299 				c = *last;
300 				*last = '\0';
301 				nls = dialog_prompt_nlstate(name);
302 				*last = c;
303 			}
304 			len = dialog_prompt_longestline(last, nls);
305 			if ((int)len > (label_size - 3)) {
306 				if (label_size > 0)
307 					label_size += 3;
308 				label_size = len;
309 				/* Room for ellipsis (unless NULL) */
310 				if (label_size > 0)
311 					label_size += 3;
312 			}
313 
314 			if (max_cols > 0 && label_size > (max_cols - pbar_size
315 			    - 9))
316 				label_size = max_cols - pbar_size - 9;
317 		}
318 
319 		if (debug)
320 			warnx("label=[%s] path=[%s] size=%lli",
321 			    curfile->name, curfile->path, curfile->length);
322 	} /* file_list */
323 
324 	/* Optionally process the contents of DIALOGRC (~/.dialogrc) */
325 	if (use_dialog) {
326 		res = parse_dialogrc();
327 		if (debug && res == 0) {
328 			warnx("Successfully read `%s' config file", DIALOGRC);
329 			warnx("use_shadow = %i (Boolean)", use_shadow);
330 			warnx("use_colors = %i (Boolean)", use_colors);
331 			warnx("gauge_color=[%s] (FBH)", gauge_color);
332 		}
333 	} else if (use_libdialog) {
334 		init_dialog(stdin, stdout);
335 		use_shadow = dialog_state.use_shadow;
336 		use_colors = dialog_state.use_colors;
337 		gauge_color[0] = 48 + dlg_color_table[GAUGE_ATTR].fg;
338 		gauge_color[1] = 48 + dlg_color_table[GAUGE_ATTR].bg;
339 		gauge_color[2] = dlg_color_table[GAUGE_ATTR].hilite ?
340 		    'b' : 'B';
341 		gauge_color[3] = '\0';
342 		end_dialog();
343 		if (debug) {
344 			warnx("Finished initializing dialog(3) library");
345 			warnx("use_shadow = %i (Boolean)", use_shadow);
346 			warnx("use_colors = %i (Boolean)", use_colors);
347 			warnx("gauge_color=[%s] (FBH)", gauge_color);
348 		}
349 	}
350 
351 	/* Enable mini progress bar automatically for stdin streams if unable
352 	 * to calculate progress (missing `lines:' syntax). */
353 	if (dpv_nfiles <= 1 && file_list != NULL && file_list->length < 0 &&
354 	    !dialog_test)
355 		pbar_size = PBAR_SIZE_DEFAULT;
356 
357 	/* If $USE_COLOR is set and non-NULL enable color; otherwise disable */
358 	if ((cp = getenv(ENV_USE_COLOR)) != 0)
359 		use_color = *cp != '\0' ? 1 : 0;
360 
361 	/* Print error and return `-1' if not given at least one name */
362 	if (dpv_nfiles == 0) {
363 		warnx("%s: no labels provided", __func__);
364 		return (-1);
365 	} else if (debug)
366 		warnx("%s: %u label%s provided", __func__, dpv_nfiles,
367 		    dpv_nfiles == 1 ? "" : "s");
368 
369 	/* If only one file and pbar size is zero, default to `-1' */
370 	if (dpv_nfiles <= 1 && pbar_size == 0)
371 		pbar_size = -1;
372 
373 	/* Print some debugging information */
374 	if (debug) {
375 		warnx("%s: %s(%i) max rows x cols = %i x %i",
376 		    __func__, use_xdialog ? XDIALOG : DIALOG,
377 		    use_libdialog ? 3 : 1, dialog_maxrows(),
378 		    dialog_maxcols());
379 	}
380 
381 	/* Xdialog(1) updates a lot slower than dialog(1) */
382 	if (dialog_test && use_xdialog)
383 		increment = XDIALOG_INCREMENT;
384 
385 	/* Always add implicit newline to pprompt (when specified) */
386 	if (pprompt != NULL && *pprompt != '\0') {
387 		len = strlen(pprompt);
388 		/*
389 		 * NOTE: pprompt = malloc(PPROMPT_MAX + 2)
390 		 * NOTE: (see getopt(2) section above for pprompt allocation)
391 		 */
392 		pprompt[len++] = '\\';
393 		pprompt[len++] = 'n';
394 		pprompt[len++] = '\0';
395 	}
396 
397 	/* Xdialog(1) requires newlines (a) escaped and (b) in triplicate */
398 	if (use_xdialog && pprompt != NULL) {
399 		/* Replace `\n' with `\n\\n\n' in pprompt */
400 		len = strlen(pprompt);
401 		len += strcount(pprompt, "\\n") * 2;
402 		if (len > DPV_PPROMPT_MAX)
403 			errx(EXIT_FAILURE, "%s: Oops, pprompt buffer overflow "
404 			    "(%zu > %i)", __func__, len, DPV_PPROMPT_MAX);
405 		if (replaceall(pprompt, "\\n", "\n\\n\n") < 0)
406 			err(EXIT_FAILURE, "%s: replaceall()", __func__);
407 	}
408 	/* libdialog requires literal newlines */
409 	else if (use_libdialog && pprompt != NULL)
410 		strexpandnl(pprompt);
411 
412 	/* Xdialog(1) requires newlines (a) escaped and (b) in triplicate */
413 	if (use_xdialog && aprompt != NULL) {
414 		/* Replace `\n' with `\n\\n\n' in aprompt */
415 		len = strlen(aprompt);
416 		len += strcount(aprompt, "\\n") * 2;
417 		if (len > DPV_APROMPT_MAX)
418 			errx(EXIT_FAILURE, "%s: Oops, aprompt buffer overflow "
419 			    " (%zu > %i)", __func__, len, DPV_APROMPT_MAX);
420 		if (replaceall(aprompt, "\\n", "\n\\n\n") < 0)
421 			err(EXIT_FAILURE, "%s: replaceall()", __func__);
422 	}
423 	/* libdialog requires literal newlines */
424 	else if (use_libdialog && aprompt != NULL)
425 		strexpandnl(aprompt);
426 
427 	/*
428 	 * Warn user about an obscure dialog(1) bug (neither Xdialog(1) nor
429 	 * libdialog are affected) in the `--gauge' widget. If the first non-
430 	 * whitespace letter of "{new_prompt}" in "XXX\n{new_prompt}\nXXX\n"
431 	 * is a number, the number can sometimes be mistaken for a percentage
432 	 * to the overall progressbar. Other nasty side-effects such as the
433 	 * entire prompt not displaying or displaying improperly are caused by
434 	 * this bug too.
435 	 *
436 	 * NOTE: When we can use color, we have a work-around... prefix the
437 	 * output with `\Zn' (used to terminate ANSI and reset to normal).
438 	 */
439 	if (use_dialog && !use_color) {
440 		backslash = 0;
441 
442 		/* First, check pprompt (falls through if NULL) */
443 		fc = pprompt;
444 		while (fc != NULL && *fc != '\0') {
445 			if (*fc == '\n') /* leading literal newline OK */
446 				break;
447 			if (!isspace(*fc) && *fc != '\\' && backslash == 0)
448 				break;
449 			else if (backslash > 0 && *fc != 'n')
450 				break;
451 			else if (*fc == '\\') {
452 				backslash++;
453 				if (backslash > 2)
454 					break; /* we're safe */
455 			}
456 			fc++;
457 		}
458 		/* First non-whitespace character that dialog(1) will see */
459 		if (fc != NULL && *fc >= '0' && *fc <= '9')
460 			warnx("%s: WARNING! text argument to `-p' begins with "
461 			    "a number (not recommended)", __func__);
462 		else if (fc > pprompt)
463 			warnx("%s: WARNING! text argument to `-p' begins with "
464 			    "whitespace (not recommended)", __func__);
465 
466 		/*
467 		 * If no pprompt or pprompt is all whitespace, check the first
468 		 * file name provided to make sure it is alright too.
469 		 */
470 		if ((pprompt == NULL || *fc == '\0') && file_list != NULL) {
471 			first_file = file_list;
472 			fc = first_file->name;
473 			while (fc != NULL && *fc != '\0' && isspace(*fc))
474 				fc++;
475 			/* First non-whitespace char that dialog(1) will see */
476 			if (fc != NULL && *fc >= '0' && *fc <= '9')
477 				warnx("%s: WARNING! File name `%s' begins "
478 				    "with a number (use `-p text' for safety)",
479 				    __func__, first_file->name);
480 		}
481 	}
482 
483 	dprompt_init(file_list);
484 		/* Reads: label_size pbar_size pprompt aprompt dpv_nfiles */
485 		/* Inits: dheight and dwidth */
486 
487 	/* Default localeconv(3) settings for dialog(3) status */
488 	setlocale(LC_NUMERIC,
489 		getenv("LC_ALL") == NULL && getenv("LC_NUMERIC") == NULL ?
490 		LC_NUMERIC_DEFAULT : "");
491 
492 	if (!debug) {
493 		/* Internally create the initial `--gauge' prompt text */
494 		dprompt_recreate(file_list, (struct dpv_file_node *)NULL, 0);
495 
496 		/* Spawn [X]dialog(1) `--gauge', returning pipe descriptor */
497 		if (use_libdialog) {
498 			status_printf("");
499 			dprompt_libprint(pprompt, aprompt, 0);
500 		} else {
501 			dprompt_sprint(init_prompt, pprompt, aprompt);
502 			dialog_out = dialog_spawn_gauge(init_prompt, &pid);
503 			dprompt_dprint(dialog_out, pprompt, aprompt, 0);
504 		}
505 	} /* !debug */
506 
507 	/* Seed the random(3) generator */
508 	if (dialog_test)
509 		srandom(0xf1eeface);
510 
511 	/* Set default/custom status line format */
512 	if (dpv_nfiles > 1) {
513 		snprintf(status_format_default, DPV_STATUS_FORMAT_MAX, "%s",
514 		    DPV_STATUS_MANY);
515 		status_format_custom = config->status_many;
516 	} else {
517 		snprintf(status_format_default, DPV_STATUS_FORMAT_MAX, "%s",
518 		    DPV_STATUS_SOLO);
519 		status_format_custom = config->status_solo;
520 	}
521 
522 	/* Add test mode identifier to default status line if enabled */
523 	if (dialog_test && (strlen(status_format_default) + 12) <
524 	    DPV_STATUS_FORMAT_MAX)
525 		strcat(status_format_default, " [TEST MODE]");
526 
527 	/* Verify custom status format */
528 	status_fmt = fmtcheck(status_format_custom, status_format_default);
529 	if (status_format_custom != NULL &&
530 	    status_fmt == status_format_default) {
531 		warnx("WARNING! Invalid status_format configuration `%s'",
532 		      status_format_custom);
533 		warnx("Default status_format `%s'", status_format_default);
534 	}
535 
536 	/* Record when we started (used to prevent updating too quickly) */
537 	(void)gettimeofday(&start, (struct timezone *)NULL);
538 
539 	/* Calculate number of microseconds in-between sub-second updates */
540 	if (status_updates_per_second != 0)
541 		status_update_usec = 1000000 / status_updates_per_second;
542 	if (dialog_updates_per_second != 0)
543 		dialog_update_usec = 1000000 / dialog_updates_per_second;
544 
545 	/*
546 	 * Process the file list [serially] (one for each argument passed)
547 	 */
548 	files_left = dpv_nfiles;
549 	list_head = file_list;
550 	for (curfile = file_list; curfile != NULL; curfile = curfile->next) {
551 		keep_going = TRUE;
552 		output_out = -1;
553 		pct = 0;
554 		nthfile++;
555 		files_left--;
556 
557 		if (dpv_interrupt)
558 			break;
559 		if (dialog_test)
560 			pct = 0 - increment;
561 
562 		/* Attempt to spawn output program for this file */
563 		if (!dialog_test && output != NULL) {
564 			mask = umask(0022);
565 			(void)umask(mask);
566 
567 			switch (output_type) {
568 			case DPV_OUTPUT_SHELL:
569 				output_out = shell_spawn_pipecmd(output,
570 				    curfile->name, &output_pid);
571 				break;
572 			case DPV_OUTPUT_FILE:
573 				path_fmt = fmtcheck(output, "%s");
574 				if (path_fmt == output)
575 					len = snprintf(pathbuf,
576 					    PATH_MAX, output, curfile->name);
577 				else
578 					len = snprintf(pathbuf,
579 					    PATH_MAX, "%s", output);
580 				if (len >= PATH_MAX) {
581 					warnx("%s:%d:%s: pathbuf[%u] too small"
582 					    "to hold output argument",
583 					    __FILE__, __LINE__, __func__,
584 					    PATH_MAX);
585 					return (-1);
586 				}
587 				if ((output_out = open(pathbuf,
588 				    O_CREAT|O_WRONLY, DEFFILEMODE & ~mask))
589 				    < 0) {
590 					warn("%s", pathbuf);
591 					return (-1);
592 				}
593 				break;
594 			default:
595 				break;
596 			}
597 		}
598 
599 		while (!dpv_interrupt && keep_going) {
600 			if (dialog_test) {
601 				usleep(50000);
602 				pct += increment;
603 				dpv_overall_read +=
604 				    (int)(random() / 512 / dpv_nfiles);
605 				    /* 512 limits fake readout to Megabytes */
606 			} else if (action != NULL)
607 				pct = action(curfile, output_out);
608 
609 			if (no_overrun || dialog_test)
610 				keep_going = (pct < 100);
611 			else {
612 				status = curfile->status;
613 				keep_going = (status == DPV_STATUS_RUNNING);
614 			}
615 
616 			/* Get current time and calculate seconds elapsed */
617 			gettimeofday(&now, (struct timezone *)NULL);
618 			now.tv_sec = now.tv_sec - start.tv_sec;
619 			now.tv_usec = now.tv_usec - start.tv_usec;
620 			if (now.tv_usec < 0)
621 				now.tv_sec--, now.tv_usec += 1000000;
622 			seconds = now.tv_sec + (now.tv_usec / 1000000.0);
623 
624 			/* Update dialog (be it dialog(3), dialog(1), etc.) */
625 			if ((dialog_updates_per_second != 0 &&
626 			   (
627 			    seconds != dialog_old_seconds ||
628 			    now.tv_usec - dialog_last_update >=
629 			        dialog_update_usec ||
630 			    nthfile != dialog_old_nthfile
631 			   )) || pct == 100
632 			) {
633 				/* Calculate overall progress (rounding up) */
634 				overall = (100 * nthfile - 100 + pct) /
635 				    dpv_nfiles;
636 				if (((100 * nthfile - 100 + pct) * 10 /
637 				    dpv_nfiles % 100) > 50)
638 					overall++;
639 
640 				dprompt_recreate(list_head, curfile, pct);
641 
642 				if (use_libdialog && !debug) {
643 					/* Update dialog(3) widget */
644 					dprompt_libprint(pprompt, aprompt,
645 					    overall);
646 				} else {
647 					/* stdout, dialog(1), or Xdialog(1) */
648 					dprompt_dprint(dialog_out, pprompt,
649 					    aprompt, overall);
650 					fsync(dialog_out);
651 				}
652 				dialog_old_seconds = seconds;
653 				dialog_old_nthfile = nthfile;
654 				dialog_last_update = now.tv_usec;
655 			}
656 
657 			/* Update the status line */
658 			if ((use_libdialog && !debug) &&
659 			    status_updates_per_second != 0 &&
660 			   (
661 			    keep_going != TRUE ||
662 			    seconds != status_old_seconds ||
663 			    now.tv_usec - status_last_update >=
664 			        status_update_usec ||
665 			    nthfile != status_old_nthfile
666 			   )
667 			) {
668 				status_printf(status_fmt, dpv_overall_read,
669 				    (dpv_overall_read / (seconds == 0 ? 1 :
670 					seconds) * 1.0),
671 				    1, /* XXX until we add parallelism XXX */
672 				    files_left);
673 				status_old_seconds = seconds;
674 				status_old_nthfile = nthfile;
675 				status_last_update = now.tv_usec;
676 			}
677 		}
678 
679 		if (!dialog_test && output_out >= 0) {
680 			close(output_out);
681 			waitpid(output_pid, (int *)NULL, 0);
682 		}
683 
684 		if (dpv_abort)
685 			break;
686 
687 		/* Advance head of list when we hit the max display lines */
688 		if (display_limit > 0 && nthfile % display_limit == 0)
689 			list_head = curfile->next;
690 	}
691 
692 	if (!debug) {
693 		if (use_libdialog)
694 			end_dialog();
695 		else {
696 			close(dialog_out);
697 			waitpid(pid, (int *)NULL, 0);
698 		}
699 		if (!keep_tite && !dpv_interrupt)
700 			printf("\n");
701 	} else
702 		warnx("%s: %lli overall read", __func__, dpv_overall_read);
703 
704 	if (dpv_interrupt || dpv_abort)
705 		return (-1);
706 	else
707 		return (0);
708 }
709 
710 /*
711  * Free allocated items initialized by dpv()
712  */
713 void
714 dpv_free(void)
715 {
716 	dialogrc_free();
717 	dprompt_free();
718 	dialog_maxsize_free();
719 	if (aprompt != NULL) {
720 		free(aprompt);
721 		aprompt = NULL;
722 	}
723 	if (pprompt != NULL) {
724 		free(pprompt);
725 		pprompt = NULL;
726 	}
727 	status_free();
728 }
729