/*
 * Copyright (c) 2000-2002 Silicon Graphics, Inc.
 * All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it would be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write the Free Software Foundation,
 * Inc.,  51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/param.h>
#include <sys/wait.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <time.h>
#include <limits.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <termios.h>
#include <getopt.h>
#include <stdint.h>
#include <sched.h>
#include <pthread.h>
#include <assert.h>
#include <string.h>
#include <uuid/uuid.h>
#include <locale.h>

#include "config.h"

#include "exit.h"
#include "types.h"
#include "stream.h"
#include "cldmgr.h"
#include "getopt.h"
#include "mlog.h"
#include "qlock.h"
#include "lock.h"
#include "dlog.h"
#include "global.h"
#include "drive.h"
#include "media.h"
#include "content.h"
#include "inventory.h"

#ifdef DUMP
/* main.c - main for dump
 */
#endif /* DUMP */
#ifdef RESTORE
/* main.c - main for restore
 */
#endif /* RESTORE */


/* structure definitions used locally ****************************************/

#ifdef RESTORE
#define VMSZ_PER	4	/* proportion of available vm to use in tree */
#endif /* RESTORE */
#define DLOG_TIMEOUT	60	/* time out operator dialog */
#define STOP_TIMEOUT	600	/* seconds after stop req. before abort */
#define ABORT_TIMEOUT	10	/* seconds after abort req. before abort */
#define MINSTACKSZ	0x02000000
#define MAXSTACKSZ	0x08000000


/* declarations of externally defined global symbols *************************/

extern void rmt_turnonmsgs(int);

/* forward declarations of locally defined global functions ******************/

void usage(void);
bool_t preemptchk(int);


/* forward declarations of locally defined static functions ******************/

static bool_t loadoptfile(int *argcp, char ***argvp);
static char * stripquotes(char *p);
static void shiftleftby1(char *p, char *endp);
static void sighandler(int);
static int childmain(void *);
static bool_t sigint_dialog(void);
static char *sigintstr(void);
#ifdef DUMP
static bool_t set_rlimits(void);
#endif /* DUMP */
#ifdef RESTORE
static bool_t set_rlimits(size64_t *);
#endif /* RESTORE */
static char *sig_numstring(int num);
static char *strpbrkquotes(char *p, const char *sep);


/* definition of locally defined global variables ****************************/

char *progname = 0;			/* used in all error output */
char *homedir = 0;			/* directory invoked from */
bool_t pipeline = BOOL_FALSE;
bool_t stdoutpiped = BOOL_FALSE;
pthread_t parenttid;
char *sistr;
size_t pgsz;
size_t pgmask;


/* definition of locally defined static variables *****************************/

static rlim64_t minstacksz;
static rlim64_t maxstacksz;
#ifdef RESTORE
static size64_t vmsz;
#endif /* RESTORE */
static time32_t stop_deadline;
static bool_t stop_in_progress;
static bool_t sighup_received;
static bool_t sigterm_received;
static bool_t sigquit_received;
static bool_t sigint_received;
/* REFERENCED */
static int sigstray_received;
static bool_t progrpt_enabledpr;
static time32_t progrpt_interval;
static time32_t progrpt_deadline;


/* definition of locally defined global functions ****************************/


