/*
 * bltBgexec.c --
 *
 *	This module implements a background "exec" command for the
 *	Tk toolkit.
 *
 * Copyright 1993-1994 by AT&T Bell Laboratories.
 * 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 the copyright notice and warranty
 * disclaimer appear in supporting documentation, and that the
 * names of AT&T Bell Laboratories any of their entities not be used
 * in advertising or publicity pertaining to distribution of the
 * software without specific, written prior permission.
 *
 * AT&T disclaims all warranties with regard to this software, including
 * all implied warranties of merchantability and fitness.  In no event
 * shall AT&T be liable for any special, indirect or consequential
 * damages or any damages whatsoever resulting from loss of use, data
 * or profits, whether in an action of contract, negligence or other
 * tortuous action, arising out of or in connection with the use or
 * performance of this software.
 *
 * bgexec command created by George Howlett.
 */

#include "blt.h"

#include <fcntl.h>
#include <signal.h>

#ifndef BGEXEC_VERSION
#define BGEXEC_VERSION "1.2"
#endif

#define BUFFER_SIZE	1000	/* Maximum number of bytes per read */
#define MAX_READS       100	/* Maximum number of successful reads before
			         * stopping to let Tk catch up on events */

typedef struct {
    char *storage;		/* Buffer to store command output (malloc-ed):
				 * Initially points to static storage */
    int used;			/* Number of characters read into the buffer */
    int size;			/* Size of buffer allocated */
    char staticSpace[BUFFER_SIZE * 2 + 1];	/* Static buffer space */

} Buffer;

typedef struct {
    Tcl_Interp *interp;		/* Interpreter containing variable */

    char *updateName;		/* Name of a Tcl variable (malloc'ed) to be
				 * updated when no more data is currently
				 * available for reading from the output pipe.
				 * It's appended with the contents of the
				 * current buffer (data which has arrived
				 * since the last idle point). If it's NULL,
				 * no updates are made */

    char *outputName;		/* Name of a Tcl variable (malloc'ed) to be
				 * set with the contents of stdout after the
				 * last UNIX subprocess has completed. Setting
				 * this variable triggers the termination of
				 * all subprocesses, regardless whether they
				 * have already completed or not */

    char *errorName;		/* Name of a Tcl variable (malloc'ed) to hold
				 * any available data from standard error */

    int outputId;		/* File descriptor for output pipe.  */
    int errorId;		/* File Descriptor for error file. */

    Buffer buffer;		/* Buffer storing subprocess' stdin/stderr */

    int numPids;		/* Number of processes created in pipeline */
    int *pidPtr;		/* Array of process Ids. */

    int keepFlag;		/* Indicates to set Tcl output variables with
				 * trailing newlines intact */
    int lastCount;		/* Number of bytes read the last time a
				 * buffer was retrieved */
    int fixMark;		/* Index of fixed newline character in buffer.
				 * If -1, no fix was made. */

} BackgroundInfo;

#ifndef sun
extern char *strdup _ANSI_ARGS_((CONST char *s));

#endif

/*
 *----------------------------------------------------------------------
 *
 * GetBuffer --
 *
 *	Returns the output currently saved in buffer storage
 *
 *----------------------------------------------------------------------
 */
static char *
GetBuffer(bufferPtr)
    Buffer *bufferPtr;
{
    bufferPtr->storage[bufferPtr->used] = '\0';
    return (bufferPtr->storage);
}

/*
 *----------------------------------------------------------------------
 *
 * InitBuffer --
 *
 *	Initializes the buffer storage, clearing any output that
 *	may have accumulated from previous usage.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Buffer storage is cleared.
 *
 *----------------------------------------------------------------------
 */
static void
InitBuffer(bufferPtr)
    Buffer *bufferPtr;
{
    bufferPtr->storage = bufferPtr->staticSpace;
    bufferPtr->size = BUFFER_SIZE * 2;
    bufferPtr->storage[0] = '\0';
    bufferPtr->used = 0;
}

/*
 *----------------------------------------------------------------------
 *
 * ResetBuffer --
 *
 *	Resets the buffer storage, freeing any malloc'ed space.
 *
 * Results:
 *	None.
 *
 *----------------------------------------------------------------------
 */
static void
ResetBuffer(bufferPtr)
    Buffer *bufferPtr;
{
    if (bufferPtr->storage != bufferPtr->staticSpace) {
	free(bufferPtr->storage);
    }
    InitBuffer(bufferPtr);
}

