/* $XConsortium: command.c,v 2.49 95/04/05 19:59:06 kaleb Exp $ */
/* $XFree86: xc/programs/xmh/command.c,v 3.4 1996/10/03 08:50:27 dawes Exp $ */

/*
 *			  COPYRIGHT 1987, 1989
 *		   DIGITAL EQUIPMENT CORPORATION
 *		       MAYNARD, MASSACHUSETTS
 *			ALL RIGHTS RESERVED.
 *
 * THE INFORMATION IN THIS SOFTWARE IS SUBJECT TO CHANGE WITHOUT NOTICE AND
 * SHOULD NOT BE CONSTRUED AS A COMMITMENT BY DIGITAL EQUIPMENT CORPORATION.
 * DIGITAL MAKES NO REPRESENTATIONS ABOUT THE SUITABILITY OF THIS SOFTWARE FOR
 * ANY PURPOSE.  IT IS SUPPLIED "AS IS" WITHOUT EXPRESS OR IMPLIED WARRANTY.
 *
 * IF THE SOFTWARE IS MODIFIED IN A MANNER CREATING DERIVATIVE COPYRIGHT
 * RIGHTS, APPROPRIATE LEGENDS MAY BE PLACED ON THE DERIVATIVE WORK IN
 * ADDITION TO THAT SET FORTH ABOVE.
 *
 *
 * Permission to use, copy, modify, and distribute this software and its
 * documentation for any purpose and without fee is hereby granted, provided
 * that the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation, and that the name of Digital Equipment Corporation not be
 * used in advertising or publicity pertaining to distribution of the software
 * without specific, written prior permission.
 */

/* command.c -- interface to exec mh commands. */

#include "xmh.h"
#include <X11/Xpoll.h>
#include <sys/ioctl.h>
#include <signal.h>
#ifndef SYSV
#include <sys/wait.h>
#endif	/* SYSV */
#if defined(SVR4) && !defined(SCO325)
#include <sys/filio.h>
#endif

/* number of user input events to queue before malloc */
#define TYPEAHEADSIZE 20

#ifndef HAS_VFORK
#define vfork() fork()
#else
#if defined(sun) && !defined(SVR4)
#include <vfork.h>
#endif
#endif

typedef struct _CommandStatus {
    Widget	popup;		 /* must be first; see PopupStatus */
    struct _LastInput lastInput; /* must be second; ditto */
    char*	shell_command;	 /* must be third; for XmhShellCommand */
    int		child_pid;
    XtInputId	output_inputId;
    XtInputId	error_inputId;
    int		output_pipe[2];
    int		error_pipe[2];
    char*	output_buffer;
    int		output_buf_size;
    char*	error_buffer;
    int		error_buf_size;
} CommandStatusRec, *CommandStatus;

typedef char* Pointer;
static void FreeStatus();
static void CheckReadFromPipe();

static void SystemError(text)
    char* text;
{
    char msg[BUFSIZ];
    sprintf( msg, "%s; errno = %d %s", text, errno, 
	     strerror(errno));
    XtWarning( msg );
}


/* Return the full path name of the given mh command. */

static char *FullPathOfCommand(str)
  char *str;
{
    static char result[100];
    (void) sprintf(result, "%s/%s", app_resources.mh_path, str);
    return result;
}


/*ARGSUSED*/
static void ReadStdout(closure, fd, id)
    XtPointer closure;
    int *fd;
    XtInputId *id;	/* unused */
{
    register CommandStatus status = (CommandStatus)closure;
    CheckReadFromPipe(*fd, &status->output_buffer, &status->output_buf_size,
		      False);
}


/*ARGSUSED*/
static void ReadStderr(closure, fd, id)
    XtPointer closure;
    int *fd;
    XtInputId *id;	/* unused */
{
    register CommandStatus status = (CommandStatus)closure;
    CheckReadFromPipe(*fd, &status->error_buffer, &status->error_buf_size,
		      False);
}


static int childdone;		/* Gets nonzero when the child process
				   finishes. */
/* ARGSUSED */
static void
ChildDone(n)
    int n;
{
    childdone++;
}

