/*
 * Copyright (c) 2001-2009, 2013, 2015 Paul Mattes.
 * 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 names of Paul Mattes nor the names of his contributors
 *       may be used to endorse or promote products derived from this software
 *       without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY PAUL MATTES "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 PAUL MATTES 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.
 */

/*
 *	child.c
 *		Child process output support.
 */
#include "globals.h"

#include <errno.h>
#include <fcntl.h>

#include "popups.h" /* must be before cpopups.h */
#include "child_popups.h"
#include "utils.h"

#define CHILD_BUF	1024

static bool child_initted = false;
static bool child_broken = false;
static bool child_discarding = false;
static int child_outpipe[2];
static int child_errpipe[2];

static struct pr3o {
    int fd;			/* file descriptor */
    ioid_t input_id;	/* input ID */
    ioid_t timeout_id; 	/* timeout ID */
    int count;		/* input count */
    char buf[CHILD_BUF];	/* input buffer */
} child_stdout = { -1, 0L, 0L, 0 },
  child_stderr = { -1, 0L, 0L, 0 };

static void child_output(iosrc_t fd, ioid_t id);
static void child_error(iosrc_t fd, ioid_t id);
static void child_otimeout(ioid_t id);
static void child_etimeout(ioid_t id);
static void child_dump(struct pr3o *p, bool is_err);

static void
init_child(void)
{
    /* If initialization failed, there isn't much we can do. */
    if (child_broken) {
	return;
    }

    /* Create pipes. */
    if (pipe(child_outpipe) < 0) {
	popup_an_errno(errno, "pipe()");
	child_broken = true;
	return;
    }
    if (pipe(child_errpipe) < 0) {
	popup_an_errno(errno, "pipe()");
	close(child_outpipe[0]);
	close(child_outpipe[1]);
	child_broken = true;
	return;
    }

    /* Make sure their read ends are closed in child processes. */
    (void) fcntl(child_outpipe[0], F_SETFD, 1);
    (void) fcntl(child_errpipe[0], F_SETFD, 1);

    /* Initialize the pop-ups. */
    child_popup_init();

    /* Express interest in their output. */
    child_stdout.fd = child_outpipe[0];
    child_stdout.input_id = AddInput(child_outpipe[0], child_output);
    child_stderr.fd = child_errpipe[0];
    child_stderr.input_id = AddInput(child_errpipe[0], child_error);

    child_initted = true;
}

/*
 * Fork a child process, with its stdout/stderr connected to pop-up windows.
 * Returns -1 for an error, 0 for child context, pid for parent context.
 */
int
fork_child(void)
{
    pid_t pid;

    /* Do initialization, if it hasn't been done already. */
    if (!child_initted) {
	init_child();
    }

    /* If output was being dumped, turn it back on now. */
    if (child_discarding) {
	child_discarding = false;
    }

    /* Fork and rearrange output. */
    pid = fork();
    if (pid == 0) {
	/* Child. */
	(void) dup2(child_outpipe[1], 1);
	(void) close(child_outpipe[1]);
	(void) dup2(child_errpipe[1], 2);
	(void) close(child_errpipe[1]);
    }
    return pid;
}

/* There's data from a child. */
static void
child_data(struct pr3o *p, bool is_err)
{
    int space;
    int nr;
    static char exitmsg[] = "Printer session exited";

    /*
     * If we're discarding output, pull it in and drop it on the floor.
     */
    if (child_discarding) {
	nr = read(p->fd, p->buf, CHILD_BUF);
	return;
    }

    /* Read whatever there is. */
    space = CHILD_BUF - p->count - 1;
    nr = read(p->fd, p->buf + p->count, space);

    /* Handle read errors and end-of-file. */
    if (nr < 0) {
	popup_an_errno(errno, "child session pipe input");
	return;
    }
    if (nr == 0) {
	if (child_stderr.timeout_id != 0L) {
	    /*
	     * Append a termination error message to whatever the
	     * child process said, and pop it up.
	     */
	    p = &child_stderr;
	    space = CHILD_BUF - p->count - 1;
	    if (p->count && *(p->buf + p->count - 1) != '\n') {
		*(p->buf + p->count) = '\n';
		p->count++;
		space--;
	    }
	    (void) strncpy(p->buf + p->count, exitmsg, space);
	    p->count += strlen(exitmsg);
	    if (p->count >= CHILD_BUF) {
		p->count = CHILD_BUF - 1;
	    }
	    child_dump(p, true);
	} else {
	    popup_an_error("%s", exitmsg);
	}
	return;
    }

    /* Add it to the buffer, and add a NULL. */
    p->count += nr;
    p->buf[p->count] = '\0';

    /*
     * If there's no more room in the buffer, dump it now.  Otherwise,
     * give it a second to generate more output.
     */
    if (p->count >= CHILD_BUF - 1) {
	child_dump(p, is_err);
    } else if (p->timeout_id == 0L) {
	p->timeout_id = AddTimeOut(1000,
		is_err? child_etimeout: child_otimeout);
    }
}

/* The child process has some output for us. */
static void
child_output(iosrc_t fd _is_unused, ioid_t id _is_unused)
{
    child_data(&child_stdout, false);
}

/* The child process has some error output for us. */
static void
child_error(iosrc_t fd _is_unused, ioid_t id _is_unused)
{
    child_data(&child_stderr, true);
}

/* Timeout from child output or error output. */
static void
child_timeout(struct pr3o *p, bool is_err)
{
    /* Forget the timeout ID. */
    p->timeout_id = 0L;

    /* Dump the output. */
    child_dump(p, is_err);
}

/* Timeout from child output. */
static void
child_otimeout(ioid_t id _is_unused)
{
    child_timeout(&child_stdout, false);
}

/* Timeout from child error output. */
static void
child_etimeout(ioid_t id _is_unused)
{
    child_timeout(&child_stderr, true);
}

/*
 * Abort button from child output.
 * Ignore output from the child process, so the user can abort it.
 */
void
child_ignore_output(void)
{
    /* Pitch pending output. */
    child_stdout.count = 0;
    child_stderr.count = 0;

    /* Remove pendnig timeouts. */
    if (child_stdout.timeout_id) {
	RemoveTimeOut(child_stdout.timeout_id);
	child_stdout.timeout_id = 0L;
    }
    if (child_stderr.timeout_id) {
	RemoveTimeOut(child_stderr.timeout_id);
	child_stderr.timeout_id = 0L;
    }

    /* Remember it. */
    child_discarding = true;
}

/* Dump pending child process output. */
static void
child_dump(struct pr3o *p, bool is_err)
{
    if (p->count) {
	/*
	 * Strip any trailing newline, and make sure the buffer is
	 * NULL terminated.
	 */
	if (p->buf[p->count - 1] == '\n') {
	    p->buf[--(p->count)] = '\0';
	} else if (p->buf[p->count]) {
	    p->buf[p->count] = '\0';
	}

	/* Dump it and clear the buffer. */
	popup_child_output(is_err, child_ignore_output, "%s", p->buf);

	p->count = 0;
    }
}
