/*
 * tnmSnmpTcl.c --
 *
 *	This module extends a Tcl interpreter with the ability to talk
 *	SNMP (Version 1 as well as Version 2).
 *
 * Copyright (c) 1994-1996 Technical University of Braunschweig.
 * Copyright (c) 1996-1997 University of Twente.
 *
 * See the file "license.terms" for information on usage and redistribution
 * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
 */

#include "tnmSnmp.h"
#include "tnmMib.h"

/*
 * The global variable TnmSnmp list contains all existing
 * session handles.
 */

TnmSnmp *tnmSnmpList = NULL;

int hexdump = 0;

/*
 * A hash table to store agent session configurations.
 */

static Tcl_HashTable aliasTable;

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

static void
EvalCmdProc	_ANSI_ARGS_((TnmSnmp *session, TnmSnmpPdu *pdu, 
			     ClientData clientData));
static void
AsyncWalkProc	_ANSI_ARGS_((TnmSnmp *session, TnmSnmpPdu *pdu, 
			     ClientData clientData));
static char *
GetOption	_ANSI_ARGS_((Tcl_Interp *interp, ClientData object, 
			     int option));
static int
SetOption	_ANSI_ARGS_((Tcl_Interp *interp, ClientData object, 
			     int option, char *value));
static int
Configured	_ANSI_ARGS_((Tcl_Interp *interp, TnmSnmp *session));

static int
ManagerCmd	_ANSI_ARGS_((ClientData	clientData, Tcl_Interp *interp,
			     int argc, char **argv));
static int
AgentCmd	_ANSI_ARGS_((ClientData	clientData, Tcl_Interp *interp,
			     int argc, char **argv));
static int
WaitSession	_ANSI_ARGS_((Tcl_Interp *interp, TnmSnmp *session, 
			     char *id));
static void
DestroySession	_ANSI_ARGS_((ClientData clientdata));

static int
Request		_ANSI_ARGS_((Tcl_Interp *interp, TnmSnmp *session,
			     int pdu_type, int argc, char **argv));
static int
AsyncWalk	_ANSI_ARGS_((Tcl_Interp *interp, TnmSnmp *session,
			     char *vbl, char *command));
static int
Walk		_ANSI_ARGS_((Tcl_Interp *interp, TnmSnmp *session,
			     char *varName, char *vbl, char *command));
static int
ExpandTable	_ANSI_ARGS_((Tcl_Interp *interp, 
			     char *tList, Tcl_DString *dst));
static int
ExpandScalars	_ANSI_ARGS_((Tcl_Interp *interp, 
			     char *sList, Tcl_DString *dst));
static int
Table		_ANSI_ARGS_((Tcl_Interp *interp, TnmSnmp *session,
			     char *table, char *arrayName));
static int
Scalars		_ANSI_ARGS_((Tcl_Interp *interp, TnmSnmp *session,
			     char *group, char *arrayName));
static void
ScalarSetVar	_ANSI_ARGS_((Tcl_Interp *interp, char *vbl,
			     char *varName, Tcl_DString *result));

/*
 * The options used to configure snmp session objects.
 */

#define TNM_SNMP_OPT_ADDRESS	     1
#define TNM_SNMP_OPT_TRAPADDRESS     2
#define TNM_SNMP_OPT_PORT	     3
#define TNM_SNMP_OPT_TRAPPORT	     4
#define TNM_SNMP_OPT_VERSION	     5
#define TNM_SNMP_OPT_READCOMMUNITY   6
#define TNM_SNMP_OPT_WRITECOMMUNITY  7
#define TNM_SNMP_OPT_TRAPCOMMUNITY   8
#define TNM_SNMP_OPT_USER	     9
#define TNM_SNMP_OPT_PASSWORD	    10
#define TNM_SNMP_OPT_CONTEXT	    11
#define TNM_SNMP_OPT_ALIAS	    13
#define TNM_SNMP_OPT_TIMEOUT	    14
#define TNM_SNMP_OPT_RETRIES	    15
#define TNM_SNMP_OPT_WINDOW	    16
#define TNM_SNMP_OPT_DELAY	    17
#ifdef TNM_SNMP_BENCH
#define TNM_SNMP_OPT_RTT	    18
#define TNM_SNMP_OPT_SENDSIZE	    19
#define TNM_SNMP_OPT_RECVSIZE	    20
#endif

static TnmTable managerOptionTable[] = {
    { TNM_SNMP_OPT_ADDRESS,        "-address" },
    { TNM_SNMP_OPT_TRAPADDRESS,    "-trapaddress" },
    { TNM_SNMP_OPT_PORT,           "-port" },
    { TNM_SNMP_OPT_TRAPPORT,       "-trapport" },
    { TNM_SNMP_OPT_VERSION,        "-version" },
    { TNM_SNMP_OPT_READCOMMUNITY,  "-community" },
    { TNM_SNMP_OPT_WRITECOMMUNITY, "-writecommunity" },
    { TNM_SNMP_OPT_TRAPCOMMUNITY,  "-trapcommunity" },
    { TNM_SNMP_OPT_USER,           "-user" },
    { TNM_SNMP_OPT_PASSWORD,       "-password" },
    { TNM_SNMP_OPT_CONTEXT,        "-context" },
    { TNM_SNMP_OPT_ALIAS,          "-alias" },
    { TNM_SNMP_OPT_TIMEOUT,        "-timeout" },
    { TNM_SNMP_OPT_RETRIES,        "-retries" },
    { TNM_SNMP_OPT_WINDOW,         "-window" },
    { TNM_SNMP_OPT_DELAY,          "-delay" },
#ifdef TNM_SNMP_BENCH
    { TNM_SNMP_OPT_RTT,            "-rtt" },
    { TNM_SNMP_OPT_SENDSIZE,       "-sendSize" },
    { TNM_SNMP_OPT_RECVSIZE,       "-recvSize" },
#endif
    { 0, NULL }
};

static TnmConfig managerConfig = {
    managerOptionTable,
    SetOption,
    GetOption
};

static TnmTable agentOptionTable[] = {
    { TNM_SNMP_OPT_TRAPADDRESS,    "-trapaddress" },
    { TNM_SNMP_OPT_PORT,           "-port" },
    { TNM_SNMP_OPT_TRAPPORT,       "-trapport" },
    { TNM_SNMP_OPT_VERSION,        "-version" },
    { TNM_SNMP_OPT_READCOMMUNITY,  "-community" },
    { TNM_SNMP_OPT_WRITECOMMUNITY, "-writecommunity" },
    { TNM_SNMP_OPT_TRAPCOMMUNITY,  "-trapcommunity" },
    { TNM_SNMP_OPT_USER,           "-user" },
    { TNM_SNMP_OPT_PASSWORD,       "-password" },
    { TNM_SNMP_OPT_CONTEXT,        "-context" },
    { TNM_SNMP_OPT_ALIAS,          "-alias" },
    { TNM_SNMP_OPT_TIMEOUT,        "-timeout" },
    { TNM_SNMP_OPT_RETRIES,        "-retries" },
    { TNM_SNMP_OPT_WINDOW,         "-window" },
    { TNM_SNMP_OPT_DELAY,          "-delay" },
    { 0, NULL }
};

static TnmConfig agentConfig = {
    agentOptionTable,
    SetOption,
    GetOption
};

/*
 * The following structure describes a Tcl command that should be
 * evaluated once we receive a response for a SNMP request.
 */

typedef struct EvalCmd {
    Tcl_Interp *interp;
    char *cmd;
    TnmVector vector;
} EvalCmd;


/*
 *----------------------------------------------------------------------
 *
 * EvalCmdProc --
 *
 *	This procedure is called once we have received the response
 *	for an asynchronous SNMP request. It evaluates a Tcl script.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Arbitrary side effects since commands are evaluated.
 *
 *----------------------------------------------------------------------
 */

static void
EvalCmdProc(session, pdu, clientData)
    TnmSnmp *session;
    TnmSnmpPdu *pdu;
    ClientData clientData;
{
    EvalCmd *ec = (EvalCmd *) clientData;
    TnmSnmpEvalCallback(ec->interp, session, pdu, ec->cmd,
			NULL, NULL, NULL, NULL);
    ckfree((char *) ec);
}

/*
 *----------------------------------------------------------------------
 *
 * EvalCmdProc --
 *
 *	This procedure is called once we have received the response
 *	during an asynchronous SNMP walk. It evaluates a Tcl script
 *	and starts another SNMP getnext if we did not reach the end
 *	of the MIB view.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Arbitrary side effects since commands are evaluated.
 *
 *----------------------------------------------------------------------
 */

