/* share/src/util $Id$
 *	Author:	Roger A. Cole
 *	Date:	10-24-90
 *
 *	Experimental Physics and Industrial Control System (EPICS)
 *
 *	Copyright 1991, the Regents of the University of California,
 *	and the University of Chicago Board of Governors.
 *
 *	This software was produced under  U.S. Government contracts:
 *	(W-7405-ENG-36) at the Los Alamos National Laboratory,
 *	and (W-31-109-ENG-38) at Argonne National Laboratory.
 *
 *	Initial development by:
 *		The Controls and Automation Group (AT-8)
 *		Ground Test Accelerator
 *		Accelerator Technology Division
 *		Los Alamos National Laboratory
 *
 *	Co-developed with
 *		The Controls and Computing Group
 *		Accelerator Systems Division
 *		Advanced Photon Source
 *		Argonne National Laboratory
 *
 * Modification Log:
 * -----------------
 * .01 10-24-90 rac	initial version
 * .02 07-30-91 rac	installed in SCCS; changed to use EPICS_ENV..
 * .03 09-11-91 joh	updated for v5 vxWorks
 * .04 12-08-91 rac	rewrite for Unix-only, without LWP
 *
 * make options
 *	-DNDEBUG	don't compile assert() checking
 */
/*+/mod***********************************************************************
* TITLE	cmdProto - prototype for a multi-threaded program with command interface
*
* DESCRIPTION
*	This skeleton can be used as the basis for a multi-threaded program
*	which interfaces to a user with text strings, as from a keyboard.
*	Several generic capabilities are provided:
*
*	o   scanning of input control lines
*	o   re-directing of input to come from a file
*	o   running as a stand-alone program, or as a server or client,
*	    with socket-based communications
*	o   on-line help
*	o   handling cleanup on exception conditions (such as bus error)
*
*	To run as a server, specify "server" as a command line option.
*
*	To run as a client, specify "hostName" as a command line option.
*
*	Under SunOS, running under dbx doesn't allow testing the error
*	handling features of the code--the debugger catches the signals.
*	To do such testing, run without dbx.
*
* To use this skeleton:
*
*	Change Zzz, zzz, and ZZZ to a desired name
*	Customize a context block
*	Remove the "server" items, if they aren't going to be used
*	Add additional commands in zzzCmdCrunch
*	Add additional help info in zzzInitAtStartup.  Usually, this
*		will mean adding to helpCmdsSpec and helpUsage.  For
*		any commands needing an individual help topic, then a
*		specific HELP_TOPIC will have to be defined.
*	If you're using sockets, assign a port number in <ports.h>, and
*		change glZzzIPPort to the IPPORT_xxx you've chosen.
*
*	SunOS
*	o   link with genLib.a
*
* BUGS
* o	this program should use tasks.h to establish priorities, stack
*	sizes, etc.
* o	not all signals are caught
* o	using this skeleton, it isn't possible to have several invocations
*	of the program running simultaneously
*
*-***************************************************************************/
#include <genDefs.h>
#include <cmdDefs.h>
#include <envDefs.h>
#include <ezsSockSubr.h>
#include <cadef.h>

#include <stdio.h>
#include <ctype.h>
#include <signal.h>
#include <setjmp.h>


/*/subhead ZZZ_CTX-------------------------------------------------------
* ZZZ_CTX - context information
*
*	A zzz descriptor is the `master handle' which is used for
*	handling business.
*----------------------------------------------------------------------------*/

typedef struct zzzCtx {
    jmp_buf     sigEnv;         /* environment for longjmp at signal time */
    int		listenSock;	/* fd for server's "listen" socket */
    int		inUse;		/* 1 says "presently engaged" by a client */
    int		inUseInit;	/* 1 says initialized */
    char	*inUseName;	/* name of client's host */
    char        *oldPrompt;	/* prompt prior to socket connect */
    int		stop;		/* 1 says stop operations */
    int		clientSock;	/* fd for socket to client */
    int         reopenIO;       /* std... should be re-directed */
    int         reopenIOoption; /* 0,1,2; option for arFreopenAllStd */
    int		oldStdinFd;	/* original fd for stdin, before redirect */
    int		oldStdoutFd;	/* original fd for stdout, before redirect */
    int		oldStderrFd;	/* original fd for stderr, before redirect */
} ZZZ_CTX;