int
main(int argc, char *argv[])
{
	int c;
#ifdef DUMP
	uid_t euid;
#endif /* DUMP */
	ix_t stix; /* stream index */
	bool_t infoonly;
#ifdef DUMP
	global_hdr_t *gwhdrtemplatep;
#endif /* DUMP */
	bool_t init_error;
	bool_t coredump_requested = BOOL_FALSE;
	int exitcode;
	rlim64_t tmpstacksz;
	struct sigaction sa;
	int prbcld_xc = EXIT_NORMAL;
	int xc;
	bool_t ok;
	/* REFERENCED */
	int rval;
	int err;

	/* sanity checks
	 */
	assert(sizeof(char_t) == 1);
	assert(sizeof(u_char_t) == 1);
	assert(sizeof(int32_t) == 4);
	assert(sizeof(uint32_t) == 4);
	assert(sizeof(size32_t) == 4);
	assert(sizeof(int64_t) == 8);
	assert(sizeof(uint64_t) == 8);
	assert(sizeof(size64_t) == 8);

	/* record the command name used to invoke
	 */
	progname = argv[0];

	/* setup I18N support */
	setlocale(LC_ALL, "");
	bindtextdomain(PACKAGE, LOCALEDIR);
	textdomain(PACKAGE);

	/* bootstrap message logging (stage 0)
	*/
	mlog_init0();

	/* Get the parent's pthread id. will be used
	 * to differentiate parent from children.
	 */
	parenttid = pthread_self();
	rval = atexit(mlog_exit_flush);
	assert(rval == 0);

	/* pre-scan the command line for the option file option.
	 * if found, create a new argv.
	 */
	ok = loadoptfile(&argc, &argv);
	if (!ok) {
		return mlog_exit(EXIT_ERROR, RV_OPT);
	}

	/* initialize message logging (stage 1)
	 */
	ok = mlog_init1(argc, argv);
	if (!ok) {
		return mlog_exit(EXIT_ERROR, RV_INIT);
	}
	/* scan the command line for the info, progress
	 * report options, and stacksz.
	 */
	minstacksz = MINSTACKSZ;
	maxstacksz = MAXSTACKSZ;
	infoonly = BOOL_FALSE;
	progrpt_enabledpr = BOOL_FALSE;
	optind = 1;
	opterr = 0;
	while ((c = getopt(argc, argv, GETOPT_CMDSTRING)) != EOF) {
		switch (c) {
                case GETOPT_MINSTACKSZ:
			if (!optarg || optarg[0] == '-') {
				mlog(MLOG_NORMAL | MLOG_ERROR | MLOG_NOLOCK,
				      _("-%c argument missing\n"),
				      c);
				usage();
				return mlog_exit(EXIT_ERROR, RV_OPT);
			}
			errno = 0;
			tmpstacksz = strtoull(optarg, 0, 0);
			if (tmpstacksz == UINT64_MAX
			     ||
			     errno == ERANGE) {
				mlog(MLOG_NORMAL | MLOG_ERROR | MLOG_NOLOCK,
				      _("-%c argument (%s) invalid\n"),
				      c,
				      optarg);
				usage();
				return mlog_exit(EXIT_ERROR, RV_OPT);
			}
			minstacksz = tmpstacksz;
			break;
                case GETOPT_MAXSTACKSZ:
			if (!optarg || optarg[0] == '-') {
				mlog(MLOG_NORMAL | MLOG_ERROR | MLOG_NOLOCK,
				      _("-%c argument missing\n"),
				      c);
				usage();
				return mlog_exit(EXIT_ERROR, RV_OPT);
			}
			errno = 0;
			tmpstacksz = strtoull(optarg, 0, 0);
			if (tmpstacksz == UINT64_MAX
			     ||
			     errno == ERANGE) {
				mlog(MLOG_NORMAL | MLOG_ERROR | MLOG_NOLOCK,
				      _("-%c argument (%s) invalid\n"),
				      c,
				      optarg);
				usage();
				return mlog_exit(EXIT_ERROR, RV_OPT);
			}
			maxstacksz = tmpstacksz;
			break;
		case GETOPT_HELP:
			infoonly = BOOL_TRUE;
			mlog_exit_hint(RV_USAGE);
			break;
		case GETOPT_PROGRESS:
			if (!optarg || optarg[0] == '-') {
				mlog(MLOG_NORMAL | MLOG_ERROR | MLOG_NOLOCK,
				      _("-%c argument missing\n"),
				      c);
				usage();
				return mlog_exit(EXIT_ERROR, RV_OPT);
			}
			progrpt_interval = (time32_t)atoi(optarg);
			if (progrpt_interval > 0) {
				progrpt_enabledpr = BOOL_TRUE;
			} else {
				progrpt_enabledpr = BOOL_FALSE;
			}
			break;
		}
	}

	/* sanity check resultant stack size limits
	 */
	if (minstacksz > maxstacksz) {
		mlog(MLOG_NORMAL
		      |
		      MLOG_ERROR
		      |
		      MLOG_NOLOCK
		      |
		      MLOG_PROC,
		      _("specified minimum stack size is larger than maximum: "
		      "min is 0x%llx,  max is 0x%llx\n"),
		      minstacksz,
		      maxstacksz);
		return mlog_exit(EXIT_ERROR, RV_INIT);
	}

	if (argc == 1) {
		infoonly = BOOL_TRUE;
	}

	/* set a progress report deadline to allow preemptchk() to
	 * report
	 */
	if (progrpt_enabledpr) {
		progrpt_deadline = time(0) + progrpt_interval;
	}

	/* intitialize the stream manager
	 */
	stream_init();

#ifdef DUMP
	/* set the memory limits to their appropriate values.
	 */
	ok = set_rlimits();
#endif /* DUMP */
#ifdef RESTORE
	/* set the memory limits to their appropriate values. this is necessary
	 * to accomodate the tree abstraction and some recursive functions.
	 * also determines maximum vm, which will be budgeted among the
	 * various abstractions.
	 */
	ok = set_rlimits(&vmsz);
#endif /* RESTORE */
	if (!ok) {
		return mlog_exit(EXIT_ERROR, RV_INIT);
	}

	/* initialize message logging (stage 2) - allocate the message lock
	 */
	ok = mlog_init2();
	if (!ok) {
		return mlog_exit(EXIT_ERROR, RV_INIT);
	}

	/* initialize the critical region lock
	 */
	lock_init();
	rmt_turnonmsgs(1); /* turn on WARNING msgs for librmt */

	mlog(MLOG_NITTY + 1, "INTGENMAX == %ld (0x%lx)\n", INTGENMAX, INTGENMAX);
	mlog(MLOG_NITTY + 1, "UINTGENMAX == %lu (0x%lx)\n", UINTGENMAX, UINTGENMAX);
	mlog(MLOG_NITTY + 1, "OFF64MAX == %lld (0x%llx)\n", OFF64MAX, OFF64MAX);
	mlog(MLOG_NITTY + 1, "OFFMAX == %ld (0x%lx)\n", OFFMAX, OFFMAX);
	mlog(MLOG_NITTY + 1, "SIZEMAX == %lu (0x%lx)\n", SIZEMAX, SIZEMAX);
	mlog(MLOG_NITTY + 1, "INOMAX == %lu (0x%lx)\n", INOMAX, INOMAX);
	mlog(MLOG_NITTY + 1, "TIMEMAX == %ld (0x%lx)\n", TIMEMAX, TIMEMAX);
	mlog(MLOG_NITTY + 1, "SIZE64MAX == %llu (0x%llx)\n", SIZE64MAX, SIZE64MAX);
	mlog(MLOG_NITTY + 1, "INO64MAX == %llu (0x%llx)\n", INO64MAX, INO64MAX);
	mlog(MLOG_NITTY + 1, "UINT64MAX == %llu (0x%llx)\n", UINT64MAX, UINT64MAX);
	mlog(MLOG_NITTY + 1, "INT64MAX == %lld (0x%llx)\n", INT64MAX, INT64MAX);
	mlog(MLOG_NITTY + 1, "UINT32MAX == %u (0x%x)\n", UINT32MAX, UINT32MAX);
	mlog(MLOG_NITTY + 1, "INT32MAX == %d (0x%x)\n", INT32MAX, INT32MAX);
	mlog(MLOG_NITTY + 1, "INT16MAX == %d (0x%x)\n", INT16MAX, INT16MAX);
	mlog(MLOG_NITTY + 1, "UINT16MAX == %u (0x%x)\n", UINT16MAX, UINT16MAX);

	/* ask the system for the true vm page size, which must be used
	 * in all mmap calls
	 */
	pgsz = (size_t)getpagesize();
	mlog(MLOG_DEBUG | MLOG_PROC,
	      "getpagesize( ) returns %u\n",
	      pgsz);
	assert((int)pgsz > 0);
	pgmask = pgsz - 1;

	/* report parent tid
         */
	mlog(MLOG_DEBUG | MLOG_PROC,
	      "parent tid is %lu\n",
	      parenttid);

	/* get the current working directory: this is where we will dump
	 * core, if necessary. some tmp files may be placed here as well.
	 */
	homedir = getcwd(0, MAXPATHLEN);
	if (!homedir) {
		mlog(MLOG_NORMAL | MLOG_ERROR,
		      _("unable to determine current directory: %s\n"),
		      strerror(errno));
		return mlog_exit(EXIT_ERROR, RV_INIT);
	}

	/* sanity check the inventory database directory, setup global paths
	 */
	ok = inv_setup_base();
	if (!ok) {
		mlog(MLOG_NORMAL | MLOG_ERROR | MLOG_NOLOCK,
		      _("both /var/lib/xfsdump and /var/xfsdump exist - fatal\n"));
		return mlog_exit(EXIT_ERROR, RV_INIT);
	}

	/* if just looking for info, oblige
	 */
	if (infoonly) {
		mlog(MLOG_NORMAL,
		      _("version %s (dump format %d.0)\n"),
		      VERSION, GLOBAL_HDR_VERSION);
		usage();
		return mlog_exit(EXIT_NORMAL, RV_OK); /* normal termination */
	}

	/* if an inventory display is requested, do it and exit
	 */
	if (!inv_DEBUG_print(argc, argv)) {
		return mlog_exit(EXIT_NORMAL, RV_OK); /* normal termination */
	}

#ifdef DUMP
	/* insist that the effective user id is root.
	 * this must appear after inv_DEBUG_print(),
	 * so it may be done without root privilege.
	 */
	euid = geteuid();
	mlog(MLOG_DEBUG | MLOG_PROC,
	      "effective user id is %d\n",
	      euid);
	if (euid != 0) {
		mlog(MLOG_NORMAL,
		      _("effective user ID must be root\n"));
		return mlog_exit(EXIT_ERROR, RV_PERM);
	}
#endif /* DUMP */

	/* initialize operator dialog capability
	 */
	ok = dlog_init(argc, argv);
	if (!ok) {
		return mlog_exit(EXIT_ERROR, RV_INIT);
	}

	/* initialize the child process manager
	 */
	ok = cldmgr_init();
	if (!ok) {
		return mlog_exit(EXIT_ERROR, RV_INIT);
	}

	/* select and instantiate a drive manager for each stream. this
	 * is the first pass at initialization, so don't do anything
	 * terribly time-consuming here. A second initialization pass
	 * will be done shortly.
	 */
	ok = drive_init1(argc, argv);
	if (!ok) {
		return mlog_exit(EXIT_ERROR, RV_INIT);
	}

	/* check the drives to see if we're in a pipeline.
	 * if not, check stdout anyway, in case someone is trying to pipe
	 * the log messages into more, tee, ...
	 */
	if (drivepp[0]->d_isunnamedpipepr) {
		mlog(MLOG_DEBUG | MLOG_NOTE,
		      "pipeline detected\n");
		pipeline = BOOL_TRUE;
	} else {
		struct stat64 statbuf;
		if (fstat64(1, &statbuf) == 0
		     &&
		     (statbuf.st_mode & S_IFMT) == S_IFIFO) {
			stdoutpiped = BOOL_TRUE;
		}
	}

	/* announce version and instructions
	 */
	sistr = sigintstr();
	mlog(MLOG_VERBOSE,
	      _("version %s (dump format %d.0)"),
	      VERSION, GLOBAL_HDR_VERSION);
	if (!pipeline && !stdoutpiped && sistr && dlog_allowed()) {
		mlog(MLOG_VERBOSE | MLOG_BARE, _(
		      " - "
		      "type %s for status and control\n"),
		      sistr);
	} else {
		mlog(MLOG_VERBOSE | MLOG_BARE,
		      "\n");
	}

#ifdef DUMP
	/* build a global write header template
	 */
	gwhdrtemplatep = global_hdr_alloc(argc, argv);
	if (!gwhdrtemplatep) {
		return mlog_exit(EXIT_ERROR, RV_INIT);
	}
#endif /* DUMP */

	/* tell mlog how many streams there are. the format of log messages
	 * depends on whether there are one or many.
	 */
	mlog_tell_streamcnt(drivecnt);

	/* initialize the state of signal processing. if in a pipeline, just
	 * want to exit when a signal is received. otherwise, hold signals so
	 * they don't interfere with sys calls; they will be released at
	 * pre-emption points and upon pausing in the main loop.
	 *
	 * note that since we're multi-threaded, handling SIGCHLD causes
	 * problems with system()'s ability to obtain a child's exit status
	 * (because the main thread may process SIGCHLD before the thread
	 * running system() calls waitpid()). likewise explicitly ignoring
	 * SIGCHLD also prevents system() from getting an exit status.
	 * therefore we don't do anything with SIGCHLD.
	 */

	sigfillset(&sa.sa_mask);
	sa.sa_flags = 0;

	/* always ignore SIGPIPE, instead handle EPIPE as part
	 * of normal sys call error handling.
	 */
	sa.sa_handler = SIG_IGN;
	sigaction(SIGPIPE, &sa, NULL);

	if (!pipeline) {
		sigset_t blocked_set;

		stop_in_progress = BOOL_FALSE;
		coredump_requested = BOOL_FALSE;
		sighup_received = BOOL_FALSE;
		sigterm_received = BOOL_FALSE;
		sigint_received = BOOL_FALSE;
		sigquit_received = BOOL_FALSE;
		sigstray_received = BOOL_FALSE;

		alarm(0);

		sigemptyset(&blocked_set);
		sigaddset(&blocked_set, SIGINT);
		sigaddset(&blocked_set, SIGHUP);
		sigaddset(&blocked_set, SIGTERM);
		sigaddset(&blocked_set, SIGQUIT);
		sigaddset(&blocked_set, SIGALRM);
		sigaddset(&blocked_set, SIGUSR1);
		pthread_sigmask(SIG_SETMASK, &blocked_set, NULL);

		sa.sa_handler = sighandler;
		sigaction(SIGINT, &sa, NULL);
		sigaction(SIGHUP, &sa, NULL);
		sigaction(SIGTERM, &sa, NULL);
		sigaction(SIGQUIT, &sa, NULL);
		sigaction(SIGALRM, &sa, NULL);
		sigaction(SIGUSR1, &sa, NULL);
	}

	/* do content initialization.
	 */
#ifdef DUMP
	ok = content_init(argc, argv, gwhdrtemplatep);
#endif /* DUMP */
#ifdef RESTORE
	ok = content_init(argc, argv, vmsz / VMSZ_PER);
#endif /* RESTORE */
	if (!ok) {
		err = mlog_exit(EXIT_ERROR, RV_INIT);
		goto err_free;
	}

	/* if in a pipeline, go single-threaded with just one stream.
	 */
	if (pipeline) {
		int exitcode;

		sa.sa_handler = sighandler;
		sigaction(SIGINT, &sa, NULL);
		sigaction(SIGHUP, &sa, NULL);
		sigaction(SIGTERM, &sa, NULL);
		sigaction(SIGQUIT, &sa, NULL);

#ifdef DUMP
		ok = drive_init2(argc,
				  argv,
				  gwhdrtemplatep);
#endif /* DUMP */
#ifdef RESTORE
		ok = drive_init2(argc,
				  argv,
				  (global_hdr_t *)0);
#endif /* RESTORE */
		if (!ok) {
			err = mlog_exit(EXIT_ERROR, RV_INIT);
			goto err_free;
		}
		ok = drive_init3();
		if (!ok) {
			err = mlog_exit(EXIT_ERROR, RV_INIT);
			goto err_free;
		}
#ifdef DUMP
		exitcode = content_stream_dump(0);
#endif /* DUMP */
#ifdef RESTORE
		exitcode = content_stream_restore(0);
#endif /* RESTORE */
		if (exitcode != EXIT_NORMAL) {
			(void)content_complete();
						/* for cleanup side-effect */
			err = mlog_exit(exitcode, RV_UNKNOWN);
		} else if (content_complete()) {
			err = mlog_exit(EXIT_NORMAL, RV_OK);
		} else {
			err = mlog_exit(EXIT_INTERRUPT, RV_UNKNOWN);
		}
		goto err_free;
	}

	/* used to skip to end if errors occur during any
	 * stage of initialization.
	 */
	init_error = BOOL_FALSE;

	/* now do the second and third passes of drive initialization.
	 * allocate per-stream write and read headers. if a drive
	 * manager uses a slave process, it should be created now,
	 * using cldmgr_create(). each drive manager may use the slave to
	 * asynchronously read the media file header, typically a very
	 * time-consuming chore. drive_init3 will synchronize with each slave.
	 */
	if (!init_error) {
#ifdef DUMP
		ok = drive_init2(argc,
				  argv,
				  gwhdrtemplatep);
#endif /* DUMP */
#ifdef RESTORE
		ok = drive_init2(argc,
				  argv,
				  (global_hdr_t *)0);
#endif /* RESTORE */
		if (!ok) {
			init_error = BOOL_TRUE;
		}
	}
	if (!init_error) {
		ok = drive_init3();
		if (!ok) {
			init_error = BOOL_TRUE;
		}
	}

	/* create a child thread for each stream. drivecnt global from
	 * drive.h, initialized by drive_init[12]
	 */
	if (!init_error) {
		for (stix = 0; stix < drivecnt; stix++) {
			ok = cldmgr_create(childmain,
					    stix,
					    "child",
					    (void *)stix);
			if (!ok) {
				init_error = BOOL_TRUE;
			}
		}
	}

	/* loop here, waiting for children to die, processing operator
	 * signals.
	 */
	if (progrpt_enabledpr) {
		(void)alarm((uint)progrpt_interval);
	}
	for (;;) {
		time32_t now;
		bool_t stop_requested = BOOL_FALSE;
		int stop_timeout = -1;
		sigset_t empty_set;

		/* if there was an initialization error,
		 * immediately stop all children.
		 */
		if (init_error) {
			stop_timeout = STOP_TIMEOUT;
			stop_requested = BOOL_TRUE;
		}

		/* if one or more children died abnormally, request a
		 * stop. furthermore, note that core should be dumped if
		 * the child explicitly exited with EXIT_FAULT.
		 */
		xc = cldmgr_join();
		if (xc) {
			if (xc == EXIT_FAULT) {
				coredump_requested = BOOL_TRUE;
				stop_timeout = ABORT_TIMEOUT;
			} else {
				stop_timeout = STOP_TIMEOUT;
			}
			prbcld_xc = xc;
			stop_requested = BOOL_TRUE;
		}

		/* all children died normally. break out.
		 */
		if (cldmgr_remainingcnt() == 0) {
			mlog(MLOG_DEBUG,
			      "all children have exited\n");
			break;
		}

		/* get the current time
		 */
		now = time(0);

		/* check for stop timeout. request a core dump and bail
		 */
		if (stop_in_progress && now >= stop_deadline) {
			mlog(MLOG_NORMAL | MLOG_ERROR,
			      _("session interrupt timeout\n"));
			coredump_requested = BOOL_TRUE;
			break;
		}

		/* operator sent SIGINT. if dialog allowed, enter dialog.
		 * otherwise treat as a hangup and request a stop.
		 */
		if (sigint_received) {
			mlog(MLOG_DEBUG | MLOG_PROC,
			      "SIGINT received\n");
			if (stop_in_progress) {
				if (dlog_allowed()) {
					(void)sigint_dialog();
				}
				/*
				mlog(MLOG_NORMAL,
				      _("session interrupt in progress: "
				      "please wait\n"));
				 */
			} else {
				if (dlog_allowed()) {
					stop_requested = sigint_dialog();
				} else {
					stop_requested = BOOL_TRUE;
				}
				stop_timeout = STOP_TIMEOUT;
			}

			/* important that this appear after dialog.
			 * allows dialog to be terminated with SIGINT,
			 * without infinite loop.
			 */
			sigint_received = BOOL_FALSE;
		}

		/* refresh the current time in case in dialog for a while
		 */
		now = time(0);

		/* request a stop on hangup
		 */
		if (sighup_received) {
			mlog(MLOG_DEBUG | MLOG_PROC,
			      "SIGHUP received\n");
			stop_requested = BOOL_TRUE;
			stop_timeout = STOP_TIMEOUT;
			sighup_received = BOOL_FALSE;
		}

		/* request a stop on termination request
		 */
		if (sigterm_received) {
			mlog(MLOG_DEBUG | MLOG_PROC,
			      "SIGTERM received\n");
			stop_requested = BOOL_TRUE;
			stop_timeout = STOP_TIMEOUT;
			sigterm_received = BOOL_FALSE;
		}

		/* operator send SIGQUIT. treat like an interrupt,
		 * but force a core dump
		 */
		if (sigquit_received) {
			mlog(MLOG_NORMAL | MLOG_PROC,
			      "SIGQUIT received\n");
			if (stop_in_progress) {
				mlog(MLOG_NORMAL,
				      _("session interrupt in progress: "
				      "please wait\n"));
				stop_deadline = now;
			} else {
				stop_requested = BOOL_TRUE;
				stop_timeout = ABORT_TIMEOUT;
				sigquit_received = BOOL_FALSE;
				coredump_requested = BOOL_TRUE;
			}
		}

		/* see if need to initiate a stop
		 */
		if (stop_requested && !stop_in_progress) {
			mlog(MLOG_NORMAL,
			      _("initiating session interrupt (timeout in %d sec)\n"),
			      stop_timeout);
			mlog_exit_hint(RV_INTR);
			stop_in_progress = BOOL_TRUE;
			cldmgr_stop();
			assert(stop_timeout >= 0);
			stop_deadline = now + (time32_t)stop_timeout;
		}

		/* set alarm if needed (note time stands still during dialog)
		 */
		if (stop_in_progress) {
			int timeout = (int)(stop_deadline - now);
			if (timeout < 0) {
				timeout = 0;
			}
			mlog(MLOG_DEBUG | MLOG_PROC,
			      "setting alarm for %d second%s\n",
			      timeout,
			      timeout == 1 ? "" : "s");
			(void)alarm((uint)timeout);
			if (timeout == 0) {
				continue;
			}
		}

		if (progrpt_enabledpr && !stop_in_progress) {
			bool_t need_progrptpr = BOOL_FALSE;
			while (now >= progrpt_deadline) {
				need_progrptpr = BOOL_TRUE;
				progrpt_deadline += progrpt_interval;
			}
			if (need_progrptpr) {
				size_t statlinecnt;
				char **statline;
				ix_t i;
				statlinecnt = content_statline(&statline);
				for (i = 0; i < statlinecnt; i++) {
					mlog(MLOG_NORMAL,
					      statline[i]);
				}
			}
			(void)alarm((uint)(progrpt_deadline
						       -
						       now));
		}

		/* sleep until next signal
		 */
		sigemptyset(&empty_set);
		sigsuspend(&empty_set);
		(void)alarm(0);
	}

	/* check if core dump requested
	 */
	if (coredump_requested) {
		mlog(MLOG_DEBUG | MLOG_PROC,
		      "core dump requested, aborting (pid %d)\n",
		      getpid());
		abort();
	}

	/* determine if dump or restore was interrupted
	 * or an initialization error occurred.
	 */
	if (init_error) {
		(void)content_complete();
		exitcode = EXIT_ERROR;
	} else {
		if (content_complete()) {
			if (prbcld_xc != EXIT_NORMAL)
				exitcode = EXIT_ERROR;
			else
				exitcode = EXIT_NORMAL;
		} else {
			exitcode = EXIT_INTERRUPT;
			if (mlog_get_hint() == RV_NONE)
				mlog_exit_hint(RV_INCOMPLETE);
		}
	}

	err = mlog_exit(exitcode, RV_UNKNOWN);

err_free:
#ifdef DUMP
	global_hdr_free(gwhdrtemplatep);
#endif /* DUMP */
	return err;
}