static void
AsyncWalkProc(session, pdu, clientData)
    TnmSnmp *session;
    TnmSnmpPdu *pdu;
    ClientData clientData;
{
    EvalCmd *ec = (EvalCmd *) clientData;
    Tcl_Interp *interp = ec->interp;
    char *command = ec->cmd;
    int i;

    if (pdu->error_status == TNM_SNMP_NOERROR) {
	TnmVector vector;
	TnmVectorInit(&vector);
	TnmSnmpVarBindFromString(interp, &vector, 
				 Tcl_DStringValue(&pdu->varbind));
	for (i = 0; i < TnmVectorSize(&vector); i++) {
	    TnmSnmpVarBind *vbPtr = 
		(TnmSnmpVarBind *) TnmVectorGet(&vector, i);
	    TnmSnmpVarBind *stPtr = 
		(TnmSnmpVarBind *) TnmVectorGet(&ec->vector, i);
	    if (TnmSnmpValidException(vbPtr->syntax) 
		|| !TnmOidInTree(&stPtr->oid, &vbPtr->oid)) {
		TnmSnmpVarBindFree(&vector);
		TnmVectorFree(&vector);
		goto done;
	    }
	}
	TnmSnmpVarBindFree(&vector);
	TnmVectorFree(&vector);
	TnmSnmpEvalCallback(interp, session, pdu, command,
			    NULL, NULL, NULL, NULL);
	pdu->type = TNM_SNMP_GETNEXT;
	pdu->request_id = TnmSnmpGetRequestId();
	(void) TnmSnmpEncode(interp, session, pdu, AsyncWalkProc, 
			     (ClientData) ec);
	return;
    }

done:
    TnmSnmpVarBindFree(&ec->vector);
    TnmVectorFree(&ec->vector);
    ckfree((char *) ec);
}

