/*
 * Copyright (C) 1980      Regents of the University of California.
 * Copyright (C) 2013-2019 Karel Zak <kzak@redhat.com>
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *      This product includes software developed by the University of
 *      California, Berkeley and its contributors.
 * 4. Neither the name of the University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */
#include <stdio.h>
#include <stdlib.h>
#include <paths.h>
#include <time.h>
#include <sys/stat.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
#include <getopt.h>
#include <unistd.h>
#include <fcntl.h>
#include <limits.h>
#include <locale.h>
#include <stddef.h>
#include <sys/wait.h>
#include <poll.h>
#include <sys/signalfd.h>
#include <assert.h>
#include <inttypes.h>

#include "closestream.h"
#include "nls.h"
#include "c.h"
#include "cctype.h"
#include "ttyutils.h"
#include "all-io.h"
#include "monotonic.h"
#include "timeutils.h"
#include "strutils.h"
#include "strv.h"
#include "xalloc.h"
#include "optutils.h"
#include "signames.h"
#include "pty-session.h"
#include "debug.h"
#include "default_shell.h"

static UL_DEBUG_DEFINE_MASK(script);
UL_DEBUG_DEFINE_MASKNAMES(script) = UL_DEBUG_EMPTY_MASKNAMES;

#define SCRIPT_DEBUG_INIT	(1 << 1)
#define SCRIPT_DEBUG_PTY	(1 << 2)
#define SCRIPT_DEBUG_IO		(1 << 3)
#define SCRIPT_DEBUG_SIGNAL	(1 << 4)
#define SCRIPT_DEBUG_MISC	(1 << 5)
#define SCRIPT_DEBUG_ALL	0xFFFF

#define DBG(m, x)       __UL_DBG(script, SCRIPT_DEBUG_, m, x)
#define ON_DBG(m, x)    __UL_DBG_CALL(script, SCRIPT_DEBUG_, m, x)

#ifdef HAVE_LIBUTEMPTER
# include <utempter.h>
#endif

#define DEFAULT_TYPESCRIPT_FILENAME "typescript"

/*
 * Script is driven by stream (stdout/stdin) activity. It's possible to
 * associate arbitrary number of log files with the stream. We have two basic
 * types of log files: "timing file" (simple or multistream) and "data file"
 * (raw).
 *
 * The same log file may be shared between both streams. For example
 * multi-stream timing file is possible to use for stdin as well as for stdout.
 */
enum {
	SCRIPT_FMT_RAW = 1,		/* raw slave/master data */
	SCRIPT_FMT_TIMING_SIMPLE,	/* (classic) in format "<delta> <offset>" */
	SCRIPT_FMT_TIMING_MULTI,	/* (advanced) multiple streams in format "<type> <delta> <offset|etc> */
};

struct script_log {
	FILE	*fp;			/* file pointer (handler) */
	int	format;			/* SCRIPT_FMT_* */
	char	*filename;		/* on command line specified name */
	struct timeval oldtime;		/* previous entry log time (SCRIPT_FMT_TIMING_* only) */
	struct timeval starttime;

	unsigned int	initialized : 1;
};

struct script_stream {
	struct script_log **logs;	/* logs where to write data from stream */
	size_t nlogs;			/* number of logs */
	char ident;			/* stream identifier */
};

struct script_control {
	uint64_t outsz;		/* current output files size */
	uint64_t maxsz;		/* maximum output files size */

	struct script_stream	out;	/* output */
	struct script_stream	in;	/* input */

	struct script_log	*siglog;	/* log for signal entries */
	struct script_log	*infolog;	/* log for info entries */

	const char *ttyname;
	const char *ttytype;
	char *command;
	char *command_norm;	/* normalized (without \n) */
	int ttycols;
	int ttylines;

	struct ul_pty *pty;	/* pseudo-terminal */
	pid_t child;		/* child pid */
	int childstatus;	/* child process exit value */

	bool	append,		/* append output */
		rc_wanted,	/* return child exit value */
		flush,		/* flush after each write */
		quiet,		/* suppress most output */
		force,		/* write output to links */
		isterm;		/* is child process running as terminal */
};

static ssize_t log_info(struct script_control *ctl, const char *name, const char *msgfmt, ...)
			__attribute__((__format__ (__printf__, 3, 4)));

static void script_init_debug(void)
{
	__UL_INIT_DEBUG_FROM_ENV(script, SCRIPT_DEBUG_, 0, SCRIPT_DEBUG);
}