/*-----------------------------------------------------------------------------
* prototypes
*----------------------------------------------------------------------------*/
int zzz();
void zzzCmdCrunch();
void zzzListenCheck();
void zzzListenSetReopen();
void zzzSigHandler();
long zzzInitAtStartup();

/*-----------------------------------------------------------------------------
* global definitions
*----------------------------------------------------------------------------*/
static CX_CMD	glZzzCxCmd;
static CX_CMD	*pglZzzCxCmd=NULL;
static ZZZ_CTX	glZzzCtx;
static ZZZ_CTX	*pglZzzCtx=NULL;
static char	*glZzzId="zzz 11/27/90";
static int	glZzzIPPort;
static int	serverMode=0;
static chid	pCh;


/*+/subr**********************************************************************
* NAME	main
*
* DESCRIPTION
*	This program is a prototype skeleton for building programs
*	which interact with a keyboard/window environment.  The program
*	can be started with any of:
*
*	% cmdProto		(runs as normal interactive program)
*	% cmdProto server	(runs as a server)
*	% cmdProto server&	(runs as a server)
*	% cmdProto hostName	(runs as a client to cmdProto on hostName)
*
*-*/
main(argc, argv)
int     argc;		/* number of command line args */
char   *argv[];		/* command line args */
{
    long	stat;		/* status return from calls */
    char	portText[10];
    int		sigNum;
    int		gooseOutput=0;

    setbuf(stdout, NULL);
    pglZzzCtx = &glZzzCtx;
    pglZzzCxCmd = &glZzzCxCmd;

    stat = zzzInitAtStartup(pglZzzCtx, pglZzzCxCmd);
    assertAlways(stat == OK);

    if (argc > 2) {
	printf("Usage: %s [server or hostName]\n", argv[0]);
	exit(1);
    }
    else if (argc == 2) {
	if (envGetConfigParam(&EPICS_CMD_PROTO_PORT, 10, portText) == NULL) {
	    printf("error getting %s\n", EPICS_CMD_PROTO_PORT.name);
	    return ERROR;
	}
	sscanf(portText, "%d", &glZzzIPPort);
	if (strcmp(argv[1], "server") != 0) {
	    execlp("cmdClient", "cmdClient", argv[1], portText, (char *)0);
	    (void)printf("couldn't exec to cmdClient\n");
	    return ERROR;
	}
	serverMode = 1;
	pglZzzCtx->inUseName = argv[1];
/*-----------------------------------------------------------------------------
*	establish a socket to use to listen for potential clients to connect
*----------------------------------------------------------------------------*/
	if (ezsCreateListenSocket(&pglZzzCtx->listenSock, glZzzIPPort) != 0) {
	    perror("create listen socket");
	    exit(1);
	}
    }
    else
	pglZzzCtx->inUseName = "shell";

#if 1
    stat = ca_task_initialize();
    assert(stat == ECA_NORMAL);
#endif

    genSigInit(zzzSigHandler);
    if ((sigNum = setjmp(pglZzzCtx->sigEnv)) != 0) {
	(void)printf("zzzTask signal received: %d\n", sigNum);
	goto zzzTaskWrapup;
    }

    while (!pglZzzCtx->stop) {
	if (serverMode)
	    zzzListenCheck(&pglZzzCxCmd);
	if (!serverMode || pglZzzCtx->inUse) {
#if 0
	    if (gooseOutput == 0)
		gooseOutput = 30 * pglZzzCxCmd->promptFlag;
#endif
	    if (cmdRead(&pglZzzCxCmd) != NULL) {
		if (serverMode && pglZzzCxCmd->inputEOF) {
		    pglZzzCxCmd->pCommand = "close";
		    pglZzzCxCmd->inputEOF = 0;
		}
		zzzCmdCrunch(&pglZzzCxCmd, pglZzzCtx);
	    }
#if 0
	    if (gooseOutput && serverMode && pglZzzCtx->inUse) {
		if (gooseOutput % 2 == 0) {
		    (void)printf("\001");
		}
		gooseOutput--;
	    }
#endif
	}
#if 1
	stat = ca_pend_event(0.001);
	assert(stat != ECA_EVDISALLOW);
#endif
        fflush(stdout);         	/* flushes needed for socket I/O */
        fflush(stderr);
        fflush(pglZzzCxCmd->dataOut);
	sleep(1);
/* MDA - usleep isn't POSIX
	usleep(100000);		/* sleep .1 second */
*/
    }

    return OK;

zzzTaskWrapup:
    if (pglZzzCtx->reopenIO) {
	(void)zzzFreopenAllStd(pglZzzCtx,pglZzzCtx->reopenIOoption);
	pglZzzCtx->reopenIO = 0;
    }
    zzzListenQuit(&pglZzzCxCmd);

    /* put code for cleanup here */

#if 1
    stat = ca_task_exit();
    if (stat != ECA_NORMAL)
	(void)printf("zzz: ca_task_exit error: %s\n", ca_message(stat));
#endif

    if (pglZzzCxCmd->dataOutRedir)
	fclose(pglZzzCxCmd->dataOut);

    fflush(stdout);		/* flushes needed for sockets */
    fflush(stderr);
}