/*
 *----------------------------------------------------------------------
 *
 * GetOption --
 *
 *	This procedure retrieves the value of a session option.
 *
 * Results:
 *	A pointer to the value formatted as a string.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static char *
GetOption(interp, object, option)
    Tcl_Interp *interp;
    ClientData object;
    int option;
{
    TnmSnmp *session = (TnmSnmp *) object;
    static char buffer[256];

    switch (option) {
    case TNM_SNMP_OPT_ADDRESS:
	return inet_ntoa(session->maddr.sin_addr);
#if 0
    case TNM_SNMP_OPT_TRAPADDRESS:
	return inet_ntoa(session->taddr.sin_addr);
#endif
    case TNM_SNMP_OPT_PORT:
	sprintf(buffer, "%u", ntohs(session->maddr.sin_port));
	return buffer;
#if 0
    case TNM_SNMP_OPT_TRAPPORT:
	sprintf(buffer, "%u", ntohs(session->taddr.sin_port));
	return buffer;
#endif
    case TNM_SNMP_OPT_VERSION:
	switch(session->version) {
	case TNM_SNMPv1:
	    return "SNMPv1";
#ifdef TNM_SNMPv2C
	case TNM_SNMPv2C:
	    return "SNMPv2c";
#endif
#ifdef TNM_SNMPv2U
	case TNM_SNMPv2U:
	    return "SNMPv2u";
	}
#endif
    case TNM_SNMP_OPT_READCOMMUNITY:
	if (session->version!=TNM_SNMPv1 && session->version!=TNM_SNMPv2C) {
	    return NULL;
	}
	return session->readCommunity ? session->readCommunity : "";
    case TNM_SNMP_OPT_WRITECOMMUNITY:
	if (session->version!=TNM_SNMPv1 && session->version!=TNM_SNMPv2C) {
	    return NULL;
	}
	return session->writeCommunity;
#if 0
    case TNM_SNMP_OPT_TRAPCOMMUNITY:
	if (session->version!=TNM_SNMPv1 && session->version!=TNM_SNMPv2C) {
	    return NULL;
	}
	return session->trapCommunity;
#endif
#ifdef TNM_SNMPv2U
    case TNM_SNMP_OPT_USER:
	if (session->version != TNM_SNMPv2U) {
	    return NULL;
	}
	memset(buffer, 0, USEC_MAX_USER);
	memcpy(buffer, session->userName, session->userNameLen);
	return buffer;
    case TNM_SNMP_OPT_PASSWORD:
	if (session->version != TNM_SNMPv2U) {
	    return NULL;
	}
	return session->password;
    case TNM_SNMP_OPT_CONTEXT:
	if (session->version != TNM_SNMPv2U) {
	    return NULL;
	}
	memset(buffer, 0, USEC_MAX_CONTEXT);
	memcpy(buffer, session->cntxt, session->cntxtLen);
	return buffer;
#endif
    case TNM_SNMP_OPT_TIMEOUT:
	sprintf(buffer, "%d", session->timeout);
	return buffer;
    case TNM_SNMP_OPT_RETRIES:
	sprintf(buffer, "%d", session->retries);
	return buffer;
    case TNM_SNMP_OPT_WINDOW:
	sprintf(buffer, "%d", session->window);
	return buffer;
    case TNM_SNMP_OPT_DELAY:
	sprintf(buffer, "%d", session->delay);
	return buffer;
#ifdef TNM_SNMP_BENCH
    case TNM_SNMP_OPT_RTT:
	sprintf(buffer, "%ld",
		(session->stats.recvTime.sec 
		 - session->stats.sendTime.sec) * 1000000
		+ (session->stats.recvTime.usec 
		   - session->stats.sendTime.usec));
	return buffer;
    case TNM_SNMP_OPT_SENDSIZE:
	sprintf(buffer, "%d", session->stats.sendSize);
	return buffer;
    case TNM_SNMP_OPT_RECVSIZE:
	sprintf(buffer, "%d", session->stats.recvSize);
	return buffer;
#endif
    }
    return NULL;
}

/*
 *----------------------------------------------------------------------
 *
 * SetOption --
 *
 *	This procedure modifies a single option of a session object.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static int
SetOption(interp, object, option, value)
    Tcl_Interp *interp;
    ClientData object;
    int option;
    char *value;
{
    TnmSnmp *session = (TnmSnmp *) object;
    int num;

    switch (option) {
    case TNM_SNMP_OPT_ADDRESS:
	return TnmSetIPAddress(interp, value, &session->maddr);
    case TNM_SNMP_OPT_TRAPADDRESS:
	return TnmSetIPAddress(interp, value, &session->taddr);
    case TNM_SNMP_OPT_PORT:
	return TnmSetIPPort(interp, "udp", value, &session->maddr);
    case TNM_SNMP_OPT_TRAPPORT:
	return TnmSetIPPort(interp, "udp", value, &session->taddr);
    case TNM_SNMP_OPT_VERSION:
	if (strcmp(value, "SNMPv1") == 0) {
	    session->version = TNM_SNMPv1;
	    return TCL_OK;
	}
#ifdef TNM_SNMPv2C
	if (strcmp(value, "SNMPv2C") == 0 || strcmp(value, "SNMPv2c") == 0) {
	    session->version = TNM_SNMPv2C;
	    return TCL_OK;
	}
#endif
#ifdef TNM_SNMPv2U
	if (strcmp(value, "SNMPv2U") == 0 || strcmp(value, "SNMPv2u") == 0) {
	    session->version = TNM_SNMPv2U;
	    return TCL_OK;
	}
#endif
	Tcl_AppendResult(interp, "unknown SNMP version \"",
			 value, "\"", (char *) NULL);
	return TCL_ERROR;
    case TNM_SNMP_OPT_READCOMMUNITY:
#ifdef TNM_SNMPv2U
	if (session->version == TNM_SNMPv2U) {
	    session->version = TNM_SNMPv1;
	}
#endif
	if (session->readCommunity) ckfree(session->readCommunity);
	session->readCommunity = ckstrdup(value);
	return TCL_OK;
    case TNM_SNMP_OPT_WRITECOMMUNITY:
#ifdef TNM_SNMPv2U
	if (session->version == TNM_SNMPv2U) {
	    session->version = TNM_SNMPv1;
	}
#endif
	if (session->writeCommunity) ckfree(session->writeCommunity);
	session->writeCommunity = (*value) ? ckstrdup(value) : NULL;
	return TCL_OK;
    case TNM_SNMP_OPT_TRAPCOMMUNITY:
#ifdef TNM_SNMPv2U
	if (session->version == TNM_SNMPv2U) {
	    session->version = TNM_SNMPv1;
	}
#endif
	if (session->trapCommunity) ckfree(session->trapCommunity);
	session->trapCommunity = (*value) ? ckstrdup(value) : NULL;
	return TCL_OK;
#ifdef TNM_SNMPv2U
    case TNM_SNMP_OPT_USER:
	session->version = TNM_SNMPv2U;
	if (strlen(value) > USEC_MAX_USER) {
	    Tcl_SetResult(interp, "user name too long", TCL_STATIC);
	    return TCL_ERROR;
	}
	session->userNameLen = strlen(value);
	memcpy(session->userName, value, session->userNameLen);
	return TCL_OK;
    case TNM_SNMP_OPT_PASSWORD:
	session->version = TNM_SNMPv2U;
	if (session->password) {
	    ckfree(session->password);
	    session->password = NULL;
	}
	if (strlen(value) == 0) {
	    session->qos &= ~ USEC_QOS_AUTH;
	} else {
	    session->password = ckstrdup(value);
	    session->qos |= USEC_QOS_AUTH;
	}
	return TCL_OK;
    case TNM_SNMP_OPT_CONTEXT:
	if (strlen(value) > USEC_MAX_CONTEXT) {
	    Tcl_SetResult(interp, "context name too long", TCL_STATIC);
	    return TCL_ERROR;
	}
	session->cntxtLen = strlen(value);
	memcpy(session->cntxt, value, session->cntxtLen);
	return TCL_OK;
#endif
    case TNM_SNMP_OPT_ALIAS:
	{
	    Tcl_HashEntry *entryPtr;
	    int i, largc, code;
	    char **largv, **argv;
	    char *alias;
	    entryPtr = Tcl_FindHashEntry(&aliasTable, value);
	    if (! entryPtr) {
		Tcl_AppendResult(interp, "unknown alias \"",
				 value, "\"", (char *) NULL);
		return TCL_ERROR;
	    }
	    alias = (char *) Tcl_GetHashValue(entryPtr);
	    if (! alias) {
		Tcl_SetResult(interp, "alias loop detected", TCL_STATIC);
		return TCL_ERROR;
	    }
	    if (Tcl_SplitList(interp, alias, &largc, &largv) != TCL_OK) {
		return TCL_ERROR;
	    }
	    Tcl_SetHashValue(entryPtr, NULL);
	    argv = (char **) ckalloc((largc + 3) * sizeof(char *));
	    argv[0] = Tcl_GetCommandName(interp, session->token);
	    argv[1] = "configure";
	    for (i = 0; i < largc; i++) {
		argv[i+2] = largv[i];
	    }
	    code = TnmSetConfig(interp, &managerConfig,
				(ClientData) session, largc + 2, argv);
	    Tcl_SetHashValue(entryPtr, alias);
	    ckfree((char *) argv);
	    ckfree((char *) largv);
	    return code;
	}
    case TNM_SNMP_OPT_TIMEOUT:
	if (TnmGetPositive(interp, value, &num) != TCL_OK) {
	    return TCL_ERROR;
	}
	session->timeout = num;
	return TCL_OK;
    case TNM_SNMP_OPT_RETRIES:
	if (TnmGetUnsigned(interp, value, &num) != TCL_OK) {
	    return TCL_ERROR;
	}
	session->retries = num;
	return TCL_OK;
    case TNM_SNMP_OPT_WINDOW:
	if (TnmGetUnsigned(interp, value, &num) != TCL_OK) {
	    return TCL_ERROR;
	}
	session->window = num;
	return TCL_OK;
    case TNM_SNMP_OPT_DELAY:
	if (TnmGetUnsigned(interp, value, &num) != TCL_OK) {
	    return TCL_ERROR;
	}
	session->delay = num;
	return TCL_OK;
    }

    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * Configured --
 *
 *	This procedure determines if all required parameters for
 *	communication with a remote SNMP entity are set.
 *
 * Results:
 *	A standard TCL result.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static int
Configured(interp, session)
    Tcl_Interp *interp;
    TnmSnmp *session;
{
    char *name = Tcl_GetCommandName(interp, session->token);

    if (! session->version) {
        Tcl_AppendResult(interp, "session \"", name, 
			 "\" not configured", (char *) NULL);
	return TCL_ERROR;
    }

    switch (session->version) {
    case TNM_SNMPv1: 
	if (! session->readCommunity) {
	    Tcl_AppendResult(interp, "session \"", name,
			     "\" has no community string", (char *) NULL);
	    return TCL_ERROR;
	}
	break;
	
#ifdef TNM_SNMPv2C
    case TNM_SNMPv2C: 
	if (! session->readCommunity) {
	    Tcl_AppendResult(interp, "session \"", name,
			     "\" has no community string", (char *) NULL);
	    return TCL_ERROR;
	}
	break;
#endif
	
#ifdef TNM_SNMPv2U
    case TNM_SNMPv2U: 
	if (session->userNameLen == 0) {
	    Tcl_AppendResult(interp, "session \"", name,
			     "\" has no user name", (char *) NULL);
	    return TCL_ERROR;
	}
	break;
#endif
	
    default: 
	Tcl_SetResult(interp, "unknown SNMP version", TCL_STATIC);
	return TCL_ERROR;
    }

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

int
Tnm_SnmpCmd(clientData, interp, argc, argv)
    ClientData clientData;
    Tcl_Interp *interp;
    int argc;
    char **argv;
{
    static int initialized = 0;
    static unsigned nextId = 0;

    TnmSnmp *session;
    int cmd, result = TCL_OK;
    char *name;

    enum commands { 
	cmdAlias, cmdAgent, cmdExpand, cmdInfo, cmdSession, cmdWait, cmdWatch 
    };

    static TnmTable cmdTable[] = {
	{ cmdAlias,	"alias" },	{ cmdAgent,	"agent" },
	{ cmdExpand,	"expand" },	{ cmdInfo,	"info" },
	{ cmdSession,	"session" },	{ cmdWait,	"wait" },
	{ cmdWatch,	"watch" },
	{ 0, NULL }
    };

    if (! initialized) {
	TnmSnmpSysUpTime();
	memset((char *) &tnmSnmpStats, 0, sizeof(TnmSnmpStats));
	Tcl_InitHashTable(&aliasTable, TCL_STRING_KEYS);
	srand(time(NULL) * getpid());
	initialized = 1;
    }

    if (argc < 2) {
	TnmWrongNumArgs(interp, 1, argv, "option ?arg arg ...?");
	return TCL_ERROR;
    }

    cmd = TnmGetTableKey(cmdTable, argv[1]);
    if (cmd == -1) {
	TnmBadOption(interp, argv[1], TnmGetTableValues(cmdTable));
	return TCL_ERROR;
    }

    switch ((enum commands) cmd) {
    case cmdAlias: {
        Tcl_HashEntry *entryPtr;
        if (argc == 2) {
	    Tcl_HashSearch search;
	    entryPtr = Tcl_FirstHashEntry(&aliasTable, &search);
	    while (entryPtr) {
	        Tcl_AppendElement(interp,
				  Tcl_GetHashKey(&aliasTable, entryPtr));
	        entryPtr = Tcl_NextHashEntry(&search);
	    }
	} else if (argc == 3) {
	    entryPtr = Tcl_FindHashEntry(&aliasTable, argv[2]);
	    if (entryPtr) {
	        Tcl_SetResult(interp, (char *) Tcl_GetHashValue(entryPtr),
			      TCL_STATIC);
	    }
	} else if (argc == 4) {
	    int isNew;
	    entryPtr = Tcl_CreateHashEntry(&aliasTable, argv[2], &isNew);
	    if (!isNew) {
		ckfree((char *) Tcl_GetHashValue(entryPtr));
	    }
	    if (*argv[3] == '\0') {
		Tcl_DeleteHashEntry(entryPtr);
	    } else {
		Tcl_SetHashValue(entryPtr, ckstrdup(argv[3]));
	    }
	} else {
	    TnmWrongNumArgs(interp, 2, argv, "?agent? ?config?");
	    result = TCL_ERROR;
	    break;
	}
	break;
    }

    case cmdAgent:
	if (TnmMibLoad(interp) != TCL_OK) {
	    result = TCL_ERROR;
	    break;
	}
	session = TnmSnmpCreateSession();
	session->flags |= TNM_SNMP_AGENT_ROLE;
	session->interp = interp;
	result = TnmSetConfig(interp, &agentConfig, (ClientData) session, 
			      argc, argv);
	if (result == TCL_OK) {
	    result = TnmSnmpAgentInit(interp, session);
	}
	if (result != TCL_OK) {
	    TnmSnmpDeleteSession(session);
	    break;
	}
#ifdef TNM_SNMPv2U
	if (session->version == TNM_SNMPv2U && session->qos & USEC_QOS_AUTH) {
	    TnmSnmpUsecGetAgentID(session);
	}
#endif

	session->nextPtr = tnmSnmpList;
	tnmSnmpList = session;

	/*
	 * Finally create a Tcl command for this session.
	 */
	
	name = TnmGetHandle(interp, "snmp", &nextId);
	session->token = Tcl_CreateCommand(interp, name, AgentCmd,
			  (ClientData) session, DestroySession);
	Tcl_SetResult(interp, name, TCL_STATIC);
	break;

    case cmdExpand: {
	TnmVector vector;
        if (argc != 3) {
	    TnmWrongNumArgs(interp, 2, argv, "varBindList");
	    result = TCL_ERROR;
	    break;
	}
	if (TnmMibLoad(interp) != TCL_OK) {
	    result = TCL_ERROR;
	    break;
	}
	TnmVectorInit(&vector);
	result = TnmSnmpVarBindFromString(interp, &vector, argv[2]);
	if (result == TCL_OK) {
	    Tcl_SetResult(interp, TnmSnmpVarBindToString(&vector), TCL_STATIC);
	}
	TnmSnmpVarBindFree(&vector);
	TnmVectorFree(&vector);
	break;
    }

    case cmdInfo:
        if (argc != 2) {
	    TnmWrongNumArgs(interp, 2, argv, (char *) NULL);
	    result = TCL_ERROR;
	    break;
	}
	for (session = tnmSnmpList; session; session = session->nextPtr) {
	    if (session->interp == interp) {
		Tcl_AppendElement(interp,
				  Tcl_GetCommandName(interp, session->token));
	    }
	}
	break;

    case cmdSession:

	/* 
	 * Initialize the SNMP manager module by opening a socket for
	 * manager communication. Polulate the MIB module with the
	 * set of default MIB definitions.
	 */

	if (TnmMibLoad(interp) != TCL_OK) {
	    result = TCL_ERROR;
	    break;
	}
	if (TnmSnmpManagerOpen(interp) != TCL_OK) {
	    result = TCL_ERROR;
	    break;
	}
	
	session = TnmSnmpCreateSession();
	session->interp = interp;
	result = TnmSetConfig(interp, &managerConfig,
			      (ClientData) session, argc, argv);
	if (result != TCL_OK) {
	    TnmSnmpDeleteSession(session);
	    break;
	}
