/*-
 * export.c --
 *	Program to handle the interaction with the customs agent over
 *	an exported process. This should, perhaps, be generalized so
 *	other programs can use it, but...maybe a library...
 *
 * Copyright (c) 1988, 1989 by the Regents of the University of California
 * Copyright (c) 1988, 1989 by Adam de Boor
 * Copyright (c) 1989 by Berkeley Softworks
 *
 * Permission to use, copy, modify, and distribute this
 * software and its documentation for any non-commercial purpose
 * and without fee is hereby granted, provided that the above copyright
 * notice appears in all copies.  The University of California,
 * Berkeley Softworks and Adam de Boor make 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 =
"$Id: export.c,v 1.48 1995/04/22 09:38:01 stolcke Exp $ ICSI (Berkeley)";
#endif /* not lint */

#include    <string.h>
#include    <sys/time.h>
#include    <signal.h>
#include    <stdio.h>
#include    <sys/wait.h>
#include    <errno.h>
extern int errno;
#include    <netdb.h>
#include    <fcntl.h>
#include    <sys/file.h>

extern char *getenv ();

#include    "customs.h"

int	    	  	rmt;  	/* Socket to remote process */
int	    	  	ret;   	/* Socket server will call us back on with
				 * exit status */
Rpc_ULong	    	id;    	/* Permit id under which the process is
				 * running */
struct sockaddr_in	server;	/* Address of server running the process */
extern int  	  	customs_Socket;
extern struct timeval	customs_RetryTimeOut;

WAIT_STATUS 	  	status;
int			exitSeen = 0;
void	    	  	DoExit();

struct timeval		checkInterval;	/* Interval between checks on remote */
Rpc_Event		checkEvent;	/* Event for same */

char			*progname;	/* program name in error messages */

/*-
 *-----------------------------------------------------------------------
 * SendSig --
 *	Pass a signal on to a remote process.  This the handler for an
 *	event scheduled by the local signal handler.
 *
 * Results:
 *	False.
 *
 * Side Effects:
 *	An RPC call is made to the import server to deliver the signal.
 *
 *-----------------------------------------------------------------------
 */
Boolean
SendSig(signo)
    int	    signo;	/* The signal number we've received */
{
    Kill_Data	  packet;
    Rpc_Stat	  rpcstat;
    
    packet.id = id;
    packet.signal = Signal_FromHost(signo);

    rpcstat = Rpc_Call (ret, &server, (Rpc_Proc)CUSTOMS_KILL,
		       sizeof(packet), (Rpc_Opaque)&packet,
		       0, (Rpc_Opaque)0,
		       CUSTOMS_KILL_NRETRY, &customs_RetryTimeOut);
    if (rpcstat != RPC_SUCCESS) {
	/*
	 * The signal couldn't be delivered, meaning that the process
	 * on the remote side vanished without leaving a notice, or maybe
	 * the machine crashed.  In any case, there's nothing more we
	 * we can do here ...
	 */
	if (exitSeen) {
	    DoExit();
	}
	fprintf(stderr, "%s: *** remote process vanished\n", progname);
	exit(2);
    }
    return (False);
}

/*-
 *-----------------------------------------------------------------------
 * PassSig --
 *	Catch a signal and pass it along. We only kill ourselves with the
 *	signal when we receive the exit status of the remote process.
 *
 * Results:
 *	None.
 *
 * Side Effects:
 *	An event is scheduled to forward the signal to the remote process.
 *	An RPC call is made to the import server to deliver the signal.
 *
 *-----------------------------------------------------------------------
 */
static SIGRET
PassSig(signo)
    int	    signo;	/* The signal number we've received */
{
    struct timeval timeout;

    /*
     * XXX: Do this early to narrow window for back-to-back signals.
     */
    SIGRESTORE(signo, PassSig);

    timeout.tv_sec = 0;
    timeout.tv_usec = 0;
    /*
     * We defer the actual signal forwarding to an RPC event to avoid
     * activating Rpc_Wait() within a signal handler.  This could lead
     * to all kinds of non-reentrant procedures to be called.
     */
    (void)Rpc_EventOnce(&timeout, SendSig, (Rpc_Opaque)signo);
}

