/* 
 * txSprite.c --
 *
 *	This file contains all of the Sprite-dependent code in Tx,
 *	such as code to glue a Tx to a pseudo-tty implemented using
 *	Sprite pdevs.
 *
 * Copyright 1989 Regents of the University of California
 * 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.  The University of California
 * makes no representations about the suitability of this
 * software for any purpose.  It is provided "as is" without
 * express or implied warranty.
 */

#ifndef lint
static char rcsid[] = "$Header: /sprite/src/lib/mx/RCS/txSprite.c,v 1.19 90/04/17 15:57:36 ouster Exp $ SPRITE (Berkeley)";
#endif /* not lint */

#define Time SpriteTime
#include <bstring.h>
#include <dev/tty.h>
#include <errno.h>
#include <pdev.h>
#include <sgtty.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/wait.h>
#include <tcl.h>
#include <fmt.h>
#include "td.h"
#undef Time
#include "mx.h"
#include "mxInt.h"

#include <alloca.h>

/*
 * Structures of the following form are created for each pseudo-tty.
 * These are passed to the window-manager and tty-manager as ClientData
 * and returned here in callbacks.
 */

typedef struct Typescript {
    Td_Terminal term;			/* Token for terminal driver info. */
    Td_Pdev pdev;			/* Token for pseudo-device connecting
					 * us to the application. */
    MxFileInfo *fileInfoPtr;		/* Structure used to manage display-
					 * related info for typescript. */
    char *command;			/* Used to buffer a command being
					 * received over the terminal output
					 * stream (command could arrive in
					 * pieces, but can't be processed until
					 * it's all here). */
    int commandUsed;			/* Actual number of bytes of command
					 * that are in use presently.  -1 means
					 * no partial command is buffered. */
    int commandSize;			/* Total number of bytes currently
					 * allocated to command buffer;  this
					 * will grow as needed. */
    int pid;				/* The top-level application process.
					 * 0 means there's no application
					 * associated with this typescript. */
    struct Typescript *nextPtr;		/* Next in list of all typescripts,
					 * or NULL for end of list. */
} Typescript;

static Typescript *typescripts = NULL;	/* List of all existing typescripts. */

/*
 * The following global variable is provided so that programs like Tx can
 * tell when there are no longer any Tx windows open.
 */

int tx_TypescriptCount = 0;

/*
 * The following typedefs are used exit return codes used to indicate
 * problems that occurred after the application process was forked but
 * before it could exec the application.
 */

#define ERR_PDEV_OPEN	100
#define ERR_SET_FAMILY	101
#define ERR_EXEC	102

/*
 * Termcap information.  Generate a valid termcap entry with something
 * like sprintf(string, tcFormat, cols, lines, tc1, tc2, ...),
 * where lines and cols are screen dimensions.
 */

char *tcFormat = "tx:co#%d:li#%d:%s%s%s%s%s%s%s%s";
char *tc1 = "al=\\Evi in\\n:bs:cd=\\Evi cd\\n:ce=\\Evi ce\\n:";
char *tc2 = "cl=\\Evi cl;selection c\\n:cm=\\Evi cu %d %d\\n:";
char *tc3 = "dl=\\Evi de\\n:do=^J:ho=\\Evi cu 0 0\\n:pt:";
char *tc4 = "rs=\\Evi le\\n:ta=\\Evi ta\\n:te=\\Evi le;set ti 0\\n:";
char *tc5 = "ti=\\Evi en;set ti 1\\n:up=\\Evi up\\n:";
char *tc6 = "ve=\\Eif [set ti]0 {} else {vi le}\\n:vs=\\Evi en\\n:";
char *tc7 = "xt:nd=\\Ecaret [mark caret forward 1 char]\\n:";
char *tc8 = "";

/*
 * Forward declarations to procedures defined later in this file:
 */

static void	TxChildDeathProc();
static void	TxCloseProc();
static void	TxInputProc();
static int	TxRawControlProc();
static void	TxSizeChangeProc();

