/* 
 * tclUnixUtil.c --
 *
 *	This file contains a collection of utility procedures that
 *	are present in the Tcl's UNIX core but not in the generic
 *	core.  For example, they do file manipulation and process
 *	manipulation.
 *
 *	Parts of this file are based on code contributed by Karl
 *	Lehenbauer, Mark Diekhans and Peter da Silva.
 *
 * Copyright (c) 1991-1994 The Regents of the University of California.
 * Copyright (c) 1994 Sun Microsystems, Inc.
 *
 * See the file "license.terms" for information on usage and redistribution
 * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
 */

#ifndef lint
static char sccsid[] = "@(#) tclUnixUtil.c 1.54 95/01/06 08:28:03";
#endif /* not lint */

#include "tclInt.h"
#include "tclPort.h"

/*
 * A linked list of the following structures is used to keep track
 * of child processes that have been detached but haven't exited
 * yet, so we can make sure that they're properly "reaped" (officially
 * waited for) and don't lie around as zombies cluttering the
 * system.
 */

typedef struct Detached {
    int pid;				/* Id of process that's been detached
					 * but isn't known to have exited. */
    struct Detached *nextPtr;		/* Next in list of all detached
					 * processes. */
} Detached;

static Detached *detList = NULL;	/* List of all detached proceses. */

/*
 * The following variables are used to keep track of all the open files
 * in the process.  These files can be shared across interpreters, so the
 * information can't be put in the Interp structure.
 */

int tclNumFiles = 0;		/* Number of entries in tclOpenFiles below.
				 * 0 means array hasn't been created yet. */
OpenFile **tclOpenFiles;	/* Pointer to malloc-ed array of pointers
				 * to information about open files.  Entry
				 * N corresponds to the file with fileno N.
				 * If an entry is NULL then the corresponding
				 * file isn't open.  If tclOpenFiles is NULL
				 * it means no files have been used, so even
				 * stdin/stdout/stderr entries haven't been
				 * setup yet. */

/*
 * Declarations for local procedures defined in this file:
 */

static int		FileForRedirect _ANSI_ARGS_((Tcl_Interp *interp,
			    char *spec, int atOk, char *arg, int flags,
			    char *nextArg, int *skipPtr, int *closePtr));
static void		MakeFileTable _ANSI_ARGS_((Interp *iPtr, int index));
static void		RestoreSignals _ANSI_ARGS_((void));

/*
 *----------------------------------------------------------------------
 *
 * Tcl_EvalFile --
 *
 *	Read in a file and process the entire file as one gigantic
 *	Tcl command.
 *
 * Results:
 *	A standard Tcl result, which is either the result of executing
 *	the file or an error indicating why the file couldn't be read.
 *
 * Side effects:
 *	Depends on the commands in the file.
 *
 *----------------------------------------------------------------------
 */