/*+/subr**********************************************************************
* NAME	zzzSigHandler - signal handling
*
* DESCRIPTION
*	These routines set up for the signals to be caught for zzzTask
*	and handle the signals when they occur.
*
* RETURNS
*	void
*
* BUGS
* o	not all signals are caught
* o	it's not clear how useful it is to catch the signals which originate
*	from the keyboard
*
*-*/
static void
zzzSigHandler(signo)
int	signo;
{
    if (signo == SIGPIPE) {
	if (serverMode)
	    zzzListenClose(&pglZzzCxCmd);
	return;
    }
    printf("entered zzzSigHandler for signal:%d\n", signo);
    signal(signo, SIG_DFL);
    longjmp(pglZzzCtx->sigEnv, signo);
}

/*+/subr**********************************************************************
* NAME	zzzCmdCrunch - process a command line
*
* DESCRIPTION
*
* RETURNS
*	void
*
* BUGS
* o	text
*
*-*/
static void
zzzCmdCrunch(ppCxCmd, pZzzCtx)
CX_CMD	**ppCxCmd;	/* IO ptr to pointer to command context */
ZZZ_CTX *pZzzCtx;	/* IO pointer to zzz context */
{
    CX_CMD	*pCxCmd;	/* local copy of pointer, for convenience */

    pCxCmd = *ppCxCmd;
    if (strcmp(pCxCmd->pCommand,			"assert0") == 0) {
/*-----------------------------------------------------------------------------
*	under SunOS, this generates SIGABRT
*----------------------------------------------------------------------------*/
	assertAlways(0);
    }
    else if (strcmp(pCxCmd->pCommand,			"quit") == 0) {
	pZzzCtx->stop = 1;
	if (serverMode)
	    zzzListenClose(ppCxCmd);
    }
    else if (strcmp(pCxCmd->pCommand,			"trap") == 0) {
	int	*j;
	j = (int *)(-1);
	j = (int *)(*j);
    }
    else if (strcmp((*ppCxCmd)->pCommand,		"close") == 0) {
	if (*ppCxCmd != (*ppCxCmd)->pCxCmdRoot)
	    (void)printf("close illegal in source'd file\n");
	else if (strcmp(pglZzzCtx->inUseName, "shell") == 0)
	    (void)printf("close illegal--zzz not running as server\n");
	else if (serverMode) {
	    zzzListenClose(ppCxCmd);
	}
    }
    else if (strcmp((*ppCxCmd)->pCommand,		"dataOut") == 0) {
	if ((*ppCxCmd)->dataOutRedir)
	    fclose ((*ppCxCmd)->dataOut);
	(*ppCxCmd)->dataOutRedir = 0;
	if (nextNonSpaceField( &(*ppCxCmd)->pLine, &(*ppCxCmd)->pField,
						&(*ppCxCmd)->delim) < 1)
	    (*ppCxCmd)->dataOut = stdout;
	else {
	    (*ppCxCmd)->dataOut = fopen((*ppCxCmd)->pField, "a");
	    if ((*ppCxCmd)->dataOut == NULL) {
		(void)printf("couldn't open %s\n", (*ppCxCmd)->pField);
		(*ppCxCmd)->dataOut = stdout;
		(*ppCxCmd)->dataOutRedir = 0;
	    }
	    else
		(*ppCxCmd)->dataOutRedir = 1;
	}
    }
    else if (strcmp((*ppCxCmd)->pCommand,		"search") == 0) {
	ca_search("jjj", &pCh);
	ca_pend_io(.001);
    }
    else if (strcmp((*ppCxCmd)->pCommand,		"clear") == 0) {
	ca_clear_channel(pCh);
	ca_pend_io(.001);
    }
    else if (strcmp((*ppCxCmd)->pCommand,		"server") == 0) {
	(void)printf( "\"server\" legal only on command line\n");
	(void)printf( "use   help usage   for more information\n");
    }
    else if (strcmp((*ppCxCmd)->pCommand,		"source") == 0) {
	cmdSource(ppCxCmd);
    }
    else {
/*----------------------------------------------------------------------------
* help (or illegal command)
*----------------------------------------------------------------------------*/
	(void)nextNonSpaceField(&pCxCmd->pLine, &pCxCmd->pField,
							&pCxCmd->delim);
	helpIllegalCommand(stdout, &pCxCmd->helpList, pCxCmd->pCommand,
							pCxCmd->pField);
    }

    return;
}