/*
 *----------------------------------------------------------------------
 *
 * Tx_SetupAndFork --
 *
 *	This is a top-level procedure that makes a window into a Tx
 *	window, associates a terminal-like structure with that
 *	window, and starts up a top-level application process in
 *	the window.  The code in this procedure is Sprite-specific.
 *
 * Results:
 *	The return result is a standard Tcl error code (usually TCL_OK).
 *	Interp->result is modified to hold an error message if an error
 *	occurs (interp->result should already have been initialized to
 *	empty by the caller).
 *
 * Side effects:
 *	A pseudo-device is created, a process is forked (if requested)
 *	and window is turned into a Tx window.  Also, this procedure
 *	creates a handler for SIGCHLD signals:  the caller better not
 *	have its own uses for this.
 *
 *----------------------------------------------------------------------
 */

int
Tx_SetupAndFork(interp, display, window, infoPtr, args)
    Tcl_Interp *interp;		/* Used to return error information
				 * in interp->result. */
    Display *display;		/* Display containing window to Tx-ify. */
    Window window;		/* Make this into a Tx window.  Must already
				 * exist on display. */
    Tx_WindowInfo *infoPtr;	/* Information about window;  passed through
				 * to Tx_Make. */
    char **args;		/* Command to execute.  If NULL, then
				 * a shell is created by default. */
{
    register Typescript *tsPtr;
    char *name;
    int result;
    Tx_WindowInfo newInfo;
    MxWindow *mxwPtr;
    int pid;
    struct sgttyb sgttyb;
    struct tchars tchars;
    struct ltchars ltchars;
    int dummy, outSize = 0;
    static int exitHandlerCreated = 0;

    if (!exitHandlerCreated) {
	exitHandlerCreated = 1;
	signal(SIGCHLD, TxChildDeathProc);
    }

    tsPtr = (Typescript *) malloc(sizeof(Typescript));
    bzero((char *) tsPtr, sizeof(tsPtr));
    tsPtr->nextPtr = typescripts;
    typescripts = tsPtr;
    name = "";
    tsPtr->pdev = Td_CreatePdev("tx", &name, &tsPtr->term,
	    TxRawControlProc, (ClientData) tsPtr);
    if (tsPtr->pdev == NULL) {
	sprintf(interp->result,
		"couldn't open terminal pseudo-device \"%.50s\": %.80s",
		name, pdev_ErrorMsg);
	result = TCL_ERROR;
	goto error;
    }
    if (ioctl(0, TIOCGETP, (char *) &sgttyb) == 0) {
	struct sgttyb buf2;

	outSize = sizeof(buf2);
	(void) Td_ControlCooked(tsPtr->term, IOC_TTY_GETP, FMT_MY_FORMAT,
		0, (char *) NULL, &outSize, (char *) &buf2,
		&dummy, &dummy);
	buf2.sg_erase = sgttyb.sg_erase;
	buf2.sg_kill = sgttyb.sg_kill;
	outSize = 0;
	(void) Td_ControlCooked(tsPtr->term, IOC_TTY_SETP, FMT_MY_FORMAT,
		sizeof(buf2), (char *) &buf2, &outSize, (char *) NULL,
		&dummy, &dummy);
    }
    if (ioctl(0, TIOCGETC, (char *) &tchars) == 0) {
	(void) Td_ControlCooked(tsPtr->term, IOC_TTY_SET_TCHARS, FMT_MY_FORMAT,
		sizeof(tchars), (char *) &tchars, &outSize, (char *) NULL,
		&dummy, &dummy);
    }
    if (ioctl(0, TIOCGLTC, (char *) &ltchars) == 0) {
	(void) Td_ControlCooked(tsPtr->term, IOC_TTY_SET_LTCHARS, FMT_MY_FORMAT,
		sizeof(ltchars), (char *) &ltchars, &outSize, (char *) NULL,
		&dummy, &dummy);
    }
    newInfo = *infoPtr;
    if (newInfo.title == NULL) {
	newInfo.title = name;
    }
    result = Tx_Make(interp, display, window, &newInfo, TxInputProc,
	    TxSizeChangeProc, TxCloseProc, (ClientData) tsPtr);
    if (result != TCL_OK) {
	goto error;
    }
    mxwPtr = TxGetMxWindow(display, window);
    tsPtr->fileInfoPtr = mxwPtr->fileInfoPtr;
    tsPtr->command = NULL;
    tsPtr->commandUsed = -1;
    tsPtr->commandSize = 0;
    XStoreName(display, window, newInfo.title);

    if (args == NULL) {
	static char *defaultArgs[] = {NULL, "-i", NULL};
	char *p;

	args = defaultArgs;
	p = getenv("SHELL");
	if (p != NULL) {
	    defaultArgs[0] = alloca((unsigned) (strlen(p) + 1));
	    strcpy(defaultArgs[0], p);
	} else {
	    defaultArgs[0] = "csh";
	}
    }
    pid = fork();
    if (pid == 0) {
	int	ttyID, i;
	char	tc[400];

	/*
	 * Set up the environment for the application.
	 */

	setenv("TTY", name);
	setenv("TERM", "tx");
	sprintf(tc, tcFormat,
		mxwPtr->width/infoPtr->fontPtr->max_bounds.width,
		mxwPtr->heightLines, tc1, tc2, tc3, tc4, tc5, tc6, tc7, tc8);
	setenv("TERMCAP", tc);
	sprintf(tc, "%d", window);
	setenv("WINDOWID", tc);
	setenv("DISPLAY", DisplayString(display));

	/*
	 * Get the standard I/O channels open on the pseudo-device.
	 */

	ttyID = open(name, O_RDWR, 0);
	if (ttyID < 0) {
	    _exit(ERR_PDEV_OPEN);
	}
	pid = getpid();
	if (setpgrp(0, pid) == -1) {
	    _exit(ERR_SET_FAMILY);
	}
	if (ioctl(ttyID, TIOCSPGRP, (char *) &pid) != 0) {
	    _exit(ERR_SET_FAMILY);
	}
	dup2(ttyID, 0);
	dup2(ttyID, 1);
	dup2(ttyID, 2);
	for (i = 3; i <= ttyID; i++) {
	    close(i);
	}

	/*
	 * Run the application.
	 */

	execvp(args[0], args);
	_exit(ERR_EXEC);
    } else if (pid == -1) {
	sprintf(interp->result,  "Tx couldn't fork application: %.100s\n",
		strerror(errno));
	result = TCL_ERROR;
	goto error;
    }
    tsPtr->pid = pid;
    tx_TypescriptCount += 1;
    return TCL_OK;

    error:
    TxCloseProc(tsPtr);
    return result;
}