/* Execute the given command, and wait until it has finished.  While the
   command is executing, watch the X socket and cause Xlib to read in any
   incoming data.  This will prevent the socket from overflowing during
   long commands.  Returns 0 if stderr empty, -1 otherwise. */

static int _DoCommandToFileOrPipe(argv, inputfd, outputfd, bufP, lenP)
  char **argv;			/* The command to execute, and its args. */
  int inputfd;			/* Input stream for command. */
  int outputfd;			/* Output stream; /dev/null if == -1 */
  char **bufP;			/* output buffer ptr if outputfd == -2 */
  int *lenP;			/* output length ptr if outputfd == -2 */
{
    XtAppContext appCtx = XtWidgetToApplicationContext(toplevel);
    int return_status;
    int old_stdin, old_stdout, old_stderr;
    int pid;
    fd_set readfds, fds;
    Boolean output_to_pipe = False;
    CommandStatus status = XtNew(CommandStatusRec);
    FD_ZERO(&fds);
    FD_SET(ConnectionNumber(theDisplay), &fds);
    DEBUG1("Executing %s ...", argv[0])

    if (inputfd != -1) {
	old_stdin = dup(fileno(stdin));
	(void) dup2(inputfd, fileno(stdin));
	close(inputfd);
    }

    if (outputfd == -1) {
	if (!app_resources.debug) { /* Throw away stdout. */
	    outputfd = open( "/dev/null", O_WRONLY, 0 );
	}
    }
    else if (outputfd == -2) {	/* make pipe */
	if (pipe(status->output_pipe) /*failed*/) {
	    SystemError( "couldn't re-direct standard output" );
	    status->output_pipe[0]=0;
	}
	else {
	    outputfd = status->output_pipe[1];
	    FD_SET(status->output_pipe[0], &fds);
	    status->output_inputId =
		XtAppAddInput( appCtx,
			   status->output_pipe[0], (XtPointer)XtInputReadMask,
			   ReadStdout, (XtPointer)status
			     );
	    status->output_buffer = NULL;
	    status->output_buf_size = 0;
	    output_to_pipe = True;
	}
    }

    if (pipe(status->error_pipe) /*failed*/) {
	SystemError( "couldn't re-direct standard error" );
	status->error_pipe[0]=0;
    }
    else {
	old_stderr = dup(fileno(stderr));
	(void) dup2(status->error_pipe[1], fileno(stderr));
	close(status->error_pipe[1]);
	FD_SET(status->error_pipe[0], &fds);
	status->error_inputId =
	    XtAppAddInput( appCtx,
			   status->error_pipe[0], (XtPointer)XtInputReadMask,
			   ReadStderr, (XtPointer)status
			  );
    }
    if (outputfd != -1) {
	old_stdout = dup(fileno(stdout));
	(void) dup2(outputfd, fileno(stdout));
	close(outputfd);
    }
    childdone = FALSE;
    status->popup = (Widget)NULL;
    status->lastInput = lastInput;
    status->error_buffer = NULL;
    status->error_buf_size = 0;
    (void) signal(SIGCHLD, ChildDone);
    pid = vfork();
    if (inputfd != -1) {
	if (pid != 0) dup2(old_stdin,  fileno(stdin));
	close(old_stdin);
    }
    if (outputfd != -1) {
	if (pid != 0) dup2(old_stdout, fileno(stdout));
	close(old_stdout);
    }
    if (status->error_pipe[0]) {
	if (pid != 0) dup2(old_stderr, fileno(stderr));
	close(old_stderr);
    }

    if (pid == -1) Punt("Couldn't fork!");
    if (pid) {			/* We're the parent process. */
	XEvent typeAheadQueue[TYPEAHEADSIZE], *eventP = typeAheadQueue;
	XEvent *altQueue = NULL;
	int type_ahead_count = 0, alt_queue_size = 0, alt_queue_count = 0;
	XtAppContext app = XtWidgetToApplicationContext(toplevel);
	int num_fds = ConnectionNumber(theDisplay)+1;
	if (output_to_pipe && status->output_pipe[0] >= num_fds)
	    num_fds = status->output_pipe[0]+1;
	if (status->error_pipe[0] >= num_fds)
	    num_fds = status->error_pipe[0]+1;
	status->child_pid = pid;
	DEBUG1( " pid=%d ", pid )
	subProcessRunning = True;
	while (!childdone) {
	    while (!(XtAppPending(app) & XtIMXEvent)) {
		/* this is gross, but the only other way is by
		 * polling on timers or an extra pipe, since we're not
		 * guaranteed to be able to malloc in a signal handler.
		 */
		readfds = fds;
                if (childdone) break;
DEBUG("blocking.\n")
		(void) Select(num_fds, &readfds, NULL, NULL, NULL);
DEBUG1("unblocked; child%s done.\n", childdone ? "" : " not")
		if (childdone) break;
		if (!FD_ISSET(ConnectionNumber(theDisplay), &readfds))
{DEBUG("reading alternate input...")
		    XtAppProcessEvent(appCtx, (unsigned)XtIMAlternateInput);
DEBUG("read.\n")}
	    }
	    if (childdone) break;
	    XtAppNextEvent(app, eventP);
	    switch(eventP->type) {
	      case LeaveNotify:
		if (type_ahead_count) {
		    /* do compress_enterleave here to save memory */
		    XEvent *prevEvent;
		    if (alt_queue_size && (alt_queue_count == 0))
			prevEvent = &typeAheadQueue[type_ahead_count-1];
		    else
			prevEvent = eventP - 1;
		    if (prevEvent->type == EnterNotify
		      && prevEvent->xany.display == eventP->xany.display
		      && prevEvent->xany.window == eventP->xany.window) {
			eventP = prevEvent;
			if (alt_queue_count > 0)
			    alt_queue_count--;
			else
			    type_ahead_count--;
			break;
		    }
		}
		/* fall through */
	      case KeyPress:
	      case KeyRelease:
	      case EnterNotify:
	      case ButtonPress:
	      case ButtonRelease:
	      case MotionNotify:
		if (type_ahead_count < TYPEAHEADSIZE) {
		    if (++type_ahead_count == TYPEAHEADSIZE) {
			altQueue = (XEvent*)XtMalloc(
				(Cardinal)TYPEAHEADSIZE*sizeof(XEvent) );     
			alt_queue_size = TYPEAHEADSIZE;
			eventP = altQueue;
		    }
		    else
			eventP++;
		}
		else {
		    if (++alt_queue_count == alt_queue_size) {
			alt_queue_size += TYPEAHEADSIZE;
			altQueue = (XEvent*)XtRealloc(
				(char*)altQueue,
				(Cardinal)alt_queue_size*sizeof(XEvent) );
			eventP = &altQueue[alt_queue_count];
		    }
		    else
			eventP++;
		}
		break;

	      default:
		XtDispatchEvent(eventP);
	    }
	}
	(void) wait(0);

	DEBUG("done\n")
	subProcessRunning = False;
	if (output_to_pipe) {
	    CheckReadFromPipe( status->output_pipe[0],
			       &status->output_buffer,
			       &status->output_buf_size,
			       True
			      );
	    *bufP = status->output_buffer;
	    *lenP = status->output_buf_size;
	    close( status->output_pipe[0] );
	    XtRemoveInput( status->output_inputId );
	}
	if (status->error_pipe[0]) {
	    CheckReadFromPipe( status->error_pipe[0],
			       &status->error_buffer,
			       &status->error_buf_size,
			       True
			      );
	    close( status->error_pipe[0] );
	    XtRemoveInput( status->error_inputId );
	}
	if (status->error_buffer != NULL) {
	    /* special case for arbitrary shell commands: capture command */
	    if ((strcmp(argv[0], "/bin/sh") == 0) &&
	        (strcmp(argv[1], "-c") == 0)) {
	        status->shell_command = XtNewString(argv[2]);
            } else status->shell_command = (char*) NULL;
	
	    while (status->error_buffer[status->error_buf_size-1]  == '\0')
		status->error_buf_size--;
	    while (status->error_buffer[status->error_buf_size-1]  == '\n')
		status->error_buffer[--status->error_buf_size] = '\0';
	    DEBUG1( "stderr = \"%s\"\n", status->error_buffer )
	    PopupNotice( status->error_buffer, FreeStatus, (Pointer)status );
	    return_status = -1;
	}
	else {
	    XtFree( (Pointer)status );
	    return_status = 0;
	}
	for (;alt_queue_count;alt_queue_count--) {
	    XPutBackEvent(theDisplay, --eventP);
	}
	if (type_ahead_count) {
	    if (alt_queue_size) eventP = &typeAheadQueue[type_ahead_count];
	    for (;type_ahead_count;type_ahead_count--) {
		XPutBackEvent(theDisplay, --eventP);
	    }
	}
    } else {			/* We're the child process. */
	/* take it from the user's path, else fall back to the mhPath */
	(void) execvp(argv[0], argv);
	(void) execv(FullPathOfCommand(argv[0]), argv);
        progName = argv[0];	/* for Punt message */
	Punt("(cannot execvp it)");
	return_status = -1;
    }
    return return_status;
}