static void init_terminal_info(struct script_control *ctl)
{
	if (ctl->ttyname || !ctl->isterm)
		return;		/* already initialized */

	get_terminal_dimension(&ctl->ttycols, &ctl->ttylines);
	get_terminal_name(&ctl->ttyname, NULL, NULL);
	get_terminal_type(&ctl->ttytype);
}

/*
 * For tests we want to be able to control time output
 */
#ifdef TEST_SCRIPT
static inline time_t script_time(time_t *t)
{
	const char *str = getenv("SCRIPT_TEST_SECOND_SINCE_EPOCH");
	int64_t sec;

	if (!str || sscanf(str, "%"SCNi64, &sec) != 1)
		return time(t);
	if (t)
		*t = (time_t)sec;
	return (time_t)sec;
}
#else	/* !TEST_SCRIPT */
# define script_time(x) time(x)
#endif

static void __attribute__((__noreturn__)) usage(void)
{
	FILE *out = stdout;
	fputs(USAGE_HEADER, out);
	fprintf(out, _(" %s [options] [<file>] [-- <command> [<argument>...]]\n"), program_invocation_short_name);

	fputs(USAGE_SEPARATOR, out);
	fputs(_("Make a typescript of a terminal session.\n"), out);

	fputs(USAGE_OPTIONS, out);
	fputs(_(" -I, --log-in <file>           log stdin to file\n"), out);
	fputs(_(" -O, --log-out <file>          log stdout to file (default)\n"), out);
	fputs(_(" -B, --log-io <file>           log stdin and stdout to file\n"), out);
	fputs(USAGE_SEPARATOR, out);

	fputs(_(" -T, --log-timing <file>       log timing information to file\n"), out);
	fputs(_(" -t[<file>], --timing[=<file>] deprecated alias to -T (default file is stderr)\n"), out);
	fputs(_(" -m, --logging-format <name>   force to 'classic' or 'advanced' format\n"), out);
	fputs(USAGE_SEPARATOR, out);

	fputs(_(" -a, --append                  append to the log file\n"), out);
	fputs(_(" -c, --command <command>       run <command> rather than an interactive shell\n"
		"                                 (alternative to '-- <command> [<argument...>]')\n"), out);
	fputs(_(" -e, --return                  return exit code of the child process\n"), out);
	fputs(_(" -f, --flush                   run flush after each write\n"), out);
	fputs(_("     --force                   use output file even when it is a link\n"), out);
	fputs(_(" -E, --echo <when>             echo input in session (auto, always or never)\n"), out);
	fputs(_(" -o, --output-limit <size>     terminate if output files exceed size\n"), out);
	fputs(_(" -q, --quiet                   be quiet\n"), out);

	fputs(USAGE_SEPARATOR, out);
	fprintf(out, USAGE_HELP_OPTIONS(31));

	fputs(USAGE_ARGUMENTS, out);
	fprintf(out, USAGE_ARG_SIZE(_("<size>")));

	fprintf(out, USAGE_MAN_TAIL("script(1)"));

	exit(EXIT_SUCCESS);
}

static struct script_log *get_log_by_name(struct script_stream *stream,
					  const char *name)
{
	size_t i;

	for (i = 0; i < stream->nlogs; i++) {
		struct script_log *log = stream->logs[i];
		if (strcmp(log->filename, name) == 0)
			return log;
	}
	return NULL;
}

static struct script_log *log_associate(struct script_control *ctl,
					struct script_stream *stream,
					const char *filename, int format)
{
	struct script_log *log;

	DBG(MISC, ul_debug("associate %s with stream", filename));

	assert(ctl);
	assert(filename);
	assert(stream);

	log = get_log_by_name(stream, filename);
	if (log)
		return log;	/* already defined */

	log = get_log_by_name(stream == &ctl->out ? &ctl->in : &ctl->out, filename);
	if (!log) {
		/* create a new log */
		log = xcalloc(1, sizeof(*log));
		log->filename = xstrdup(filename);
		log->format = format;
	}

	/* add log to the stream */
	stream->logs = xreallocarray(stream->logs,
			stream->nlogs + 1, sizeof(log));
	stream->logs[stream->nlogs] = log;
	stream->nlogs++;

	/* remember where to write info about signals */
	if (format == SCRIPT_FMT_TIMING_MULTI) {
		if (!ctl->siglog)
			ctl->siglog = log;
		if (!ctl->infolog)
			ctl->infolog = log;
	}

	return log;
}