/*
 *----------------------------------------------------------------------
 *
 * TxCloseProc --
 *
 *	This procedure is invoked by the window portion of Tx when
 *	the last window on a typescript goes away.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	All of our data structures are cleaned up.  The application
 *	process is deleted if it still exists, and the pseudo-device
 *	stuff is all deleted.
 *
 *----------------------------------------------------------------------
 */

static void
TxCloseProc(tsPtr)
    register Typescript *tsPtr;		/* Pointer to our information about
					 * this typescript. */
{
    register Typescript *prevPtr;

    if (typescripts == tsPtr) {
	typescripts = tsPtr->nextPtr;
    } else {
	for (prevPtr = typescripts; prevPtr != NULL;
		prevPtr = prevPtr->nextPtr) {
	    if (prevPtr->nextPtr == tsPtr) {
		prevPtr->nextPtr = tsPtr->nextPtr;
		break;
	    }
	}
    }
    tx_TypescriptCount -= 1;
    if (tsPtr->pdev != 0) {
	Td_DeletePdev(tsPtr->pdev);
    }
    if (tsPtr->command != NULL) {
	free(tsPtr->command);
    }
    if (tsPtr->pid != 0) {
	(void) kill(tsPtr->pid, SIGHUP);
    }
    free((char *) tsPtr);
}

/*
 *----------------------------------------------------------------------
 *
 * TxInputProc --
 *
 *	This procedure is called by the Tx window widget when input
 *	characters are ready to be passed to the terminal.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Pass off the characters in string to the terminal driver.
 *
 *----------------------------------------------------------------------
 */

static void
TxInputProc(tsPtr, string)
    Typescript *tsPtr;			/* Information about typescript
					 * (passed as clientData). */
    char *string;			/* NULL-terminated string of input
					 * characters from window. */
{
    Td_PutRaw(tsPtr->term, strlen(string), string);
}

/*
 *----------------------------------------------------------------------
 *
 * TxSizeChangeProc --
 *
 *	This procedure is invoked whenever the size of the main window
 *	for a typescript changes size.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	A TIOCSWINSZ IOControl is simulated on the window so that
 *	applications will see the new size.
 *
 *----------------------------------------------------------------------
 */