static void
CheckReadFromPipe( fd, bufP, lenP, waitEOF )
    int fd;
    char **bufP;
    int *lenP;
    Bool waitEOF;
{
    int nread;
/*  DEBUG2( " CheckReadFromPipe #%d,len=%d,", fd, *lenP )  */
#ifdef FIONREAD
    if (!ioctl( fd, FIONREAD, &nread )) {
/*      DEBUG1( "nread=%d ...", nread )			   */
	if (nread) {
	    int old_end = *lenP;
	    *bufP = XtRealloc( *bufP, (Cardinal) ((*lenP += nread) + 1) );
	    read( fd, *bufP+old_end, nread );
	    (*bufP)[old_end+nread] = '\0';
	}
	return;
    }
#endif
    do {
	char buf[512];
	int old_end = *lenP;
	nread = read( fd, buf, 512 );
	if (nread <= 0)
	    break;
	*bufP = XtRealloc( *bufP, (Cardinal) ((*lenP += nread) + 1) );
	memmove( *bufP+old_end, buf, (int) nread );
	(*bufP)[old_end+nread] = '\0';
    } while (waitEOF);
}


/* ARGSUSED */
static void FreeStatus( w, closure, call_data )
    Widget w;			/* unused */
    Pointer closure;
    Pointer call_data;		/* unused */
{
    CommandStatus status = (CommandStatus)closure;
    if (status->popup != (Widget)NULL) {
	XtPopdown( status->popup );
	XtDestroyWidget( status->popup );
    }
    if (status->error_buffer != NULL) XtFree(status->error_buffer);
    XtFree( closure );
}

