/*
 * Copyright (c)2004 The DragonFly Project.  All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 
 *   Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * 
 *   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.
 * 
 *   Neither the name of the DragonFly Project 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 COPYRIGHT HOLDERS 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
 * COPYRIGHT HOLDERS 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. 
 */

/*
 * commands.c
 * Execute a queued series of commands, updating a DFUI progress bar
 * as each is executed.
 * $Id: commands.c,v 1.22 2004/09/25 02:39:54 cpressey Exp $
 */

#include <sys/time.h>
#include <sys/types.h>

#include <signal.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "aura/mem.h"
#include "aura/buffer.h"
#include "aura/popen.h"

#include "dfui/dfui.h"

#define	NEEDS_COMMANDS_STRUCTURE_DEFINITIONS
#include "commands.h"
#undef	NEEDS_COMMANDS_STRUCTURE_DEFINITIONS

#include "diskutil.h"
#include "functions.h"
#include "uiutil.h"

extern void	abort_backend(void);

/*
 * Create a new queue of commands.
 */
struct commands	*
commands_new(void)
{
	struct commands *cmds;

	AURA_MALLOC(cmds, commands);

	cmds->head = NULL;
	cmds->tail = NULL;

	return(cmds);
}

/*
 * Add a new, empty command to an existing queue of commands.
 */
static struct command *
command_new(struct commands *cmds)
{
	struct command *cmd;

	AURA_MALLOC(cmd, command);

	cmd->cmdline = NULL;
	cmd->desc = NULL;
	cmd->log_mode = COMMAND_LOG_VERBOSE;
	cmd->failure_mode = COMMAND_FAILURE_ABORT;
	cmd->tag = NULL;
	cmd->result = COMMAND_RESULT_NEVER_EXECUTED;
	cmd->output = NULL;

	cmd->next = NULL;
	if (cmds->head == NULL)
		cmds->head = cmd;
	else
		cmds->tail->next = cmd;

	cmd->prev = cmds->tail;
	cmds->tail = cmd;

	return(cmd);
}

/*
 * Add a new shell command to an existing queue of commands.
 * The command can be specified as a format string followed by
 * any number of arguments, in the style of "sprintf".
 */
struct command *
command_add(struct commands *cmds, char *fmt, ...)
{
	va_list args;
	struct command *cmd;

	cmd = command_new(cmds);

	va_start(args, fmt);
	vasprintf(&cmd->cmdline, fmt, args);
	va_end(args);

	return(cmd);
}

/*
 * Set the log mode of the given command.
 * Valid log modes are:
 *   COMMAND_LOG_SILENT - do not log anything at all
 *   COMMAND_LOG_QUIET - only log command name and exit code, not output
 *   COMMAND_LOG_VERBOSE - log everything
 */
void
command_set_log_mode(struct command *cmd, int log_mode)
{
	cmd->log_mode = log_mode;
}

/*
 * Set the failure mode of the given command.
 * Valid failure modes are:
 *   COMMAND_FAILURE_IGNORE - ignore failures and carry on
 *   COMMAND_FAILURE_WARN - issue a non-critical warning
 *   COMMAND_FAILURE_ABORT - halt the command chain and ask the user
 */
void
command_set_failure_mode(struct command *cmd, int failure_mode)
{
	cmd->failure_mode = failure_mode;
}

/*
 * Set the description of the given command.  If present, it will
 * be displayed in the progress bar instead of the command line.
 */
void
command_set_desc(struct command *cmd, char *fmt, ...)
{
	va_list args;

	if (cmd->desc != NULL)
		free(cmd->desc);

	va_start(args, fmt);
	vasprintf(&cmd->desc, fmt, args);
	va_end(args);
}

/*
 * Set an arbitrary tag on the command.
 */
void
command_set_tag(struct command *cmd, char *fmt, ...)
{
	va_list args;

	if (cmd->tag != NULL)
		free(cmd->tag);

	va_start(args, fmt);
	vasprintf(&cmd->tag, fmt, args);
	va_end(args);
}

struct command *
command_get_first(struct commands *cmds)
{
	return(cmds->head);
}

struct command *
command_get_next(struct command *cmd)
{
	return(cmd->next);
}

char *
command_get_cmdline(struct command *cmd)
{
	return(cmd->cmdline);
}

char *
command_get_tag(struct command *cmd)
{
	return(cmd->tag);
}

int
command_get_result(struct command *cmd)
{
	return(cmd->result);
}

/*
 * Allow the user to view the command log.
 */
void
view_command_log(struct i_fn_args *a)
{
	struct dfui_form *f;
	struct dfui_response *r;
	struct aura_buffer *error_log;

	error_log = aura_buffer_new(1024);
	aura_buffer_cat_file(error_log, "%sinstall.log", a->tmp);

	f = dfui_form_create(
	    "error_log",
	    "Error Log",
	    aura_buffer_buf(error_log),
	    "",

	    "p",	"role", "informative",
	    "p",	"minimum_width", "72",
	    "p",	"monospaced", "true",

	    "a",	"ok", "OK", "", "",
	    NULL);

	if (!dfui_be_present(a->c, f, &r))
		abort_backend();

	dfui_form_free(f);
	dfui_response_free(r);

	aura_buffer_free(error_log);
}

