/* * generic/dpRPC.c * * Todo: * Make activation records dynamic structure. * Change DpRPC/DpRDO to use diff parameters. Parsing should be done * in dpCmds.c * * This file contains code for sending and receiving Tcl-DP * RPCs and RDOs. All of this code is OS and, to some extent, * transport layer independent. * * An RPC/RDO is a message formatted as follows: * * --------------------------------------------------------------------- * | len:6 | space:1 | tok:1 | space:1 | id:6 | space:1 | msg:len-16 | * --------------------------------------------------------------------- * * Length is a 6 byte character string that gives the length * of the rest of the message (including the header) * as a decimal integer. Whitespace is prepended to pad it to * 6 bytes if the message length is less than 100,000 bytes. * * Tok is a character that tells what this packet * represents in RPC/RDO terms. It can be one of four * values, one each for RPC, RDO, error, or return value * messages. See the #defines for TOK_* below for details. * * A space follows the token, followed by a 6 digit numeric * identifier, formatted as a decimal string with leading * whitespace padding. The id field uniquely identifies the * RPC call, and is returned to the sender with the RPC's * return value or error. The id field is ignored for RDO calls. * * A space is tacked on after that and then the message * follows. For RPC and RDO messages, the message is a * Tcl command string to evaluate. For return values, * message contains a Tcl return value. For errors, message * contains a Tcl list with the errorCode and errorInfo variables * set. * * When a message comes in, ReadRPCChannel is called. Its * job is to read the available messages from the channel and * process each message. It first reads all available input * from the channel. There are 3 cases: * o if the message is short, the input is buffered, and * ReadRPCChannel returns. * o if the input is badly formatted (invalid length field, or * bad token), or some other error occurs, the channel is closed. * o otherwise, each complete message is processed using * ProcessRPCMessage. Any remaining incomplete data is * buffered. * * ProcessRPCMessage processes an incoming RPC, RDO, error, or * return value message. There are 4 cases: * o if the message is an RPC message, the global variable dp_rpcFile * is set to the channel identifier and the string is evaluated. * Security checks, if any, are performed at this point. * If an error code is generated, a return message is sent on * the channel consisting of the errorCode and errorInfo variable, * formatted as a Tcl list. If no error is generated, the return * value is sent on the channel. * o if the message is an RDO message, processing proceeds as with * RPC, but no return value is sent in any case (if a return value * is requested in an RDO, the evaluated string will contain a * call to dp_RDO). * o if the message is a return value or error message, the message * is stored in the activeRPCs global variable, which stores the * values that dp_RPC should return in interp->result and the * errorCode (typically, TCL_OK or TCL_ERROR). * * When an RPC is sent, the message is sent (along with a unique id), * and the process typically enters an event loop, waiting for the * return message or error. The id is used as an index in the * activeRPCs global variable. When the return value (or error) * comes back, the waiting field for the coreesponding RPC is cleared. * If a timeout occurs, the timedOut field will be set, in which case * the RPC will return (with an appropriate message). * * Timeouts cause a garbage collection problem in the activeRPCs * structure. Since each RPC corresponds to a unique outstanding * RPC message, only 1000 RPCs can be active at any time. If * RPCs time out, then that slot in the activeRPCs structure can't * be reused until the RPC returns. If the RPC never returns, we * still need to eventually free them. We know we can free a slot * if a return value message comes back (with the appropriate id) or * when the associated channel closes. * * To solve this problem, we store the time the RPC was sent in the * slot. If we ever run out of slots, the oldest timed out slots * will be reclaimed. */ #include "generic/dpInt.h" #include "tclInt.h" /* * If DBG() is defined, you will get ALOT of garbage on stdout * cooresponding to what is happening in the RPC subsystem. */ #ifndef DEBUG #define DBG(x) #else #define DBG(x) x #endif /* * The following tokens appear as the first characters * in the RPC messages which are transmitted between processes. */ #define TOK_RPC 'e' #define TOK_RDO 'd' #define TOK_RET 'r' #define TOK_ERR 'x' #define NO_TOKEN (Tcl_TimerToken)(-1) #define RPC_BUFFER_SIZE 8192 /* * One of the following structures is maintained for each Tcl channel * that is receiving/sending RPCs. */ typedef struct RPCChannel { char *name; /* Name of channel in Tcl interpreter */ Tcl_Interp *interp; /* Associated interpreter */ Tcl_Channel chan; /* Associated channel */ char *buffer; /* Any buffered input */ int error; /* Has an error occured on this channel? */ int bufLen; /* Length of any buffered input */ int maxLen; /* Number of bytes allocated to buffer */ char *checkCmd; /* Tcl command to run to check RPCs */ struct RPCChannel *next; int flags; /* Channel status */ #define CHAN_BUSY 1 #define CHAN_FREE 2 } RPCChannel; static RPCChannel *registeredChannels = NULL; /* * One of the following structures exists for each active RPC. The * structures are held in a hash table. */ typedef struct ActiveRPC { int flags; /* Flags (see below) */ int id; /* ID for the RPC */ unsigned long time; /* Time RPC was sent (for garbage collection) */ Tcl_TimerToken token; /* Token for timeout */ RPCChannel *chanPtr; /* Associated channel */ int returnValue; /* Value to return from dp_RPC */ char *result; /* Value to set in interp->result */ } ActiveRPC; static Tcl_HashTable activeRPCs; static int activeId; /* Next free id */ /* * Values for ActiveRPC->flags */ #define RPC_WAITING (1<<0) /* Waiting for a reply */ #define RPC_TIMEDOUT (1<<1) /* Timed out, no reply yet. When reply * comes, clear it */ #define RPC_CANCELLED (1<<2) /* Cancelled by user -- same semantics * for clearing as timeout */ /* * Forward Declarations */ static void DpReadRPCChannel _ANSI_ARGS_((RPCChannel *rpcChanPtr)); static void DpReadRPCChannelCallback _ANSI_ARGS_((ClientData clientData, int mask)); static void DpProcessRPCMessage _ANSI_ARGS_((Tcl_Interp *interp, RPCChannel *chan, int id, int token, char *message)); static int DpCheckRPC _ANSI_ARGS_((Tcl_Interp *interp, RPCChannel *rcPtr, char *rpcStr)); static void DpTimeoutHandler _ANSI_ARGS_((ClientData clientData)); static int DpParseEventList _ANSI_ARGS_((Tcl_Interp *interp, char *eventList, int *maskPtr)); static ActiveRPC *MakeActivationRecord _ANSI_ARGS_((RPCChannel *rpcChanPtr, int setTimeout, int timeout)); static int DpRegisterRPCChannel _ANSI_ARGS_((Tcl_Interp *interp, char *chanName, char *checkCmd)); static int DpDeleteRPCChannel _ANSI_ARGS_((Tcl_Interp *interp, RPCChannel *rpcChanPtr)); int Dp_RPCCmd _ANSI_ARGS_((ClientData clientData, Tcl_Interp *interp, int argc, char **argv)); int Dp_RDOCmd _ANSI_ARGS_((ClientData clientData, Tcl_Interp *interp, int argc, char **argv)); int Dp_AdminCmd _ANSI_ARGS_((ClientData clientData, Tcl_Interp *interp, int argc, char **argv)); int Dp_CancelRPCCmd _ANSI_ARGS_((ClientData unused, Tcl_Interp *interp, int argc, char **argv)); int Dp_RPCInit _ANSI_ARGS_((Tcl_Interp *interp)); static int DpSendRPCMessage _ANSI_ARGS_((Tcl_Channel chan, int token, int id, char *mesgStr)); /* * The following strings are used to provide callback and/or error * catching for RDOs. c = callback, e = onerror, ce = both */ /* * sprintf template when both callback and onerror are specified. * Params are cmd, onerror, callback */ static char *ceCmdTemplate = "if [catch {%s} dp_rv] {\ dp_RDO $dp_rpcFile set errorInfo \"$errorInfo\n while remotely executing\n%s\"; \ dp_RDO $dp_rpcFile eval \"%s \\{$dp_rv\\}\"\ } else {\ dp_RDO $dp_rpcFile eval \"%s \\{$dp_rv\\}\"\ }"; /* * sprintf template when just onerror is specified. * Params are cmd, onerror */ static char *eCmdTemplate = "if [catch {%s} dp_rv] {\ dp_RDO $dp_rpcFile set errorInfo \"$errorInfo\n while remotely executing\n%s\"; \ dp_RDO $dp_rpcFile eval \"%s \\{$dp_rv\\}\"\ }"; /* * sprintf template when just callback is specified. * Params are cmd, callback */ static char *cCmdTemplate = "set dp_rv [%s]; dp_RDO $dp_rpcFile eval \"%s \\{$dp_rv\\}\""; /* *-------------------------------------------------------------- * * ReadRPCChannel -- * * This function is called by the event loop (through * ReadRPCChannelCallback) whenever the file is readable * (i.e., there's an inbound message). * It reads all available input on the channel, and calls * ProcessRPCMessage on each valid message. If the input * is badly formatted, the channel is closed and an error * is returned. If the message is incomplete, it is buffered. * * Results: * None * * Side effects: * May close the channel (in which case a background error is * triggered). * *-------------------------------------------------------------- */ static void DpReadRPCChannel (rpcChanPtr) RPCChannel *rpcChanPtr; { char *buffer; int bufLen, maxLen; char str[100]; char scratch[100000]; Tcl_DString dstr; int blocking; int numRead; char *curMsg; int id, msgLen; char token, saveChar; /* * Make sure the socket's non-blocking (it was set to be non-blocking * when it was registered, and shouldn't have changed. If it did, then * it's an error in the user program). */ memset(scratch, 0, 1000); Tcl_DStringInit(&dstr); Tcl_GetChannelOption ( #ifdef _TCL80 rpcChanPtr->interp, #endif rpcChanPtr->chan, "-blocking", &dstr); Tcl_GetBoolean (rpcChanPtr->interp, Tcl_DStringValue(&dstr), &blocking); if (blocking) { sprintf (str, "Channel %s must be in non-blocking mode", rpcChanPtr->name); Tcl_SetResult (rpcChanPtr->interp, str, TCL_VOLATILE); Tcl_BackgroundError (rpcChanPtr->interp); DpDeleteRPCChannel (rpcChanPtr->interp, rpcChanPtr); return; } Tcl_DStringFree(&dstr); /* * If everything's ok, read in up to 1000000 bytes of data (the maximum * message length) and append it onto the buffer (expanding if necessary) * * Note that we only read in 100000 bytes since more causes stack * problems in Win32. There are other systems that limit a thread's * stack to 64k also, so this is not a good solution. This is a * gaping wound in the RPC code that needs to be fixed. * * You need help if you are sending a megabyte RPC anyhow. :) */ numRead = Tcl_Read (rpcChanPtr->chan, scratch, sizeof(scratch)); if (numRead < 1) { if (Tcl_Eof(rpcChanPtr->chan)) { /* * End of File on the RPC channel. This will usually happen * if the TCP connection is reset. * * We'll just close the channel quietly. */ Tcl_Channel chan = rpcChanPtr->chan; Tcl_Interp *interp = rpcChanPtr->interp; DpDeleteRPCChannel(rpcChanPtr->interp, rpcChanPtr); DBG(printf("Closing EOF'd RPC channel")); DpClose(interp, chan); return; } else { return; } } buffer = rpcChanPtr->buffer; bufLen = rpcChanPtr->bufLen; maxLen = rpcChanPtr->maxLen; if (bufLen + numRead > maxLen) { char *newBuffer; maxLen = bufLen + numRead + 1024; newBuffer = ckalloc (maxLen); memcpy (newBuffer, buffer, bufLen); ckfree (buffer); buffer = newBuffer; } memcpy (buffer+bufLen, scratch, numRead); bufLen += numRead; /* * Process all the messages, one by one. We break out of this loop * when there's not a complete message left in the buffer */ curMsg = buffer; while (1) { /* * Make sure message length field is there. */ if (bufLen < 6) { break; } /* * Ok, we have the length field of the message. Make sure it's * valid. */ memcpy(str, curMsg, 6); str[6] = 0; if (Tcl_GetInt(rpcChanPtr->interp, str, &msgLen) != TCL_OK) { goto badFormat; } /* * Got a valid length field. Make sure rest of message is there. */ if (bufLen < msgLen) { break; } saveChar = curMsg[msgLen]; curMsg[msgLen] = '\0'; DBG(printf("\nIncoming RPC: %s on %s\n", curMsg, Tcl_GetChannelName(rpcChanPtr->chan))); /* * Ok, we've got enough data. Extract the fields and call * ProcessRPCMessage. If we get a bad message, shut down the channel. * Recall that the message format is: * --------------------------------------------------------------------- * | len:6 | space:1 | tok:1 | space:1 | id:6 | space:1 | msg:len-16 | * --------------------------------------------------------------------- */ token = curMsg[7]; memcpy(str, &curMsg[9], 6); str[6] = 0; if (Tcl_GetInt(rpcChanPtr->interp, str, &id) != TCL_OK) { goto badFormat; } DpProcessRPCMessage(rpcChanPtr->interp, rpcChanPtr, id, token, &curMsg[16]); curMsg += msgLen; *curMsg = saveChar; bufLen -= msgLen; if (rpcChanPtr->flags & CHAN_FREE) { DpDeleteRPCChannel (rpcChanPtr->interp, rpcChanPtr); return; } } /* * Done! Shift down any remaining data to the beginning of the buffer * and return! */ memcpy (buffer, curMsg, bufLen); rpcChanPtr->bufLen = bufLen; rpcChanPtr->buffer = buffer; rpcChanPtr->maxLen = maxLen; return; badFormat: DBG(printf("Bad RPC packet: %s\n", curMsg)); sprintf(str, "Received badly formatted packet on RPC channel %s", rpcChanPtr->name); Tcl_SetResult(rpcChanPtr->interp, str, TCL_VOLATILE); Tcl_BackgroundError(rpcChanPtr->interp); return; } /* *-------------------------------------------------------------- * * DpReadRPCChannelCallback -- * * This function is called by the event handling system * whenever the channel is readable. Calls ReadRPCChannel * on the channel. * * Results: * None * * Side effects: * None. * *-------------------------------------------------------------- */ static void DpReadRPCChannelCallback (clientData, mask) ClientData clientData; int mask; { DpReadRPCChannel((RPCChannel *)clientData); } /* *-------------------------------------------------------------- * * DpProcessRPCMessage -- * * Process an incoming RPC, RDO, error, or return value message. * See the documentation at the top of this file for details. * * Results: * None. * * Side effects: * Since a Tcl command is evaluated, almost anything can happen. * * ProcessRPCMessage processes an incoming RPC, RDO, error, or * return value message. There are 4 cases: * * o if the message is an RPC message, the global variable dp_rpcFile * is set to the channel identifier and the string is evaluated. * Security checks, if any, are performed at this point. * If an error code is generated, a return message is sent on * the channel consisting of the errorCode and errorInfo variable, * formatted as a Tcl list. If no error is generated, the return * value is sent on the channel. * o if the message is an RDO message, processing proceeds as with * RPC, but no return value is sent in any case (if a return value * is requested in an RDO, the evaluated string will contain a * call to dp_RDO). * o if the message is a return value or error message, the message * is stored in the activeRPCs global variable, which stores the * values that dp_RPC should return in interp->result and the * errorCode (typically, TCL_OK or TCL_ERROR). * *-------------------------------------------------------------- */ static void DpProcessRPCMessage(interp, chan, id, token, message) Tcl_Interp *interp; /* (in) Tcl interpreter for error reporting */ RPCChannel *chan; /* (in) Incoming RPC data channel */ int id; /* (in) Integer id for RPC */ int token; /* (in) Message token */ char *message; /* (in) The command to evaluate (zero terminated) */ { ActiveRPC *arPtr = NULL; RPCChannel *rcPtr; Tcl_HashEntry *entryPtr; int retCode, len; char **argv; int argc; rcPtr = chan; rcPtr->flags |= CHAN_BUSY; entryPtr = Tcl_FindHashEntry(&activeRPCs, (char *)id); if (entryPtr != NULL) { arPtr = (ActiveRPC *) Tcl_GetHashValue(entryPtr); } if (((token == TOK_RET) || (token == TOK_ERR)) && (arPtr == NULL)) { DBG(printf("Received reply to cancelled RPC\n")); rcPtr->flags &= ~CHAN_BUSY; return; } switch (token) { case TOK_RPC: Tcl_SetVar(interp, "dp_rpcFile", rcPtr->name, TCL_GLOBAL_ONLY); if (DpCheckRPC(interp, rcPtr, message) == TCL_OK) { retCode = Tcl_GlobalEval(interp, message); if (retCode != TCL_OK) { char *rv[2]; char *errMsg; rv[0] = interp->result; rv[1] = Tcl_GetVar(interp, "errorInfo", TCL_GLOBAL_ONLY); errMsg = Tcl_Merge(2, rv); if (DpSendRPCMessage(rcPtr->chan, TOK_ERR, id, errMsg) != TCL_OK) { ckfree(errMsg); goto error; } ckfree(errMsg); } else { if (DpSendRPCMessage(rcPtr->chan, TOK_RET, id, interp->result) != TCL_OK) { goto error; } } } else { if (DpSendRPCMessage(rcPtr->chan, TOK_ERR, id, "RPC authorization denied") != TCL_OK) { goto error; } } break; case TOK_RDO: /* * RDOs are very simple. */ Tcl_SetVar(interp, "dp_rpcFile", rcPtr->name, TCL_GLOBAL_ONLY); if (DpCheckRPC(interp, rcPtr, message) == TCL_OK) { Tcl_GlobalEval(interp, message); } break; case TOK_ERR: /* * Split the error message into the result and info * and save both. */ Tcl_SplitList(interp, message, &argc, &argv); Tcl_AddErrorInfo(rcPtr->interp, argv[1]); len = strlen(argv[0]) + 1; arPtr->result = ckalloc(len); memcpy(arPtr->result, argv[0], len); ckfree((char *)argv); arPtr->flags = 0; arPtr->returnValue = TCL_ERROR; break; case TOK_RET: /* * Provide the return result to the interp. */ len = strlen(message) + 1; arPtr->result = ckalloc(len); memcpy(arPtr->result, message, len); arPtr->flags = 0; arPtr->returnValue = TCL_OK; break; default: fprintf(stderr, "Invalid token received in incoming RPC.\n"); break; } rcPtr->flags &= ~CHAN_BUSY; return; error: Tcl_AppendResult(interp, "Error sending RPC response on \"", Tcl_GetChannelName(rcPtr->chan), "\"", NULL); rcPtr->flags &= ~CHAN_BUSY; return; } /* ---------------------------------------------------- * * DpCheckRPC -- * * Checks an incoming RPC with a user-defined function. * * Returns * * TCL_OK if allowed or TCL_ERROR if not. * * Side Effects * * The command checking procedure is evaluated. * * ----------------------------------------------------- */ static int DpCheckRPC(interp, rcPtr, rpcStr) Tcl_Interp *interp; RPCChannel *rcPtr; char *rpcStr; { Tcl_DString cmd; int resCode; if (rcPtr->checkCmd == NULL) { return TCL_OK; } Tcl_DStringInit(&cmd); Tcl_DStringAppend(&cmd, rcPtr->checkCmd, -1); Tcl_DStringAppend(&cmd, " {", -1); Tcl_DStringAppend(&cmd, rpcStr, -1); Tcl_DStringAppend(&cmd, "}", -1); resCode = Tcl_GlobalEval(interp, Tcl_DStringValue(&cmd)); Tcl_DStringFree(&cmd); return resCode; } /* *-------------------------------------------------------------- * * TimeoutHandler -- * * This function is called by a timer handler when the associated * RPC times out * * Results: * None. * * Side effects: * The associated RPC will return as soon as possible. * *-------------------------------------------------------------- */ static void DpTimeoutHandler (clientData) ClientData clientData; /* Id of the RPC that timed out */ { int id = (int)clientData; ActiveRPC *activePtr; Tcl_HashEntry *entryPtr; /* * Cancel the RPC by setting the flag on its active record (the * event loop with the waiting RPC will notice this when it's its * turn to return). */ entryPtr = Tcl_FindHashEntry(&activeRPCs, (char *)id); if (entryPtr != NULL) { activePtr = (ActiveRPC *)Tcl_GetHashValue(entryPtr); activePtr->flags = RPC_TIMEDOUT; } } /* *-------------------------------------------------------------- * * ParseEventList -- * * Helper function to parse "-events" option of dp_RPC * * Results: * A standard tcl result. * * Side effects: * None. * *-------------------------------------------------------------- */ static int DpParseEventList (interp, eventList, maskPtr) Tcl_Interp *interp; /* Tcl interpreter for error reporting */ char *eventList; /* Event list to parse */ int *maskPtr; /* Returned event mask */ { int i; int eventc; char **eventv; int events = 0; if (Tcl_SplitList(interp, eventList, &eventc, &eventv) != TCL_OK) { Tcl_AppendResult(interp, "bad parameter \"", eventList, "\" for -events flag", (char *) NULL); return TCL_ERROR; } for (i = 0; i < eventc; i++) { if (strcmp(eventv[i], "x") == 0) *maskPtr |= TCL_WINDOW_EVENTS; else if (strcmp(eventv[i], "window") == 0) *maskPtr |= TCL_WINDOW_EVENTS; else if (strcmp(eventv[i], "rpc") == 0) *maskPtr |= TCL_FILE_EVENTS; else if (strcmp(eventv[i], "file") == 0) *maskPtr |= TCL_FILE_EVENTS; else if (strcmp(eventv[i], "timer") == 0) *maskPtr |= TCL_TIMER_EVENTS; else if (strcmp(eventv[i], "idle") == 0) *maskPtr |= TCL_IDLE_EVENTS; else { Tcl_AppendResult(interp, "unknown event type \"", eventv[i], "\" : should be all, or list of ", "x, window, rpc, file, timer, or idle", (char *) NULL); ckfree((char *) eventv); return TCL_ERROR; } } ckfree((char *) eventv); return TCL_OK; } /* *-------------------------------------------------------------- * * MakeActivationRecord -- * * Create an activation record for a new, outbound RPC. * * Results: * Pointer to the new record, or NULL on error. * * Side effects: * Creates a timer handler that may need to be cleared * later on. The hash table activeRPCs now contains an * entry for the activation record, so it'll have to be * free'd later on. * *-------------------------------------------------------------- */ static ActiveRPC * MakeActivationRecord (rpcChanPtr, setTimeout, timeout) RPCChannel *rpcChanPtr; /* Channel we're sending RPC on. */ int setTimeout; /* Should we set a timeout? */ int timeout; /* Timeout (in milliseconds) */ { int i; ActiveRPC *newRecPtr; Tcl_HashEntry *entryPtr; int isNew; /* * Create/initialize new activation record */ newRecPtr = (ActiveRPC *)ckalloc (sizeof(ActiveRPC)); newRecPtr->flags = RPC_WAITING; newRecPtr->time = TclpGetSeconds(); newRecPtr->chanPtr = rpcChanPtr; newRecPtr->id = 0; /* * Find a free id, and install a new activation record for it */ for (i = activeId; i != activeId-1; i = (i+1)%1000000) { entryPtr = Tcl_CreateHashEntry(&activeRPCs, (char *)i, &isNew); if (isNew) { DBG(printf("Adding %s:%d to hash table\n", Tcl_GetChannelName(newRecPtr->chanPtr->chan), i)); Tcl_SetHashValue(entryPtr, (ClientData)newRecPtr); newRecPtr->id = i; activeId = i+1; if (setTimeout) { newRecPtr->token = Tcl_CreateTimerHandler(timeout, DpTimeoutHandler, (ClientData)i); } else { newRecPtr->token = NO_TOKEN; } return newRecPtr; } } /* * Couldn't find a free one -- return error */ return NULL; } /* *-------------------------------------------------------------- * * DpRPCCmd -- * * Send an RPC message and wait for a reply. * * Results: * A standard Tcl result * * Side effects: * A message is sent on the channel, and the procedure blocks * until the return value is received or the RPC times out. * *-------------------------------------------------------------- */ /* ARGSUSED */ int Dp_RPCCmd (clientData, interp, argc, argv) ClientData clientData; /* ignored */ Tcl_Interp *interp; /* tcl interpreter */ int argc; /* Number of arguments */ char *argv[]; /* Arg list */ { RPCChannel *rpcChanPtr; ActiveRPC *activePtr, *arPtr; int i; int rpcArgc; char **rpcArgv; char *command; Tcl_HashEntry *entryPtr; int returnValue; int rc = TCL_OK; /* * The default values for the value-option pairs */ int timeout = -1; char *timeoutReturn = NULL; int events = TCL_FILE_EVENTS; /* * Flags to indicate that a certain option has been set by the * command line */ int setTimeout = 0; int setTimeoutReturn = 0; if (argc < 3) { goto usage; } for (rpcChanPtr = registeredChannels; rpcChanPtr != NULL; rpcChanPtr = rpcChanPtr->next) { if (strcmp(argv[1], rpcChanPtr->name) == 0) { break; } } if (rpcChanPtr == NULL) { Tcl_AppendResult(interp, "Attempt to send RPC over unregistered ", "channel. Use dp_admin to register channel first.", NULL); return TCL_ERROR; } /* * Ok, found the channel and it's valid. Now, parse the value/option * pairs. */ for (i=2; ichan, TOK_RPC, activePtr->id, command) != TCL_OK) { Tcl_AppendResult(interp, "Error sending RPC on channel ", Tcl_GetChannelName(rpcChanPtr->chan), NULL); ckfree((char *) command); rc = TCL_ERROR; goto cleanup; } ckfree((char *) command); /* * Wait for reply in an event loop. */ while (activePtr->flags == RPC_WAITING) { Tcl_DoOneEvent (events); } /* * If we got a timeout, call the timeout handler (if there is one) * or trigger an error. * * If the RPC was cancelled, clear the timer event and return an * error. * * If there wasn't a timeout or error, clear the timer event, set * the variables according to the reply, and return. */ if (activePtr->flags & RPC_TIMEDOUT) { if (timeoutReturn) { /* * Allocate memory for timeout proc and channel name */ char *cmd = ckalloc(strlen(timeoutReturn) + 16); sprintf(cmd, "%s %s", timeoutReturn, Tcl_GetChannelName(rpcChanPtr->chan)); rc = Tcl_GlobalEval(interp, cmd); ckfree(cmd); goto cleanup; } else { Tcl_AppendResult(interp, "RPC timed out on channel ", argv[1], NULL); rc = TCL_ERROR; goto cleanup; } } else { if (activePtr->token != NO_TOKEN) { Tcl_DeleteTimerHandler(activePtr->token); } if (activePtr->flags & RPC_CANCELLED) { Tcl_AppendResult(interp, "RPC cancelled on channel ", argv[1], NULL); rc = TCL_ERROR; goto cleanup; } Tcl_SetResult (interp, activePtr->result, TCL_VOLATILE); rc = activePtr->returnValue; ckfree((char *)activePtr->result); goto cleanup; } cleanup: entryPtr = Tcl_FindHashEntry(&activeRPCs, (char *)activePtr->id); if (entryPtr != NULL) { arPtr = (ActiveRPC *) Tcl_GetHashValue(entryPtr); DBG(printf("Deleting %s:%d from hash table\n", Tcl_GetChannelName(arPtr->chanPtr->chan), arPtr->id, activePtr->id)); Tcl_DeleteHashEntry(entryPtr); } ckfree((char *)activePtr); return rc; usage: Tcl_AppendResult(interp, "Usage:\n", "\"", argv[0], " ?-timeout milliseconds ?-timeoutReturn callback??", " ?-events eventList? command ?args ...?\"\n", NULL); return TCL_ERROR; arg_missing: Tcl_AppendResult(interp, "value for \"", argv[argc-1], "\" missing", NULL); return TCL_ERROR; } /* *-------------------------------------------------------------- * * Dp_RDOCmd -- * * Send an RDO message and return. * * Results: * A standard Tcl result * * Side effects: * A message is sent on the channel. This may cause the receiving * process to send us a reply, which will invoke DpReadRPCChannel. * *-------------------------------------------------------------- */ /* ARGSUSED */ int Dp_RDOCmd (clientData, interp, argc, argv) ClientData clientData; /* ignored */ Tcl_Interp *interp; /* tcl interpreter */ int argc; /* Number of arguments */ char *argv[]; /* Arg list */ { RPCChannel *rpcChanPtr; int rpcArgc, i; char **rpcArgv; char *command, *cmdStr; char *onerror, *callback; char *cmd; if (argc < 3) { Tcl_AppendResult(interp, "Wrong number of args", NULL); goto usage; } for (rpcChanPtr = registeredChannels; rpcChanPtr != NULL; rpcChanPtr = rpcChanPtr->next) { if (strcmp(argv[1], rpcChanPtr->name) == 0) { break; } } if (rpcChanPtr == NULL) { Tcl_AppendResult(interp, "Attempted to send RDO over unregistered ", "channel.\nUse dp_admin to register channel first.", NULL); return TCL_ERROR; } /* * Ok, found the channel and it's valid. Now, parse the value/option * pairs. */ onerror = callback = NULL; for (i=2; ichan, TOK_RDO, 0, command); ckfree((char *) command); return TCL_OK; usage: Tcl_AppendResult(interp, " Usage:\n", "\"", argv[0], " ?-events eventList? ?-callback script? ?-onerror script? command ?args ...?\"\n", NULL); return TCL_ERROR; arg_missing: Tcl_AppendResult(interp, "value for \"", argv[argc-1], "\" missing", NULL); return TCL_ERROR; } /* ---------------------------------------------------- * * DpCancelRPC -- * * Breaks off the current RPC(s?) and forces it * to return an error. * * Returns * * Nothing. * * Side Effects * * All waiting RPCs on the given channel are cancelled. * * ----------------------------------------------------- */ static void DpCancelRPC(rpcChanPtr) RPCChannel *rpcChanPtr; /* (in) RPCChannel to cancel */ { ActiveRPC *rpcList; Tcl_HashEntry *hashEntry; Tcl_HashSearch hashStruct; for (hashEntry = Tcl_FirstHashEntry(&activeRPCs, &hashStruct); hashEntry != NULL; hashEntry = Tcl_NextHashEntry(&hashStruct)) { rpcList = (ActiveRPC *) Tcl_GetHashValue(hashEntry); if (rpcList->chanPtr == rpcChanPtr) { /* * Mark it as cancelled so the event * loop will exit. */ rpcList->flags = RPC_CANCELLED; } } } /* ---------------------------------------------------- * * Dp_CancelRPCCmd -- * * Command parser for the dp_CancelRPC command. * * Returns * * TCL_OK or TCL_ERROR. * * Side Effects * * RPCs may be cancelled. * * ----------------------------------------------------- */ int Dp_CancelRPCCmd(unused, interp, argc, argv) ClientData unused; Tcl_Interp *interp; int argc; char **argv; { RPCChannel *chanPtr; Tcl_Channel chan; int mode, rc = TCL_OK, i; char *chanName; if (argc < 2) { Tcl_AppendResult(interp, "wrong # args: should be \"", argv[0], " [?chanID? ?chanID? ... | all]", (char *) NULL); return TCL_ERROR; } if ((argc == 2) && !strcmp(argv[1], "all")) { for (chanPtr = registeredChannels; chanPtr != NULL; chanPtr = chanPtr->next) { DpCancelRPC(chanPtr); } } else { /* * Now we need to cycle through all the channel IDs given, * find the actual channel structure and then find the RPC * structure cooresponding to that channel. */ for (i = 1; i != argc; i++) { chan = Tcl_GetChannel(interp, argv[i], &mode); if (chan == NULL) { return TCL_ERROR; } else { chanName = Tcl_GetChannelName(chan); for (chanPtr = registeredChannels; chanPtr != NULL; chanPtr = chanPtr->next) { if (!strcmp(Tcl_GetChannelName(chanPtr->chan), chanName)) { DpCancelRPC(chanPtr); break; } } } } } return rc; } /* *-------------------------------------------------------------- * * DpRegisterRPCChannel -- * * This function turns a Tcl-DP channel into an RPC channel * by creating an event handler and the appropriate data structure. * * Results: * A standard Tcl result * * Side effects: * From now on, whenever the channel is readable, ReadRPCChannel * will be called * *-------------------------------------------------------------- */ static int DpRegisterRPCChannel (interp, chanName, checkCmd) Tcl_Interp *interp; char *chanName; char *checkCmd; { Tcl_Channel chan; int mode; RPCChannel *prev, *searchPtr; RPCChannel *newRpcChannelPtr; /* * Make sure the channel hasn't been registered already. */ for (prev = NULL, searchPtr = registeredChannels; searchPtr != NULL; prev = searchPtr, searchPtr = searchPtr->next) { if (strcmp(chanName, searchPtr->name) == 0) { Tcl_AppendResult(interp, "Channel ", chanName, " is already registered", NULL); return TCL_ERROR; } } /* * Check to make sure we can find the channel, and that it's * readable/writeable */ chan = Tcl_GetChannel(interp, chanName, &mode); if (chan == NULL) { return TCL_ERROR; } if (mode != (TCL_READABLE | TCL_WRITABLE)) { Tcl_AppendResult(interp, "Can't use channel ", chanName, " for RPC -- channel is not both readable and writeable", NULL); return TCL_ERROR; } /* * Things look ok! Make the file non-blocking, create a file handler * for the channel, and return */ if (Tcl_SetChannelOption (interp, chan, "-blocking", "0") != TCL_OK) { return TCL_ERROR; } newRpcChannelPtr = (RPCChannel *)ckalloc(sizeof(RPCChannel)); newRpcChannelPtr->name = ckalloc(strlen(chanName) + 1); strcpy(newRpcChannelPtr->name, chanName); newRpcChannelPtr->interp = interp; newRpcChannelPtr->error = 0; newRpcChannelPtr->bufLen = 0; newRpcChannelPtr->maxLen = RPC_BUFFER_SIZE; newRpcChannelPtr->buffer = ckalloc(RPC_BUFFER_SIZE); memset(newRpcChannelPtr->buffer, 0, RPC_BUFFER_SIZE); newRpcChannelPtr->chan = chan; newRpcChannelPtr->checkCmd = NULL; newRpcChannelPtr->flags = 0; if (checkCmd) { newRpcChannelPtr->checkCmd = ckalloc(strlen(checkCmd) + 1); strcpy(newRpcChannelPtr->checkCmd, checkCmd); } newRpcChannelPtr->next = registeredChannels; registeredChannels = newRpcChannelPtr; Tcl_CreateChannelHandler(chan, TCL_READABLE, DpReadRPCChannelCallback, (ClientData)newRpcChannelPtr); return TCL_OK; } /* *-------------------------------------------------------------- * * DpDeleteRPCChannel -- * * Clears the RPC channel handler created by CreateRPCChannel * and frees up the data structures associated with it. * * Results: * A standard Tcl result * * Side effects: * rpcChanPtr is free'd and should not be referenced. * *-------------------------------------------------------------- */ static int DpDeleteRPCChannel (interp, rpcChanPtr) Tcl_Interp *interp; RPCChannel *rpcChanPtr; { Tcl_HashEntry *entryPtr; Tcl_HashSearch hashSearchRec; RPCChannel *searchPtr, *prev; ActiveRPC *activePtr; Tcl_Channel chan; int mode; if (rpcChanPtr == NULL) { return TCL_ERROR; } if (rpcChanPtr->flags & CHAN_BUSY) { rpcChanPtr->flags |= CHAN_FREE; return TCL_OK; } for (searchPtr = registeredChannels, prev = NULL; searchPtr != NULL; prev = searchPtr, searchPtr = searchPtr->next) { if (!strcmp(rpcChanPtr->name, searchPtr->name)) { break; } } /* * Clear the file handler, clear all active RPCs, * and free its memory. */ if ((chan = Tcl_GetChannel(interp, searchPtr->name, &mode)) != NULL) { Tcl_DeleteChannelHandler(chan, DpReadRPCChannelCallback, (ClientData)searchPtr); } DpCancelRPC(searchPtr); if (prev == NULL) { registeredChannels = searchPtr->next; } else { prev->next = searchPtr->next; } ckfree((char *) searchPtr->name); ckfree((char *) searchPtr->buffer); if (searchPtr->checkCmd) { ckfree((char *) searchPtr->checkCmd); } ckfree((char *) searchPtr); return TCL_OK; } /* *-------------------------------------------------------------- * * Dp_AdminCmd -- * * This function implements the "dp_admin" command. It * handles administrative functions of the RPC module. * It's usage: * * dp_admin register ?-check checkCmd? * dp_admin delete * * If called with "register", the channel is made into an RPC * channel (i.e., RPCs can be sent/received across it). If called * with "delete", RPCs will no longer be sent/received across it. * * Results: * A standard tcl result. * * Side effects: * Once a channel is registered, all input on the channel will * be received through DpReadRPCChannel(), which will be called * from the event loop. * *-------------------------------------------------------------- */ /* ARGSUSED */ int Dp_AdminCmd (clientData, interp, argc, argv) ClientData clientData; /* ignored */ Tcl_Interp *interp; /* tcl interpreter */ int argc; /* Number of arguments */ char *argv[]; /* Arg list */ { char c; int len; Tcl_Channel chan = NULL; RPCChannel *searchPtr; char *checkCmd = NULL; if (argc != 3 && argc != 5) { Tcl_AppendResult(interp, "Wrong number of args", NULL); goto usage; } c = argv[1][0]; len = strlen(argv[1]); /* * Check to see if we got a check procedure. */ if (argc == 5) { if (!strcmp(argv[3], "-check")) { checkCmd = argv[4]; if (!strcmp(checkCmd, "none")) { checkCmd = NULL; } } else { goto usage; } } /* ------------------------ REGISTER ------------------------------ */ if ((c == 'r') && (strncmp(argv[1], "register", len) == 0)) { return DpRegisterRPCChannel (interp, argv[2], checkCmd); /* ------------------------ DELETE -------------------------------- */ } else if ((c == 'd') && (strncmp(argv[1], "delete", len) == 0)) { for (searchPtr = registeredChannels; searchPtr != NULL; searchPtr = searchPtr->next) { if (!strcmp(argv[2], searchPtr->name)) { break; } } if (searchPtr == NULL) { Tcl_AppendResult(interp, "Channel \"", argv[2], "\" not registered.", NULL); return TCL_ERROR; } return DpDeleteRPCChannel (interp, searchPtr); /* ------------------------ ERROR --------------------------------- */ } else { goto usage; } usage: Tcl_AppendResult(interp, " Possible usages:\n", "\"", argv[0], " register ?-check checkCmd?\"\n", "\"", argv[0], " delete \"\n", NULL); return TCL_ERROR; } /* *-------------------------------------------------------------- * * DpSendRPCMessage -- * * Send a formatted RPC packet out on the specified channel. * * Results: * TCL_OK or TCL_ERROR. * * Side effects: * None * *-------------------------------------------------------------- */ static int DpSendRPCMessage (chan, token, id, mesgStr) Tcl_Channel chan; int token; /* in: msg type token */ int id; /* in: RPC ID # */ char *mesgStr; /* in: actual RPC string */ { char *bufStr; int result, totalLength; totalLength = strlen(mesgStr) + 16; bufStr = ckalloc(totalLength + 1); sprintf(bufStr, "%6d %c %6d %s", totalLength, (char)token, id, mesgStr); DBG(printf("\nSending RPC: %s on %s\n", bufStr, Tcl_GetChannelName(chan))); result = Tcl_Write(chan, bufStr, totalLength); ckfree(bufStr); if (result != totalLength) { return TCL_ERROR; } return TCL_OK; } /* *-------------------------------------------------------------- * * DpRPCInit -- * * This function initializes the global data structures in * this module. * * * Results: * A standard Tcl result. * * Side effects: * None. * *-------------------------------------------------------------- */ int DpRPCInit (interp) Tcl_Interp *interp; { Tcl_InitHashTable(&activeRPCs, TCL_ONE_WORD_KEYS); activeId = 0; return TCL_OK; }