#define ULO(f, o)	fprintf(stderr,		\
				 "%*s[ -%c %s ]\n",	\
				 ps,			\
				 ns,			\
				 o,			\
				 f),			\
			ps = pfxsz

#define ULN(f)	fprintf(stderr,		\
				 "%*s[ %s ]\n",		\
				 ps,			\
				 ns,			\
				 f),			\
			ps = pfxsz

void
usage(void)
{
	int pfxsz;
	int ps = 0;
	char *ns = "";

	pfxsz = fprintf(stderr, _("%s: usage: %s "),
				progname, basename(progname));

#ifdef DUMP
	ULO(_("(dump DMF dualstate files as offline)"),	GETOPT_DUMPASOFFLINE);
	ULO(_("<blocksize>"),				GETOPT_BLOCKSIZE);
	ULO(_("<media change alert program> "),		GETOPT_ALERTPROG);
	ULO(_("<dump media file size> "),		GETOPT_FILESZ);
	ULO(_("(allow files to be excluded)"),		GETOPT_EXCLUDEFILES);
	ULO(_("<destination> ..."),			GETOPT_DUMPDEST);
	ULO(_("(help)"),				GETOPT_HELP);
	ULO(_("<level>"),				GETOPT_LEVEL);
	ULO(_("(force usage of minimal rmt)"),		GETOPT_MINRMT);
	ULO(_("(overwrite tape)"),			GETOPT_OVERWRITE);
	ULO(_("<seconds between progress reports>"),	GETOPT_PROGRESS);
	ULO(_("<use QIC tape settings>"),		GETOPT_QIC);
	ULO(_("<subtree> ..."),				GETOPT_SUBTREE);
	ULO(_("<file> (use file mtime for dump time"),	GETOPT_DUMPTIME);
	ULO(_("<verbosity {silent, verbose, trace}>"),	GETOPT_VERBOSITY);
	ULO(_("<maximum file size>"),			GETOPT_MAXDUMPFILESIZE);
	ULO(_("(don't dump extended file attributes)"),	GETOPT_NOEXTATTR);
	ULO(_("<base dump session id>"),		GETOPT_BASED);
#ifdef REVEAL
	ULO(_("(generate tape record checksums)"),	GETOPT_RECCHKSUM);
#endif /* REVEAL */
	ULO(_("(skip unchanged directories)"),		GETOPT_NOUNCHANGEDDIRS);
	ULO(_("(pre-erase media)"),			GETOPT_ERASE);
	ULO(_("(don't prompt)"),			GETOPT_FORCE);
#ifdef REVEAL
	ULO(_("<minimum thread stack size>"),		GETOPT_MINSTACKSZ);
	ULO(_("<maximum thread stack size>"),		GETOPT_MAXSTACKSZ);
#endif /* REVEAL */
	ULO(_("(display dump inventory)"),		GETOPT_INVPRINT);
	ULO(_("(inhibit inventory update)"),		GETOPT_NOINVUPDATE);
	ULO(_("(generate format 2 dump)"),		GETOPT_FMT2COMPAT);
	ULO(_("<session label>"),			GETOPT_DUMPLABEL);
	ULO(_("<media label> ..."),			GETOPT_MEDIALABEL);
#ifdef REVEAL
	ULO(_("(timestamp messages)"),			GETOPT_TIMESTAMP);
#endif /* REVEAL */
	ULO(_("<options file>"),			GETOPT_OPTFILE);
#ifdef REVEAL
	ULO(_("(pin down I/O buffers)"),		GETOPT_RINGPIN);
#endif /* REVEAL */
	ULO(_("(resume)"),				GETOPT_RESUME);
	ULO(_("(don't timeout dialogs)"),		GETOPT_NOTIMEOUTS);
#ifdef REVEAL
	ULO(_("(unload media when change needed)"),	GETOPT_UNLOAD);
	ULO(_("(show subsystem in messages)"),		GETOPT_SHOWLOGSS);
	ULO(_("(show verbosity in messages)"),		GETOPT_SHOWLOGLEVEL);
#endif /* REVEAL */
	ULO(_("<I/O buffer ring length>"),		GETOPT_RINGLEN);
	ULN(_("- (stdout)"));
	ULN(_("<source (mntpnt|device)>"));
#endif /* DUMP */
#ifdef RESTORE
	ULO(_("<alt. workspace dir> ..."),		GETOPT_WORKSPACE);
	ULO(_("<blocksize>"),				GETOPT_BLOCKSIZE);
	ULO(_("<media change alert program> "),		GETOPT_ALERTPROG);
	ULO(_("(don't overwrite existing files)"),	GETOPT_EXISTING);
	ULO(_("<source> ..."),				GETOPT_DUMPDEST);
	ULO(_("(help)"),				GETOPT_HELP);
	ULO(_("(interactive)"),				GETOPT_INTERACTIVE);
	ULO(_("(force usage of minimal rmt)"),		GETOPT_MINRMT);
	ULO(_("<file> (restore only if newer than)"),	GETOPT_NEWER);
	ULO(_("(restore owner/group even if not root)"),GETOPT_OWNER);
	ULO(_("<seconds between progress reports>"),	GETOPT_PROGRESS);
	ULO(_("<use QIC tape settings>"),		GETOPT_QIC);
	ULO(_("(cumulative restore)"),			GETOPT_CUMULATIVE);
	ULO(_("<subtree> ..."),				GETOPT_SUBTREE);
	ULO(_("(contents only)"),			GETOPT_TOC);
	ULO(_("<verbosity {silent, verbose, trace}>"),	GETOPT_VERBOSITY);
	ULO(_("(use small tree window)"),		GETOPT_SMALLWINDOW);
	ULO(_("(don't restore extended file attributes)"), GETOPT_NOEXTATTR);
	ULO(_("(restore root dir owner/permissions)"),	GETOPT_ROOTPERM);
	ULO(_("(restore DMAPI event settings)"),	GETOPT_SETDM);
#ifdef REVEAL
	ULO(_("(check tape record checksums)"),		GETOPT_RECCHKSUM);
#endif /* REVEAL */
	ULO(_("(don't overwrite if changed)"),		GETOPT_CHANGED);
	ULO(_("(don't prompt)"),			GETOPT_FORCE);
	ULO(_("(display dump inventory)"),		GETOPT_INVPRINT);
	ULO(_("(inhibit inventory update)"),		GETOPT_NOINVUPDATE);
	ULO(_("(force use of format 2 generation numbers)"), GETOPT_FMT2COMPAT);
	ULO(_("<session label>"),			GETOPT_DUMPLABEL);
#ifdef REVEAL
	ULO(_("(timestamp messages)"),			GETOPT_TIMESTAMP);
#endif /* REVEAL */
	ULO(_("<options file>"),			GETOPT_OPTFILE);
#ifdef REVEAL
	ULO(_("(pin down I/O buffers)"),		GETOPT_RINGPIN);
#endif /* REVEAL */
	ULO(_("(force interrupted session completion)"),GETOPT_SESSCPLT);
	ULO(_("(resume)"),				GETOPT_RESUME);
	ULO(_("<session id>"),				GETOPT_SESSIONID);
	ULO(_("(don't timeout dialogs)"),		GETOPT_NOTIMEOUTS);
#ifdef REVEAL
	ULO(_("(unload media when change needed)"),	GETOPT_UNLOAD);
	ULO(_("(show subsystem in messages)"),		GETOPT_SHOWLOGSS);
	ULO(_("(show verbosity in messages)"),		GETOPT_SHOWLOGLEVEL);
#endif /* REVEAL */
	ULO(_("<excluded subtree> ..."),		GETOPT_NOSUBTREE);
	ULO(_("<I/O buffer ring length>"),		GETOPT_RINGLEN);
	ULN(_("- (stdin)"));
	ULN(_("<destination>"));
#endif /* RESTORE */

	/* anywhere usage is called we will exit shortly after...
	 * catch all of those cases below
	 */

	(void) mlog_exit(EXIT_ERROR, RV_OPT);
}