/* Execute the given command, waiting until it's finished.  Put the output
   in the specified file path.  Returns 0 if stderr empty, -1 otherwise */

DoCommand(argv, inputfile, outputfile)
  char **argv;			/* The command to execute, and its args. */
  char *inputfile;		/* Input file for command. */
  char *outputfile;		/* Output file for command. */
{
    int fd_in, fd_out;
    int status;

    if (inputfile != NULL) {
	FILEPTR file = FOpenAndCheck(inputfile, "r");
	fd_in = dup(fileno(file));
	myfclose(file);
    }
    else
	fd_in = -1;

    if (outputfile) {
	FILEPTR file = FOpenAndCheck(outputfile, "w");
	fd_out = dup(fileno(file));
	myfclose(file);
    }
    else
	fd_out = -1;

    status = _DoCommandToFileOrPipe( argv, fd_in, fd_out, (char **) NULL,
				    (int *) NULL );
    return status;
}

/* Execute the given command, waiting until it's finished.  Put the output
   in a newly mallocced string, and return a pointer to that string. */

char *DoCommandToString(argv)
char ** argv;
{
    char *result = NULL;
    int len = 0;
    _DoCommandToFileOrPipe( argv, -1, -2, &result, &len );
    if (result == NULL) result = XtMalloc((Cardinal) 1);
    result[len] = '\0';
    DEBUG1("('%s')\n", result)
    return result;
}
    

/* Execute the command to a temporary file, and return the name of the file. */

char *DoCommandToFile(argv)
  char **argv;
{
    char *name;
    FILEPTR file;
    int fd;
    name = MakeNewTempFileName();
    file = FOpenAndCheck(name, "w");
    fd = dup(fileno(file));
    myfclose(file);
    _DoCommandToFileOrPipe(argv, -1, fd, (char **) NULL, (int *) NULL);
    return name;
}