#ifdef REMOTE_CHECK_INTERVAL
/*-
 *-----------------------------------------------------------------------
 * CheckRemote --
 *	Check existence of remote process by sending a signal 0.
 *
 * Results:
 *	True.
 *
 * Side Effects:
 *	We may die if the process no longer exists.
 *
 *-----------------------------------------------------------------------
 */
static Boolean
CheckRemote ()
{
    SendSig(0);
    return (True);
}
#endif /* REMOTE_CHECK_INTERVAL */


/*-
 *-----------------------------------------------------------------------
 * Drain --
 *	Wait for the remote socket to become writable again, handling
 *	any output from the remote side in the mean time.
 *
 * Results:
 *	None.
 *
 * Side Effects:
 *	If 'what' is RPC_WRITABLE, Transfer is reinstalled as the stream
 *	server for stdin...
 *
 *-----------------------------------------------------------------------
 */
/*ARGSUSED*/
void
Drain(stream, arg, what)
    int	    	  stream;
    Rpc_Opaque 	  arg;
    int	    	  what;
{
    void	  Transfer();
    
    if (what & RPC_READABLE) {
	/*
	 * Transfer any data from remote side
	 */
	Transfer(rmt, 1);
    }
    if (what & RPC_WRITABLE) {
	/*
	 * Socket has drained enough, reinstall the regular handlers
	 * for both streams
	 */
	Rpc_Watch(rmt, RPC_READABLE, Transfer, (Rpc_Opaque)1);
	Rpc_Watch(0, RPC_READABLE, Transfer, (Rpc_Opaque)rmt);
    }
}

/*-
 *-----------------------------------------------------------------------
 * Transfer --
 *	Transfer data from one source to another.
 *
 * Results:
 *	None.
 *
 * Side Effects:
 *	Data are read from source and written to dest.
 *
 *-----------------------------------------------------------------------
 */
void
Transfer (source, dest)
    int	    source;
    int	    dest;
{
    char    buf[BUFSIZ];
    int	    cc, cw;

    do {
	cc = read(source, buf, sizeof(buf));
    }
    while ((cc < 0) && (errno == EINTR));
    if (cc < 0) {
	if ((errno == EWOULDBLOCK) && exitSeen) {
	    /*
	     * We have transfered the last bit of data after getting
	     * a status change from the remote process.  Now replicate
	     * the status change locally.
	     */
	    DoExit();
	} else {
	    fprintf(stderr, "%s: read: %s\n", progname, strerror(errno));
	}
    } else if (cc == 0) {
	if (source == 0) {
	    /*
	     * When we reach the end-of-file for our input, we want the remote
	     * process to reach that state, too, so we half-shutdown our socket
	     * to it.
	     */
	    if (shutdown(rmt, 1) < 0) {
		fprintf(stderr, "%s: shutdown: %s\n",
				progname, strerror(errno));
		exit(3);
	    }
	} else if (exitSeen) {
	    /*
	     * We've gotten an EOF on the socket and customs has already sent
	     * us an exit signal, so perform the exit now.
	     */
	    DoExit();
	}
	Rpc_Ignore(source);
    } else {
	do {
	    cw = write (dest, buf, cc);
	}
	while ((cw < 0) && (errno == EINTR));
	if (cw != cc) {
	    if (errno != EWOULDBLOCK) {
		if (errno == EPIPE && dest == rmt) {
		    /*
		     * Connection to remote side was lost. This means that
		     * both the process and the server died, so there's
		     * no point in waiting for the exit status...
		     */
		    fprintf(stderr, "%s: *** connection closed\n", progname);
		    if (exitSeen) {
			DoExit();
		    }
		} else {
		    fprintf(stderr, "%s: write: %s\n",
					progname, strerror(errno));
		}
		exit(2);
	    } else {
		/*
		 * If we can't write because it'd block, we must be transfering
		 * from local to remote. In such a case, we ignore further input
		 * from stdin, and wait for the output socket to become writable.
		 * Drain() will reset the handler for 0.
		 */
		Rpc_Ignore(0);
		Rpc_Watch(rmt, RPC_READABLE|RPC_WRITABLE, Drain, (Rpc_Opaque)0);
	    }
	}
    }
}
/*ARGSUSED*/
void
SwapExit(length, data)
    int	    	  length;
    Exit_Data	  *data;
{
    Rpc_SwapLong(sizeof(data->id), &data->id);
    Rpc_SwapLong(sizeof(data->status), &data->status);
    Rpc_SwapLong(sizeof(data->signal), &data->signal);
}