#ifdef TNM_SNMPv2U
	if (session->version == TNM_SNMPv2U && session->qos & USEC_QOS_AUTH) {
	    TnmSnmpUsecGetAgentID(session);
	}
#endif

	session->nextPtr = tnmSnmpList;
	tnmSnmpList = session;

	/*
	 * Finally create a Tcl command for this session.
	 */

	name = TnmGetHandle(interp, "snmp", &nextId);
	session->token = Tcl_CreateCommand(interp, name, ManagerCmd,
			  (ClientData) session, DestroySession);
	Tcl_SetResult(interp, name, TCL_STATIC);
	break;

    case cmdWait:
        if (argc != 2) {
	    TnmWrongNumArgs(interp, 2, argv, (char *) NULL);
	    result = TCL_ERROR;
	    break;
	}
      repeat:
	for (session = tnmSnmpList; session; session = session->nextPtr) {
	    if (TnmSnmpQueueRequest(session, NULL)) {
		Tcl_DoOneEvent(0);
		goto repeat;
	    }
	}
	break;

    case cmdWatch:
	if (argc > 3) {
	    TnmWrongNumArgs(interp, 2, argv, "?bool?");
            result = TCL_ERROR;
	    break;
        }
	if (argc == 3) {
	    if (Tcl_GetBoolean(interp, argv[2], &hexdump) != TCL_OK) {
		result = TCL_ERROR;
		break;
	    }
	}
	Tcl_SetResult(interp, hexdump ? "1" : "0", TCL_STATIC);
	break;
    }

    return result;
}

/*
 *----------------------------------------------------------------------
 *
 * ManagerCmd --
 *
 *	This procedure is invoked to process a manager command.
 *	See the user documentation for details on what it does.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	See the user documentation.
 *
 *----------------------------------------------------------------------
 */