/*+/subr**********************************************************************
* NAME	zzzListenCheck - listen for client connections
*
* DESCRIPTION
*
* RETURNS
*
* BUGS
* o	cleanup of listen socket isn't dealt with.  Under SunOS, the system
*	seems to clean up OK.
*
*	In fact, under SunOS, shutdown and/or close seem to inhibit proper
*	cleanup by the system.
*
* SEE ALSO
*
* EXAMPLE
*
*-*/
void
zzzListenCheck(ppCxCmd)
CX_CMD	**ppCxCmd;	/* IO ptr to pointer to command context */
{
    long	stat;
    char	clientName[30];
    static char	sockPrompt[80];

/*-----------------------------------------------------------------------------
*	check to see if a connect attempt has been made.  If the server is
*	already in use by a client, connect attempts will be rejected.
*----------------------------------------------------------------------------*/
    if (ezsListenExclusiveUse(&pglZzzCtx->clientSock,
			    &pglZzzCtx->listenSock, &pglZzzCtx->inUse,
			    clientName, glZzzId) < 0) {
	perror("check for connect attempt");
	exit(1);
    }

    if (pglZzzCtx->inUse && pglZzzCtx->inUseInit == 0) {
/*-----------------------------------------------------------------------------
*	When a connect attempt succeeds, change stdin and stdout to use
*	the socket, so that server I/O (i.e., I/O for this program) will
*       automatically be re-directed to the client.  This must be done
*	for all tasks in this program.
*
*	The prompt is changed to a special prompt which ends witn \n.  This
*	is done to avoid buffer flushing problems on some operating systems.
*	A special prefix of "#p#r#" is added to the normal prompt so that
*	the client knows that some massaging needs to be done before
*	displaying the prompt to the user.
*----------------------------------------------------------------------------*/
	(void)printf("%s connected\n", clientName);
	if (zzzFreopenAllStd(pglZzzCtx, 1) != OK)
	    assertAlways(0);
	pglZzzCtx->oldPrompt = (*ppCxCmd)->prompt;
	assert(strlen(pglZzzCtx->oldPrompt) < 72);
	(void)sprintf(sockPrompt, "#p#r#%s\n", pglZzzCtx->oldPrompt);
	(*ppCxCmd)->prompt = sockPrompt;
	zzzListenSetReopen(2);	/* reopen all other tasks to socket */
	pglZzzCtx->inUseInit = 1;
	(*ppCxCmd)->promptFlag = 1;
#if 0
	stat = zzzStartup(serverMode, clientName);
	assertAlways(stat == OK);
#endif
    }

}