/* returns TRUE if preemption
 */
bool_t
preemptchk(int flg)
{
	bool_t preempt_requested;
	int i;
	int sigs[] = { SIGINT, SIGHUP, SIGTERM, SIGQUIT };
	int num_sigs = sizeof(sigs) / sizeof(sigs[0]);
	sigset_t pending_set, handle_set;

	/* see if a progress report needed
	 */
	if (progrpt_enabledpr) {
		time32_t now = time(0);
		bool_t need_progrptpr = BOOL_FALSE;
		while (now >= progrpt_deadline) {
			need_progrptpr = BOOL_TRUE;
			progrpt_deadline += progrpt_interval;
		}
		if (need_progrptpr) {
			size_t statlinecnt;
			char **statline;
			ix_t i;
			statlinecnt = content_statline(&statline);
			for (i = 0; i < statlinecnt; i++) {
				mlog(MLOG_NORMAL,
				      statline[i]);
			}
		}
	}

	/* Progress report only */
	if (flg == PREEMPT_PROGRESSONLY) {
		return BOOL_FALSE;
	}

	/* signals not caught if in a pipeline
	 */
	if (pipeline) {
		return BOOL_FALSE;
	}

	/* release signals momentarily to let any pending ones
	 * invoke signal handler and set flags
	 */
	sigpending(&pending_set);
	for (i = 0; i < num_sigs; i++) {
		if (sigismember(&pending_set, sigs[i]) == 1) {
			sigfillset(&handle_set);
			sigdelset(&handle_set, sigs[i]);
			sigsuspend(&handle_set);
		}
	}

	preempt_requested = BOOL_FALSE;

	if (sigint_received) {
		mlog(MLOG_DEBUG | MLOG_PROC,
		      "SIGINT received (preempt)\n");
		if (dlog_allowed()) {
			preempt_requested = sigint_dialog();
		} else {
			preempt_requested = BOOL_TRUE;
		}
		/* important that this appear after dialog.
		 * allows dialog to be terminated with SIGINT,
		 * without infinite loop.
		 */
		sigint_received = BOOL_FALSE;
	}

	if (sighup_received) {
		mlog(MLOG_DEBUG | MLOG_PROC,
		      "SIGHUP received (prempt)\n");
		preempt_requested = BOOL_TRUE;
		sighup_received = BOOL_FALSE;
	}

	if (sigterm_received) {
		mlog(MLOG_DEBUG | MLOG_PROC,
		      "SIGTERM received (prempt)\n");
		preempt_requested = BOOL_TRUE;
		sigterm_received = BOOL_FALSE;
	}

	if (sigquit_received) {
		mlog(MLOG_DEBUG | MLOG_PROC,
		      "SIGQUIT received (preempt)\n");
		preempt_requested = BOOL_TRUE;
		sigquit_received = BOOL_FALSE;
	}

	return preempt_requested;
}