static int log_close(struct script_control *ctl,
		      struct script_log *log,
		      const char *msg,
		      int status)
{
	int rc = 0;

	if (!log || !log->initialized)
		return 0;

	DBG(MISC, ul_debug("closing %s", log->filename));

	switch (log->format) {
	case SCRIPT_FMT_RAW:
	{
		char buf[FORMAT_TIMESTAMP_MAX];
		time_t tvec = script_time((time_t *)NULL);

		strtime_iso(&tvec, ISO_TIMESTAMP, buf, sizeof(buf));
		if (msg)
			fprintf(log->fp, _("\nScript done on %s [<%s>]\n"), buf, msg);
		else
			fprintf(log->fp, _("\nScript done on %s [COMMAND_EXIT_CODE=\"%d\"]\n"), buf, status);
		break;
	}
	case SCRIPT_FMT_TIMING_MULTI:
	{
		struct timeval now = { 0 }, delta = { 0 };

		gettime_monotonic(&now);
		timersub(&now, &log->starttime, &delta);

		log_info(ctl, "DURATION", "%"PRId64".%06"PRId64,
			(int64_t)delta.tv_sec,
			(int64_t)delta.tv_usec);
		log_info(ctl, "EXIT_CODE", "%d", status);
		break;
	}
	case SCRIPT_FMT_TIMING_SIMPLE:
		break;
	}

	if (close_stream(log->fp) != 0) {
		warn(_("write failed: %s"), log->filename);
		rc = -errno;
	}

	free(log->filename);
	memset(log, 0, sizeof(*log));

	return rc;
}

static int log_flush(struct script_control *ctl __attribute__((__unused__)), struct script_log *log)
{

	if (!log || !log->initialized)
		return 0;

	DBG(MISC, ul_debug("flushing %s", log->filename));

	fflush(log->fp);
	return 0;
}

static void log_free(struct script_control *ctl, struct script_log *log)
{
	size_t i;

	if (!log)
		return;

	/* the same log is possible to reference from more places, remove all
	 * (TODO: maybe use include/list.h to make it more elegant)
	 */
	if (ctl->siglog == log)
		ctl->siglog = NULL;
	else if (ctl->infolog == log)
		ctl->infolog = NULL;

	for (i = 0; i < ctl->out.nlogs; i++) {
		if (ctl->out.logs[i] == log)
			ctl->out.logs[i] = NULL;
	}
	for (i = 0; i < ctl->in.nlogs; i++) {
		if (ctl->in.logs[i] == log)
			ctl->in.logs[i] = NULL;
	}
	free(log);
}

static int log_start(struct script_control *ctl,
		      struct script_log *log)
{
	if (log->initialized)
		return 0;

	DBG(MISC, ul_debug("opening %s", log->filename));

	assert(log->fp == NULL);

	/* open the log */
	log->fp = fopen(log->filename,
			ctl->append && log->format == SCRIPT_FMT_RAW ?
			"a" UL_CLOEXECSTR :
			"w" UL_CLOEXECSTR);
	if (!log->fp) {
		warn(_("cannot open %s"), log->filename);
		return -errno;
	}

	/* write header, etc. */
	switch (log->format) {
	case SCRIPT_FMT_RAW:
	{
		int x = 0;
		char buf[FORMAT_TIMESTAMP_MAX];
		time_t tvec = script_time((time_t *)NULL);

		strtime_iso(&tvec, ISO_TIMESTAMP, buf, sizeof(buf));
		fprintf(log->fp, _("Script started on %s ["), buf);

		if (ctl->command)
			x += fprintf(log->fp, "COMMAND=\"%s\"", ctl->command_norm);

		if (ctl->isterm) {
			init_terminal_info(ctl);

			if (ctl->ttytype)
				x += fprintf(log->fp, "%*sTERM=\"%s\"", !!x, "", ctl->ttytype);
			if (ctl->ttyname)
				x += fprintf(log->fp, "%*sTTY=\"%s\"", !!x, "", ctl->ttyname);

			x += fprintf(log->fp, "%*sCOLUMNS=\"%d\" LINES=\"%d\"", !!x, "",
					ctl->ttycols, ctl->ttylines);
		} else
			fprintf(log->fp, _("%*s<not executed on terminal>"), !!x, "");

		fputs("]\n", log->fp);
		break;
	}
	case SCRIPT_FMT_TIMING_SIMPLE:
	case SCRIPT_FMT_TIMING_MULTI:
		gettime_monotonic(&log->oldtime);
		gettime_monotonic(&log->starttime);
		break;
	}

	log->initialized = 1;
	return 0;
}