/*+/subr**********************************************************************
* NAME	zzzListenClose
*
*-*/
long
zzzListenClose(ppCxCmd)
CX_CMD	**ppCxCmd;	/* IO ptr to pointer to command context */
{
    if (pglZzzCtx->inUse) {
#if 0
	shutdown(pglZzzCtx->clientSock, 2);
#endif
	close(pglZzzCtx->clientSock);
	pglZzzCtx->clientSock = -1;
	pglZzzCtx->inUse = 0;
	pglZzzCtx->inUseInit = 0;
	if (zzzFreopenAllStd(pglZzzCtx, 0) != OK)
	    assertAlways(0);
	(void)printf("%s disconnected\n", pglZzzCtx->inUseName);
	(*ppCxCmd)->prompt = pglZzzCtx->oldPrompt;
	zzzListenSetReopen(0);	/* reopen all other tasks to std... */
    }
}

/*+/subr**********************************************************************
* NAME	zzzListenQuit
*
*-*/
long
zzzListenQuit(ppCxCmd)
CX_CMD	**ppCxCmd;	/* IO ptr to pointer to command context */
{
    if (pglZzzCtx->clientSock != -1) {
	shutdown(pglZzzCtx->clientSock, 2);
	close(pglZzzCtx->clientSock);
    }
#if 0
    shutdown(pglZzzCtx->listenSock, 2);
    close(pglZzzCtx->listenSock);
#endif
    return OK;
}

/*+/subr**********************************************************************
* NAME	zzzListenSetReopen
*
*-*/
void
zzzListenSetReopen(option)
int	option;		/* I option for reopen for use with zzzFreopenAllStd */
{
    pglZzzCtx->reopenIOoption = option;
    pglZzzCtx->reopenIO = 1;
}

/*+/subr**********************************************************************
* NAME	zzzFreopenAllStd - reopen stdin, stdout, and stderr
*
* DESCRIPTION
*
* RETURNS
*	OK, or
*	ERROR	(an error message has been printed)
*
* BUGS
* o	text
*
*-*/
static int
zzzFreopenAllStd(pCtx, option)
ZZZ_CTX *pCtx;		/* IO pointer to zzz context */
int	option;		/* I activity selector:
				0  set back to use old fd's
				1  use client socket, save old fd's
				2  use client socket, don't save old fd's */
{
    fflush(stdout);
    fflush(stderr);
    if (option == 0) {
	if (ezsFreopenToFd(stdin, &pCtx->oldStdinFd, NULL) < 0) {
	    perror("restore stdin");
	    return ERROR;
	}
	if (ezsFreopenToFd(stdout, &pCtx->oldStdoutFd, NULL) < 0) {
	    perror("restore stdout");
	    return ERROR;
	}
	if (ezsFreopenToFd(stderr, &pCtx->oldStderrFd, NULL) < 0) {
	    perror("restore stderr");
	    return ERROR;
	}
    }
    else if (option == 1) {
	if (ezsFreopenToFd(stdin, &pCtx->clientSock, &pCtx->oldStdinFd) < 0) {
	    perror("reopen stdin");
	    return ERROR;
	}
	if (ezsFreopenToFd(stdout, &pCtx->clientSock, &pCtx->oldStdoutFd) < 0) {
	    perror("reopen stdout");
	    return ERROR;
	}
	if (ezsFreopenToFd(stderr, &pCtx->clientSock, &pCtx->oldStderrFd) < 0) {
	    perror("reopen stderr");
	    return ERROR;
	}
    }
    else if (option == 2) {
	if (ezsFreopenToFd(stdin, &pCtx->clientSock, NULL) < 0) {
	    perror("reopen stdin");
	    return ERROR;
	}
	if (ezsFreopenToFd(stdout, &pCtx->clientSock, NULL) < 0) {
	    perror("reopen stdout");
	    return ERROR;
	}
	if (ezsFreopenToFd(stderr, &pCtx->clientSock, NULL) < 0) {
	    perror("reopen stderr");
	    return ERROR;
	}
    }
    return OK;
}