/*
 * Preview a set of commands.
 */
void
commands_preview(struct dfui_connection *c, struct commands *cmds)
{
	struct command *cmd;
	struct aura_buffer *preview;

	preview = aura_buffer_new(1024);

	for (cmd = cmds->head; cmd != NULL; cmd = cmd->next) {
		aura_buffer_cat(preview, cmd->cmdline);
		aura_buffer_cat(preview, "\n");
	}

	inform(c, aura_buffer_buf(preview));

	aura_buffer_free(preview);
}

/*
 * The command chain executing engine proper follows.
 */

/*
 * Read from the pipe that was opened to the executing commands
 * and update the progress bar as data comes and (and/or as the
 * read from the pipe times out.)
 */
static int
pipe_loop(struct i_fn_args *a, struct dfui_progress *pr,
	  struct command *cmd, int *cancelled)
{
	FILE *cmdout = NULL;
	struct timeval tv = { 1, 0 };
	char cline[256];
	char *command;
	pid_t pid;
	fd_set r;
	int n;

	asprintf(&command, "(%s) 2>&1 </dev/null", cmd->cmdline);
	fflush(stdout);
	cmdout = aura_popen(command, "r");
	free(command);

	if (cmdout == NULL) {
		i_log(a, "! could not aura_popen() command");
		return(COMMAND_RESULT_POPEN_ERR);
	}
	pid = aura_pgetpid(cmdout);
#ifdef DEBUG
	fprintf(stderr, "+ pid = %d\n", pid);
#endif

	/*
	 * Loop, selecting on the command and a timeout.
	 */
	for (;;) {
		if (*cancelled)
			break;
		FD_ZERO(&r);
		FD_SET(fileno(cmdout), &r);
		n = select(fileno(cmdout) + 1, &r, NULL, NULL, &tv);
#ifdef DEBUG
		fprintf(stderr, "+ select() = %d\n", n);
#endif
		if (n < 0) {
			/* Error */
			i_log(a, "! select() failed\n");
			aura_pclose(cmdout);
			return(COMMAND_RESULT_SELECT_ERR);
		} else if (n == 0) {
			/* Timeout */
			if (!dfui_be_progress_update(a->c, pr, cancelled))
				abort_backend();
#ifdef DEBUG
			fprintf(stderr, "+ cancelled = %d\n", *cancelled);
#endif
		} else {
			/* Data came in */
			fgets(cline, 255, cmdout);
			while (strlen(cline) > 0 && cline[strlen(cline) - 1] == '\n')
				cline[strlen(cline) - 1] = '\0';
			if (feof(cmdout))
				break;
			if (!dfui_be_progress_update(a->c, pr, cancelled))
				abort_backend();
			if (cmd->log_mode == COMMAND_LOG_VERBOSE) {
				i_log(a, "| %s", cline);
			} else if (cmd->log_mode != COMMAND_LOG_SILENT) {
				fprintf(stderr, "| %s", cline);
			}
		}
	}

	if (*cancelled) {
#ifdef DEBUG
		fprintf(stderr, "+ killing %d\n", pid);
#endif
		n = kill(pid, SIGTERM);
#ifdef DEBUG
		fprintf(stderr, "+ kill() = %d\n", n);
#endif
	}

#ifdef DEBUG
	fprintf(stderr, "+ pclosing %d\n", fileno(cmdout));
#endif
	n = aura_pclose(cmdout) / 256;
#ifdef DEBUG
	fprintf(stderr, "+ pclose() = %d\n", n);
#endif
	return(n);
}

/*
 * Execute a single command.
 * Return value is a COMMAND_RESULT_* constant, or
 * a value from 0 to 255 to indicate the exit code
 * from the utility.
 */
static int
command_execute(struct i_fn_args *a, struct dfui_progress *pr,
		struct commands *cmds, struct command *cmd)
{
	FILE *log = NULL;
	char *filename;
	int cancelled = 0, done = 0, report_done = 0;

	if (cmd->desc != NULL)
		dfui_info_set_short_desc(dfui_progress_get_info(pr), cmd->desc);
	else
		dfui_info_set_short_desc(dfui_progress_get_info(pr), cmd->cmdline);

	if (!dfui_be_progress_update(a->c, pr, &cancelled)) 
		  abort_backend();