/*-
 *-----------------------------------------------------------------------
 * Exit --
 *	Handle CUSTOMS_EXIT call from import server. This process doesn't
 *	actually exit until we get an end-of-file on the socket to the
 *	remote side. This allows any error message from the customs agent
 *	to be printed before we exit.
 *
 * Results:
 *	None.
 *
 * Side Effects:
 *	exitSeen is set true and status is set to the returned status.
 *
 *-----------------------------------------------------------------------
 */
/*ARGSUSED*/
void
Exit(from, msg, len, data)
    struct sockaddr_in	*from;
    Rpc_Message	  	msg;
    int	    	  	len;
    Rpc_Opaque 	  	data;
{
    Exit_Data	  	*eVal = (Exit_Data *)data;
    
    if (checkEvent && !WIFSTOPPED(status)) {
	Rpc_EventDelete(checkEvent);
    }

    WSTATUS(status) = eVal->status;
    /*
     * XXX: Aarrrgh! There is no non-kludgy, portable way to do this
     * on systems that don't have a union wait.
     */
    if (WIFSTOPPED(status)) {
	WSTATUS(status) = (Signal_ToHost(eVal->signal)<<8) | WSTOPPED;
		/* status.w_stopsig = Signal_ToHost(eVal->signal); */
    } else if (WIFSIGNALED(status)) {
	WSTATUS(status) = Signal_ToHost(eVal->signal);
		/* status.w_termsig = Signal_ToHost(eVal->signal); */
    }
    exitSeen = 1;
    Rpc_Return(msg, 0, (Rpc_Opaque)0);

    /*
     * If the remote process stopped we try to get all its output so far
     * and then suspend ourselves.
     * However, if the remote process has terminated, we simply continue
     * reading its output until we get the EOF, as which point DoExit()
     * will be called.  This ensures that none of the output gets lost.
     */
    if (WIFSTOPPED(status)) {
	/*
	 * Let Transfer() read all remaining data from the socket,
	 * until none is left (note socket is in non-blocking mode).
	 * At this point it will call DoExit(), which in turn will clear
	 * exitSeen.  After we continue ourselves the loop will be
	 * exited.
	 */
	while (exitSeen) {
	    Transfer(rmt, 1);
	}
    }
}


/*-
 *-----------------------------------------------------------------------
 * DoExit --
 *	Exit in the same way the remote process did.
 *
 * Results:
 *	None.
 *
 * Side Effects:
 *	The process will change state, either stopping, dying horribly
 *	or just exiting cleanly.
 *
 *-----------------------------------------------------------------------
 */