static void
TxSizeChangeProc(tsPtr, sizePtr)
    Typescript *tsPtr;			/* Information about typescript
					 * (passed as clientData). */
    struct winsize *sizePtr;		/* Pointer to new size information. */
{
    int outputSize, dummy1, dummy2;

    outputSize = 0;
    (void) Td_ControlCooked(tsPtr->term, IOC_TTY_SET_WINDOW_SIZE, FMT_MY_FORMAT,
	    sizeof(struct winsize), (char *) sizePtr, &outputSize,
	    (char *) NULL, &dummy1, &dummy2);
}

/*
 *----------------------------------------------------------------------
 *
 * TxRawControlProc --
 *
 *	This procedure is invoked by the terminal driver as the control
 *	procedure for the "raw" end of the terminal.  Its function here
 *	is to fetch characters from the terminal's output buffer, when
 *	they're ready, and put them on the display (or process them as
 *	commands, if there are Tx commands embedded in the output).
 *
 * Results:
 *	Always returns 0.
 *
 * Side effects:
 *	Information gets added to the display.
 *
 *----------------------------------------------------------------------
 */

	/* ARGSUSED */
static int
TxRawControlProc(tsPtr, operation, inSize, inBuffer, outSize, outBuffer)
    register Typescript *tsPtr;	/* Our information about this typescript
				 * (passed as ClientData) . */
    int operation;		/* Identifies control operation being
				 * invoked, e.g. TD_COOKED_SIGNAL. */
    int inSize;			/* Number of bytes of input data available
				 * to us. */
    char *inBuffer;		/* Pointer to input data. */
    int outSize;		/* Maximum number of bytes of output data
				 * we can return to caller. */
    char *outBuffer;		/* Area in which to store output data for
				 * caller. */
{
#define OUT_BUFFER_SIZE 2000
    register char *p;
    char *start;
    char buffer[OUT_BUFFER_SIZE+1];
    int count;

    /*
     * The only control operation that's handled here is TD_RAW_OUTPUT_READY.
     * Anything else is ignored.
     */

    if (operation != TD_RAW_OUTPUT_READY) {
	return 0;
    }

    while (1) {
	count = Td_GetRaw(tsPtr->term, OUT_BUFFER_SIZE, buffer);
	if (count == 0) {
	    break;
	}
	buffer[count] = 0;
    
	/*
	 * Process the new output characters in chunks.  Each chunk is
	 * either a) normal output characters, or b) a Tx command, which
	 * is the characters between ESC and the next newline.  Also,
	 * throw away null characters if they appear in the output.
	 */

	for (p = buffer; *p != 0; ) {
    
	    /*
	     * Find the rest of the current command, if there's a partially-
	     * complete command.
	     */
    
	    if (tsPtr->commandUsed >= 0) {
		while (*p != 0) {
		    MxWindow *mxwPtr;

		    /*
		     * Grow the command buffer if it has overflowed.
		     */

		    if (tsPtr->commandUsed >= tsPtr->commandSize) {
			char *newBuffer;
			int newSize;

			newSize = 2*tsPtr->commandSize;
			if (newSize < 20) {
			    newSize = 20;
			}
			newBuffer = (char *) malloc((unsigned) newSize);
			bcopy(tsPtr->command, newBuffer, tsPtr->commandUsed);
			if (tsPtr->command != NULL) {
			    free(tsPtr->command);
			}
			tsPtr->command = newBuffer;
			tsPtr->commandSize = newSize;
		    }

		    if (*p == '\n') {
			tsPtr->command[tsPtr->commandUsed] = 0;
			mxwPtr = tsPtr->fileInfoPtr->mxwPtr;
			(void) Tx_Command(mxwPtr->display, mxwPtr->w,
				tsPtr->command);
			tsPtr->commandUsed = -1;
			p++;
			break;
		    }
		    tsPtr->command[tsPtr->commandUsed] = *p;
		    tsPtr->commandUsed++;
		    p++;
		}
	    }
    
	    /*
	     * Grab up a chunk of characters, and output them.
	     */
    
	    if (*p == 0) {
		break;
	    }
	    for (start = p; *p != 0; p++) {
		if ((*p == '\33')
			&& !(tsPtr->fileInfoPtr->flags & NO_ESCAPES)) {
		    tsPtr->commandUsed = 0;
		    *p = 0;
		    p++;
		    break;
		}
	    }
	    Tx_Output(tsPtr->fileInfoPtr, start);
	}
	if (count != OUT_BUFFER_SIZE) {
	    break;
	}
    }

    return 0;
}