/*+/subr**********************************************************************
* NAME	zzzInitAtStartup - initialization for zzz
*
* DESCRIPTION
*	Perform several initialization duties:
*	o   initialize the zzz context
*	o   initialize the command context block
*	o   initialize the help information
*
* RETURNS
*	OK, or
*	ERROR
*
* BUGS
* o	text
*
*-*/
static long
zzzInitAtStartup(pZzzCtx, pCxCmd)
ZZZ_CTX	*pZzzCtx;
CX_CMD	*pCxCmd;
{
    /* code to initialize zzz context here */
    pZzzCtx->inUse = 0;
    pZzzCtx->listenSock = -1;
    pZzzCtx->clientSock = -1;
    pZzzCtx->oldStdinFd = -1;
    pZzzCtx->oldStdoutFd = -1;
    pZzzCtx->oldStderrFd = -1;

    cmdInitContext(pCxCmd, "  zzz:  ");

/*-----------------------------------------------------------------------------
* help information initialization
*----------------------------------------------------------------------------*/
    helpInit(&pCxCmd->helpList);
/*-----------------------------------------------------------------------------
* help info--generic commands
*----------------------------------------------------------------------------*/
	helpTopicAdd(&pCxCmd->helpList, &pCxCmd->helpCmds, "commands", "\n\
Generic commands are:\n\
   bg\n\
   close                     (use    help usage   for more info)\n\
   dataOut    [filePath]     (default is to normal output)\n\
   help       [topic]\n\
   quit (or ^D)              (use    help usage   for more info)\n\
   source     filePath\n\
");
/*-----------------------------------------------------------------------------
* help info--bg command
*----------------------------------------------------------------------------*/
    helpTopicAdd(&pCxCmd->helpList, &pCxCmd->helpBg, "bg", "\n\
Under SunOS, no bg command is directly available.  Instead, if zzz\n\
is being run from the C shell (csh), it can be put in the background\n\
using several steps from the keyboard (with the first % on each line being\n\
the prompt from csh):\n\
	type ^Z, then type\n\
	% bg %zzz\n\
\n\
To move zzz to the foreground, type\n\
	% fg %zzz\n\
");
/*-----------------------------------------------------------------------------
* help info--zzz-specific commands
*----------------------------------------------------------------------------*/
	helpTopicAdd(&pCxCmd->helpList, &pCxCmd->helpCmdsSpec, "commands", "\n\
zzz-specific commands are:\n\
   abcdef\n\
\n\
Output from commands flagged with * can be routed to a file by using the\n\
\"dataOut filePath\" command.  The present contents of the file are\n\
preserved, with new output being written at the end.\n\
");
/*-----------------------------------------------------------------------------
* help info--zzz usage information
*----------------------------------------------------------------------------*/
	helpTopicAdd(&pCxCmd->helpList, &pCxCmd->helpUsage, "usage", "\n\
zzz performs some functions.  This is a skeleton program.\n\
\n\
For information on moving zzz into the background, use the \"help bg\"\n\
command.\n\
\n\
zzz can be run as a server by:\n\
    under SunOS:    % zzz server\n\
\n\
zzz can be run as a client connecting to a zzz server on \"hostName\" by:\n\
    under SunOS:    % zzz hostName\n\
\n\
    When running zzz as a client, two commands are of special interest:\n\
\n\
    close	shuts down the client but leaves the server running\n\
    quit	shuts down both the client and the server\n\
");
    return OK;
}