static int
ManagerCmd(clientData, interp, argc, argv)
    ClientData clientData;
    Tcl_Interp *interp;
    int argc;
    char **argv;
{
    TnmSnmp *session = (TnmSnmp *) clientData;
    int cmd, code, nonRepeaters, maxRepetitions;

    enum commands {
	cmdCget, cmdConfigure, cmdDestroy, cmdGet, cmdGetBulk, cmdGetNext, 
	cmdScalars, cmdSet, cmdTbl, cmdWait, cmdWalk, cmdZZZ
    };

    static TnmTable cmdTable[] = {
	{ cmdCget,	"cget" },
	{ cmdConfigure,	"configure" },
	{ cmdDestroy,	"destroy" },
	{ cmdGet,	"get" },
	{ cmdGetBulk,	"getbulk" },
	{ cmdGetNext,	"getnext" },
	{ cmdScalars,	"scalars" },
	{ cmdSet,	"set" },
	{ cmdTbl,	"table" },
	{ cmdWait,	"wait" },
	{ cmdWalk,	"walk" },
	{ cmdZZZ,	"zzz" },
	{ 0, NULL }
    };

    if (argc < 2) {
	TnmWrongNumArgs(interp, 1, argv, "option ?arg arg ...?");
        return TCL_ERROR;
    }

    cmd = TnmGetTableKey(cmdTable, argv[1]);
    if (cmd == -1) {
	TnmBadOption(interp, argv[1], TnmGetTableValues(cmdTable));
	return TCL_ERROR;
    }

    /*
     * First handle all commands that do not necessarily need a
     * fully configured SNMP session.
     */

    switch ((enum commands) cmd) {
    case cmdCget:
	return TnmGetConfig(interp, &managerConfig,
			    (ClientData) session, argc, argv);

    case cmdConfigure:

	/*
	 * This call to WaitSession() is needed to ensure that a 
	 * configuration change does not affect queued requests.
	 */

	Tcl_Preserve((ClientData) session);
	WaitSession(interp, session, NULL);
 	code = TnmSetConfig(interp, &managerConfig,
			    (ClientData) session, argc, argv);
	if (code != TCL_OK) {
	    Tcl_Release((ClientData) session);
	    return TCL_ERROR;
	}
#ifdef TNM_SNMPv2U
	if (session->version == TNM_SNMPv2U && session->qos & USEC_QOS_AUTH) {
	    TnmSnmpUsecGetAgentID(session);
	}
#endif
	Tcl_Release((ClientData) session);
	return TCL_OK;

    case cmdDestroy:
	Tcl_DeleteCommandFromToken(interp, session->token);
	return TCL_OK;

    case cmdWait:
	if (argc == 2) {
	    return WaitSession(interp, session, NULL);
	} else if (argc == 3) {
	    return WaitSession(interp, session, argv[2]);
	}
	TnmWrongNumArgs(interp, 2, argv, "?request?");
	return TCL_ERROR;

    default:
	break;
    }

    /*
     * All commands handled below need a well configured session.
     * Check if we have one and return an error if something is
     * still incomplete.
     */

    if (Configured(interp, session) != TCL_OK) {
	return TCL_ERROR;
    }

    switch ((enum commands) cmd) {
    case cmdGet:
	if (argc < 3 || argc > 4) {
	    TnmWrongNumArgs(interp, 2, argv, "varBindList ?script?");
	    return TCL_ERROR;
	}
	return Request(interp, session, TNM_SNMP_GET, argc-1, argv+1);

    case cmdGetNext:
	if (argc < 3 || argc > 4) {
	    TnmWrongNumArgs(interp, 2, argv, "varBindList ?script?");
	    return TCL_ERROR;
	}
	return Request(interp, session, TNM_SNMP_GETNEXT, argc-1, argv+1);

    case cmdGetBulk:
	if (argc < 5 || argc > 6) {
	    TnmWrongNumArgs(interp, 2, argv, 
		    "non-repeaters max-repetitions varBindList ?script?");
	    return TCL_ERROR;
	}
	if (Tcl_GetInt(interp, argv[2], &nonRepeaters) != TCL_OK) {
	    return TCL_ERROR;
	}
	if (nonRepeaters < 0) nonRepeaters = 0;
	if (Tcl_GetInt(interp, argv[3], &maxRepetitions) != TCL_OK) {
	    return TCL_ERROR;
	}
	if (maxRepetitions < 0) maxRepetitions = 0;

	return Request(interp, session, TNM_SNMP_GETBULK, argc-1, argv+1);

    case cmdSet:
	if (argc < 3 || argc > 4) {
	    TnmWrongNumArgs(interp, 2, argv, "varBindList ?script?");
	    return TCL_ERROR;
	}
	return Request(interp, session, TNM_SNMP_SET, argc-1, argv+1);

    case cmdTbl:
	if (argc != 4) {
	    TnmWrongNumArgs(interp, 2, argv, "table arrayName");
	    return TCL_ERROR;
	}
	return Table(interp, session, argv[2], argv[3]);

    case cmdScalars:
	if (argc != 4) {
	    TnmWrongNumArgs(interp, 2, argv, "group arrayName");
	    return TCL_ERROR;
	}
	return Scalars(interp, session, argv[2], argv[3]);

    case cmdWalk:
	if (argc != 5) {
	    TnmWrongNumArgs(interp, 2, argv, "varName list command");
	    return TCL_ERROR;
	}
	return Walk(interp, session, argv[2], argv[3], argv[4]);

    case cmdZZZ:
	if (argc != 4) {
	    TnmWrongNumArgs(interp, 2, argv, "list command");
	    return TCL_ERROR;
	}
	return AsyncWalk(interp, session, argv[2], argv[3]);
	
    default:
	break;
    }

    if (strcmp(argv[1], "inform") == 0) {
	if (session->version == TNM_SNMPv1) {
	    char *name = Tcl_GetCommandName(interp, session->token);
	    Tcl_AppendResult(interp, "inform option not allowed on ",
			     "SNMPv1 session \"", name, "\"", (char *) NULL);
	    return TCL_ERROR;
	}  else if (session->version & TNM_SNMPv2) {
	    return Request(interp, session, TNM_SNMP_INFORM, argc-1, argv+1);
	} else {
	    Tcl_SetResult(interp, "unknown SNMP version", TCL_STATIC);
            return TCL_ERROR;
	}

    } else if (strcmp(argv[1], "trap") == 0) {
	if (session->version == TNM_SNMPv1) {
	    return Request(interp, session, TNM_SNMPv1_TRAP, argc-1, argv+1);
	} else if (session->version & TNM_SNMPv2) {
	    return Request(interp, session, TNM_SNMPv2_TRAP, argc-1, argv+1);
	} else {
	    Tcl_SetResult(interp, "unknown SNMP version", TCL_STATIC);
            return TCL_ERROR;
        }

    } else if (strcmp(argv[1], "bind") == 0) {
	int event;
	TnmSnmpBinding *bindPtr = session->bindPtr;
	if (argc < 4 || argc > 5) {
	    TnmWrongNumArgs(interp, 2, argv, "label event ?command?");
	    return TCL_ERROR;
	}
	event = TnmGetTableKey(tnmSnmpEventTable, argv[3]);
	if (argv[2][0] == '\0') {
	    
	    if (event < 0 || (event & TNM_SNMP_GENERIC_BINDINGS) == 0) {
		Tcl_AppendResult(interp, "unknown event \"", argv[3], 
				 "\": use trap, inform, recv, send, ",
				 "begin, end, or report", (char *) NULL);
		return TCL_ERROR;
	    }
	    if (event & (TNM_SNMP_TRAP_EVENT | TNM_SNMP_INFORM_EVENT)) {
		if (! (session->flags & TNM_SNMP_TRAP_SINK)) {
		    if (TnmSnmpTrapOpen(interp) != TCL_OK) {
			return TCL_ERROR;
		    }
		    session->flags |= TNM_SNMP_TRAP_SINK;
		}
	    }
	    while (bindPtr) {
		if (bindPtr->event == event) break;
		bindPtr = bindPtr->nextPtr;
	    }
	    if (argc == 4) {
		if (bindPtr) {
		    Tcl_SetResult(interp, bindPtr->command, TCL_STATIC);
		}
	    } else {
		if (! bindPtr) {
		    bindPtr = (TnmSnmpBinding *) ckalloc(sizeof(TnmSnmpBinding));
		    memset((char *) bindPtr, 0, sizeof(TnmSnmpBinding));
		    bindPtr->event = event;
		    bindPtr->nextPtr = session->bindPtr;
		    session->bindPtr = bindPtr;
		}
		if (bindPtr->command) {
		    ckfree (bindPtr->command);
		}
		bindPtr->command = ckstrdup(argv[4]);
	    }
	} else {

	    char *oidstr = TnmMibGetOid(argv[2]);
	    int code;
	    
	    if (!oidstr) {
		Tcl_AppendResult(interp, "no object \"", argv[2], "\"",
				 (char *) NULL);
		return TCL_ERROR;
	    }
	    
	    if (event < 0 || (event & TNM_SNMP_INSTANCE_BINDINGS) == 0) {
		Tcl_AppendResult(interp, "unknown event \"", argv[3],
				 "\": use get, set, create, check, ",
				 "commit, rollback, format, or scan", 
				 (char *) NULL);
		return TCL_ERROR;
	    }
	    
	    if (argc == 5) {
		TnmOid oid;
		TnmOidInit(&oid);
		code = TnmOidFromString(&oid, oidstr);
		if (code == TCL_OK) {
		    code = TnmSnmpSetNodeBinding(session, &oid, event, 
						 argv[4]);
		}
		TnmOidFree(&oid);
		if (code != TCL_OK) {
		    Tcl_AppendResult(interp, "unknown instance \"",
				     argv[2], "\"", (char *) NULL);
		    return TCL_ERROR;
		}
	    } else {
		char *cmd = NULL;
		TnmOid oid;
		TnmOidInit(&oid);
		code = TnmOidFromString(&oid, oidstr);
		if (code == TCL_OK) {
		    cmd = TnmSnmpGetNodeBinding(session, &oid, event);
		}
		TnmOidFree(&oid);
		Tcl_SetResult(interp, (cmd) ? cmd : "", TCL_STATIC);
	    }
	}
	return TCL_OK;

    } else if (strcmp(argv[1], "instance") == 0) {
	int code;
        if (argc < 4 || argc > 5) {
	    TnmWrongNumArgs(interp, 2, argv, "oid varName ?defval?");
	    return TCL_ERROR;
	}

#if 0
	
        if (! session->agentInterp) {
	    char *name = Tcl_GetCommandName(interp, session->token);
	    Tcl_AppendResult(interp, "invalid agent session \"", 
			     name, "\"", (char *) NULL);
	    return TCL_ERROR;
	}
	code = TnmSnmpCreateNode(session->agentInterp, argv[2], argv[3],
				 (argc > 4) ? argv[4] : "");
	if (code != TCL_OK) {
	    if (interp != session->agentInterp) {
		Tcl_SetResult(interp, session->agentInterp->result, 
			      TCL_VOLATILE);
		Tcl_ResetResult(session->agentInterp);
	    }
	    return code;
	}
	return TCL_OK;
#endif
    }

    Tcl_AppendResult(interp, "bad option \"", argv[1], "\": should be ",
		     "configure, cget, wait, destroy, ",
		     "get, getnext, getbulk, set, trap, inform, walk, ",
		     "scalars, table, instance, or bind", (char *) NULL);
    return TCL_ERROR;
}

/*
 *----------------------------------------------------------------------
 *
 * AgentCmd --
 *
 *	This procedure is invoked to process a agent command.
 *	See the user documentation for details on what it does.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	See the user documentation.
 *
 *----------------------------------------------------------------------
 */