void
DoExit()
{
    /*
     * Indicate exit is handled to avoid duplication.
     */
    exitSeen = False;

    if (WIFSTOPPED(status)) {
	SIGSET_T mask, oldmask;

	SIGNAL(WSTOPSIG(status), SIG_DFL);

	SIGEMPTYSET(mask);
	SIGSETMASK(mask, oldmask);

	kill (getpid(), WSTOPSIG(status));

	SIGSETMASK(oldmask, mask);

	SIGNAL(WSTOPSIG(status), PassSig);
    } else if (WIFSIGNALED(status)) {
	/*
	 * We don't want our core dump messing up the other one,
	 * so we change to a (we hope) non-writable directory before
	 * commiting suicide.
	 */
	chdir ("/");
	SIGNAL(WTERMSIG(status), SIG_DFL);
	kill (getpid(), WTERMSIG(status));
    } else {
	exit (WEXITSTATUS(status));
    }
}

    
/*-
 *-----------------------------------------------------------------------
 * main --
 *	Usage:
 *	    export -id connection-fd return-fd id
 *	    export [-same] [-uselocal] [-exclusive] [-force] \
 *	    	   [-attr <string> ...] [<command>]
 *
 *	In the first form, the idea is the exporting program will have
 *	contacted the customs agent already and told it what to do. This
 *	program then simply shuffles I/O and passes signals and the exit
 *	status along...
 *
 *	In the second form, this will arrange for exportation of the given
 *	command.  The export process will then forward any I/O and signals
 *	to/from the remote process.  If no command is supplied, the user's
 *	SHELL is executed.  If the command cannot be exported it is run
 *	locally.  Any number of -attr flags may be used to restrict the
 *	choice of remote hosts. The -same, -uselocal, and -exclusive flags
 *	add the EXPORT_SAME, EXPORT_USELOCAL and EXPORT_EXCLUSIVE options
 *	to the export request, respectively.
 *
 * Results:
 *	The exit status of the remote process.
 *
 * Side Effects:
 *	Well...
 *
 *-----------------------------------------------------------------------
 */
