/*
 * 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.3 2004/07/11 02:34:22 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 <dfui/dfui.h>
#include <dfui/extbuf.h>
#include <dfui/mem.h>
#include <dfui/popen.h>

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

extern void	abort_backend(void);

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

	DFUI_MALLOC(cmds, commands);

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

	return(cmds);
}

/*
 * Add a new, empty command to an existing queue of commands.
 * This is typically not called directly by client code.
 */
struct command *
command_new(struct commands *cmds)
{
	struct command *cmd;

	DFUI_MALLOC(cmd, command);

	cmd->cmd = NULL;
	cmd->callback = NULL;
	cmd->userdata = NULL;
	cmd->desc = NULL;
	cmd->logged = 1;

	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, following "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->cmd, fmt, args);
	va_end(args);

	return(cmd);
}

struct command *
command_add_logless(struct commands *cmds, char *fmt, ...)
{
	va_list args;
	struct command *cmd;

	cmd = command_new(cmds);

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

	cmd->logged = 0;
	return(cmd);
}

/*
 * Add a new C function to an existing queue of commands.
 * The function is given as a callback (function pointer).
 * The function must take two arguments; a FILE * specifying
 * where error output is to go, and a void * which can point
 * to arbitrary user data used by the callback.  The callback
 * should return zero on success, and non-zero on error, like
 * a shell utility.
 * The arbitrary user data and a description string can also
 * be specified when adding the C function.
 */
struct command *
command_add_function(struct commands *cmds, int (*callback)(FILE *, void *),
    void *userdata, char *desc)
{
	struct command *cmd;

	cmd = command_new(cmds);

	cmd->callback = callback;
	cmd->userdata = userdata;
	asprintf(&cmd->cmd, "[internal] %s", desc);
	cmd->desc = dfui_strdup(desc);

	return(cmd);
}

int
command_set_desc(struct command *cmd, char *desc)
{
	if (cmd->desc != NULL)
		free(cmd->desc);
	cmd->desc = dfui_strdup(desc);
	return(1);
}

void
view_error_log(struct dfui_connection *c)
{
	struct dfui_form *f;
	struct dfui_response *r;
	struct extbuf *error_log;

	error_log = extbuf_new(1024);
	extbuf_cat_file(error_log, INSTALLER_TEMP "install.log");

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

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

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

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

	dfui_form_free(f);
	dfui_response_free(r);

	extbuf_free(error_log);
}

void
commands_preview(struct commands *cmds)
{
	struct command *cmd;
	char buf[8192];

	strcpy(buf, "");
	for (cmd = cmds->head; cmd != NULL; cmd = cmd->next) {
		strcat(buf, cmd->cmd);
		strcat(buf, "\n");
	}

	inform(buf);
}

int
pipe_loop(struct dfui_connection *c, struct dfui_progress *pr,
    struct command *cmd, FILE *log, 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->cmd);
	fflush(stdout);
	cmdout = dfui_popen(command, "r");
	free(command);

	if (cmdout == NULL) {
		fprintfo(log, "! could not dfui_popen() command\n");
		return(256);
	}
	pid = dfui_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 */
			fprintfo(log, "! select() failed\n");
			dfui_pclose(cmdout);
			return(258);
		} else if (n == 0) {
			/* Timeout */
			if (!dfui_be_progress_update(c, pr, cancelled))
				abort_backend();
#ifdef DEBUG
			fprintf(stderr, "+ cancelled = %d\n", *cancelled);
#endif
		} else {
			/* Data came in */
			fgets(cline, 255, cmdout);
			if (feof(cmdout))
				break;
			if (!dfui_be_progress_update(c, pr, cancelled))
				abort_backend();
			if (cmd->logged) {
				fprintfo(log, "| %s", cline);
			} else {
				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 = dfui_pclose(cmdout) / 256;
#ifdef DEBUG
	fprintf(stderr, "+ pclose() = %d\n", n);
#endif
	return(n);
}

/*
 * Execute a single command.
 * Result codes:
 *   0 - 255: exit code from utility
 *       256: pipe could not be opened
 *       257: execution was cancelled by user
 *	 258: select() failed
 */
int
command_execute(struct dfui_connection *c, struct dfui_progress *pr,
    struct command *cmd)
{
	FILE *log = NULL;
	int cancelled = 0, done = 0, report_done = 0, result = 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->cmd);

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

	while (!done) {
		log = fopen(INSTALLER_TEMP "install.log", "a");
		fprintfo(log, ",-<<< Executing `%s'\n", cmd->cmd);

		if (cmd->callback == NULL)
			result = pipe_loop(c, pr, cmd, log, &cancelled);
		else
			result = cmd->callback(log, cmd->userdata);

		fprintfo(log, "`->>> Exit status: %d\n\n", result);
		if (log != NULL)
			fclose(log);

		if (cancelled) {
			if (!dfui_be_progress_end(c))
				abort_backend();

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

			if (!dfui_be_progress_begin(c, pr))
				abort_backend();

		} else if (result != 0) {
			if (!dfui_be_progress_end(c))
				abort_backend();

			report_done = 0;
			while (!report_done) {
				switch (dfui_be_present_dialog(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->cmd, result)) {
				case 1:
					/* View Log */
					view_error_log(c);
					break;
				case 2:
					/* Retry */
					report_done = 1;
					break;
				case 3:
					/* Cancel */
					report_done = 1;
					done = 1;
					break;
				case 4:
					/* Skip */
					result = 0;
					report_done = 1;
					done = 1;
					break;
				}
			}

			if (!dfui_be_progress_begin(c, pr))
				abort_backend();

		} else {
			done = 1;
		}
	}

	return(result);
}

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

	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(c, pr))
		abort_backend();

	i = 1;
	cmd = cmds->head;
	while (cmd != NULL) {
		if ((result = command_execute(c, pr, cmd)) != 0)
			break;
		dfui_progress_set_amount(pr, (i++ * 100) / n);
		cmd = cmd->next;
	}

	if (!dfui_be_progress_end(c))
		abort_backend();

	dfui_progress_free(pr);

	if (result != 0)
		return(0);

	return(1);
}

/*
 * 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->cmd != NULL)
			free(cmd->cmd);
		if (cmd->desc != NULL)
			free(cmd->desc);
		DFUI_FREE(cmd, command);
		cmd = next;
	}
	DFUI_FREE(cmds, commands);
}