static int logging_start(struct script_control *ctl)
{
	size_t i;

	/* start all output logs */
	for (i = 0; i < ctl->out.nlogs; i++) {
		int rc = log_start(ctl, ctl->out.logs[i]);
		if (rc)
			return rc;
	}

	/* start all input logs */
	for (i = 0; i < ctl->in.nlogs; i++) {
		int rc = log_start(ctl, ctl->in.logs[i]);
		if (rc)
			return rc;
	}
	return 0;
}

static ssize_t log_write(struct script_control *ctl,
		      struct script_stream *stream,
		      struct script_log *log,
		      char *obuf, size_t bytes)
{
	int rc;
	ssize_t ssz = 0;
	struct timeval now, delta;

	if (!log->fp)
		return 0;

	DBG(IO, ul_debug(" writing [file=%s]", log->filename));

	switch (log->format) {
	case SCRIPT_FMT_RAW:
		DBG(IO, ul_debug("  log raw data"));
		rc = fwrite_all(obuf, 1, bytes, log->fp);
		if (rc) {
			warn(_("cannot write %s"), log->filename);
			return rc;
		}
		ssz = bytes;
		break;

	case SCRIPT_FMT_TIMING_SIMPLE:
		DBG(IO, ul_debug("  log timing info"));

		gettime_monotonic(&now);
		timersub(&now, &log->oldtime, &delta);
		ssz = fprintf(log->fp, "%"PRId64".%06"PRId64" %zd\n",
			(int64_t)delta.tv_sec, (int64_t)delta.tv_usec, bytes);
		if (ssz < 0)
			return -errno;

		log->oldtime = now;
		break;

	case SCRIPT_FMT_TIMING_MULTI:
		DBG(IO, ul_debug("  log multi-stream timing info"));

		gettime_monotonic(&now);
		timersub(&now, &log->oldtime, &delta);
		ssz = fprintf(log->fp, "%c %"PRId64".%06"PRId64" %zd\n",
			stream->ident,
			(int64_t)delta.tv_sec, (int64_t)delta.tv_usec, bytes);
		if (ssz < 0)
			return -errno;

		log->oldtime = now;
		break;
	default:
		break;
	}

	if (ctl->flush)
		fflush(log->fp);
	return ssz;
}

static ssize_t log_stream_activity(
			struct script_control *ctl,
			struct script_stream *stream,
			char *buf, size_t bytes)
{
	size_t i;
	ssize_t outsz = 0;

	for (i = 0; i < stream->nlogs; i++) {
		ssize_t ssz = log_write(ctl, stream, stream->logs[i], buf, bytes);

		if (ssz < 0)
			return ssz;
		outsz += ssz;
	}

	return outsz;
}

static ssize_t __attribute__ ((__format__ (__printf__, 3, 4)))
	log_signal(struct script_control *ctl, int signum, const char *msgfmt, ...)
{
	struct script_log *log;
	struct timeval now, delta;
	char msg[BUFSIZ] = {0};
	va_list ap;
	ssize_t sz;

	assert(ctl);

	log = ctl->siglog;
	if (!log)
		return 0;

	assert(log->format == SCRIPT_FMT_TIMING_MULTI);
	DBG(IO, ul_debug("  writing signal to multi-stream timing"));

	gettime_monotonic(&now);
	timersub(&now, &log->oldtime, &delta);

	if (msgfmt) {
		int rc;
		va_start(ap, msgfmt);
		rc = vsnprintf(msg, sizeof(msg), msgfmt, ap);
		va_end(ap);
		if (rc < 0)
			*msg = '\0';
	}

	if (*msg)
		sz = fprintf(log->fp, "S %"PRId64".%06"PRId64" SIG%s %s\n",
			(int64_t)delta.tv_sec, (int64_t)delta.tv_usec,
			signum_to_signame(signum), msg);
	else
		sz = fprintf(log->fp, "S %"PRId64".%06"PRId64" SIG%s\n",
			(int64_t)delta.tv_sec, (int64_t)delta.tv_usec,
			signum_to_signame(signum));

	log->oldtime = now;
	return sz;
}

static ssize_t log_info(struct script_control *ctl, const char *name, const char *msgfmt, ...)
{
	struct script_log *log;
	char msg[BUFSIZ] = {0};
	va_list ap;
	ssize_t sz;

	assert(ctl);

	log = ctl->infolog;
	if (!log)
		return 0;

	assert(log->format == SCRIPT_FMT_TIMING_MULTI);
	DBG(IO, ul_debug("  writing info to multi-stream log"));

	if (msgfmt) {
		int rc;
		va_start(ap, msgfmt);
		rc = vsnprintf(msg, sizeof(msg), msgfmt, ap);
		va_end(ap);
		if (rc < 0)
			*msg = '\0';
	}

	if (*msg)
		sz = fprintf(log->fp, "H %f %s %s\n", 0.0, name, msg);
	else
		sz = fprintf(log->fp, "H %f %s\n", 0.0, name);

	return sz;
}