static int
AgentCmd(clientData, interp, argc, argv)
    ClientData clientData;
    Tcl_Interp *interp;
    int argc;
    char **argv;
{
    TnmSnmp *session = (TnmSnmp *) clientData;
    int cmd, code;

    enum commands {
	cmdCget, cmdConfigure, cmdDestroy
    };

    static TnmTable cmdTable[] = {
	{ cmdCget,	"cget" },
	{ cmdConfigure,	"configure" },
	{ cmdDestroy,	"destroy" },
	{ 0, NULL }
    };

    if (argc < 2) {
	TnmWrongNumArgs(interp, 1, argv, "option ?arg arg ...?");
        return TCL_ERROR;
    }

    cmd = TnmGetTableKey(cmdTable, argv[1]);
    if (cmd == -1) {
	TnmBadOption(interp, argv[1], TnmGetTableValues(cmdTable));
	return TCL_ERROR;
    }

    switch ((enum commands) cmd) {
    case cmdCget:
	return TnmGetConfig(interp, &agentConfig,
			    (ClientData) session, argc, argv);

    case cmdConfigure:

	/*
	 * This call to WaitSession() is needed to ensure that a 
	 * configuration change does not affect queued requests.
	 */

	Tcl_Preserve((ClientData) session);
	WaitSession(interp, session, NULL);
 	code = TnmSetConfig(interp, &agentConfig,
			    (ClientData) session, argc, argv);
	if (code == TCL_OK) {
	    code = TnmSnmpAgentInit(interp, session);
	}
	if (code != TCL_OK) {
	    Tcl_Release((ClientData) session);
	    return TCL_ERROR;
	}
#ifdef TNM_SNMPv2U
	if (session->version == TNM_SNMPv2U && session->qos & USEC_QOS_AUTH) {
	    TnmSnmpUsecGetAgentID(session);
	}
#endif
	Tcl_Release((ClientData) session);
	break;

    case cmdDestroy:
	Tcl_DeleteCommandFromToken(interp, session->token);
	break;
    }

    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * WaitSession --
 *
 *	This procedure processes events until either the list of
 *	outstanding requests is empty or until the given request
 *	is no longer in the list of outstanding requests.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	Tcl events are processed which can cause arbitrary side effects.
 *
 *----------------------------------------------------------------------
 */

static int
WaitSession(interp, session, request)
    Tcl_Interp *interp;
    TnmSnmp *session;
    char *request;
{
    u_int id = 0;
    char *name = Tcl_GetCommandName(interp, session->token);

    if (! name) {
	return TCL_OK;
    }

    if (request) {
	char *p;
	for (p = request; isdigit(*p); p++) {
	    id = 10 * id + *p - '0';
	}
    }
    
    /*
     * Do not use the session pointer! We have to search for the
     * session name after each single event because the session
     * may be deleted as a side effect of the event.
     */

    name = ckstrdup(name);
  repeat:
    for (session = tnmSnmpList; session; session = session->nextPtr) {
	char *thisName = Tcl_GetCommandName(interp, session->token);
	if (strcmp(thisName, name) != 0) continue;
	if (! id) {
	    if (TnmSnmpQueueRequest(session, NULL)) {
		Tcl_DoOneEvent(0);
		goto repeat;
	    }
	} else {
	    if (TnmSnmpFindRequest(id)) {
		Tcl_DoOneEvent(0);
		goto repeat;
	    }
	}
    }
    
    ckfree(name);
    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * DestroySession --
 *
 *	This procedure is invoked when a session handle is deleted.
 *	It frees the associated session structure and de-installs all
 *	pending events. If it is the last session, we also close the
 *	socket for manager communication.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static void
DestroySession(clientData)
    ClientData clientData;
{
    TnmSnmp **sPtrPtr, *session = (TnmSnmp *) clientData;

    sPtrPtr = &tnmSnmpList;
    while (*sPtrPtr && (*sPtrPtr) != session) {
	sPtrPtr = &(*sPtrPtr)->nextPtr;
    }

    if (*sPtrPtr) {
	(*sPtrPtr) = session->nextPtr;
    }

    TnmSnmpDeleteSession(session);

    if (tnmSnmpList == NULL) {
	TnmSnmpManagerClose();
    }
}

/*
 *----------------------------------------------------------------------
 *
 * Request --
 *
 *	This procedure creates a pdu structure and calls TnmSnmpEncode
 *	to send the packet to the destination.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static int
Request(interp, session, pdu_type, argc, argv)
    Tcl_Interp *interp;
    TnmSnmp *session;
    int pdu_type;
    int argc;
    char **argv;
{
    char *cmd = NULL;
    TnmSnmpPdu _pdu;
    TnmSnmpPdu *pdu = &_pdu;
    
    /* 
     * Initialize the PDU:
     */

    pdu->addr	      = session->maddr;
    pdu->type         = pdu_type;
    pdu->request_id   = TnmSnmpGetRequestId();
    pdu->error_status = TNM_SNMP_NOERROR;
    pdu->error_index  = 0;    
    pdu->trapOID      = NULL;
    Tcl_DStringInit (&pdu->varbind);

#ifdef TNM_SNMP_BENCH
    memset((char *) &session->stats, 0, sizeof(session->stats));
#endif

    /*
     * Check # of arguments:
     */
    
    if ((pdu->type == TNM_SNMP_GETBULK && argc < 4) 
	|| (pdu->type == TNM_SNMPv1_TRAP && argc < 3)
	|| (pdu->type == TNM_SNMPv2_TRAP && argc < 3)
	|| (pdu->type == TNM_SNMP_INFORM && argc < 3)
	|| (argc < 2)) {
	goto usage;
    }
    
    /*
     * Read NonRepeaters and MaxRepetitions for GetBulkRequest:
     */
    
    if (pdu->type == TNM_SNMP_GETBULK) {
	int num;
	if (--argc) {
	    if (Tcl_GetInt(interp, *++argv, &num) != TCL_OK) goto errorExit;
	    pdu->error_status = (num < 0) ? 0 : num;
	}
	if (--argc) {
	    if (Tcl_GetInt(interp, *++argv, &num) != TCL_OK) goto errorExit;
	    pdu->error_index  = (num < 0) ? 0 : num;
	}
    } else if (pdu->type == TNM_SNMPv1_TRAP 
	       || pdu->type == TNM_SNMPv2_TRAP
	       || pdu->type == TNM_SNMP_INFORM) {
	argc--;
	if (TnmIsOid(*++argv)) {
	    pdu->trapOID = ckstrdup(*argv);
	} else {
	    char *tmp = TnmMibGetOid(*argv);
	    if (! tmp) {
		Tcl_AppendResult(interp,  "no object \"", *argv, "\"",
				 (char *) NULL);
		goto errorExit;	
	    }    
	    pdu->trapOID = ckstrdup(tmp);
	}
    } else {
	pdu->error_status = TNM_SNMP_NOERROR;
	pdu->error_index  = 0;
    }
    
    /*
     * Check for varbind-list and split it into args:
     */
    
    if (!argc) goto usage;    
    Tcl_DStringAppend(&pdu->varbind, *++argv, -1);

    /*
     * Check for the callback function:
     */
    
    if (--argc && *++argv != NULL) {
	cmd = *argv;
    }

    if (cmd) {
	EvalCmd *ec = (EvalCmd *) ckalloc(sizeof(EvalCmd) + strlen(cmd) + 1);
	ec->interp = interp;
	ec->cmd = (char *) ec + sizeof(EvalCmd);
	strcpy(ec->cmd, cmd);
	if (TnmSnmpEncode(interp, session, pdu, EvalCmdProc, 
			   (ClientData) ec) != TCL_OK) {
	    goto errorExit;
	}
    } else {
	if (TnmSnmpEncode(interp, session, pdu, NULL, NULL) != TCL_OK) {
	    goto errorExit;
	}
    }

    if (pdu->trapOID) ckfree(pdu->trapOID);
    Tcl_DStringFree(&pdu->varbind);
    return TCL_OK;
    
  usage:
#if 0
    if (pdu->type == TNM_SNMP_GETBULK) {
	Tcl_AppendResult(interp, "wrong # args: should be \"", session->name,
			 " getbulk non-repeaters max-repetitions ",
			 "list ?callback?\"", (char *) NULL);
    } else if (pdu->type == TNM_SNMPv1_TRAP 
	       || pdu->type == TNM_SNMPv2_TRAP) {
	Tcl_AppendResult(interp, "wrong # args: should be \"", session->name,
			 " trap snmpTrapOID list\"", (char *) NULL);
    } else if (pdu->type == TNM_SNMP_INFORM) {
	Tcl_AppendResult(interp, "wrong # args: should be \"", session->name,
			 " inform snmpTrapOID list\"", (char *) NULL);
    } else {
	Tcl_AppendResult(interp, "wrong # args: should be \"", session->name,
			 " ", *argv, " list ?callback?\"", (char *) NULL);
    }
#endif
    
  errorExit:
    if (pdu->trapOID) ckfree(pdu->trapOID);
    Tcl_DStringFree(&pdu->varbind);
    return TCL_ERROR;
}

/*
 *----------------------------------------------------------------------
 *
 * AsyncWalk --
 *
 *	This procedure walks a MIB tree. It evaluates the given Tcl
 *	command foreach varbind retrieved using getbulk requests.
 *	First, all variables contained in the list argument are
 *	converted to their OIDs. Then we start an asynchronous loop
 *	using gebulk requests until we get an error or until one
 *	returned variable starts with an OID not being a valid prefix.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static int
AsyncWalk(interp, session, vbl, cmd)
    Tcl_Interp *interp;
    TnmSnmp *session;
    char *vbl;
    char *cmd;
{
    TnmSnmpPdu _pdu;
    TnmSnmpPdu *pdu = &_pdu;
    int code;
    EvalCmd *ecPtr;

    /* 
     * Initialize the PDU:
     */

    pdu->addr	      = session->maddr;
    pdu->type         = TNM_SNMP_GETNEXT;
    pdu->request_id   = TnmSnmpGetRequestId();
    pdu->error_status = TNM_SNMP_NOERROR;
    pdu->error_index  = 0;    
    pdu->trapOID      = NULL;
    Tcl_DStringInit (&pdu->varbind);

#ifdef TNM_SNMP_BENCH
    memset((char *) &session->stats, 0, sizeof(session->stats));
#endif

    /*
     * The structure where we keep all information about this
     * asynchronous walk.
     */

    ecPtr = (EvalCmd *) ckalloc(sizeof(EvalCmd) + strlen(cmd) + 1);
    TnmVectorInit(&ecPtr->vector);
    code = TnmSnmpVarBindFromString(interp, &ecPtr->vector, vbl);
    if (code != TCL_OK) {
	ckfree((char *) ecPtr);
	return TCL_ERROR;
    }
    Tcl_DStringAppend(&pdu->varbind, vbl, -1);

    ecPtr->interp = interp;
    ecPtr->cmd = (char *) ecPtr + sizeof(EvalCmd);
    strcpy(ecPtr->cmd, cmd);
    if (TnmSnmpEncode(interp, session, pdu, AsyncWalkProc, 
		      (ClientData) ecPtr) != TCL_OK) {
	return TCL_ERROR;
    }

    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * Walk --
 *
 *	This procedure walks a MIB tree. It evaluates the given Tcl
 *	command foreach varbind retrieved using getbulk requests.
 *	First, all variables contained in the list argument are
 *	converted to their OIDs. Then we loop using gebulk requests
 *	until we get an error or until one returned variable starts
 *	with an OID not being a valid prefix.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static int
Walk(interp, session, varName, vbl, command)
    Tcl_Interp *interp;
    TnmSnmp *session;
    char *varName;
    char *vbl;
    char *command;
{
    int i, j, k, result;
    int oidc, respc;
    char **oidv = NULL, **respv = NULL;
    TnmSnmpPdu _pdu, *pdu = &_pdu;
    int numRepeaters = 0;
    
    /*
     * Initialize the PDU.
     */

    pdu->addr         = session->maddr;
    pdu->type         = TNM_SNMP_GETBULK;
    pdu->request_id   = TnmSnmpGetRequestId();
    pdu->error_status = TNM_SNMP_NOERROR;
    pdu->error_index  = 0;    
    pdu->trapOID      = NULL;
    Tcl_DStringInit(&pdu->varbind);

    /*
     * Save the oid prefix contained in list in oidv and oidc.
     */
    
    result = Tcl_SplitList(interp, vbl, &oidc, &oidv);
    if (result != TCL_OK) {
	return result;
    }
    if (oidc == 0) {
	result = TCL_OK;
	goto loopDone;
    }
    
    for (i = 0; i < oidc; i++) {
	char *tmp = TnmMibGetOid(oidv[i]);
	if (!tmp) {
	    Tcl_AppendResult(interp,  "no object \"", oidv[i], "\"",
			     (char *) NULL);
	    ckfree((char *) oidv);
	    Tcl_DStringFree(&pdu->varbind);
            return TCL_ERROR;
	}
	oidv[i] = ckalloc(strlen(tmp) + 2);
	strcpy(oidv[i], tmp);
	strcat(oidv[i], ".");
	Tcl_DStringAppendElement(&pdu->varbind, tmp);
    }

    while (1) {

	pdu->type         = TNM_SNMP_GETBULK;
	pdu->request_id   = TnmSnmpGetRequestId();

	/* 
	 * Set the non-repeaters and the max-repetitions for the getbulk
	 * operation. We use the sequence 8 16 24 32 40 48 to increase
	 * the `warp' factor with every repetition.
	 *
	 * Some measurements show some real bad effects. If you increase
	 * the warp factor too much, you will risk timeouts because the
	 * agent might need a lot of time to build the response. If the
	 * agent does not cache response packets, you will get very bad 
	 * performance. Therefore, I have limited the `warp' factor to
	 * 24 which works well with scotty's default parameters on an
	 * Ethernet. (This number should not be hard coded but perhaps
	 * it is better so because everyone should read this comment
	 * before playing with these parameters.)
	 */

	if (numRepeaters < 24 ) {
	    numRepeaters += 8;
	}

	pdu->error_status = 0;
	pdu->error_index  = (numRepeaters / oidc > 0) 
	    ? numRepeaters / oidc : 1;

	result = TnmSnmpEncode(interp, session, pdu, NULL, NULL);
	if (result == TCL_ERROR 
	    && (strncmp(interp->result, "noSuchName ", 11) == 0)) {
	    result = TCL_OK;
	    goto loopDone;
	}
	if (result != TCL_OK) {
            break;
        }
	
	if (respv) ckfree((char *) respv);
	result = Tcl_SplitList(interp, interp->result, &respc, &respv);
	if (result != TCL_OK) {
	    goto loopDone;
	}

	if (respc < oidc) {
	    Tcl_SetResult(interp, "response with wrong # of varbinds",
			  TCL_STATIC);
	    result = TCL_ERROR;
	    goto loopDone;
	}

	for (j = 0; j < respc / oidc; j++) {

	    for (i = 0; i < oidc; i++) {
		if (strncmp(oidv[i], respv[j * oidc + i], 
			    strlen(oidv[i])) != 0) {
		    result = TCL_OK;
		    goto loopDone;
		}
	    }

	    Tcl_DStringFree(&pdu->varbind);
	    for (k = j * oidc; k < (j+1) * oidc; k++) {
		int vbc;
		char **vbv;
		result = Tcl_SplitList(interp, respv[k], &vbc, &vbv);
		if (result != TCL_OK) {
		    goto loopDone;
		}
		if (strcmp(vbv[1], "endOfMibView") == 0) {
		    ckfree((char *) vbv);
		    result = TCL_OK;
		    goto loopDone;
		}
		ckfree((char *) vbv);
		Tcl_DStringAppendElement(&pdu->varbind, respv[k]);
	    }

	    if (Tcl_SetVar(interp, varName, Tcl_DStringValue(&pdu->varbind),
			   TCL_LEAVE_ERR_MSG) == NULL) {
		result = TCL_ERROR;
		goto loopDone;
	    }

	    result = Tcl_Eval(interp, command);
	    if (result != TCL_OK) {
		if (result == TCL_CONTINUE) {
		    result = TCL_OK;
		} else if (result == TCL_BREAK) {
		    result = TCL_OK;
		    goto loopDone;
		} else if (result == TCL_ERROR) {
		    char msg[100];
		    sprintf(msg, "\n    (\"%s walk\" body line %d)",
			    Tcl_GetCommandName(interp, session->token),
			    interp->errorLine);
		    Tcl_AddErrorInfo(interp, msg);
		    goto loopDone;
		} else {
		    goto loopDone;
		}
	    }
	}
    }

  loopDone:
    for (i = 0; i < oidc; i++) {
	ckfree(oidv[i]);
    }
    ckfree((char *) oidv);
    if (respv) ckfree((char *) respv);
    Tcl_DStringFree(&pdu->varbind);

    /*
     * noSuchName errors mark the end of a SNMPv1 MIB view and hence 
     * they are no real errors. So we ignore them here.
     */

    if (result == TCL_ERROR 
	&& (strncmp(interp->result, "noSuchName", 10) == 0)) {
	result = TCL_OK;
    }

    if (result == TCL_OK) {
	Tcl_ResetResult(interp);
    }
    return result;
}

/*
 *----------------------------------------------------------------------
 *
 * ExpandTable --
 *
 *	This procedure expands the list of table variables or a single
 *	table name into a list of MIB instances.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static int
ExpandTable(interp, tList, dst)
    Tcl_Interp *interp;
    char *tList;
    Tcl_DString *dst;
{
    int i, argc, code;
    char **argv = NULL;
    TnmMibNode *nodePtr, *entryPtr = NULL, *tablePtr = NULL;
    
    code = Tcl_SplitList(interp, tList, &argc, &argv);
    if (code != TCL_OK) {
	return TCL_ERROR;
    }

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

	/*
	 * Lookup the given object.
	 */

	nodePtr = TnmMibFindNode(argv[i], NULL, 0);
	if (! nodePtr) {
	    Tcl_AppendResult(interp, "unknown mib table \"", argv[i], "\"",
			     (char *) NULL);
	    ckfree((char *) argv);
	    return TCL_ERROR;
	}

	/*
	 * Locate the entry (SEQUENCE) that contains this object.
	 */

	switch (nodePtr->syntax) {
	  case ASN1_SEQUENCE:
	    entryPtr = nodePtr;
	    break;
	  case ASN1_SEQUENCE_OF: 
	    if (nodePtr->childPtr) {
		entryPtr = nodePtr->childPtr;
	    }
	    break;
	  default:
	    if (nodePtr->parentPtr && nodePtr->childPtr == NULL
		&& nodePtr->parentPtr->syntax == ASN1_SEQUENCE) {
		entryPtr = nodePtr->parentPtr;
	    } else {
	    unknownTable:
		Tcl_AppendResult(interp, "not a table \"", argv[i], "\"",
				 (char *) NULL);
		ckfree((char *) argv);
		return TCL_ERROR;
	    }
	}

	/*
	 * Check whether all objects belong to the same table.
	 */

	if (entryPtr == NULL || entryPtr->parentPtr == NULL) {
	    goto unknownTable;
	}

	if (tablePtr == NULL) {
	    tablePtr = entryPtr->parentPtr;
	}
	if (tablePtr != entryPtr->parentPtr) {
	    Tcl_AppendResult(interp, "instances not in the same table",
			     (char *) NULL);
	    ckfree((char *) argv);
	    return TCL_ERROR;
	}

	/*
	 * Now add the nodes to the list. Expand SEQUENCE nodes to
	 * include all child nodes. Check the access mode which must
	 * allow at least read access.
	 */

	if (nodePtr == entryPtr || nodePtr == tablePtr) {
	    TnmMibNode *nPtr;
	    for (nPtr = entryPtr->childPtr; nPtr; nPtr=nPtr->nextPtr) {
		if (nPtr->access != TNM_MIB_NOACCESS) {
		    Tcl_DStringAppendElement(dst, nPtr->label);
		}
	    }
	} else {
	    if (nodePtr->access != TNM_MIB_NOACCESS) {
		Tcl_DStringAppendElement(dst, nodePtr->label);
	    }
	}
    }

    ckfree((char *) argv);
    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * Table --
 *
 *	This procedure retrieves a conceptual SNMP table and stores
 *	the values in a Tcl array.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	Tcl variables are modified.
 *
 *----------------------------------------------------------------------
 */