int
Tcl_EvalFile(interp, fileName)
    Tcl_Interp *interp;		/* Interpreter in which to process file. */
    char *fileName;		/* Name of file to process.  Tilde-substitution
				 * will be performed on this name. */
{
    int fileId, result;
    struct stat statBuf;
    char *cmdBuffer, *oldScriptFile;
    Interp *iPtr = (Interp *) interp;
    Tcl_DString buffer;

    Tcl_ResetResult(interp);
    oldScriptFile = iPtr->scriptFile;
    iPtr->scriptFile = fileName;
    fileName = Tcl_TildeSubst(interp, fileName, &buffer);
    if (fileName == NULL) {
	goto error;
    }
    fileId = open(fileName, O_RDONLY, 0);
    if (fileId < 0) {
	Tcl_AppendResult(interp, "couldn't read file \"", fileName,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	goto error;
    }
    if (fstat(fileId, &statBuf) == -1) {
	Tcl_AppendResult(interp, "couldn't stat file \"", fileName,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	close(fileId);
	goto error;
    }
    cmdBuffer = (char *) ckalloc((unsigned) statBuf.st_size+1);
    if (read(fileId, cmdBuffer, (size_t) statBuf.st_size) != statBuf.st_size) {
	Tcl_AppendResult(interp, "error in reading file \"", fileName,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	close(fileId);
	ckfree(cmdBuffer);
	goto error;
    }
    if (close(fileId) != 0) {
	Tcl_AppendResult(interp, "error closing file \"", fileName,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	ckfree(cmdBuffer);
	goto error;
    }
    cmdBuffer[statBuf.st_size] = 0;
    result = Tcl_Eval(interp, cmdBuffer);
    if (result == TCL_RETURN) {
	result = TCL_OK;
    }
    if (result == TCL_ERROR) {
	char msg[200];

	/*
	 * Record information telling where the error occurred.
	 */

	sprintf(msg, "\n    (file \"%.150s\" line %d)", fileName,
		interp->errorLine);
	Tcl_AddErrorInfo(interp, msg);
    }
    ckfree(cmdBuffer);
    iPtr->scriptFile = oldScriptFile;
    Tcl_DStringFree(&buffer);
    return result;

    error:
    iPtr->scriptFile = oldScriptFile;
    Tcl_DStringFree(&buffer);
    return TCL_ERROR;
}

/*
 *----------------------------------------------------------------------
 *
 * Tcl_DetachPids --
 *
 *	This procedure is called to indicate that one or more child
 *	processes have been placed in background and will never be
 *	waited for;  they should eventually be reaped by
 *	Tcl_ReapDetachedProcs.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

void
Tcl_DetachPids(numPids, pidPtr)
    int numPids;		/* Number of pids to detach:  gives size
				 * of array pointed to by pidPtr. */
    int *pidPtr;		/* Array of pids to detach. */
{
    register Detached *detPtr;
    int i;

    for (i = 0; i < numPids; i++) {
	detPtr = (Detached *) ckalloc(sizeof(Detached));
	detPtr->pid = pidPtr[i];
	detPtr->nextPtr = detList;
	detList = detPtr;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * Tcl_ReapDetachedProcs --
 *
 *	This procedure checks to see if any detached processes have
 *	exited and, if so, it "reaps" them by officially waiting on
 *	them.  It should be called "occasionally" to make sure that
 *	all detached processes are eventually reaped.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Processes are waited on, so that they can be reaped by the
 *	system.
 *
 *----------------------------------------------------------------------
 */

void
Tcl_ReapDetachedProcs()
{
    register Detached *detPtr;
    Detached *nextPtr, *prevPtr;
    int status, result;

    for (detPtr = detList, prevPtr = NULL; detPtr != NULL; ) {
	result = waitpid(detPtr->pid, &status, WNOHANG);
	if ((result == 0) || ((result == -1) && (errno != ECHILD))) {
	    prevPtr = detPtr;
	    detPtr = detPtr->nextPtr;
	    continue;
	}
	nextPtr = detPtr->nextPtr;
	if (prevPtr == NULL) {
	    detList = detPtr->nextPtr;
	} else {
	    prevPtr->nextPtr = detPtr->nextPtr;
	}
	ckfree((char *) detPtr);
	detPtr = nextPtr;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * Tcl_CreatePipeline --
 *
 *	Given an argc/argv array, instantiate a pipeline of processes
 *	as described by the argv.
 *
 * Results:
 *	The return value is a count of the number of new processes
 *	created, or -1 if an error occurred while creating the pipeline.
 *	*pidArrayPtr is filled in with the address of a dynamically
 *	allocated array giving the ids of all of the processes.  It
 *	is up to the caller to free this array when it isn't needed
 *	anymore.  If inPipePtr is non-NULL, *inPipePtr is filled in
 *	with the file id for the input pipe for the pipeline (if any):
 *	the caller must eventually close this file.  If outPipePtr
 *	isn't NULL, then *outPipePtr is filled in with the file id
 *	for the output pipe from the pipeline:  the caller must close
 *	this file.  If errFilePtr isn't NULL, then *errFilePtr is filled
 *	with a file id that may be used to read error output after the
 *	pipeline completes.
 *
 * Side effects:
 *	Processes and pipes are created.
 *
 *----------------------------------------------------------------------
 */

int
Tcl_CreatePipeline(interp, argc, argv, pidArrayPtr, inPipePtr,
	outPipePtr, errFilePtr)
    Tcl_Interp *interp;		/* Interpreter to use for error reporting. */
    int argc;			/* Number of entries in argv. */
    char **argv;		/* Array of strings describing commands in
				 * pipeline plus I/O redirection with <,
				 * <<,  >, etc.  Argv[argc] must be NULL. */
    int **pidArrayPtr;		/* Word at *pidArrayPtr gets filled in with
				 * address of array of pids for processes
				 * in pipeline (first pid is first process
				 * in pipeline). */
    int *inPipePtr;		/* If non-NULL, input to the pipeline comes
				 * from a pipe (unless overridden by
				 * redirection in the command).  The file
				 * id with which to write to this pipe is
				 * stored at *inPipePtr.  -1 means command
				 * specified its own input source. */
    int *outPipePtr;		/* If non-NULL, output to the pipeline goes
				 * to a pipe, unless overriden by redirection
				 * in the command.  The file id with which to
				 * read frome this pipe is stored at
				 * *outPipePtr.  -1 means command specified
				 * its own output sink. */
    int *errFilePtr;		/* If non-NULL, all stderr output from the
				 * pipeline will go to a temporary file
				 * created here, and a descriptor to read
				 * the file will be left at *errFilePtr.
				 * The file will be removed already, so
				 * closing this descriptor will be the end
				 * of the file.  If this is NULL, then
				 * all stderr output goes to our stderr.
				 * If the pipeline specifies redirection
				 * then the fill will still be created
				 * but it will never get any data. */
{
    int *pidPtr = NULL;		/* Points to malloc-ed array holding all
				 * the pids of child processes. */
    int numPids = 0;		/* Actual number of processes that exist
				 * at *pidPtr right now. */
    int cmdCount;		/* Count of number of distinct commands
				 * found in argc/argv. */
    char *input = NULL;		/* If non-null, then this points to a
				 * string containing input data (specified
				 * via <<) to be piped to the first process
				 * in the pipeline. */
    int inputId = -1;		/* If >= 0, gives file id to use as input for
				 * first process in pipeline (specified via
				 * < or <@). */
    int closeInput = 0;		/* If non-zero, then must close inputId
				 * when cleaning up (zero means the file needs
				 * to stay open for some other reason). */
    int outputId = -1;		/* Writable file id for output from last
				 * command in pipeline (could be file or pipe).
				 * -1 means use stdout. */
    int closeOutput = 0;	/* Non-zero means must close outputId when
				 * cleaning up (similar to closeInput). */
    int errorId = -1;		/* Writable file id for error output from
				 * all commands in pipeline. -1 means use
				 * stderr. */
    int closeError = 0;		/* Non-zero means must close errorId when
				 * cleaning up. */
    int pipeIds[2];		/* File ids for pipe that's being created. */
    int firstArg, lastArg;	/* Indexes of first and last arguments in
				 * current command. */
    int skip;			/* Number of arguments to skip (because they
				 * specify redirection). */
    int maxFd;			/* Highest known file descriptor (used to
				 * close off extraneous file descriptors in
				 * child process). */
    int errPipeIds[2];		/* Used for communication between parent and
				 * child processes.  If child encounters
				 * error during startup it returns error
				 * message via pipe.  If child starts up
				 * OK, it closes pipe without anything in
				 * it. */
    int lastBar;
    char *execName;
    int i, j, pid, count;
    char *p;
    Tcl_DString buffer;
    char errSpace[200];

    if (inPipePtr != NULL) {
	*inPipePtr = -1;
    }
    if (outPipePtr != NULL) {
	*outPipePtr = -1;
    }
    if (errFilePtr != NULL) {
	*errFilePtr = -1;
    }
    pipeIds[0] = pipeIds[1] = -1;
    errPipeIds[0] = errPipeIds[1] = -1;

    /*
     * First, scan through all the arguments to figure out the structure
     * of the pipeline.  Process all of the input and output redirection
     * arguments and remove them from the argument list in the pipeline.
     * Count the number of distinct processes (it's the number of "|"
     * arguments plus one) but don't remove the "|" arguments.
     */

    cmdCount = 1;
    lastBar = -1;
    for (i = 0; i < argc; i++) {
	if ((argv[i][0] == '|') && (((argv[i][1] == 0))
		|| ((argv[i][1] == '&') && (argv[i][2] == 0)))) {
	    if ((i == (lastBar+1)) || (i == (argc-1))) {
		interp->result = "illegal use of | or |& in command";
		return -1;
	    }
	    lastBar = i;
	    cmdCount++;
	    continue;
	} else if (argv[i][0] == '<') {
	    if ((inputId >= 0) && closeInput) {
		close(inputId);
	    }
	    inputId = -1;
	    skip = 1;
	    if (argv[i][1] == '<') {
		input = argv[i]+2;
		if (*input == 0) {
		    input = argv[i+1];
		    if (input == 0) {
			Tcl_AppendResult(interp, "can't specify \"", argv[i],
				"\" as last word in command", (char *) NULL);
			goto error;
		    }
		    skip = 2;
		}
	    } else {
		input = 0;
		inputId = FileForRedirect(interp, argv[i]+1, 1, argv[i],
			O_RDONLY, argv[i+1], &skip, &closeInput);
		if (inputId < 0) {
		    goto error;
		}
	    }
	} else if (argv[i][0] == '>') {
	    int append, useForStdErr, useForStdOut, mustClose, fd, atOk, flags;

	    skip = atOk = 1;
	    append = useForStdErr = 0;
	    useForStdOut = 1;
	    if (argv[i][1] == '>') {
		p = argv[i] + 2;
		append = 1;
		atOk = 0;
		flags = O_WRONLY|O_CREAT;
	    } else {
		p = argv[i] + 1;
		flags = O_WRONLY|O_CREAT|O_TRUNC;
	    }
	    if (*p == '&') {
		useForStdErr = 1;
		p++;
	    }
	    fd = FileForRedirect(interp, p, atOk, argv[i], flags, argv[i+1],
		    &skip, &mustClose);
	    if (fd < 0) {
		goto error;
	    }
	    if (append) {
		lseek(fd, 0L, 2);
	    }

	    /*
	     * Got the file descriptor.  Now use it for standard output,
	     * standard error, or both, depending on the redirection.
	     */

	    if (useForStdOut) {
		if ((outputId > 0) && closeOutput) {
		    close(outputId);
		}
		outputId = fd;
		closeOutput = mustClose;
	    }
	    if (useForStdErr) {
		if ((errorId > 0) && closeError) {
		    close(errorId);
		}
		errorId = fd;
		closeError = (useForStdOut) ? 0 : mustClose;
	    }
	} else if ((argv[i][0] == '2') && (argv[i][1] == '>')) {
	    int append, atOk, flags;

	    if ((errorId > 0) && closeError) {
		close(errorId);
	    }
	    skip = 1;
	    p = argv[i] + 2;
	    if (*p == '>') {
		p++;
		append = 1;
		atOk = 0;
		flags = O_WRONLY|O_CREAT;
	    } else {
		append = 0;
		atOk = 1;
		flags = O_WRONLY|O_CREAT|O_TRUNC;
	    }
	    errorId = FileForRedirect(interp, p, atOk, argv[i], flags,
		    argv[i+1], &skip, &closeError);
	    if (errorId < 0) {
		goto error;
	    }
	    if (append) {
		lseek(errorId, 0L, 2);
	    }
	} else {
	    continue;
	}
	for (j = i+skip; j < argc; j++) {
	    argv[j-skip] = argv[j];
	}
	argc -= skip;
	i -= 1;			/* Process next arg from same position. */
    }
    if (argc == 0) {
	interp->result =  "didn't specify command to execute";
	return -1;
    }

    if (inputId < 0) {
	if (input != NULL) {
	    char inName[L_tmpnam];
	    int length;

	    /*
	     * The input for the first process is immediate data coming from
	     * Tcl.  Create a temporary file for it and put the data into the
	     * file.
	     */

	    tmpnam(inName);
	    inputId = open(inName, O_RDWR|O_CREAT|O_TRUNC, 0600);
	    closeInput = 1;
	    if (inputId < 0) {
		Tcl_AppendResult(interp,
			"couldn't create input file \"", inName,
			"\" for command: ", Tcl_PosixError(interp),
			(char *) NULL);
		goto error;
	    }
	    length = strlen(input);
	    if (write(inputId, input, (size_t) length) != length) {
		Tcl_AppendResult(interp,
			"couldn't write file input for command: ",
			Tcl_PosixError(interp), (char *) NULL);
		goto error;
	    }
	    if ((lseek(inputId, 0L, 0) == -1) || (unlink(inName) == -1)) {
		Tcl_AppendResult(interp,
			"couldn't reset or remove input file for command: ",
			Tcl_PosixError(interp), (char *) NULL);
		goto error;
	    }
	} else if (inPipePtr != NULL) {
	    /*
	     * The input for the first process in the pipeline is to
	     * come from a pipe that can be written from this end.
	     */

	    if (pipe(pipeIds) != 0) {
		Tcl_AppendResult(interp,
			"couldn't create input pipe for command: ",
			Tcl_PosixError(interp), (char *) NULL);
		goto error;
	    }
	    inputId = pipeIds[0];
	    closeInput = 1;
	    *inPipePtr = pipeIds[1];
	    pipeIds[0] = pipeIds[1] = -1;
	}
    }

    /*
     * Set up a pipe to receive output from the pipeline, if no other
     * output sink has been specified.
     */

    if ((outputId < 0) && (outPipePtr != NULL)) {
	if (pipe(pipeIds) != 0) {
	    Tcl_AppendResult(interp,
		    "couldn't create output pipe: ",
		    Tcl_PosixError(interp), (char *) NULL);
	    goto error;
	}
	outputId = pipeIds[1];
	closeOutput = 1;
	*outPipePtr = pipeIds[0];
	pipeIds[0] = pipeIds[1] = -1;
    }

    /*
     * Set up the standard error output sink for the pipeline, if
     * requested.  Use a temporary file which is opened, then deleted.
     * Could potentially just use pipe, but if it filled up it could
     * cause the pipeline to deadlock:  we'd be waiting for processes
     * to complete before reading stderr, and processes couldn't complete
     * because stderr was backed up.
     */

    if (errFilePtr != NULL) {
	char errName[L_tmpnam];

	tmpnam(errName);
	*errFilePtr = open(errName, O_RDONLY|O_CREAT|O_TRUNC, 0600);
	if (*errFilePtr < 0) {
	    errFileError:
	    Tcl_AppendResult(interp,
		    "couldn't create error file \"", errName,
		    "\" for command: ", Tcl_PosixError(interp),
		    (char *) NULL);
	    goto error;
	}
	if (errorId < 0) {
	    errorId = open(errName, O_WRONLY|O_CREAT|O_TRUNC, 0600);
	    if (errorId < 0) {
		goto errFileError;
	    }
	    closeError = 1;
	}
	if (unlink(errName) == -1) {
	    Tcl_AppendResult(interp,
		    "couldn't remove error file for command: ",
		    Tcl_PosixError(interp), (char *) NULL);
	    goto error;
	}
    }

    /*
     * Find the largest file descriptor used so far, so that we can
     * clean up all the extraneous file descriptors in the child
     * processes we create.
     */

    maxFd = inputId;
    if (outputId > maxFd) {
	maxFd = outputId;
    }
    if (errorId > maxFd) {
	maxFd = errorId;
    }
    if ((inPipePtr != NULL) && (*inPipePtr > maxFd)) {
	maxFd = *inPipePtr;
    }
    if ((outPipePtr != NULL) && (*outPipePtr > maxFd)) {
	maxFd = *outPipePtr;
    }
    if ((errFilePtr != NULL) && (*errFilePtr > maxFd)) {
	maxFd = *errFilePtr;
    }

    /*
     * Scan through the argc array, forking off a process for each
     * group of arguments between "|" arguments.
     */

    pidPtr = (int *) ckalloc((unsigned) (cmdCount * sizeof(int)));
    for (i = 0; i < numPids; i++) {
	pidPtr[i] = -1;
    }
    Tcl_ReapDetachedProcs();
    for (firstArg = 0; firstArg < argc; numPids++, firstArg = lastArg+1) {
	int joinThisError;
	int curOutputId;

	joinThisError = 0;
	for (lastArg = firstArg; lastArg < argc; lastArg++) {
	    if (argv[lastArg][0] == '|') {
		if (argv[lastArg][1] == 0) {
		    break;
		}
		if ((argv[lastArg][1] == '&') && (argv[lastArg][2] == 0)) {
		    joinThisError = 1;
		    break;
		}
	    }
	}
	argv[lastArg] = NULL;
	if (lastArg == argc) {
	    curOutputId = outputId;
	} else {
	    if (pipe(pipeIds) != 0) {
		Tcl_AppendResult(interp, "couldn't create pipe: ",
			Tcl_PosixError(interp), (char *) NULL);
		goto error;
	    }
	    curOutputId = pipeIds[1];
	    if (pipeIds[0] > maxFd) {
		maxFd = pipeIds[0];
	    }
	    if (pipeIds[1] > maxFd) {
		maxFd = pipeIds[1];
	    }
	}

	/*
	 * Create a pipe that the child can use to return error
	 * information if anything goes wrong.  Set the close-on-exec
	 * flag for the write end of the pipe so that it will be
	 * closed automatically if the child succesfully execs the
	 * new subprocess.
	 */

	if (pipe(errPipeIds) != 0) {
	    Tcl_AppendResult(interp, "couldn't create pipe: ",
		    Tcl_PosixError(interp), (char *) NULL);
	    goto error;
	}
	if (errPipeIds[0] > maxFd) {
	    maxFd = errPipeIds[0];
	}
	if (errPipeIds[1] > maxFd) {
	    maxFd = errPipeIds[1];
	}
	if (fcntl(errPipeIds[1], F_SETFD, FD_CLOEXEC) != 0) {
	    Tcl_AppendResult(interp, "couldn't set close on exec for pipe: ",
		    Tcl_PosixError(interp), (char *) NULL);
	    goto error;
	}
	execName = Tcl_TildeSubst(interp, argv[firstArg], &buffer);
	pid = vfork();
	if (pid == 0) {
	    if (((inputId != -1) && (dup2(inputId, 0) == -1))
		    || ((curOutputId != -1) && (dup2(curOutputId, 1) == -1))
		    || (joinThisError && (dup2(1, 2) == -1))
		    || (!joinThisError && (errorId != -1)
			    && (dup2(errorId, 2) == -1))) {
		sprintf(errSpace,
			"%dforked process couldn't set up input/output: ",
			errno);
		write(errPipeIds[1], errSpace, (size_t) strlen(errSpace));
		_exit(1);
	    }
	    for (i = 3; i <= maxFd; i++) {
		if (i != errPipeIds[1]) {
		    close(i);
		}
	    }
	    RestoreSignals();
	    execvp(execName, &argv[firstArg]);
	    sprintf(errSpace, "%dcouldn't execute \"%.150s\": ", errno,
		    argv[firstArg]);
	    write(errPipeIds[1], errSpace, (size_t) strlen(errSpace));
	    _exit(1);
	}
	Tcl_DStringFree(&buffer);
	if (pid == -1) {
	    Tcl_AppendResult(interp, "couldn't fork child process: ",
		    Tcl_PosixError(interp), (char *) NULL);
	    goto error;
	}

	/*
	 * Read back from the error pipe to see if the child startup
	 * up OK.  The info in the pipe (if any) consists of a decimal
	 * errno value followed by an error message.
	 */

	close(errPipeIds[1]);
	errPipeIds[1] = -1;
	count = read(errPipeIds[0], errSpace, (size_t) (sizeof(errSpace) - 1));
	if (count > 0) {
	    char *end;
	    errSpace[count] = 0;
	    errno = strtol(errSpace, &end, 10);
	    Tcl_AppendResult(interp, end, Tcl_PosixError(interp),
		    (char *) NULL);
	    goto error;
	}
	close(errPipeIds[0]);
	errPipeIds[0] = -1;
	pidPtr[numPids] = pid;

	/*
	 * Close off our copies of file descriptors that were set up for
	 * this child, then set up the input for the next child.
	 */

	if ((inputId != -1) && closeInput) {
	    close(inputId);
	}
	if ((curOutputId != -1) && (curOutputId != outputId)) {
	    close(curOutputId);
	}
	inputId = pipeIds[0];
	closeInput = 1;
	pipeIds[0] = pipeIds[1] = -1;
    }
    *pidArrayPtr = pidPtr;

    /*
     * All done.  Cleanup open files lying around and then return.
     */

cleanup:
    if ((inputId != -1) && closeInput) {
	close(inputId);
    }
    if ((outputId != -1) && closeOutput) {
	close(outputId);
    }
    if ((errorId != -1) && closeError) {
	close(errorId);
    }
    return numPids;

    /*
     * An error occurred.  There could have been extra files open, such
     * as pipes between children.  Clean them all up.  Detach any child
     * processes that have been created.
     */

    error:
    if ((inPipePtr != NULL) && (*inPipePtr != -1)) {
	close(*inPipePtr);
	*inPipePtr = -1;
    }
    if ((outPipePtr != NULL) && (*outPipePtr != -1)) {
	close(*outPipePtr);
	*outPipePtr = -1;
    }
    if ((errFilePtr != NULL) && (*errFilePtr != -1)) {
	close(*errFilePtr);
	*errFilePtr = -1;
    }
    if (pipeIds[0] != -1) {
	close(pipeIds[0]);
    }
    if (pipeIds[1] != -1) {
	close(pipeIds[1]);
    }
    if (errPipeIds[0] != -1) {
	close(errPipeIds[0]);
    }
    if (errPipeIds[1] != -1) {
	close(errPipeIds[1]);
    }
    if (pidPtr != NULL) {
	for (i = 0; i < numPids; i++) {
	    if (pidPtr[i] != -1) {
		Tcl_DetachPids(1, &pidPtr[i]);
	    }
	}
	ckfree((char *) pidPtr);
    }
    numPids = -1;
    goto cleanup;
}

/*
 *----------------------------------------------------------------------
 *
 * FileForRedirect --
 *
 *	This procedure does much of the work of parsing redirection
 *	operators.  It handles "@" if specified and allowed, and a file
 *	name, and opens the file if necessary.
 *
 * Results:
 *	The return value is the descriptor number for the file.  If an
 *	error occurs then -1 is returned and an error message is left
 *	in interp->result.  Several arguments are side-effected; see
 *	the argument list below for details.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static int
FileForRedirect(interp, spec, atOk, arg, flags, nextArg, skipPtr, closePtr)
    Tcl_Interp *interp;			/* Intepreter to use for error
					 * reporting. */
    register char *spec;			/* Points to character just after
					 * redirection character. */
    int atOk;				/* Non-zero means '@' notation is
					 * OK, zero means it isn't. */
    char *arg;				/* Pointer to entire argument
					 * containing spec:  used for error
					 * reporting. */
    int flags;				/* Flags to use for opening file. */
    char *nextArg;			/* Next argument in argc/argv
					 * array, if needed for file name.
					 * May be NULL. */
    int *skipPtr;			/* This value is incremented if
					 * nextArg is used for redirection
					 * spec. */
    int *closePtr;			/* This value is set to 1 if the file
					 * that's returned must be closed, 0
					 * if it was specified with "@" so
					 * it must be left open. */
{
    int writing = (flags & O_WRONLY);
    FILE *f;
    int fd;

    if (atOk && (*spec == '@')) {
	spec++;
	if (*spec == 0) {
	    spec = nextArg;
	    if (spec == NULL) {
		goto badLastArg;
	    }
	    *skipPtr += 1;
	}
	if (Tcl_GetOpenFile(interp, spec, writing, 1, &f) != TCL_OK) {
	    return -1;
	}
	*closePtr = 0;
	fd = fileno(f);
	if (writing) {
	    /*
	     * Be sure to flush output to the file, so that anything
	     * written by the child appears after stuff we've already
	     * written.
	     */

	    fflush(f);
	}
    } else {
	if (*spec == 0) {
	    spec = nextArg;
	    if (spec == NULL) {
		goto badLastArg;
	    }
	    *skipPtr += 1;
	}
	fd = open(spec, flags, 0666);
	if (fd < 0) {
	    Tcl_AppendResult(interp, "couldn't ",
		    (writing) ? "write" : "read", " file \"", spec, "\": ",
		    Tcl_PosixError(interp), (char *) NULL);
	    return -1;
	}
	*closePtr = 1;
    }
    return fd;

    badLastArg:
    Tcl_AppendResult(interp, "can't specify \"", arg,
	    "\" as last word in command", (char *) NULL);
    return -1;
}

/*
 *----------------------------------------------------------------------
 *
 * RestoreSignals --
 *
 *	This procedure is invoked in a forked child process just before
 *	exec-ing a new program to restore all signals to their default
 *	settings.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Signal settings get changed.
 *
 *----------------------------------------------------------------------
 */

static void
RestoreSignals()
{
#ifdef SIGABRT
    signal(SIGABRT, SIG_DFL);
#endif
#ifdef SIGALRM
    signal(SIGALRM, SIG_DFL);
#endif
#ifdef SIGFPE
    signal(SIGFPE, SIG_DFL);
#endif
#ifdef SIGHUP
    signal(SIGHUP, SIG_DFL);
#endif
#ifdef SIGILL
    signal(SIGILL, SIG_DFL);
#endif
#ifdef SIGINT
    signal(SIGINT, SIG_DFL);
#endif
#ifdef SIGPIPE
    signal(SIGPIPE, SIG_DFL);
#endif
#ifdef SIGQUIT
    signal(SIGQUIT, SIG_DFL);
#endif
#ifdef SIGSEGV
    signal(SIGSEGV, SIG_DFL);
#endif
#ifdef SIGTERM
    signal(SIGTERM, SIG_DFL);
#endif
#ifdef SIGUSR1
    signal(SIGUSR1, SIG_DFL);
#endif
#ifdef SIGUSR2
    signal(SIGUSR2, SIG_DFL);
#endif
#ifdef SIGCHLD
    signal(SIGCHLD, SIG_DFL);
#endif
#ifdef SIGCONT
    signal(SIGCONT, SIG_DFL);
#endif
#ifdef SIGTSTP
    signal(SIGTSTP, SIG_DFL);
#endif
#ifdef SIGTTIN
    signal(SIGTTIN, SIG_DFL);
#endif
#ifdef SIGTTOU
    signal(SIGTTOU, SIG_DFL);
#endif
}

/*
 *----------------------------------------------------------------------
 *
 * Tcl_PosixError --
 *
 *	This procedure is typically called after UNIX kernel calls
 *	return errors.  It stores machine-readable information about
 *	the error in $errorCode returns an information string for
 *	the caller's use.
 *
 * Results:
 *	The return value is a human-readable string describing the
 *	error, as returned by strerror.
 *
 * Side effects:
 *	The global variable $errorCode is reset.
 *
 *----------------------------------------------------------------------
 */

char *
Tcl_PosixError(interp)
    Tcl_Interp *interp;		/* Interpreter whose $errorCode variable
				 * is to be changed. */
{
    char *id, *msg;

    id = Tcl_ErrnoId();
    msg = strerror(errno);
    Tcl_SetErrorCode(interp, "POSIX", id, msg, (char *) NULL);
    return msg;
}

/*
 *----------------------------------------------------------------------
 *
 * MakeFileTable --
 *
 *	Create or enlarge the file table for the interpreter, so that
 *	there is room for a given index.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The file table for iPtr will be created if it doesn't exist
 *	(and entries will be added for stdin, stdout, and stderr).
 *	If it already exists, then it will be grown if necessary.
 *
 *----------------------------------------------------------------------
 */

	/* ARGSUSED */
static void
MakeFileTable(iPtr, index)
    Interp *iPtr;		/* Interpreter whose table of files is
				 * to be manipulated. */
    int index;			/* Make sure table is large enough to
				 * hold at least this index. */
{
    /*
     * If the table doesn't even exist, then create it and initialize
     * entries for standard files.
     */

    if (tclNumFiles == 0) {
	OpenFile *oFilePtr;
	int i;

	if (index < 2) {
	    tclNumFiles = 3;
	} else {
	    tclNumFiles = index+1;
	}
	tclOpenFiles = (OpenFile **) ckalloc((unsigned)
		((tclNumFiles)*sizeof(OpenFile *)));
	for (i = tclNumFiles-1; i >= 0; i--) {
	    tclOpenFiles[i] = NULL;
	}

	oFilePtr = (OpenFile *) ckalloc(sizeof(OpenFile));
	oFilePtr->f = stdin;
	oFilePtr->f2 = NULL;
	oFilePtr->permissions = TCL_FILE_READABLE;
	oFilePtr->numPids = 0;
	oFilePtr->pidPtr = NULL;
	oFilePtr->errorId = -1;
	tclOpenFiles[0] = oFilePtr;

	oFilePtr = (OpenFile *) ckalloc(sizeof(OpenFile));
	oFilePtr->f = stdout;
	oFilePtr->f2 = NULL;
	oFilePtr->permissions = TCL_FILE_WRITABLE;
	oFilePtr->numPids = 0;
	oFilePtr->pidPtr = NULL;
	oFilePtr->errorId = -1;
	tclOpenFiles[1] = oFilePtr;

	oFilePtr = (OpenFile *) ckalloc(sizeof(OpenFile));
	oFilePtr->f = stderr;
	oFilePtr->f2 = NULL;
	oFilePtr->permissions = TCL_FILE_WRITABLE;
	oFilePtr->numPids = 0;
	oFilePtr->pidPtr = NULL;
	oFilePtr->errorId = -1;
	tclOpenFiles[2] = oFilePtr;
    } else if (index >= tclNumFiles) {
	int newSize;
	OpenFile **newPtrArray;
	int i;

	newSize = index+1;
	newPtrArray = (OpenFile **) ckalloc((unsigned)
		((newSize)*sizeof(OpenFile *)));
	memcpy((VOID *) newPtrArray, (VOID *) tclOpenFiles,
		tclNumFiles*sizeof(OpenFile *));
	for (i = tclNumFiles; i < newSize; i++) {
	    newPtrArray[i] = NULL;
	}
	ckfree((char *) tclOpenFiles);
	tclNumFiles = newSize;
	tclOpenFiles = newPtrArray;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * Tcl_EnterFile --
 *
 *	This procedure is used to enter an already-open file into the
 *	file table for an interpreter so that the file can be read
 *	and written with Tcl commands.
 *
 * Results:
 *	There is no return value, but interp->result is set to
 *	hold Tcl's id for the open file, such as "file4".
 *
 * Side effects:
 *	"File" is added to the files accessible from interp.
 *
 *----------------------------------------------------------------------
 */

void
Tcl_EnterFile(interp, file, permissions)
    Tcl_Interp *interp;		/* Interpreter in which to make file
				 * available. */
    FILE *file;			/* File to make available in interp. */
    int permissions;		/* Ops that may be done on file:  OR-ed
				 * combinination of TCL_FILE_READABLE and
				 * TCL_FILE_WRITABLE. */
{
    Interp *iPtr = (Interp *) interp;
    int fd;
    register OpenFile *oFilePtr;

    fd = fileno(file);
    if (fd >= tclNumFiles) {
	MakeFileTable(iPtr, fd);
    }
    oFilePtr = tclOpenFiles[fd];

    /*
     * It's possible that there already appears to be a file open in
     * the slot.  This could happen, for example, if the application
     * closes a file behind our back so that we don't have a chance
     * to clean up.  This is probably a bad idea, but if it happens
     * just discard the information in the old record (hopefully the
     * application is smart enough to have really cleaned everything
     * up right).
     */

    if (oFilePtr == NULL) {
	oFilePtr = (OpenFile *) ckalloc(sizeof(OpenFile));
	tclOpenFiles[fd] = oFilePtr;
    }
    oFilePtr->f = file;
    oFilePtr->f2 = NULL;
    oFilePtr->permissions = permissions;
    oFilePtr->numPids = 0;
    oFilePtr->pidPtr = NULL;
    oFilePtr->errorId = -1;
    if (fd <= 2) {
	if (fd == 0) {
	    interp->result = "stdin";
	} else if (fd == 1) {
	    interp->result = "stdout";
	} else {
	    interp->result = "stderr";
	}
    } else {
	sprintf(interp->result, "file%d", fd);
    }
}

/*
 *----------------------------------------------------------------------
 *
 * Tcl_GetOpenFile --
 *
 *	Given a string identifier for an open file, find the corresponding
 *	open file structure, if there is one.
 *
 * Results:
 *	A standard Tcl return value.  If the open file is successfully
 *	located and meets any usage check requested by checkUsage, TCL_OK
 *	is returned and *filePtr is modified to hold a pointer to its
 *	FILE structure.  If an error occurs then TCL_ERROR is returned
 *	and interp->result contains an error message.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

int
Tcl_GetOpenFile(interp, string, forWriting, checkUsage, filePtr)
    Tcl_Interp *interp;		/* Interpreter in which to find file. */
    char *string;		/* String that identifies file. */
    int forWriting;		/* 1 means the file is going to be used
				 * for writing, 0 means for reading. */
    int checkUsage;		/* 1 means verify that the file was opened
				 * in a mode that allows the access specified
				 * by "forWriting". */
    FILE **filePtr;		/* Store pointer to FILE structure here. */
{
    OpenFile *oFilePtr;
    int fd = 0;			/* Initial value needed only to stop compiler
				 * warnings. */
    Interp *iPtr = (Interp *) interp;

    if ((string[0] == 'f') && (string[1] == 'i') && (string[2] == 'l')
	    & (string[3] == 'e')) {
	char *end;

	fd = strtoul(string+4, &end, 10);
	if ((end == string+4) || (*end != 0)) {
	    goto badId;
	}
    } else if ((string[0] == 's') && (string[1] == 't')
	    && (string[2] == 'd')) {
	if (strcmp(string+3, "in") == 0) {
	    fd = 0;
	} else if (strcmp(string+3, "out") == 0) {
	    fd = 1;
	} else if (strcmp(string+3, "err") == 0) {
	    fd = 2;
	} else {
	    goto badId;
	}
    } else {
	badId:
	Tcl_AppendResult(interp, "bad file identifier \"", string,
		"\"", (char *) NULL);
	return TCL_ERROR;
    }

    if (fd >= tclNumFiles) {
	if ((tclNumFiles == 0) && (fd <= 2)) {
	    MakeFileTable(iPtr, fd);
	} else {
	    notOpen:
	    Tcl_AppendResult(interp, "file \"", string, "\" isn't open",
		    (char *) NULL);
	    return TCL_ERROR;
	}
    }
    oFilePtr = tclOpenFiles[fd];
    if (oFilePtr == NULL) {
	goto notOpen;
    }
    if (forWriting) {
	if (checkUsage && !(oFilePtr->permissions & TCL_FILE_WRITABLE)) {
	    Tcl_AppendResult(interp, "\"", string,
		    "\" wasn't opened for writing", (char *) NULL);
	    return TCL_ERROR;
	}
	if (oFilePtr->f2 != NULL) {
	    *filePtr = oFilePtr->f2;
	} else {
	    *filePtr = oFilePtr->f;
	}
    } else {
	if (checkUsage && !(oFilePtr->permissions & TCL_FILE_READABLE)) {
	    Tcl_AppendResult(interp, "\"", string,
		    "\" wasn't opened for reading", (char *) NULL);
	    return TCL_ERROR;
	}
	*filePtr = oFilePtr->f;
    }
    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * Tcl_FilePermissions --
 *
 *	Given a FILE * pointer, return the read/write permissions
 *	associated with the open file.
 *
 * Results:
 *	If file is currently open, the return value is an OR-ed
 *	combination of TCL_FILE_READABLE and TCL_FILE_WRITABLE,
 *	which indicates the operations permitted on the open file.
 *	If the file isn't open then the return value is -1.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

int
Tcl_FilePermissions(file)
    FILE *file;			/* File for which permissions are wanted. */
{
    register OpenFile *oFilePtr;
    int i, fd;

    /*
     * First try the entry in tclOpenFiles given by the file descriptor
     * for the file.  If that doesn't match then search all the entries
     * in tclOpenFiles.
     */

    if (file != NULL) {
	fd = fileno(file);
	if (fd < tclNumFiles) {
	    oFilePtr = tclOpenFiles[fd];
	    if ((oFilePtr != NULL) && (oFilePtr->f == file)) {
		return oFilePtr->permissions;
	    }
	}
    }
    for (i = 0; i < tclNumFiles; i++) {
	oFilePtr = tclOpenFiles[i];
	if (oFilePtr == NULL) {
	    continue;
	}
	if ((oFilePtr->f == file) || (oFilePtr->f2 == file)) {
	    return oFilePtr->permissions;
	}
    }
    return -1;
}

/*
 *----------------------------------------------------------------------
 *
 * TclOpen, etc. --
 *
 *	Below are a bunch of procedures that are used by Tcl instead
 *	of system calls.  Each of the procedures executes the
 *	corresponding system call and retries automatically
 *	if the system call was interrupted by a signal.
 *
 * Results:
 *	Whatever the system call would normally return.
 *
 * Side effects:
 *	Whatever the system call would normally do.
 *
 * NOTE:
 *	This should be the last page of this file, since it undefines
 *	the macros that redirect read etc. to the procedures below.
 *
 *----------------------------------------------------------------------
 */

#undef open
int
TclOpen(path, oflag, mode)
    char *path;
    int oflag;
    int mode;
{
    int result;
    while (1) {
	result = open(path, oflag, mode);
	if ((result != -1) || (errno != EINTR)) {
	    return result;
	}
    }
}

#undef read
int
TclRead(fd, buf, numBytes)
    int fd;
    VOID *buf;
    size_t numBytes;
{
    int result;
    while (1) {
	result = read(fd, buf, (size_t) numBytes);
	if ((result != -1) || (errno != EINTR)) {
	    return result;
	}
    }
}

#undef waitpid
extern pid_t waitpid _ANSI_ARGS_((pid_t pid, int *stat_loc, int options));

/*
 * Note:  the #ifdef below is needed to avoid compiler errors on systems
 * that have ANSI compilers and also define pid_t to be short.  The
 * problem is a complex one having to do with argument type promotion.
 */

#ifdef _USING_PROTOTYPES_
int
TclWaitpid _ANSI_ARGS_((pid_t pid, int *statPtr, int options))
#else
int
TclWaitpid(pid, statPtr, options)
    pid_t pid;
    int *statPtr;
    int options;
#endif /* _USING_PROTOTYPES_ */
{
    int result;
    while (1) {
	result = waitpid(pid, statPtr, options);
	if ((result != -1) || (errno != EINTR)) {
	    return result;
	}
    }
}

#undef write
int
TclWrite(fd, buf, numBytes)
    int fd;
    VOID *buf;
    size_t numBytes;
{
    int result;
    while (1) {
	result = write(fd, buf, (size_t) numBytes);
	if ((result != -1) || (errno != EINTR)) {
	    return result;
	}
    }
}