/* definition of locally defined static functions ****************************/

static bool_t
loadoptfile(int *argcp, char ***argvp)
{
	char *optfilename;
	ix_t optfileix = 0;
	int fd;
	size_t sz;
	int i;
	struct stat64 stat;
	char *argbuf;
	char *p;
	size_t tokencnt;
	int nread;
	const char *sep = " \t\n\r";
	char **newargv;
	int c;
	int rval;

	/* see if option specified
	 */
	optind = 1;
	opterr = 0;
	optfilename =  0;
	while ((c = getopt(*argcp, *argvp, GETOPT_CMDSTRING)) != EOF) {
		switch (c) {
		case GETOPT_OPTFILE:
			if (!optarg || optarg[0] == '-') {
				mlog(MLOG_NORMAL | MLOG_ERROR | MLOG_NOLOCK,
				      _("-%c argument missing\n"),
				      c);
				usage();
				return BOOL_FALSE;
			}
			if (optfilename) {
				mlog(MLOG_NORMAL | MLOG_ERROR | MLOG_NOLOCK,
				      _("-%c allowed only once\n"),
				      c);
				usage();
				return BOOL_FALSE;
			}
			optfilename = optarg;
			assert(optind > 2);
			optfileix = (ix_t)optind - 2;
			break;
		}
	}
	if (!optfilename)  {
		return BOOL_TRUE;
	}

	/* attempt to open the option  file
	 */
	errno = 0;
	fd = open(optfilename, O_RDONLY);
	if (fd  < 0) {
		mlog(MLOG_ERROR | MLOG_NOLOCK,
		      _("cannot open option file %s: %s (%d)\n"),
		      optfilename,
		      strerror(errno),
		      errno);
		return BOOL_FALSE;
	}

	/* get file status
	 */
	rval = fstat64(fd, &stat);
	if (rval) {
		mlog(MLOG_ERROR | MLOG_NOLOCK,
		      _("cannot stat option file %s: %s (%d)\n"),
		      optfilename,
		      strerror(errno),
		      errno);
		close(fd);
		return BOOL_FALSE;
	}

	/* ensure the file is ordinary
	 */
	if ((stat.st_mode & S_IFMT) != S_IFREG) {
		mlog(MLOG_ERROR | MLOG_NOLOCK,
		      _("given option file %s is not ordinary file\n"),
		      optfilename);
		close(fd);
		return BOOL_FALSE;
	}

	/* calculate the space required for the cmd line options.
	 * skip the GETOPT_OPTFILE option which put us here!
	 */
	sz = 0;
	for (i =  0; i < *argcp; i++) {
		if (i == (int)optfileix) {
			i++; /* to skip option argument */
			continue;
		}
		sz += strlen((*argvp)[i]) + 1;
	}

	/* add in the size of the option file (plus one byte in case
	 * option file ends without newline, and one NULL for safety)
	 */
	sz += (size_t)stat.st_size + 2;

	/* allocate an argument buffer
	 */
	argbuf = (char *)malloc(sz);
	assert(argbuf);

	/* copy arg0 (the executable's name) in first
	 */
	p = argbuf;
	i = 0;
	sprintf(p, "%s ", ( * argvp)[ i]);
	p += strlen((*argvp)[i]) + 1;
	i++;

	/* copy the options file into the buffer after the given args
	 */
	nread = read(fd, (void *)p, (size_t)stat.st_size);
	if (nread < 0) {
		mlog(MLOG_ERROR | MLOG_NOLOCK,
		      _("read of option file %s failed: %s (%d)\n"),
		      optfilename,
		      strerror(errno),
		      errno);
		close(fd);
		return BOOL_FALSE;
	}
	assert((off64_t)nread == stat.st_size);
	p += (size_t)stat.st_size;
	*p++ = ' ';

	/* copy the remaining command line args into the buffer
	 */
	for (; i < *argcp; i++) {
		if (i == (int)optfileix) {
			i++; /* to skip option argument */
			continue;
		}
		sprintf(p, "%s ", ( * argvp)[ i]);
		p += strlen((*argvp)[i]) + 1;
	}

	/* null-terminate the entire buffer
	 */
	*p++ = 0;
	assert((size_t)(p - argbuf) <= sz);

	/* change newlines and carriage returns into spaces
	 */
	for (p = argbuf; *p; p++) {
		if (strchr("\n\r", ( int)( *p))) {
			*p = ' ';
		}
	}

	/* count the tokens in the buffer
	 */
	tokencnt = 0;
	p = argbuf;
	for (;;) {
		/* start at the first non-separator character
		 */
		while (*p && strchr(sep, (int)(*p))) {
			p++;
		}

		/* done when NULL encountered
		 */
		if (!*p) {
			break;
		}

		/* we have a token
		 */
		tokencnt++;

		/* find the end of the first token
		 */
		p = strpbrkquotes(p, sep);

		/* if no more separators, all tokens seen
		 */
		if (!p) {
			break;
		}
	}

	/* if no arguments, can return now
	 */
	if (!tokencnt) {
		close(fd);
		return BOOL_TRUE;
	}

	/* allocate a new argv array to hold the tokens
	 */
	newargv = (char **)calloc(tokencnt, sizeof(char *));
	assert(newargv);

	/* null-terminate tokens and place in new argv, after
	 * extracting quotes and escapes
	 */
	p = argbuf;
	for (i = 0 ; ; i++) {
		char *endp = 0;

		/* start at the first non-separator character
		 */
		while (*p && strchr(sep, (int)*p)) {
			p++;
		}

		/* done when NULL encountered
		 */
		if (!*p) {
			break;
		}

		/* better not disagree with counting scan!
		 */
		assert(i < (int)tokencnt);

		/* find the end of the first token
		 */
		endp = strpbrkquotes(p, sep);

		/* null-terminate if needed
		 */
		if (endp) {
			*endp = 0;
		}

		/* strip quotes and escapes
		 */
		p = stripquotes(p);

		/* stick result in new argv array
		 */
		newargv[i] = p;

		/* if no more separators, all tokens seen
		 */
		if (!endp) {
			break;
		}

		p = endp + 1;
	}

	/* return new argc anr argv
	 */
	close(fd);
	*argcp = (int)tokencnt;
	*argvp = newargv;
	return BOOL_TRUE;
}

/* parent and children share this handler.
 */
static void
sighandler(int signo)
{
	/* dialog gets first crack at the signal
	 */
	if (dlog_sighandler(signo))
		return;

	/* if in pipeline, don't do anything risky. just quit.
	 */
	if (pipeline) {
		int rval;

		mlog(MLOG_TRACE | MLOG_NOTE | MLOG_NOLOCK | MLOG_PROC,
		      _("received signal %d (%s): cleanup and exit\n"),
		      signo,
		      sig_numstring(signo));

		if (content_complete()) {
			rval = EXIT_NORMAL;
		} else {
			rval = EXIT_INTERRUPT;
		}
		mlog_exit(rval, RV_NONE);
		exit(rval);
	}

	switch (signo) {
	case SIGHUP:
		/* immediately disable further dialogs
		*/
		dlog_desist();
		sighup_received = BOOL_TRUE;
		break;
	case SIGTERM:
		/* immediately disable further dialogs
		*/
		dlog_desist();
		sigterm_received = BOOL_TRUE;
		break;
	case SIGINT:
		sigint_received = BOOL_TRUE;
		break;
	case SIGQUIT:
		/* immediately disable further dialogs
		 */
		dlog_desist();
		sigquit_received = BOOL_TRUE;
		break;
	case SIGALRM:
	case SIGUSR1:
		break;
	default:
		sigstray_received = signo;
		break;
	}
}

static int
childmain(void *arg1)
{
	ix_t stix;
	int exitcode;
	drive_t *drivep;

	/* Determine which stream I am.
	 */
	stix = (ix_t)arg1;

	/* tell the content manager to begin.
	 */
#ifdef DUMP
	exitcode = content_stream_dump(stix);
#endif /* DUMP */
#ifdef RESTORE
	exitcode = content_stream_restore(stix);
#endif /* RESTORE */

	/* let the drive manager shut down its slave thread
	 */
	drivep = drivepp[stix];
	(*drivep->d_opsp->do_quit)(drivep);

	return exitcode;
}


/* ARGSUSED */
static void
prompt_prog_cb(void *uctxp, dlog_pcbp_t pcb, void *pctxp)
{
	/* query: ask for a dump label
	 */
	(*pcb)(pctxp,
		   progrpt_enabledpr
		   ?
		   _("please enter seconds between progress reports, "
		   "or 0 to disable")
		   :
		   _("please enter seconds between progress reports"));
}

/* SIGINTR dialog
 *
 * side affect is to change verbosity level.
 * return code of BOOL_TRUE indicates a stop was requested.
 */
#define PREAMBLEMAX	(7 + 2 * STREAM_SIMMAX)
#define QUERYMAX	3
#define CHOICEMAX	9
#define ACKMAX		7
#define POSTAMBLEMAX	3