	while (!done) {
		asprintf(&filename, "%sinstall.log", a->tmp);
		log = fopen(filename, "a");
		free(filename);

		if (cmd->log_mode != COMMAND_LOG_SILENT)
			i_log(a, ",-<<< Executing `%s'", cmd->cmdline);
		cmd->result = pipe_loop(a, pr, cmd, &cancelled);
		if (cmd->log_mode != COMMAND_LOG_SILENT)
			i_log(a, "`->>> Exit status: %d\n", cmd->result);

		if (log != NULL)
			fclose(log);

		if (cancelled) {
			if (!dfui_be_progress_end(a->c))
				abort_backend();

			report_done = 0;
			while (!report_done) {
				switch (dfui_be_present_dialog(a->c, "Cancelled",
				    "View Log|Retry|Cancel|Skip",
				    "Execution of the command\n\n%s\n\n"
				    "was cancelled.",
				    cmd->cmdline)) {
				case 1:
					/* View Log */
					view_command_log(a);
					break;
				case 2:
					/* Retry */
					cancelled = 0;
					report_done = 1;
					break;
				case 3:
					/* Cancel */
					cmd->result = COMMAND_RESULT_CANCELLED;
					report_done = 1;
					done = 1;
					break;
				case 4:
					/* Skip */
					cmd->result = COMMAND_RESULT_SKIPPED;
					report_done = 1;
					done = 1;
					break;
				}
			}

			if (!dfui_be_progress_begin(a->c, pr))
				abort_backend();

		} else if (cmd->failure_mode == COMMAND_FAILURE_IGNORE) {
			cmd->result = 0;
			done = 1;
		} else if (cmd->result != 0 && cmd->failure_mode != COMMAND_FAILURE_WARN) {
			if (!dfui_be_progress_end(a->c))
				abort_backend();

			report_done = 0;
			while (!report_done) {
				switch (dfui_be_present_dialog(a->c, "Command Failed!",
				    "View Log|Retry|Cancel|Skip",
				    "Execution of the command\n\n%s\n\n"
				    "FAILED with a return code of %d.",
				    cmd->cmdline, cmd->result)) {
				case 1:
					/* View Log */
					view_command_log(a);
					break;
				case 2:
					/* Retry */
					report_done = 1;
					break;
				case 3:
					/* Cancel */
					/* XXX need a better way to retain actual result */
					cmd->result = COMMAND_RESULT_CANCELLED;
					report_done = 1;
					done = 1;
					break;
				case 4:
					/* Skip */
					/* XXX need a better way to retain actual result */
					cmd->result = COMMAND_RESULT_SKIPPED;
					report_done = 1;
					done = 1;
					break;
				}
			}

			if (!dfui_be_progress_begin(a->c, pr))
				abort_backend();

		} else {
			done = 1;
		}
	}

	return(cmd->result);
}

/*
 * Execute a series of external utility programs.
 * Returns 1 if everything executed OK, 0 if one of the
 * critical commands failed or if the user cancelled.
 */
int
commands_execute(struct i_fn_args *a, struct commands *cmds)
{
	struct dfui_progress *pr;
	struct command *cmd;
	int i;
	int n = 0;
	int result = 0;
	int return_val = 1;

	cmd = cmds->head;
	while (cmd != NULL) {
		n++;
		cmd = cmd->next;
	}

	pr = dfui_progress_new(dfui_info_new(
	    "Executing Commands",
	    "Executing Commands",
	    ""),
	    0);

	if (!dfui_be_progress_begin(a->c, pr))
		abort_backend();

	i = 1;
	for (cmd = cmds->head; cmd != NULL; cmd = cmd->next, i++) {
		result = command_execute(a, pr, cmds, cmd);
		if (result == COMMAND_RESULT_CANCELLED) {
			return_val = 0;
			break;
		}
		if (result > 0 && result < 256) {
			return_val = 0;
			if (cmd->failure_mode == COMMAND_FAILURE_ABORT) {
				break;
			}
		}
		dfui_progress_set_amount(pr, (i * 100) / n);
	}

	if (!dfui_be_progress_end(a->c))
		abort_backend();

	dfui_progress_free(pr);

	return(return_val);
}

/*
 * Free the memory allocated for a queue of commands.  This invalidates
 * the pointer passed to it.
 */
void
commands_free(struct commands *cmds)
{
	struct command *cmd, *next;

	cmd = cmds->head;
	while (cmd != NULL) {
		next = cmd->next;
		if (cmd->cmdline != NULL)
			free(cmd->cmdline);
		if (cmd->desc != NULL)
			free(cmd->desc);
		if (cmd->tag != NULL)
			free(cmd->tag);
		AURA_FREE(cmd, command);
		cmd = next;
	}
	AURA_FREE(cmds, commands);
}

/*
 * Command generators.
 */

/*
 * Generate a command to create a generic device node
 * only if it does not already exist.
 */
struct command *
command_add_ensure_dev(struct i_fn_args *a, struct commands *cmds,
		       char *dev_name)
{
	struct command *cmd;
	char *dev_basename;

	dev_basename = basename(dev_name);

	cmd = command_add(cmds,	"cd %sdev && %s%s %s || %s%s MAKEDEV %s",
	    a->os_root,
	    a->os_root, cmd_name(a, "TEST_DEV"),
	    dev_basename,
	    a->os_root, cmd_name(a, "SH"),
	    dev_basename);

	free(dev_basename);

	return(cmd);
}