/*
 *----------------------------------------------------------------------
 *
 * GrowBuffer --
 *
 *	Doubles the size of the current buffer.
 *
 * Results:
 *	None.
 *
 *----------------------------------------------------------------------
 */
static int
GrowBuffer(bufferPtr)
    Buffer *bufferPtr;
{
    char *newPtr;

    /*
     * Allocate a new buffer, double the old size
     */

    bufferPtr->size += bufferPtr->size;
    newPtr = (char *)malloc(sizeof(char) * (bufferPtr->size + 1));
    if (newPtr == NULL) {
	return TCL_ERROR;
    }
    strcpy(newPtr, bufferPtr->storage);
    if (bufferPtr->storage != bufferPtr->staticSpace) {
	free((char *)bufferPtr->storage);
    }
    bufferPtr->storage = newPtr;
    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * AppendOutputToBuffer --
 *
 *	Appends any available data from a given file descriptor to the
*	buffer.
 *
 * Results:
 *	Returns TCL_OK when EOF is found, TCL_RETURN if reading
 *	data would block, and TCL_ERROR if an error occured.
 *
 *----------------------------------------------------------------------
 */
static int
AppendOutputToBuffer(f, bufferPtr)
    int f;
    Buffer *bufferPtr;
{
    int numBytes, bytesLeft;
    register int i;

    /*
     * ------------------------------------------------------------------
     *
     * Worry about indefinite postponement.
     *
     * Typically we want to stay in the read loop as long as it takes
     * to collect up all the data that's currently available.  But if it's
     * coming in at a constant high rate, we need to arbitrarily break out
     * at some point. This allows for both setting the output variable and
     * the Tk program to handle idle events.
     *
     * -------------------------------------------------------------------
     */

    for (i = 0; i < MAX_READS; i++) {

	/*
	 * Allocate a larger buffer when the number of remaining bytes
	 * is below a threshold (BUFFER_SIZE).
	 */

	bytesLeft = bufferPtr->size - bufferPtr->used;
	if (bytesLeft < BUFFER_SIZE) {
	    GrowBuffer(bufferPtr);
	    bytesLeft = bufferPtr->size - bufferPtr->used;
	}
	numBytes = read(f, bufferPtr->storage + bufferPtr->used,
	    bytesLeft);

	if (numBytes == 0) {	/* EOF: break out of loop. */
	    return TCL_OK;
	}
	if (numBytes < 0) {

	    /*
	     * Either an error has occurred or no more data is currently
	     * available to read.
	     */
#ifdef O_NONBLOCK
	    if (errno == EAGAIN) {
#else
	    if (errno == EWOULDBLOCK) {
#endif /*O_NONBLOCK*/
		break;
	    }
	    bufferPtr->storage[0] = '\0';
	    return TCL_ERROR;
	}
	bufferPtr->used += numBytes;
	bufferPtr->storage[bufferPtr->used] = '\0';
    }
    return TCL_RETURN;
}

/*
 *----------------------------------------------------------------------
 *
 * FixNewline --
 *
 *	Clips off the trailing newline in the buffer (if one exists).
 *	Saves the location in the buffer where the fix was made.
 *
 *----------------------------------------------------------------------
 */
static void
FixNewline(infoPtr)
    BackgroundInfo *infoPtr;
{
    Buffer *bufferPtr = &(infoPtr->buffer);

    infoPtr->fixMark = -1;
    if (bufferPtr->used > 0) {
	int mark = bufferPtr->used - 1;

	if (bufferPtr->storage[mark] == '\n') {
	    bufferPtr->storage[mark] = '\0';
	    infoPtr->fixMark = mark;
	}
    }
}

/*
 *----------------------------------------------------------------------
 *
 * UnfixNewline --
 *
 *	Restores the previously clipped newline in the buffer.
 *	The fixMark field indicates whether one was clipped.
 *
 *----------------------------------------------------------------------
 */
static void
UnfixNewline(infoPtr)
    BackgroundInfo *infoPtr;
{
    Buffer *bufferPtr = &(infoPtr->buffer);

    if (infoPtr->fixMark >= 0) {
	bufferPtr->storage[infoPtr->fixMark] = '\n';
	infoPtr->fixMark = -1;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * GetLastAppended --
 *
 *	Returns the output saved from the last time this routine
 *	was called.
 *
 *----------------------------------------------------------------------
 */
static char *
GetLastAppended(infoPtr)
    BackgroundInfo *infoPtr;
{
    Buffer *bufferPtr = &(infoPtr->buffer);
    char *string;

    bufferPtr->storage[bufferPtr->used] = '\0';
    string = bufferPtr->storage + infoPtr->lastCount;
    infoPtr->lastCount = bufferPtr->used;
    return (string);
}

/*
 *----------------------------------------------------------------------
 *
 * FakeOutputProc --
 *
 *	This procedure is called at the next idle point when no output
 *	is available from UNIX subprocesses because output has been
 *	redirected to a file (outputId is -1).  This procedure is used
 *	to break out of a possible "tkwait variable" command which
 *	may follow.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Variable is set, causing the a possible "tkwait" command
 *	to finish.
 *
 *----------------------------------------------------------------------
 */

 /* ARGSUSED */
static void
FakeOutputProc(clientData)
    ClientData clientData;	/* File output information. */
{
    BackgroundInfo *infoPtr = (BackgroundInfo *)clientData;

    Tcl_SetVar2(infoPtr->interp, infoPtr->outputName, (char *)NULL,
	GetBuffer(&(infoPtr->buffer)), TCL_GLOBAL_ONLY);

    if (infoPtr->updateName != NULL) {
	free(infoPtr->updateName);
    }
    if (infoPtr->outputName != NULL) {
	free(infoPtr->outputName);
    }
    if (infoPtr->errorName != NULL) {
	free(infoPtr->errorName);
    }
    if (infoPtr->pidPtr != NULL) {
	free((char *)infoPtr->pidPtr);
	Tcl_ReapDetachedProcs();
    }
    free((char *)infoPtr);
}

/*
 *----------------------------------------------------------------------
 *
 * CleanupProc --
 *
 *	This procedure cleans up the BackgroundInfo data structure
 *	associated with the detached subprocesses.  It is called
 *	when the variable associated with UNIX subprocesses has been
 *	overwritten.  This usually occurs when the subprocesses have
 *	completed or an error was detected.  However, it may be used
 *	to terminate the detached processes from the Tcl program by
 *	setting the associated variable.
 *
 * Results:
 *	Always returns NULL.
 *
 * Side effects:
 *	The output stream is closed, the variable trace is deleted,
 *	and memory allocated to the BackgroundInfo structure released.
 *	In addition, the subprocesses are signaled for termination.
 *
 *----------------------------------------------------------------------
 */

 /* ARGSUSED */
static char *
CleanupProc(clientData, interp, part1, part2, flags)
    ClientData clientData;	/* File output information. */
    Tcl_Interp *interp;
    char *part1, *part2;
    int flags;
{
    BackgroundInfo *infoPtr = (BackgroundInfo *)clientData;

    if (!(flags & (TCL_TRACE_WRITES | TCL_GLOBAL_ONLY))) {
	return NULL;
    }
    close(infoPtr->outputId);

    if (infoPtr->errorId >= 0) {

	/*
	 * If an error variable needs to be set, reset the error file
	 * descriptor and read the captured stderr from the temporary file
	 */

	if ((infoPtr->errorName != NULL) &&
	    (lseek(infoPtr->errorId, 0L, 0) >= 0)) {
	    int result;

	    ResetBuffer(&(infoPtr->buffer));
	    do {
		result = AppendOutputToBuffer(infoPtr->errorId,
		    &(infoPtr->buffer));
	    } while (result == TCL_RETURN);

	    if (result == TCL_OK) {
		if (!infoPtr->keepFlag) {
		    FixNewline(infoPtr);
		}
		Tcl_SetVar2(infoPtr->interp, infoPtr->errorName, (char *)NULL,
		    GetBuffer(&(infoPtr->buffer)), TCL_GLOBAL_ONLY);
	    } else if (result == TCL_ERROR) {
		Tcl_AppendResult(infoPtr->interp, "error appending buffer: ",
		    Tcl_PosixError(infoPtr->interp), (char *)NULL);
		Tk_BackgroundError(infoPtr->interp);
	    }
	}
	close(infoPtr->errorId);
    }
    Tk_DeleteFileHandler(infoPtr->outputId);
    Tcl_UntraceVar2(interp, part1, part2, flags, CleanupProc, clientData);

    if (infoPtr->updateName != NULL) {
	free(infoPtr->updateName);
    }
    if (infoPtr->errorName != NULL) {
	free(infoPtr->errorName);
    }
    if (infoPtr->outputName != NULL) {
	free(infoPtr->outputName);
    }
    ResetBuffer(&(infoPtr->buffer));

    if (infoPtr->pidPtr != NULL) {
	register int i;

	for (i = 0; i < infoPtr->numPids; i++) {
	    kill(infoPtr->pidPtr[i], 1);
	}
	free((char *)infoPtr->pidPtr);
	Tcl_ReapDetachedProcs();
    }
    free((char *)infoPtr);
    return NULL;
}

/*
 *----------------------------------------------------------------------
 *
 * BackgroundProc --
 *
 *	This procedure is called when output from the detached command
 *	is available.  The output is read and saved in a buffer in
 *	the BackgroundInfo structure.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Data is stored in infoPtr->buffer.  This character array may
 *	be increased as more space is required to contain the output
 *	of the command.
 *
 *----------------------------------------------------------------------
 */

 /* ARGSUSED */
static void
BackgroundProc(clientData, mask)
    ClientData clientData;	/* File output information. */
    int mask;			/* Not used. */
{
    BackgroundInfo *infoPtr = (BackgroundInfo *)clientData;
    int result;

    result = AppendOutputToBuffer(infoPtr->outputId, &(infoPtr->buffer));
    if (result == TCL_RETURN) {
	if (infoPtr->updateName != NULL) {
	    if (!infoPtr->keepFlag) {
		FixNewline(infoPtr);
	    }
	    Tcl_SetVar2(infoPtr->interp, infoPtr->updateName, (char *)NULL,
		GetLastAppended(infoPtr), (TCL_GLOBAL_ONLY | TCL_APPEND_VALUE));
	    if (!infoPtr->keepFlag) {
		UnfixNewline(infoPtr);
	    }
	}
	return;
    }
    if (result == TCL_ERROR) {
	Tcl_AppendResult(infoPtr->interp, "error appending buffer: ",
	    Tcl_PosixError(infoPtr->interp), (char *)NULL);
	Tk_BackgroundError(infoPtr->interp);
    }
    /*
     * We're here if we've seen EOF or an error has occurred.
     * In either case, set the variable to trigger the cleanup procedure.
     */
    if (!infoPtr->keepFlag) {
	FixNewline(infoPtr);
    }
    if (infoPtr->updateName != NULL) {
	Tcl_SetVar2(infoPtr->interp, infoPtr->updateName, (char *)NULL,
	    GetLastAppended(infoPtr), (TCL_GLOBAL_ONLY | TCL_APPEND_VALUE));
    }
    Tcl_SetVar2(infoPtr->interp, infoPtr->outputName, (char *)NULL,
	GetBuffer(&(infoPtr->buffer)), TCL_GLOBAL_ONLY);

}

/*
 *----------------------------------------------------------------------
 *
 * Blt_BgExecCmd --
 *
 *	This procedure is invoked to process the "bgexec" Tcl command.
 *	See the user documentation for details on what it does.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	See the user documentation.
 *
 *----------------------------------------------------------------------
 */

 /* ARGSUSED */
static int
BgExecCmd(clientData, interp, argc, argv)
    ClientData clientData;	/* Not used. */
    Tcl_Interp *interp;		/* Current interpreter. */
    int argc;			/* Number of arguments. */
    char **argv;		/* Argument strings. */
{
    int outputId;		/* File id for output pipe.  -1
				 * means command overrode. */
    int errorId = -1;
    int *errFilePtr;
    int *pidPtr;
    int numPids;
    BackgroundInfo *infoPtr;
    register int i;
    int parseSwitches;

    if (argc < 3) {
	Tcl_AppendResult(interp, "wrong # args: should be \"", argv[0],
	    " ?switches? varName command args", (char *)NULL);
	return TCL_ERROR;
    }
    infoPtr = (BackgroundInfo *)malloc(sizeof(BackgroundInfo));
    if (infoPtr == NULL) {
	interp->result = "can't allocate file info structure";
	return TCL_ERROR;
    }
    infoPtr->interp = interp;
    infoPtr->keepFlag = 0;
    infoPtr->errorName = infoPtr->updateName = infoPtr->outputName = NULL;
    InitBuffer(&(infoPtr->buffer));

    parseSwitches = 1;
    errFilePtr = NULL;		/* By default, stderr goes to the tty */

    for (i = 1; i < argc; i++) {
	if ((parseSwitches) && (argv[i][0] == '-')) {
	    int length = strlen(argv[i]);
	    char c = argv[i][1];

	    if ((c == 'u') && (strncmp(argv[i], "-updatevar", length) == 0)) {
		i++;
		if (i == argc) {
		    Tcl_AppendResult(interp, "no variable for \"-updatevar\"",
			(char *)NULL);
		    goto error;
		}
		infoPtr->updateName = strdup(argv[i]);
	    } else if ((c == 'e') &&
		(strncmp(argv[i], "-errorvar", length) == 0)) {
		i++;
		if (i == argc) {
		    Tcl_AppendResult(interp, "no variable for \"-errorvar\"",
			(char *)NULL);
		    goto error;
		}
		infoPtr->errorName = strdup(argv[i]);
		errFilePtr = &errorId;
	    } else if ((c == 'o') &&
		(strncmp(argv[i], "-outputvar", length) == 0)) {
		i++;
		if (i == argc) {
		    Tcl_AppendResult(interp, "no variable for \"-outputvar\"",
			(char *)NULL);
		    goto error;
		}
		infoPtr->outputName = strdup(argv[i]);
	    } else if ((c == 'k') &&
		(strncmp(argv[i], "-keepnewline", length) == 0)) {
		infoPtr->keepFlag = 1;
	    } else if ((c == '-') && (argv[i][2] == '\0')) {
		parseSwitches = 0;
	    } else {
		Tcl_AppendResult(interp, "bad switch \"", argv[i], "\": ",
		    "should be -errorvar, -keepnewline, -outputvar, ",
		    "-updatevar, or --", (char *)NULL);
		goto error;
	    }
	} else {
	    if (infoPtr->outputName == NULL) {
		infoPtr->outputName = strdup(argv[i++]);
	    }
	    break;
	}
    }
    if ((infoPtr->outputName == NULL) || (argc == i)) {
	Tcl_AppendResult(interp, "missing command: should be \"", argv[0],
	    " ?switches? varName command ?args?\"", (char *)NULL);
	goto error;
    }
    numPids = Tcl_CreatePipeline(interp, argc - i, argv + i, &pidPtr,
	(int *)NULL, &outputId, errFilePtr);
    if (numPids < 0) {
	goto error;
    }
    infoPtr->outputId = outputId;
    infoPtr->errorId = errorId;
    infoPtr->numPids = numPids;
    infoPtr->pidPtr = pidPtr;
    infoPtr->lastCount = 0;
    infoPtr->fixMark = -1;
    Tcl_DetachPids(numPids, pidPtr);

    /*
     * If output has been redirected, fake an update of the variable,
     * by arranging for a procedure to modify the variable at the next
     * idle point.
     */

    if (outputId == -1) {
	Tk_DoWhenIdle(FakeOutputProc, (ClientData)infoPtr);
    } else {

	/* Make the output on the descriptor non-blocking */

#ifdef O_NONBLOCK
	fcntl(outputId, F_SETFL, O_NONBLOCK);
#else
	fcntl(outputId, F_SETFL, O_NDELAY);
#endif
	/*
	 * Create a file handler for the output stream and put a
	 * trace on the variable.
	 */

	Tk_CreateFileHandler(outputId, TK_READABLE, BackgroundProc,
	    (ClientData)infoPtr);
	Tcl_TraceVar2(interp, infoPtr->outputName, (char *)NULL,
	    (TCL_TRACE_WRITES | TCL_GLOBAL_ONLY), CleanupProc,
	    (ClientData)infoPtr);
    }

    return TCL_OK;
  error:
    if (infoPtr != NULL) {
	free((char *)infoPtr);
    }
    return TCL_ERROR;
}

/*
 *----------------------------------------------------------------------
 *
 * Blt_BgExecInit --
 *
 *	This procedure is invoked to initialize the "bgexec" Tcl command.
 *	See the user documentation for details on what it does.
 *
 * Results:
 *	Nothing.
 *
 * Side effects:
 *	See the user documentation.
 *
 *----------------------------------------------------------------------
 */
int
Blt_BgExecInit(interp)
    Tcl_Interp *interp;
{
    Tk_Window tkwin;

    if (Blt_FindCmd(interp, "blt_bgexec", (ClientData *)NULL) == TCL_OK) {
	Tcl_AppendResult(interp, "\"blt_bgexec\" command already exists",
	    (char *)NULL);
	return TCL_ERROR;
    }
    tkwin = Tk_MainWindow(interp);
    if (tkwin == NULL) {
	Tcl_AppendResult(interp, "\"blt_bgexec\" requires Tk", (char *)NULL);
	return TCL_ERROR;
    }
    Tcl_SetVar2(interp, "blt_versions", "blt_bgexec", BGEXEC_VERSION,
	TCL_GLOBAL_ONLY);
    Tcl_CreateCommand(interp, "blt_bgexec", BgExecCmd, (ClientData)tkwin,
	(Tcl_CmdDeleteProc *)NULL);
    return TCL_OK;
}
