/*
** 1998-09-26 -	This module deals with grabbing (i.e. redirecting) a sub-process' output.
**		Very useful when running external commands. This code was extracted from the
**		remains of the old (bloated) command module. If the interface seems rather
**		un-intuitive, that is probably because it's a mess. :^)
** 1999-03-02 -	Cleaned up interface with textviewing module, geting rid (I hope) of some
**		nasty races having to do with closing it down in mid-capture.
** 1999-03-08 -	Did some changes due to seemingly different semantics in GTK+ 1.2.0. This
**		requires handling of GDK_INPUT_EXCEPTION input condition (um, perhaps it
**		did in 1.0.6 too, but it worked even if I didn't care).
*/

#include "gentoo.h"

#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

#include <gdk/gdkkeysyms.h>

#include "textview.h"

#include "cmdgrab.h"

/* ----------------------------------------------------------------------------------------- */

/* I can never be trusted to remember which end of a pipe is which, so these
** three really help out. Er, of course only the first two apply to pipes.
*/
#define	FD_INPUT	(0)
#define	FD_OUTPUT	(1)
#define	FD_ERROR	(2)

#define	GRAB_CHUNK	(32768)		/* We read this many bytes on each "input available" callback. */

typedef struct {
	MainInfo	*min;
	pid_t		child;
	int		fd_out, fd_err;		/* File descriptors for output & error channels. */
	gint		tag_out, tag_err;	/* GTK+ input tags for those channels. */
	gint		evt_del;		/* Delete event handler. */
	GtkWidget	*txv;			/* Text viewing window. */
} GrabInfo;

/* ----------------------------------------------------------------------------------------- */

/* 1998-09-26 -	Given a command row, create two pipes (one for stdout and one for stderr)
**		in the given fd-arrays. Returns 1 if everything is OK, 0 if something failed.
*/
int cgr_grab_init(MainInfo *min, CmdRow *row, int fd_out[2], int fd_err[2])
{
	if(row->type == CRTP_EXTERNAL)
	{
		if(row->extra.external.gflags & CGF_GRABOUTPUT)
		{
			if(pipe(fd_out) == 0)
			{
				if(pipe(fd_err) == 0)
					return 1;
				close(fd_out[0]);
				close(fd_out[1]);
			}
			return 0;
		}
	}
	return 1;		/* If we do nothing, we succeed. */
}

/* ----------------------------------------------------------------------------------------- */

/* 1998-09-26 -	Connect the current process' stdout and stderr file descriptors to the correct
**		ends of the two pipes <fd_out> and <fd_err>.
**		This is run just after a fork (and before exec, of course).
*/
void cgr_set_child_pipes(MainInfo *min, CmdRow *row, int fd_out[2], int fd_err[2])
{
	if(row->type == CRTP_EXTERNAL && row->extra.external.gflags & CGF_GRABOUTPUT)
	{
		assert(close(FD_OUTPUT) == 0);
		assert(dup(fd_out[FD_OUTPUT]) == FD_OUTPUT);
		assert(close(fd_out[FD_INPUT]) == 0);

		assert(close(FD_ERROR) == 0);
		assert(dup(fd_err[FD_OUTPUT]) == FD_ERROR);
		assert(close(fd_err[FD_INPUT]) == 0);
	}
}

/* ----------------------------------------------------------------------------------------- */

/* 1998-09-26 -	This gets called when the user tries to delete (i.e., close) the output
**		window. We now need to disconnect the input grabbers, kill the child,
**		free the grabinfo, and close the window. We don't do need to do much of
**		that, though; killing the children generates a last callback-call, and
**		so we can reuse the close-down code already there!
*/
static gint evt_grab_deleted(GtkWidget *wid, GdkEvent *evt, gpointer data)
{
	GrabInfo	*gri = (GrabInfo *) data;

	kill(gri->child, SIGTERM);
	gri->txv = NULL;

	return FALSE;
}

/* 1999-02-23 -	User just pressed a key in the grab window. If Escape, close down. */
static gint evt_grab_keypress(GtkWidget *wid, GdkEventKey *event, gpointer user)
{
	GrabInfo	*gri;

	if(event->keyval == GDK_Escape)
	{
		if((gri = gtk_object_get_data(GTK_OBJECT(wid), "gri")) != NULL)
		{
			kill(gri->child, SIGTERM);
			gri->txv = NULL;
		}
		txv_close(wid);
	}
	return FALSE;
}