static int
Table(interp, session, table, arrayName)
    Tcl_Interp *interp;
    TnmSnmp *session;
    char *table;
    char *arrayName;
{
    int i, largc, code;
    TnmSnmpPdu _pdu, *pdu = &_pdu;
    Tcl_DString varList;
    char **largv;
    
    /*
     * A special hack to make sure that the given array name 
     * is actually known as an array.
     */

    Tcl_SetVar2(interp, arrayName, "foo", "", 0);
    Tcl_UnsetVar(interp, arrayName, 0);

    /*
     * Initialize the PDU.
     */

    pdu->addr         = session->maddr;
    pdu->type         = TNM_SNMP_GETBULK;
    pdu->request_id   = TnmSnmpGetRequestId();
    pdu->error_status = TNM_SNMP_NOERROR;
    pdu->error_index  = 0;    
    pdu->trapOID      = NULL;
    Tcl_DStringInit(&pdu->varbind);
    Tcl_DStringInit(&varList);

    /*
     * Expand the given table list to create the complete getnext varbind.
     */

    code = ExpandTable(interp, table, &varList);
    if (code != TCL_OK) {
        return TCL_ERROR;
    }

    if (Tcl_DStringLength(&varList) == 0) {
	return TCL_OK;
    }

    /*
     *
     */

    code = Tcl_SplitList(interp, Tcl_DStringValue(&varList),
			 &largc, &largv);
    if (code != TCL_OK) {
	Tcl_DStringFree(&varList);
	return TCL_ERROR;
    }

    for (i = 0; i < largc; i++) {
	TnmWriteMessage(largv[i]);
	TnmWriteMessage("\n");
    }

    ckfree((char *) largv);
    Tcl_DStringFree(&varList);
    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * ExpandScalars --
 *
 *	This procedure expands the list of scalar or group names
 *	into a list of MIB instances.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static int
ExpandScalars(interp, sList, dst)
    Tcl_Interp *interp;
    char *sList;
    Tcl_DString *dst;
{
    int argc, code, i;
    char **argv = NULL;
    TnmMibNode *nodePtr;
    TnmOid oid;

    code = Tcl_SplitList(interp, sList, &argc, &argv);
    if (code != TCL_OK) {
	return TCL_ERROR;
    }

    TnmOidInit(&oid);
    for (i = 0; i < argc; i++) {

	nodePtr = TnmMibFindNode(argv[i], NULL, 0);
	if (nodePtr == NULL) {
	    Tcl_AppendResult(interp, "unknown mib object \"", argv[i], "\"",
			     (char *) NULL);
	    ckfree((char *) argv);
	    return TCL_ERROR;
	}

	/*
	 * Skip the node if it is a table or an entry node.
	 */

	if (nodePtr->syntax == ASN1_SEQUENCE 
	    || nodePtr->syntax == ASN1_SEQUENCE_OF) {
	    continue;
	}

	/*
	 * Try to expand to child nodes if the node has child nodes, 
	 * Ignore all nodes which itsef have childs and which are
	 * not accessible.
	 */

	if (nodePtr->childPtr) {
	    for (nodePtr = nodePtr->childPtr; 
		 nodePtr; nodePtr=nodePtr->nextPtr) {
		if (nodePtr->access == TNM_MIB_NOACCESS || nodePtr->childPtr) {
		    continue;
		}
		TnmMibNodeToOid(nodePtr, &oid);
		TnmOidAppend(&oid, 0);
		Tcl_DStringAppendElement(dst, TnmOidToString(&oid));
		TnmOidFree(&oid);
	    }

	} else if (nodePtr->access != TNM_MIB_NOACCESS) {
	    TnmMibNodeToOid(nodePtr, &oid);
	    TnmOidAppend(&oid, 0);
	    Tcl_DStringAppendElement(dst, TnmOidToString(&oid));
	    TnmOidFree(&oid);

	} else {
	    Tcl_AppendResult(interp, "object \"", argv[0],
			     "\" not accessible", (char *) NULL);
	    ckfree((char *) argv);
	    return TCL_ERROR;
	}
    }

    ckfree((char *) argv);
    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * Scalars --
 *
 *	This procedure extracts the variables and values contained in
 *	the varbindlist vbl and set corresponding array Tcl variables.
 *	The list of array names modified is appended to result.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	Tcl variables are modified.
 *
 *----------------------------------------------------------------------
 */

static int
Scalars(interp, session, group, arrayName)
    Tcl_Interp *interp;
    TnmSnmp *session;
    char *group;
    char *arrayName;
{
    int i, largc, code;
    TnmSnmpPdu _pdu, *pdu = &_pdu;
    Tcl_DString varList;
    Tcl_DString result;
    char **largv;
    
    /*
     * A special hack to make sure that the given array name 
     * is actually known as an array.
     */

    Tcl_SetVar2(interp, arrayName, "foo", "", 0);
    Tcl_UnsetVar(interp, arrayName, 0);

    /*
     * Initialize the PDU.
     */

    pdu->addr         = session->maddr;
    pdu->type         = TNM_SNMP_GET;
    pdu->request_id   = TnmSnmpGetRequestId();
    pdu->error_status = TNM_SNMP_NOERROR;
    pdu->error_index  = 0;    
    pdu->trapOID      = NULL;
    Tcl_DStringInit(&pdu->varbind);
    Tcl_DStringInit(&varList);
    Tcl_DStringInit(&result);

    /*
     * Expand the given scalar list to create the complete get varbind.
     */

    code = ExpandScalars(interp, group, &varList);
    if (code != TCL_OK) {
        return TCL_ERROR;
    }

    if (Tcl_DStringLength(&varList) == 0) {
	return TCL_OK;
    }

    /*
     * First try to retrieve all variables in one get request. This
     * may fail because the PDU causes a tooBig error or the agent
     * responds to missing variables with a noSuchName error.
     */

    Tcl_DStringAppend(&pdu->varbind, Tcl_DStringValue(&varList),
		      Tcl_DStringLength(&varList));
    code = TnmSnmpEncode(interp, session, pdu, NULL, NULL);
    if (code == TCL_OK) {	
	ScalarSetVar(interp, interp->result, arrayName, &result);
	Tcl_DStringFree(&varList);
	Tcl_DStringResult(interp, &result);
	return TCL_OK;
    }

    /*
     * Stop if we got no response since the agent is not
     * talking to us. This saves some time-outs.
     */

    if (strcmp(interp->result, "noResponse") == 0) {
	return TCL_ERROR;
    }

    /*
     * If we had no success, try every single varbind with one
     * request. Ignore errors so we just collect existing variables.
     */

    code = Tcl_SplitList(interp, Tcl_DStringValue(&varList),
			 &largc, &largv);
    if (code != TCL_OK) {
	Tcl_DStringFree(&varList);
	return TCL_ERROR;
    }

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

	pdu->type         = TNM_SNMP_GET;
	pdu->request_id   = TnmSnmpGetRequestId();
	pdu->error_status = TNM_SNMP_NOERROR;
	pdu->error_index  = 0;    
	Tcl_DStringInit(&pdu->varbind);
	Tcl_DStringAppend(&pdu->varbind, largv[i], -1);

	code = TnmSnmpEncode(interp, session, pdu, NULL, NULL);
	if (code != TCL_OK) {
	    continue;
	}

	ScalarSetVar(interp, interp->result, arrayName, &result);
    }
    ckfree((char *) largv);
    Tcl_DStringFree(&varList);
    Tcl_DStringResult(interp, &result);
    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * ScalarSetVar --
 *
 *	This procedure extracts the variables and values contained in
 *	the varbindlist vbl and set corresponding array Tcl variables.
 *	The list of array names modified is appended to result.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Tcl variables are modified.
 *
 *----------------------------------------------------------------------
 */

static void
ScalarSetVar(interp, vbl, varName, result)
    Tcl_Interp *interp;
    char *vbl;
    char *varName;
    Tcl_DString *result;
{
    int i, code;
    char *name;
    TnmSnmpVarBind *vbPtr;
    TnmMibNode *nodePtr;
    TnmVector vector;

    TnmVectorInit(&vector);
    code = TnmSnmpVarBindFromString(interp, &vector, vbl);
    if (code != TCL_OK) {
	return;
    }
    
    for (i = 0; i < TnmVectorSize(&vector); i++) {
	vbPtr = (TnmSnmpVarBind *) TnmVectorGet(&vector, i);
	if (TnmSnmpValidException(vbPtr->syntax)) {
	    continue;
	}
	name = TnmOidToString(&vbPtr->oid);
	nodePtr = TnmMibFindNode(name, NULL, 0);
	if (nodePtr) {
	    name = nodePtr->label;
	}
	Tcl_SetVar2(interp, varName, name, TnmSnmpValueToString(vbPtr), 0);
	Tcl_DStringAppendElement(result, name);
    }

    TnmSnmpVarBindFree(&vector);
    TnmVectorFree(&vector);
}