main (argc, argv)
    int	    argc; 	/* Number of arguments */
    char    **argv;	/* The arguments themselves */
{
    ExportPermit  	permit;
    int	    	  	raLen;
    struct servent  	*sep;
    SIGRET		(*oldhandler)();

    if (progname = strrchr(argv[0], '/')) {
	progname++;
    } else {
	progname = argv[0];
    }

    if (argc > 1 && strcmp (argv[1], "-id") == 0) {
	(void)setuid(getuid());
	if (argc < 5) {
	    fprintf(stderr,
			"Usage:\n\t%s -id <connection-fd> <return-fd> <id>\n",
			progname);
	    exit(2);
	}
	rmt = atoi(argv[2]);
	ret = atoi(argv[3]);
	(void)sscanf(argv[4], "%x", &id);
	permit.id = 0;
    } else {
	Lst 	attributes;
	int	flags;

	flags = 0;
	attributes = Lst_Init(FALSE);

	/*
	 * Parse attribute options
	 */
	for (argv++, argc--; argc > 0; argv++, argc--) {
	    if (strcmp (argv[0], "-same") == 0) {
		flags |= EXPORT_SAME;
	    } else if (strcmp (argv[0], "-uselocal") == 0) {
		flags |= EXPORT_USELOCAL;
	    } else if (strcmp (argv[0], "-exclusive") == 0) {
		flags |= EXPORT_EXCLUSIVE;
	    } else if (strcmp (argv[0], "-force") == 0) {
		flags |= EXPORT_FORCE;
	    } else if (strcmp (argv[0], "-attr") == 0) {
		if (argc == 1) {
		    fprintf(stderr, "%s: missing attribute value\n", progname);
		    exit (2);
		} else {
		    (void)Lst_AtEnd (attributes, (ClientData)argv[1]);
		    argv++; argc--;
		}
	    } else if (*argv[0] == '-') {
		fprintf(stderr,
"Usage:\n\t%s [-same] [-uselocal] [-exclusive] [-force] \n\
\t\t[-attr <string> ...] <command>\n",
				progname);
		exit (2);
	    } else {
		break;
	    }
	}

	/*
	 * If no command was given, use SHELL
	 */
	if (argc == 0) {
	    argv--;
	    argc++;

	    argv[0] = getenv ("SHELL");
	    if (!argv[0]) {
		argv[0] = "/bin/sh";
	    }
	}
	/*
	 * Start export.
	 */
	Customs_Init();
	(void)setuid(getuid());
	rmt = Customs_RawExportAttr(argv[0], argv, (char *)NULL, flags,
				    attributes, &customs_Socket, &permit);
	if (rmt < 0) {
	    if ((rmt == CUSTOMS_NOEXPORT) && !permit.id) {
		fprintf(stderr, "%s: no suitable host available\n", progname);
		fflush(stderr);

		/*
		 * The local host doesn't satisfy the requested attributes,
		 * so it's no use running the command locally.
		 */
		exit (1);
	    } else if ((rmt == CUSTOMS_NOEXPORT) && (flags&EXPORT_USELOCAL)) {
		/*
		 * Skip error message if -uselocal was given
		 */
		; 
	    } else {
		/*
		 * Some unexpected error -- report it.
		 * Print hostname for remote import errors.
		 */
		fprintf(stderr, "%s: %s\n", progname,
				Customs_ErrorMessage(rmt, &permit.addr));
		fflush(stderr);
	    }

	    execvp(argv[0], &argv[0]);
	    fprintf(stderr, "%s: %s: %s\n", progname, argv[0], strerror(errno));
	    exit(3);
	} else {
	    ret = customs_Socket;
	    id = permit.id;
	}
	Lst_Destroy (attributes, NOFREE);
    }

    /*
     * Install RPC server for the remote server to return the exit status
     * of the process.
     */
    Rpc_ServerCreate(ret, (Rpc_Proc)CUSTOMS_EXIT, Exit,
		     SwapExit, Rpc_SwapNull, (Rpc_Opaque)0);
    
    /*
     * Print startup messages regardless of tostop
     */
    oldhandler = SIGNAL(SIGTTOU, SIG_IGN);

    raLen = sizeof(server);
    if (getpeername (rmt, (struct sockaddr *)&server, &raLen) < 0) {
	fprintf(stderr, "%s: getpeername: %s\n", progname, strerror(errno));
	exit(2);
    }

    /*
     * Make sure the other side can send us its return status
     */
    if (!Rpc_IsAllowed(ret, &server)) {
 	(void)Rpc_Allow(server.sin_addr.s_addr);
    }

    if (permit.id != 0) {
	/*
	 * Only do this if we did the exportation ourselves
	 */
	fprintf(stderr, "*** exported to %s\n",
			Customs_Hostname(&server.sin_addr));
	fflush(stderr);
    }

    SIGNAL(SIGTTOU, oldhandler);

    SIGNAL(SIGHUP, PassSig);
    SIGNAL(SIGINT, PassSig);
    SIGNAL(SIGQUIT, PassSig);
    SIGNAL(SIGTERM, PassSig);
    SIGNAL(SIGTSTP, PassSig);
    SIGNAL(SIGCONT, PassSig);
    /*
     * SIGTTIN and SIGTTOU are best handled locally by default action
     * and not forwarded to the remote process.  This is because we have no
     * good way of restarting the read/write calls in Transfer().
     */
#ifdef SIGWINCH
    SIGNAL(SIGWINCH, PassSig);
#endif
#ifdef SIGWINDOW
    SIGNAL(SIGWINDOW, PassSig);
#endif
#ifdef SIGXCPU
    SIGNAL(SIGXCPU, PassSig);
#endif
    SIGNAL(SIGUSR1, PassSig);
    SIGNAL(SIGUSR2, PassSig);

    SIGNAL(SIGPIPE, SIG_IGN);

    /*
     * We want to avoid I/O deadlock, so place the remote socket into
     * non-blocking mode, allowing us to delay transfering our own input until
     * the remote side can handle it while still accepting its output.
     */
    fcntl(rmt, F_SETFL, FNDELAY);

    /*
     * Set up an event that checks on the existence of the remote process,
     * so we don't hang around forever in case the host machine dies etc.
     * This is necessary since, in the absence of input the remote job,
     * the TCP connection would never generate an error for us (SO_KEEPALIVE
     * doesn't seem to work).
     */
#ifdef REMOTE_CHECK_INTERVAL
    checkInterval.tv_sec = REMOTE_CHECK_INTERVAL;
    checkInterval.tv_usec = 0;
    checkEvent = Rpc_EventCreate(&checkInterval, CheckRemote, (Rpc_Opaque)0);
#else
    checkEvent = (Rpc_Event)0;
#endif

    /*
     * Install Drain as the initial stream server for rmt to make
     * sure it's writable before bothering to read anything from 0...
     */
    Rpc_Watch(rmt, RPC_READABLE|RPC_WRITABLE, Drain, (Rpc_Opaque)0);
    Rpc_Run();
}