static void logging_done(struct script_control *ctl, const char *msg)
{
	int status;
	size_t i;

	DBG(MISC, ul_debug("stop logging"));

	if (WIFSIGNALED(ctl->childstatus))
		status = WTERMSIG(ctl->childstatus) + 0x80;
	else
		status = WEXITSTATUS(ctl->childstatus);

	DBG(MISC, ul_debug(" status=%d", status));

	/* close all output logs */
	for (i = 0; i < ctl->out.nlogs; i++) {
		struct script_log *log = ctl->out.logs[i];
		log_close(ctl, log, msg, status);
		log_free(ctl, log);
	}
	free(ctl->out.logs);
	ctl->out.logs = NULL;
	ctl->out.nlogs = 0;

	/* close all input logs */
	for (i = 0; i < ctl->in.nlogs; i++) {
		struct script_log *log = ctl->in.logs[i];
		log_close(ctl, log, msg, status);
		log_free(ctl, log);
	}
	free(ctl->in.logs);
	ctl->in.logs = NULL;
	ctl->in.nlogs = 0;
}

static void callback_child_die(
			void *data,
			pid_t child __attribute__((__unused__)),
			int status)
{
	struct script_control *ctl = (struct script_control *) data;

	ctl->child = (pid_t) -1;
	ctl->childstatus = status;
}

static void callback_child_sigstop(
			void *data __attribute__((__unused__)),
			pid_t child)
{
	DBG(SIGNAL, ul_debug(" child stop by SIGSTOP -- stop parent too"));
	kill(getpid(), SIGSTOP);
	DBG(SIGNAL, ul_debug(" resume"));
	kill(child, SIGCONT);
}

static int callback_log_stream_activity(void *data, int fd, char *buf, size_t bufsz)
{
	struct script_control *ctl = (struct script_control *) data;
	ssize_t ssz = 0;

	DBG(IO, ul_debug("stream activity callback"));

	/* from stdin (user) to command */
	if (fd == STDIN_FILENO)
		ssz = log_stream_activity(ctl, &ctl->in, buf, (size_t) bufsz);

	/* from command (master) to stdout and log */
	else if (fd == ul_pty_get_childfd(ctl->pty))
		ssz = log_stream_activity(ctl, &ctl->out, buf, (size_t) bufsz);

	if (ssz < 0)
		return (int) ssz;

	DBG(IO, ul_debug(" append %zd bytes [summary=%" PRIu64 ", max=%" PRIu64 "]", ssz,
				ctl->outsz, ctl->maxsz));

	ctl->outsz += ssz;

	/* check output limit */
	if (ctl->maxsz != 0 && ctl->outsz >= ctl->maxsz) {
		if (!ctl->quiet)
			printf(_("Script terminated, max output files size %"PRIu64" exceeded.\n"), ctl->maxsz);
		DBG(IO, ul_debug("output size %"PRIu64", exceeded limit %"PRIu64, ctl->outsz, ctl->maxsz));
		logging_done(ctl, _("max output size exceeded"));
		return 1;
	}
	return 0;
}

static int callback_log_signal(void *data, struct signalfd_siginfo *info, void *sigdata)
{
	struct script_control *ctl = (struct script_control *) data;
	ssize_t ssz = 0;

	switch (info->ssi_signo) {
	case SIGWINCH:
	{
		struct winsize *win = (struct winsize *) sigdata;
		ssz = log_signal(ctl, info->ssi_signo, "ROWS=%d COLS=%d",
					win->ws_row, win->ws_col);
		break;
	}
	case SIGTERM:
		FALLTHROUGH;
	case SIGINT:
		FALLTHROUGH;
	case SIGQUIT:
		ssz = log_signal(ctl, info->ssi_signo, NULL);
		break;
	default:
		/* no log */
		break;
	}

	return ssz < 0 ? ssz : 0;
}

static int callback_flush_logs(void *data)
{
	struct script_control *ctl = (struct script_control *) data;
	size_t i;

	for (i = 0; i < ctl->out.nlogs; i++) {
		int rc = log_flush(ctl, ctl->out.logs[i]);
		if (rc)
			return rc;
	}

	for (i = 0; i < ctl->in.nlogs; i++) {
		int rc = log_flush(ctl, ctl->in.logs[i]);
		if (rc)
			return rc;
	}
	return 0;
}