static bool_t
sigint_dialog(void)
{
	fold_t fold;
	char **statline;
	ix_t i;
	size_t statlinecnt;
	char *preamblestr[PREAMBLEMAX];
	size_t preamblecnt;
	char *querystr[QUERYMAX];
	size_t querycnt;
	char *choicestr[CHOICEMAX];
	size_t choicecnt;
	char *ackstr[ACKMAX];
	size_t ackcnt;
	char *postamblestr[POSTAMBLEMAX];
	size_t postamblecnt;
	size_t interruptix;
	size_t verbosityix;
	size_t metricsix;
	size_t controlix;
	size_t ioix;
	size_t mediachangeix;
#ifdef RESTORE
	size_t piix;
	size_t roix;
#endif /* RESTORE */
	size_t progix;
	size_t mllevix;
	size_t mlssix;
	size_t mltsix;
	size_t continueix;
	size_t allix;
	size_t nochangeix;
	size_t responseix;
	int ssselected = 0;
	bool_t stop_requested = BOOL_FALSE;

	/* preamble: the content status line, indicate if interrupt happening
	 */
	fold_init(fold, _("status and control dialog"), '=');
	statlinecnt = content_statline(&statline);
	preamblecnt = 0;
	preamblestr[preamblecnt++ ] = "\n";
	preamblestr[preamblecnt++] = fold;
	preamblestr[preamblecnt++ ] = "\n";
	preamblestr[preamblecnt++ ] = "\n";
	for (i = 0; i < statlinecnt; i++) {
		preamblestr[preamblecnt++] = statline[i];
	}
	if (stop_in_progress) {
		preamblestr[preamblecnt++] =
			_("\nsession interrupt in progress\n");
	}
	preamblestr[preamblecnt++ ] = "\n";
	assert(preamblecnt <= PREAMBLEMAX);
	dlog_begin(preamblestr, preamblecnt);

	/* top-level query: a function of session interrupt status
	 */
	querycnt = 0;
	querystr[querycnt++ ] = _("please select one of "
				 "the following operations\n");
	assert(querycnt <= QUERYMAX);
	choicecnt = 0;
	if (!stop_in_progress) {
		interruptix = choicecnt;
		choicestr[choicecnt++ ] = _("interrupt this session");
	} else {
		interruptix = SIZEMAX; /* never happen */
	}

	verbosityix = choicecnt;
	choicestr[choicecnt++ ] = _("change verbosity");
	metricsix = choicecnt;
	choicestr[choicecnt++ ] = _("display metrics");
	if (content_media_change_needed) {
		mediachangeix = choicecnt;
		choicestr[choicecnt++ ] = _("confirm media change");
	} else {
		mediachangeix = SIZEMAX; /* never happen */
	}
	controlix = choicecnt;
	choicestr[choicecnt++ ] = _("other controls");
	continueix = choicecnt;
	choicestr[choicecnt++ ] = _("continue");
	assert(choicecnt <= CHOICEMAX);

	responseix = dlog_multi_query(querystr,
				       querycnt,
				       choicestr,
				       choicecnt,
				       0,		/* hilitestr */
				       IXMAX,		/* hiliteix */
				       0,		/* defaultstr */
				       continueix,	/* defaultix */
				       DLOG_TIMEOUT,	/* timeout */
				       continueix,	/* timeout ix */
				       continueix,	/* sigint ix */
				       continueix,	/* sighup ix */
				       continueix);	/* sigquit ix */
	if (responseix == interruptix) {
		ackcnt = 0;
		ackstr[ackcnt++ ] = "\n";
		dlog_multi_ack(ackstr,
				ackcnt);
		querycnt = 0;
		querystr[querycnt++ ] = _("please confirm\n");
		assert(querycnt <= QUERYMAX);
		choicecnt = 0;
		interruptix = choicecnt;
		choicestr[choicecnt++ ] = _("interrupt this session");
		nochangeix = choicecnt;
		choicestr[choicecnt++ ] = _("continue");
		assert(choicecnt <= CHOICEMAX);
		responseix = dlog_multi_query(querystr,
					       querycnt,
					       choicestr,
					       choicecnt,
					       0,	/* hilitestr */
					       IXMAX,	/* hiliteix */
					       0,       /* defaultstr */
					       nochangeix, /* defaultix */
					       DLOG_TIMEOUT,/* timeout */
					       nochangeix, /* timeout ix */
					       nochangeix, /* sigint ix */
					       nochangeix, /* sighup ix */
					       nochangeix);/* sigquit ix */
		ackcnt = 0;
		if (responseix == nochangeix) {
			ackstr[ackcnt++ ] = _("continuing\n");
		} else {
			ackstr[ackcnt++ ] = _("interrupt request accepted\n");
			stop_requested = BOOL_TRUE;
		}
		dlog_multi_ack(ackstr,
				ackcnt);
	} else if (responseix == verbosityix) {
		ackcnt = 0;
		ackstr[ackcnt++ ] = "\n";
		dlog_multi_ack(ackstr,
				ackcnt);
		querycnt = 0;
		querystr[querycnt++ ] = _("please select one of "
					 "the following subsystems\n");
		assert(querycnt <= QUERYMAX);
		choicecnt = 0;
		/* number of lines must match number of subsystems
		 */
		for (choicecnt = 0; choicecnt < MLOG_SS_CNT; choicecnt++) {
			choicestr[choicecnt] = mlog_ss_names[choicecnt];
		}
		allix = choicecnt;
		choicestr[choicecnt++ ] = _("all of the above");
		nochangeix = choicecnt;
		choicestr[choicecnt++ ] = _("no change");
		assert(choicecnt <= CHOICEMAX);
		responseix = dlog_multi_query(querystr,
					       querycnt,
					       choicestr,
					       choicecnt,
					       0,	/* hilitestr */
					       IXMAX,	/* hiliteix */
					       0,       /* defaultstr */
					       allix,   /* defaultix */
					       DLOG_TIMEOUT,/* timeout */
					       nochangeix, /* timeout ix */
					       nochangeix, /* sigint ix */
					       nochangeix, /* sighup ix */
					       nochangeix);/* sigquit ix */
		ackcnt = 0;
		if (responseix == nochangeix) {
			ackstr[ackcnt++ ] = _("no change\n");
		} else if (responseix == allix) {
			ssselected = -1;
			ackstr[ackcnt++ ] = _("all subsystems selected\n\n");
		} else {
			ssselected = (int)responseix;
			ackstr[ackcnt++ ] = "\n";
		}
		dlog_multi_ack(ackstr,
				ackcnt);
		if (responseix != nochangeix) {
			querycnt = 0;
			querystr[querycnt++ ] = ("please select one of the "
						  "following verbosity levels\n");
			assert(querycnt <= QUERYMAX);
			choicecnt = 0;
			choicestr[choicecnt++ ] = _("silent");
			choicestr[choicecnt++ ] = _("verbose");
			choicestr[choicecnt++ ] = _("trace");
			choicestr[choicecnt++ ] = _("debug");
			choicestr[choicecnt++ ] = _("nitty");
			choicestr[choicecnt++ ] = _("nitty + 1");
			nochangeix = choicecnt;
			choicestr[choicecnt++ ] = _("no change");
			assert(choicecnt <= CHOICEMAX);
			responseix = dlog_multi_query(querystr,
						       querycnt,
						       choicestr,
						       choicecnt,
						       ssselected == -1
						       ?
						       0
						       :
						    _(" (current)"),/* hilitestr */
						       ssselected == -1
						       ?
						       IXMAX
						       :
			     (ix_t)mlog_level_ss[ssselected], /* hiliteix */
						       0,       /* defaultstr */
						      nochangeix,/* defaultix */
						    DLOG_TIMEOUT,/* timeout */
						    nochangeix, /* timeout ix */
						     nochangeix, /* sigint ix */
						     nochangeix, /* sighup ix */
						    nochangeix);/* sigquit ix */
			ackcnt = 0;
			if (responseix == nochangeix
			     ||
			     (ssselected >= 0
			       &&
			       responseix
			       ==
			       (ix_t)mlog_level_ss[ssselected])) {
				ackstr[ackcnt++ ] = _("no change\n");
			} else {
				if (ssselected < 0) {
					ix_t ssix;
					assert(ssselected == -1);
					for (ssix = 0
					      ;
					      ssix < MLOG_SS_CNT
					      ;
					      ssix++) {
						mlog_level_ss[ssix] =
							(int)responseix;
					}
				} else {
					mlog_level_ss[ssselected] =
							(int)responseix;
				}
				ackstr[ackcnt++ ] = _("level changed\n");
			}
			dlog_multi_ack(ackstr,
					ackcnt);
		}
	} else if (responseix == metricsix) {
		ackcnt = 0;
		ackstr[ackcnt++ ] = "\n";
		dlog_multi_ack(ackstr,
				ackcnt);
		querycnt = 0;
		querystr[querycnt++ ] = _("please select one of "
					  "the following metrics\n");
		assert(querycnt <= QUERYMAX);
		choicecnt = 0;
		ioix = choicecnt;
		choicestr[choicecnt++ ] = _("I/O");
#ifdef RESTORE
		piix = choicecnt;
		choicestr[choicecnt++ ] = _("media inventory status");
		roix = choicecnt;
		choicestr[choicecnt++ ] = _("needed media objects");
#endif /* RESTORE */
		nochangeix = choicecnt;
		choicestr[choicecnt++ ] = _("continue");
		assert(choicecnt <= CHOICEMAX);
		responseix = dlog_multi_query(querystr,
					       querycnt,
					       choicestr,
					       choicecnt,
					       0,           /* hilitestr */
					       IXMAX,       /* hiliteix */
					       0,           /* defaultstr */
					       nochangeix,  /* defaultix */
					       DLOG_TIMEOUT,
					       nochangeix, /* timeout ix */
					       nochangeix, /* sigint ix */
					       nochangeix, /* sighup ix */
					       nochangeix);/* sigquit ix */
		if (responseix != nochangeix) {
			ackcnt = 0;
			ackstr[ackcnt++ ] = "\n";
			dlog_multi_ack(ackstr,
					ackcnt);
		}
		if (responseix == ioix) {
			drive_display_metrics();
#ifdef RESTORE
		} else if (responseix == piix) {
			content_showinv();
		} else if (responseix == roix) {
			content_showremainingobjects();
#endif /* RESTORE */
		}

		if (responseix != nochangeix) {
			querycnt = 0;
			querystr[querycnt++ ] = "\n";
			assert(querycnt <= QUERYMAX);
			choicecnt = 0;
			nochangeix = choicecnt;
			choicestr[choicecnt++ ] = _("continue");
			assert(choicecnt <= CHOICEMAX);
			responseix = dlog_multi_query(querystr,
						       querycnt,
						       choicestr,
						       choicecnt,
						       0,        /* hilitestr */
						       IXMAX,    /* hiliteix */
						       0,       /* defaultstr */
						       nochangeix,/* defaultix*/
						       DLOG_TIMEOUT,
						       nochangeix,/*timeout ix*/
						       nochangeix,/* sigint ix*/
						       nochangeix,/* sighup ix*/
						       nochangeix);/*sigquitix*/
		}
		ackcnt = 0;
		ackstr[ackcnt++ ] = _("continuing\n");
		dlog_multi_ack(ackstr,
				ackcnt);
	} else if (responseix == mediachangeix) {
		ackcnt = 0;
		dlog_multi_ack(ackstr,
				ackcnt);
		ackcnt = 0;
		ackstr[ackcnt++] = content_mediachange_query();
		dlog_multi_ack(ackstr,
				ackcnt);
	} else if (responseix == controlix) {
		ackcnt = 0;
		ackstr[ackcnt++ ] = "\n";
		dlog_multi_ack(ackstr,
				ackcnt);
		querycnt = 0;
		querystr[querycnt++ ] = _("please select one of "
					   "the following controls\n");
		assert(querycnt <= QUERYMAX);
		choicecnt = 0;
		progix = choicecnt;
		if (progrpt_enabledpr) {
		    choicestr[choicecnt++ ] = _("change interval of "
					         "or disable progress reports");
		} else {
		    choicestr[choicecnt++ ] = _("enable progress reports");
		}
		mllevix = choicecnt;
		if (mlog_showlevel) {
			choicestr[choicecnt++ ] = _("hide log message levels");
		} else {
			choicestr[choicecnt++ ] = _("show log message levels");
		}
		mlssix = choicecnt;
		if (mlog_showss) {
			choicestr[choicecnt++ ] = _("hide log message subsystems");
		} else {
			choicestr[choicecnt++ ] = _("show log message subsystems");
		}
		mltsix = choicecnt;
		if (mlog_timestamp) {
			choicestr[choicecnt++ ] = _("hide log message timestamps");
		} else {
			choicestr[choicecnt++ ] = _("show log message timestamps");
		}
		nochangeix = choicecnt;
		choicestr[choicecnt++ ] = _("continue");
		assert(choicecnt <= CHOICEMAX);
		responseix = dlog_multi_query(querystr,
					       querycnt,
					       choicestr,
					       choicecnt,
					       0,           /* hilitestr */
					       IXMAX,       /* hiliteix */
					       0,           /* defaultstr */
					       nochangeix,  /* defaultix */
					       DLOG_TIMEOUT,
					       nochangeix, /* timeout ix */
					       nochangeix, /* sigint ix */
					       nochangeix, /* sighup ix */
					       nochangeix);/* sigquit ix */
		ackcnt = 0;
		if (responseix == progix) {
			char buf[10];
			const size_t ncix = 1;
			const size_t okix = 2;

			ackstr[ackcnt++ ] = "\n";
			dlog_multi_ack(ackstr,
					ackcnt);
			ackcnt = 0;
			responseix = dlog_string_query(prompt_prog_cb,
							0,
							buf,
							sizeof(buf),
							DLOG_TIMEOUT,
							ncix,/* timeout ix */
							ncix, /* sigint ix */
							ncix,  /* sighup ix */
							ncix,  /* sigquit ix */
							okix);
			if (responseix == okix) {
				int newinterval;
				newinterval = atoi(buf);
				if (!strlen(buf)) {
					ackstr[ackcnt++ ] = _("no change\n");
				} else if (newinterval > 0) {
					time32_t newdeadline;
					char intervalbuf[64];
					newdeadline = time(0) + (time32_t)newinterval;
					if (progrpt_enabledpr) {
						if ((time32_t)newinterval == progrpt_interval) {
							ackstr[ackcnt++ ] = _("no change\n");
						} else {
							ackstr[ackcnt++ ] = _("changing progress report interval to ");
							sprintf(intervalbuf,
								 _("%d seconds\n"),
								 newinterval);
							assert(strlen(intervalbuf)
								<
								sizeof(intervalbuf));
							ackstr[ackcnt++] = intervalbuf;
							if (progrpt_deadline > newdeadline) {
								progrpt_deadline = newdeadline;
							}
						}
					} else {
						ackstr[ackcnt++ ] = _("enabling progress reports at ");
						sprintf(intervalbuf,
							 _("%d second intervals\n"),
							 newinterval);
						assert(strlen(intervalbuf)
							<
							sizeof(intervalbuf));
						ackstr[ackcnt++] = intervalbuf;
						progrpt_enabledpr = BOOL_TRUE;
						progrpt_deadline = newdeadline;
					}
					progrpt_interval = (time32_t)newinterval;
				} else {
					if (progrpt_enabledpr) {
						ackstr[ackcnt++ ] = _("disabling progress reports\n");
					} else {
						ackstr[ackcnt++ ] = _("no change\n");
					}
					progrpt_enabledpr = BOOL_FALSE;
				}
			} else {
				ackstr[ackcnt++ ] = _("no change\n");
			}
		} else if (responseix == mllevix) {
			mlog_showlevel = !mlog_showlevel;
			if (mlog_showlevel) {
				ackstr[ackcnt++ ] = _("showing log message levels\n");
			} else {
				ackstr[ackcnt++ ] = _("hiding log message levels\n");
			}
		} else if (responseix == mlssix) {
			mlog_showss = !mlog_showss;
			if (mlog_showss) {
				ackstr[ackcnt++ ] = _("showing log message subsystems\n");
			} else {
				ackstr[ackcnt++ ] = _("hiding log message subsystems\n");
			}
		} else if (responseix == mltsix) {
			mlog_timestamp = !mlog_timestamp;
			if (mlog_timestamp) {
				ackstr[ackcnt++ ] = _("showing log message timestamps\n");
			} else {
				ackstr[ackcnt++ ] = _("hiding log message timestamps\n");
			}
		}
		dlog_multi_ack(ackstr,
				ackcnt);
	} else {
		ackcnt = 0;
		ackstr[ackcnt++ ] = _("continuing\n");
		dlog_multi_ack(ackstr,
				ackcnt);
	}

	fold_init(fold, _("end dialog"), '-');
	postamblecnt = 0;
	postamblestr[postamblecnt++ ] = "\n";
	postamblestr[postamblecnt++] = fold;
	postamblestr[postamblecnt++ ] = "\n\n";
	assert(postamblecnt <= POSTAMBLEMAX);
	dlog_end(postamblestr,
		  postamblecnt);

	return stop_requested;
}