/* 1998-09-26 -	Here's a callback for the gtk_input_add() stuff. This gets called when there's
**		something to read from our child process, on either its stdout or stderr pipes.
**		We read it out (in small chunks) and use a GtkText widget to display the stuff.
**		This feels a lot more stable than the previous implementation, by the way.
** 1998-12-15 -	Now shows the window as the first output appears. Makes the window invisible
**		for commands that don't cause output. Note the manual destruction for that case.
** 1999-04-22 -	Added quick fix for weird condition: we get called with GDK_INPUT_READ although
**		there is nothing more to read. Added explicit grab termination via recursion.
*/
static void grab_callback(gpointer data, gint fd, GdkInputCondition cond)
{
	char		buf[GRAB_CHUNK];
	int		got;
	GrabInfo	*gri = (GrabInfo *) data;

	if(cond & GDK_INPUT_READ)
	{
		if((got = read(fd, buf, sizeof buf - 1)) > 0)	/* Able to read from pipe? */
		{
			buf[got] = '\0';
			if(gri->txv != NULL)
			{
				gtk_widget_show(gri->txv);
				txv_put_text(gri->txv, buf, got);
			}
		}
		else		/* Recursively give up. Seems to help dead-locking. */
		{
			grab_callback(gri, gri->fd_out, GDK_INPUT_EXCEPTION);
			grab_callback(gri, gri->fd_err, GDK_INPUT_EXCEPTION);
			return;
		}
	}
	if(cond & GDK_INPUT_EXCEPTION)
	{
		if(fd == gri->fd_out)				/* End of standard output? */
		{
			gtk_input_remove(gri->tag_out);
			close(gri->fd_out);
			gri->fd_out = -1;
		}
		else if(fd == gri->fd_err)			/* End of standard input? */
		{
			gtk_input_remove(gri->tag_err);
			close(gri->fd_err);
			gri->fd_err = -1;
		}

		if((gri->fd_out == -1) && (gri->fd_err == -1))	/* Both channels closed down? */
		{
			if(gri->txv != NULL)			/* Window still open? */
			{
				gtk_signal_disconnect(GTK_OBJECT(gri->txv), gri->evt_del);
				txv_connect_keypress(gri->txv, NULL, NULL);
				txv_thaw(gri->txv);
				txv_enable(gri->txv);
				if(!GTK_WIDGET_REALIZED(gri->txv))
					gtk_widget_destroy(gri->txv);
			}
			g_free(gri);
		}
	}
}

/* 1998-09-26 -	Set up two GTK+ input listeners, one on <fd_out> and one on <fd_err>. Is
**		cool (?) enough to share a single output window between these two channels.
*/
static void grab_output(MainInfo *min, char *prog, pid_t child, int fd_out, int fd_err)
{
	char		buf[MAXNAMLEN + 32];
	GrabInfo	*gri;

	gri = g_malloc(sizeof *gri);
	gri->min = min;
	gri->child = child;
	gri->fd_out = fd_out;
	gri->fd_err = fd_err;
	if((gri->txv = txv_open(min, NULL)) != NULL)
	{
		txv_freeze(gri->txv);
		g_snprintf(buf, sizeof buf, "Output of %s (pid %d)", prog, (int) child);
		txv_set_label(gri->txv, buf);
		gtk_object_set_data(GTK_OBJECT(gri->txv), "gri", gri);
		gri->evt_del = gtk_signal_connect(GTK_OBJECT(gri->txv), "delete_event", GTK_SIGNAL_FUNC(evt_grab_deleted), gri);
		txv_connect_keypress(gri->txv, GTK_SIGNAL_FUNC(evt_grab_keypress), gri);
		gri->tag_out = gtk_input_add_full(gri->fd_out, GDK_INPUT_READ | GDK_INPUT_EXCEPTION, grab_callback, NULL, gri, NULL);
		gri->tag_err = gtk_input_add_full(gri->fd_err, GDK_INPUT_READ | GDK_INPUT_EXCEPTION, grab_callback, NULL, gri, NULL);
	}
}

/* 1998-09-26 -	Set up the pipes as is necessary in the parent end. */
void cgr_set_parent_pipes(MainInfo *min, CmdRow *row, char *prog, int child, int fd_out[2], int fd_err[2])
{
	if(row->type == CRTP_EXTERNAL && row->extra.external.gflags & CGF_GRABOUTPUT)
	{
		assert(close(fd_out[FD_OUTPUT]) == 0);
		assert(close(fd_err[FD_OUTPUT]) == 0);
		grab_output(min, prog, child, fd_out[FD_INPUT], fd_err[FD_INPUT]);
	}
}