static void die_if_link(struct script_control *ctl, const char *filename)
{
	struct stat s;

	if (ctl->force)
		return;
	if (lstat(filename, &s) == 0 && (S_ISLNK(s.st_mode) || s.st_nlink > 1))
		errx(EXIT_FAILURE,
		     _("output file `%s' is a link\n"
		       "Use --force if you really want to use it.\n"
		       "Program not started."), filename);
}

int main(int argc, char **argv)
{
	struct script_control ctl = {
		.out = { .ident = 'O' },
		.in  = { .ident = 'I' },
	};
	struct ul_pty_callbacks *cb;
	int ch, format = 0, caught_signal = 0, rc = 0, echo = 1;
	const char *outfile = NULL, *infile = NULL;
	const char *timingfile = NULL, *shell = NULL;
	char **cmd_argv = NULL;
	int cmd_argc = 0;

	enum { FORCE_OPTION = CHAR_MAX + 1 };

	static const struct option longopts[] = {
		{"append", no_argument, NULL, 'a'},
		{"command", required_argument, NULL, 'c'},
		{"echo", required_argument, NULL, 'E'},
		{"return", no_argument, NULL, 'e'},
		{"flush", no_argument, NULL, 'f'},
		{"force", no_argument, NULL, FORCE_OPTION,},
		{"log-in", required_argument, NULL, 'I'},
		{"log-out", required_argument, NULL, 'O'},
		{"log-io", required_argument, NULL, 'B'},
		{"log-timing", required_argument, NULL, 'T'},
		{"logging-format", required_argument, NULL, 'm'},
		{"output-limit", required_argument, NULL, 'o'},
		{"quiet", no_argument, NULL, 'q'},
		{"timing", optional_argument, NULL, 't'},
		{"version", no_argument, NULL, 'V'},
		{"help", no_argument, NULL, 'h'},
		{NULL, 0, NULL, 0}
	};
	static const ul_excl_t excl[] = {       /* rows and cols in ASCII order */
		{ 'T', 't' },
		{ 0 }
	};
	int excl_st[ARRAY_SIZE(excl)] = UL_EXCL_STATUS_INIT;
	setlocale(LC_ALL, "");
	/*
	 * script -t prints time delays as floating point numbers.  The example
	 * program (scriptreplay) that we provide to handle this timing output
	 * is a perl script, and does not handle numbers in locale format (not
	 * even when "use locale;" is added).  So, since these numbers are not
	 * for human consumption, it seems easiest to set LC_NUMERIC here.
	 */
	setlocale(LC_NUMERIC, "C");
	bindtextdomain(PACKAGE, LOCALEDIR);
	textdomain(PACKAGE);
	close_stdout_atexit();

	script_init_debug();
	ON_DBG(PTY, ul_pty_init_debug(0xFFFF));

	ctl.isterm = isatty(STDIN_FILENO);

	/*
	 * Pre-scan for "--" separator to split argv into options+outfile
	 * and command parts. This allows getopt to permute arguments
	 * before "--" (backward compatible with "script file -c cmd").
	 */
	ch = ul_find_argv_separator(argc, argv, longopts);
	if (ch > 0) {
		cmd_argv = argv + ch + 1;
		cmd_argc = argc - ch - 1;
		argc = ch;
	}

	while ((ch = getopt_long(argc, argv, "aB:c:eE:fI:O:o:qm:T:t::Vh", longopts, NULL)) != -1) {

		err_exclusive_options(ch, longopts, excl, excl_st);

		switch (ch) {
		case 'a':
			ctl.append = 1;
			break;
		case 'c':
			free(ctl.command);
			ctl.command = xstrdup(optarg);
			free(ctl.command_norm);
			ctl.command_norm = xstrdup(ctl.command);
			ul_strrep(ctl.command_norm, '\n', ' ');
			break;
		case 'E':
			if (strcmp(optarg, "auto") == 0)
				; /* keep default */
			else if (strcmp(optarg, "never") == 0)
				echo = 0;
			else if (strcmp(optarg, "always") == 0)
				echo = 1;
			else
				errx(EXIT_FAILURE, _("unsupported echo mode: '%s'"), optarg);
			break;
		case 'e':
			ctl.rc_wanted = 1;
			break;
		case 'f':
			ctl.flush = 1;
			break;
		case FORCE_OPTION:
			ctl.force = 1;
			break;
		case 'B':
			log_associate(&ctl, &ctl.in, optarg, SCRIPT_FMT_RAW);
			log_associate(&ctl, &ctl.out, optarg, SCRIPT_FMT_RAW);
			infile = outfile = optarg;
			break;
		case 'I':
			log_associate(&ctl, &ctl.in, optarg, SCRIPT_FMT_RAW);
			infile = optarg;
			break;
		case 'O':
			log_associate(&ctl, &ctl.out, optarg, SCRIPT_FMT_RAW);
			outfile = optarg;
			break;
		case 'o':
			ctl.maxsz = strtosize_or_err(optarg, _("failed to parse output limit size"));
			break;
		case 'q':
			ctl.quiet = 1;
			break;
		case 'm':
			if (c_strcasecmp(optarg, "classic") == 0)
				format = SCRIPT_FMT_TIMING_SIMPLE;
			else if (c_strcasecmp(optarg, "advanced") == 0)
				format = SCRIPT_FMT_TIMING_MULTI;
			else
				errx(EXIT_FAILURE, _("unsupported logging format: '%s'"), optarg);
			break;
		case 't':
			if (optarg && *optarg == '=')
				optarg++;
			timingfile = optarg ? optarg : "/dev/stderr";
			break;
		case 'T' :
			timingfile = optarg;
			break;
		case 'V':
			print_version(EXIT_SUCCESS);
		case 'h':
			usage();
		default:
			errtryhelp(EXIT_FAILURE);
		}
	}

	argc -= optind;
	argv += optind;

	/*
	 * The "--" separator splits non-option arguments into <outfile>
	 * (before "--") and <command> (after "--"). The "--" and command
	 * arguments have been already separated by the pre-scan above.
	 *
	 * Possible combinations are:
	 *
	 * script [options] <outfile>
	 * script [options] -- <command ...>
	 * script [options] <outfile> -- <command ...>
	 */

	/* default if no --log-* specified */
	if (!outfile && !infile) {
		if (argc > 0) {
			outfile = argv[0];
			argc--;
			argv++;
		} else {
			die_if_link(&ctl, DEFAULT_TYPESCRIPT_FILENAME);
			outfile = DEFAULT_TYPESCRIPT_FILENAME;
		}

		/* associate stdout with typescript file */
		log_associate(&ctl, &ctl.out, outfile, SCRIPT_FMT_RAW);
	}

	/* concat arguments after "--" as command */
	if (cmd_argc > 0) {
		if (ctl.command != NULL) {
			warnx(_("option --command and '-- program' are mutually exclusive"));
			errtryhelp(EXIT_FAILURE);
		}

		ctl.command = ul_strv_join(cmd_argv, " ");
		if (!ctl.command)
			errx(EXIT_FAILURE, _("failed to concatenate arguments"));

		ctl.command_norm = xstrdup(ctl.command);
		ul_strrep(ctl.command_norm, '\n', ' ');
	}

	if (argc > 0) {
		warnx(_("unexpected number of arguments"));
		errtryhelp(EXIT_FAILURE);
	}

	if (timingfile) {
		/* the old SCRIPT_FMT_TIMING_SIMPLE should be used when
		 * recoding output only (just for backward compatibility),
		 * otherwise switch to new format. */
		if (!format)
			format = infile || (outfile && infile) ?
					SCRIPT_FMT_TIMING_MULTI :
					SCRIPT_FMT_TIMING_SIMPLE;

		else if (format == SCRIPT_FMT_TIMING_SIMPLE && outfile && infile)
			errx(EXIT_FAILURE, _("log multiple streams is mutually "
					     "exclusive with 'classic' format"));
		if (outfile)
			log_associate(&ctl, &ctl.out, timingfile, format);
		if (infile)
			log_associate(&ctl, &ctl.in, timingfile, format);
	}

	shell = ul_default_shell(0, NULL);

	ctl.pty = ul_new_pty(ctl.isterm);
	if (!ctl.pty)
		err(EXIT_FAILURE, "failed to allocate PTY handler");

	ul_pty_slave_echo(ctl.pty, echo);

	ul_pty_set_callback_data(ctl.pty, (void *) &ctl);
	cb = ul_pty_get_callbacks(ctl.pty);
	cb->child_die = callback_child_die;
	cb->child_sigstop = callback_child_sigstop;
	cb->log_stream_activity = callback_log_stream_activity;
	cb->log_signal = callback_log_signal;
	cb->flush_logs = callback_flush_logs;

	if (!ctl.quiet) {
		printf(_("Script started"));
		if (outfile)
			printf(_(", output log file is '%s'"), outfile);
		if (infile)
			printf(_(", input log file is '%s'"), infile);
		if (timingfile)
			printf(_(", timing file is '%s'"), timingfile);
		printf(_(".\n"));
	}


	if (ul_pty_setup(ctl.pty))
		err(EXIT_FAILURE, _("failed to create pseudo-terminal"));

#ifdef HAVE_LIBUTEMPTER
	utempter_add_record(ul_pty_get_childfd(ctl.pty), NULL);
#endif

	if (ul_pty_signals_setup(ctl.pty))
		err(EXIT_FAILURE, _("failed to initialize signals handler"));
	fflush(stdout);

	/*
	 * We have terminal, do not use err() from now, use "goto done"
	 */

	switch ((int) (ctl.child = fork())) {
	case -1: /* error */
		warn(_("cannot create child process"));
		rc = -errno;
		goto done;

	case 0: /* child */
	{
		const char *shname;

		ul_pty_init_slave(ctl.pty);

		signal(SIGTERM, SIG_DFL); /* because /etc/csh.login */

		shname = strrchr(shell, '/');
		shname = shname ? shname + 1 : shell;

		if (access(shell, X_OK) == 0) {
			if (ctl.command)
				execl(shell, shname, "-c", ctl.command, (char *)NULL);
			else
				execl(shell, shname, "-i", (char *)NULL);
		} else {
			if (ctl.command)
				execlp(shname, shname, "-c", ctl.command, (char *)NULL);
			else
				execlp(shname, shname, "-i", (char *)NULL);
		}

		err(EXIT_FAILURE, "failed to execute %s", shell);
		break;
	}
	default:
		break;
	}

	/* parent */
	ul_pty_set_child(ctl.pty, ctl.child);

	rc = logging_start(&ctl);
	if (rc)
		goto done;

	/* add extra info to advanced timing file */
	if (timingfile && format == SCRIPT_FMT_TIMING_MULTI) {
		char buf[FORMAT_TIMESTAMP_MAX];
		time_t tvec = script_time((time_t *)NULL);

		strtime_iso(&tvec, ISO_TIMESTAMP, buf, sizeof(buf));
		log_info(&ctl, "START_TIME", "%s", buf);

		if (ctl.isterm) {
			init_terminal_info(&ctl);
			log_info(&ctl, "TERM", "%s", ctl.ttytype);
			log_info(&ctl, "TTY", "%s", ctl.ttyname);
			log_info(&ctl, "COLUMNS", "%d", ctl.ttycols);
			log_info(&ctl, "LINES", "%d", ctl.ttylines);
		}
		log_info(&ctl, "SHELL", "%s", shell);
		if (ctl.command)
			log_info(&ctl, "COMMAND", "%s", ctl.command_norm);
		log_info(&ctl, "TIMING_LOG", "%s", timingfile);
		if (outfile)
			log_info(&ctl, "OUTPUT_LOG", "%s", outfile);
		if (infile)
			log_info(&ctl, "INPUT_LOG", "%s", infile);
	}

	/* this is the main loop */
	rc = ul_pty_proxy_master(ctl.pty);

	/* all done; cleanup and kill */
	caught_signal = ul_pty_get_delivered_signal(ctl.pty);

	if (!caught_signal && ctl.child != (pid_t)-1)
		ul_pty_wait_for_child(ctl.pty);	/* final wait */

	if (caught_signal && ctl.child != (pid_t)-1) {
		fprintf(stderr, "\nSession terminated, killing shell...");
		kill(ctl.child, SIGTERM);
		sleep(2);
		kill(ctl.child, SIGKILL);
		fprintf(stderr, " ...killed.\n");
	}

done:
	ul_pty_cleanup(ctl.pty);
	logging_done(&ctl, NULL);

	if (!ctl.quiet)
		printf(_("Script done.\n"));

#ifdef HAVE_LIBUTEMPTER
	if (ul_pty_get_childfd(ctl.pty) >= 0)
		utempter_remove_record(ul_pty_get_childfd(ctl.pty));
#endif
	ul_free_pty(ctl.pty);
	free(ctl.command);
	free(ctl.command_norm);

	/* default exit code */
	rc = rc ? EXIT_FAILURE : EXIT_SUCCESS;

	/* exit code based on child status */
	if (ctl.rc_wanted && rc == EXIT_SUCCESS) {
		if (WIFSIGNALED(ctl.childstatus))
			rc = WTERMSIG(ctl.childstatus) + 0x80;
		else
			rc = WEXITSTATUS(ctl.childstatus);
	}

	DBG(MISC, ul_debug("done [rc=%d]", rc));
	return rc;
}