static char *
sigintstr(void)
{
	int ttyfd;
	static char buf[20];
	struct termios termios;
	cc_t intchr;
	int rval;

	ttyfd = dlog_fd();
	if (ttyfd == -1) {
		return 0;
	}

	rval = tcgetattr(ttyfd, &termios);
	if (rval) {
		mlog(MLOG_NITTY | MLOG_PROC,
		      "could not get controlling terminal information: %s\n",
		      strerror(errno));
		return 0;
	}

	intchr = termios.c_cc[VINTR];
	mlog(MLOG_NITTY | MLOG_PROC,
	      "tty fd: %d; terminal interrupt character: %c (0%o)\n",
	      ttyfd,
	      intchr,
	      intchr);

	if (intchr < ' ') {
		sprintf(buf, "^%c", intchr + '@');
	} else if (intchr == 0177) {
		sprintf(buf, "DEL");
	} else {
		sprintf(buf, "%c", intchr);
	}
	assert(strlen(buf) < sizeof(buf));

	return buf;
}

#ifdef DUMP
static bool_t
set_rlimits(void)
#endif /* DUMP */
#ifdef RESTORE
static bool_t
set_rlimits(size64_t *vmszp)
#endif /* RESTORE */
{
	struct rlimit64 rlimit64;
#ifdef RESTORE
	size64_t vmsz;
#endif /* RESTORE */
	/* REFERENCED */
	int rval;

	assert(minstacksz <= maxstacksz);

	rval = getrlimit64(RLIMIT_AS, &rlimit64);

	assert(!rval);
	mlog(MLOG_NITTY | MLOG_NOLOCK | MLOG_PROC,
	      "RLIMIT_AS org cur 0x%llx max 0x%llx\n",
	      rlimit64.rlim_cur,
	      rlimit64.rlim_max);
#ifdef RESTORE
	if (rlimit64.rlim_cur != RLIM64_INFINITY) {
		rlimit64.rlim_cur = rlimit64.rlim_max;
		(void)setrlimit64(RLIMIT_AS, &rlimit64);
		rval = getrlimit64(RLIMIT_AS, &rlimit64);
		assert(!rval);
		mlog(MLOG_NITTY | MLOG_NOLOCK | MLOG_PROC,
			"RLIMIT_VMEM now cur 0x%llx max 0x%llx\n",
			rlimit64.rlim_cur,
			rlimit64.rlim_max);
	}

	vmsz = (size64_t)rlimit64.rlim_cur;
#endif /* RESTORE */

	assert(minstacksz <= maxstacksz);
	rval = getrlimit64(RLIMIT_STACK, &rlimit64);
	assert(!rval);
	mlog(MLOG_NITTY | MLOG_NOLOCK | MLOG_PROC,
	      "RLIMIT_STACK org cur 0x%llx max 0x%llx\n",
	      rlimit64.rlim_cur,
	      rlimit64.rlim_max);
	if (rlimit64.rlim_cur < minstacksz) {
		if (rlimit64.rlim_max < minstacksz) {
			mlog(MLOG_DEBUG
			      |
			      MLOG_NOLOCK
			      |
			      MLOG_PROC,
			      "raising stack size hard limit "
			      "from 0x%llx to 0x%llx\n",
			      rlimit64.rlim_max,
			      minstacksz);
			rlimit64.rlim_cur = minstacksz;
			rlimit64.rlim_max = minstacksz;
			(void)setrlimit64(RLIMIT_STACK, &rlimit64);
			rval = getrlimit64(RLIMIT_STACK, &rlimit64);
			assert(!rval);
			if (rlimit64.rlim_cur < minstacksz) {
				mlog(MLOG_NORMAL
				      |
				      MLOG_WARNING
				      |
				      MLOG_NOLOCK
				      |
				      MLOG_PROC,
				      _("unable to raise stack size hard limit "
				      "from 0x%llx to 0x%llx\n"),
				      rlimit64.rlim_max,
				      minstacksz);
			}
		} else {
			mlog(MLOG_DEBUG
			      |
			      MLOG_NOLOCK
			      |
			      MLOG_PROC,
			      "raising stack size soft limit "
			      "from 0x%llx to 0x%llx\n",
			      rlimit64.rlim_cur,
			      minstacksz);
			rlimit64.rlim_cur = minstacksz;
			(void)setrlimit64(RLIMIT_STACK, &rlimit64);
			rval = getrlimit64(RLIMIT_STACK, &rlimit64);
			assert(!rval);
			if (rlimit64.rlim_cur < minstacksz) {
				mlog(MLOG_NORMAL
				      |
				      MLOG_WARNING
				      |
				      MLOG_NOLOCK
				      |
				      MLOG_PROC,
				      _("unable to raise stack size soft limit "
				      "from 0x%llx to 0x%llx\n"),
				      rlimit64.rlim_cur,
				      minstacksz);
			}
		}
	} else if (rlimit64.rlim_cur > maxstacksz) {
		mlog(MLOG_DEBUG
		      |
		      MLOG_NOLOCK
		      |
		      MLOG_PROC,
		      "lowering stack size soft limit "
		      "from 0x%llx to 0x%llx\n",
		      rlimit64.rlim_cur,
		      maxstacksz);
		rlimit64.rlim_cur = maxstacksz;
		(void)setrlimit64(RLIMIT_STACK, &rlimit64);
		rval = getrlimit64(RLIMIT_STACK, &rlimit64);
		assert(!rval);
		if (rlimit64.rlim_cur > maxstacksz) {
			mlog(MLOG_NORMAL
			      |
			      MLOG_WARNING
			      |
			      MLOG_NOLOCK
			      |
			      MLOG_PROC,
			      _("unable to lower stack size soft limit "
			      "from 0x%llx to 0x%llx\n"),
			      rlimit64.rlim_cur,
			      maxstacksz);
		}
	}
	mlog(MLOG_NITTY | MLOG_NOLOCK | MLOG_PROC,
	      "RLIMIT_STACK new cur 0x%llx max 0x%llx\n",
	      rlimit64.rlim_cur,
	      rlimit64.rlim_max);

	rval = getrlimit64(RLIMIT_DATA, &rlimit64);
	assert(!rval);
	mlog(MLOG_NITTY | MLOG_NOLOCK | MLOG_PROC,
	      "RLIMIT_DATA org cur 0x%llx max 0x%llx\n",
	      rlimit64.rlim_cur,
	      rlimit64.rlim_max);

	rval = getrlimit64(RLIMIT_FSIZE, &rlimit64);
	assert(!rval);
	mlog(MLOG_NITTY | MLOG_NOLOCK | MLOG_PROC,
	      "RLIMIT_FSIZE org cur 0x%llx max 0x%llx\n",
	      rlimit64.rlim_cur,
	      rlimit64.rlim_max);
	rlimit64.rlim_cur = rlimit64.rlim_max;
	(void)setrlimit64(RLIMIT_FSIZE, &rlimit64);
	rlimit64.rlim_cur = RLIM64_INFINITY;
	(void)setrlimit64(RLIMIT_FSIZE, &rlimit64);
	rval = getrlimit64(RLIMIT_FSIZE, &rlimit64);
	assert(!rval);
	mlog(MLOG_NITTY | MLOG_NOLOCK | MLOG_PROC,
	      "RLIMIT_FSIZE now cur 0x%llx max 0x%llx\n",
	      rlimit64.rlim_cur,
	      rlimit64.rlim_max);

	rval = getrlimit64(RLIMIT_CPU, &rlimit64);
	assert(!rval);
	mlog(MLOG_NITTY | MLOG_NOLOCK | MLOG_PROC,
	      "RLIMIT_CPU cur 0x%llx max 0x%llx\n",
	      rlimit64.rlim_cur,
	      rlimit64.rlim_max);
	rlimit64.rlim_cur = rlimit64.rlim_max;
	(void)setrlimit64(RLIMIT_CPU, &rlimit64);
	rval = getrlimit64(RLIMIT_CPU, &rlimit64);
	assert(!rval);
	mlog(MLOG_NITTY | MLOG_NOLOCK | MLOG_PROC,
	      "RLIMIT_CPU now cur 0x%llx max 0x%llx\n",
	      rlimit64.rlim_cur,
	      rlimit64.rlim_max);

#ifdef RESTORE
	*vmszp = vmsz;
#endif /* RESTORE */
	return BOOL_TRUE;
}