/*
 *----------------------------------------------------------------------
 *
 * TxChildDeathProc --
 *
 *	This procedure is a signal handler that is invoked when (if)
 *	the child dies.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	If the child died naturally, then the typescript is closed.
 *	If the child terminated abnormally, a message is printed in
 *	the typescript and the typescript stays around, so the user
 *	can read the message.
 *
 *----------------------------------------------------------------------
 */
static void
TxChildDeathProc()
{
    int	pid;
    Typescript  *tsPtr;
    union wait	status;
    char	msg[100];
    static char *sigNames[] = {
	"undefined (signal 0)",
	"hangup",
	"interrupt",
	"debug",
	"illegal instruction",
	"undefined (signal 5)",
	"undefined (signal 6)",
	"undefined (signal 7)",
	"floating-point exception",
	"kill",
	"migration",
	"segmentation violation",
	"undefined (signal 12)",
	"broken pipe",
	"alarm clock",
	"software termination",
	"urgent I/O condition",
	"suspended",
	"suspended (TSTP)",
	"continue",
	"child status changed",
	"suspended (tty input)",
	"undefined (signal 22)",
	"undefined (signal 23)",
	"undefined (signal 24)",
	"undefined (signal 25)",
	"undefined (signal 26)",
	"undefined (signal 27)",
	"undefined (signal 28)",
	"migrated home",
	"user-defined signal 1",
	"user-defined signal 2",
    };

    /*
     * Get pid of dead child and find the window associated with the
     * child.  If there's no longer a typescript around for this child,
     * then just return.
     */
    pid = wait3(&status, WNOHANG|WUNTRACED, (struct rusage *) 0);
    if (pid == 0) {
	return;
    }
    for (tsPtr = typescripts; tsPtr != NULL; tsPtr = tsPtr->nextPtr) {
	if (tsPtr->pid == pid) {
	    tsPtr->pid = 0;
	    break;
	}
    }
    if (tsPtr == NULL) {
	return;
    }

    /*
     * If the child terminated normally, then destroy all the windows
     * on this typescript.  Otherwise keep the window around and display
     * an error message in it.
     */

    if (WIFEXITED(status)) {
	if (status.w_retcode == 0) {
	    MxWindow *mxwPtr;

	    for (mxwPtr = tsPtr->fileInfoPtr->mxwPtr; mxwPtr != NULL;
		    mxwPtr = mxwPtr->nextPtr) {
		XDestroyWindow(mxwPtr->display, mxwPtr->w);
		mxwPtr->flags |= DESTROYED;
		XFlush(mxwPtr->display);
	    }
	    return;
	} else {
	    if (status.w_retcode == ERR_PDEV_OPEN) {
		sprintf(msg, "Couldn't open application end of pseudo-device.");
	    } else if (status.w_retcode == ERR_SET_FAMILY) {
		sprintf(msg, "Couldn't set process family for terminal.");
	    } else if (status.w_retcode == ERR_EXEC) {
		sprintf(msg, "Couldn't execute application program.");
	    } else {
		sprintf(msg, "Application terminated with status %d.",
			status.w_retcode);
	    }
	}
    } else if (WIFSTOPPED(status)) {
	sprintf(msg, "Application stopped because of \"%s\" signal.",
		sigNames[status.w_stopsig & 037]);
    } else {
	sprintf(msg, "Application killed by \"%s\" signal.",
		sigNames[status.w_termsig & 037]);
    }
    Tx_Output(tsPtr->fileInfoPtr, "\r\n\n");
    Tx_Output(tsPtr->fileInfoPtr, msg);
    Tx_Output(tsPtr->fileInfoPtr, "\r\nThis window is no longer usable.");
    Tx_Update();
    Mx_Update();
    XFlush(tsPtr->fileInfoPtr->mxwPtr->display);
    return;
}