struct sig_printmap {
	int num;
	char *string;
};

typedef struct sig_printmap sig_printmap_t;

static sig_printmap_t sig_printmap[] = {
	{SIGHUP,	"SIGHUP"},
	{SIGINT,	"SIGINT"},
	{SIGQUIT,	"SIGQUIT"},
	{SIGILL,	"SIGILL"},
	{SIGABRT,	"SIGABRT"},
	{SIGFPE,	"SIGFPE"},
	{SIGBUS,	"SIGBUS"},
	{SIGSEGV,	"SIGSEGV"},
#ifdef SIGSYS
	{SIGSYS,	"SIGSYS"},
#endif
	{SIGPIPE,	"SIGPIPE"},
	{SIGALRM,	"SIGALRM"},
	{SIGTERM,	"SIGTERM"},
	{SIGUSR1,	"SIGUSR1"},
	{SIGUSR2,	"SIGUSR2"},
	{SIGCHLD,	"SIGCHLD"},
	{SIGPWR,	"SIGPWR"},
	{SIGURG,	"SIGURG"},
	{SIGPOLL,	"SIGPOLL"},
	{SIGXCPU,	"SIGXCPU"},
	{SIGXFSZ,	"SIGXFSZ"},
#if HIDDEN
	{SIGRTMIN,	"SIGRTMIN"},
	{SIGRTMAX,	"SIGRTMAX"},
#endif
	{0,		"???"}
};

static char *
sig_numstring(int num)
{
	sig_printmap_t *p = sig_printmap;
	sig_printmap_t *endp = sig_printmap
			       +
			       (sizeof(sig_printmap)
			         /
			         sizeof(sig_printmap[0]));
	for (; p < endp; p++) {
		if (p->num == num) {
			return p->string;
		}
	}

	return "???";
}

static char *
strpbrkquotes(char *p, const char *sep)
{
	bool_t prevcharwasbackslash = BOOL_FALSE;
	bool_t inquotes = BOOL_FALSE;

	for (;; p++) {
		if (*p == 0) {
			return 0;
		}

		if (*p == '\\') {
			if (!prevcharwasbackslash) {
				prevcharwasbackslash = BOOL_TRUE;
			} else {
				prevcharwasbackslash = BOOL_FALSE;
			}
			continue;
		}

		if (*p == '"') {
			if (prevcharwasbackslash) {
				prevcharwasbackslash = BOOL_FALSE;
				continue;
			}
			if (inquotes) {
				inquotes = BOOL_FALSE;
			} else {
				inquotes = BOOL_TRUE;
			}
			continue;
		}

		if (!inquotes) {
			if (strchr(sep, (int)(*p))) {
				return p;
			}
		}

		prevcharwasbackslash = BOOL_FALSE;
	}
	/* NOTREACHED */
}

static char *
stripquotes(char *p)
{
	size_t len = strlen(p);
	char *endp;
	char *nextp;
	bool_t justremovedbackslash;

	if (len > 2 && p[0 ] == '"') {
		p++;
		len--;
		if (len && p[len - 1 ] == '"') {
			p[len - 1] = 0;
			len--;
		}
	}

	endp = p + len;
	justremovedbackslash = BOOL_FALSE;

	for (nextp = p; nextp < endp;) {
		if (*nextp == '\\' && !justremovedbackslash) {
			shiftleftby1(nextp, endp);
			endp--;
			justremovedbackslash = BOOL_TRUE;
		} else {
			justremovedbackslash = BOOL_FALSE;
			nextp++;
		}
	}

	return p;
}

static void
shiftleftby1(char *p, char *endp)
{
	for (; p < endp; p++) {
		*p = p[1];
	}
}
