/* * tclWinConsole.c -- * * This file implements the Windows-specific console functions, and the * "console" channel driver. Windows 7 or later required. * * Copyright © 2022 Ashok P. Nadkarni * * See the file "license.terms" for information on usage and redistribution of * this file, and for a DISCLAIMER OF ALL WARRANTIES. */ #define TCL_CONSOLE_DEBUG #ifdef TCL_CONSOLE_DEBUG #undef NDEBUG /* Enable asserts */ #endif #include "tclWinInt.h" #include #include /* * A general note on the design: The console channel driver differs from most * other drivers in the following respects: * * - There can be at most 3 console handles at any time since Windows does * support allocation of more than one console (with three handles * corresponding to stdin, stdout, stderr) * * - Consoles are created / inherited at process startup. There is currently * no way in Tcl to programmatically create a console. Even if there were * added the above Windows limitation would still apply. * * - Unlike files, sockets etc. where there is a one-to-one * correspondence between Tcl channels and operating system handles, * std* channels are shared amongst threads which means there can be * multiple Tcl channels corresponding to a single console handle. * * - Even with multiple threads, more than one file event handler is unlikely. * It does not make sense for multiple threads to register handlers for * stdin because the input would be randomly fragmented amongst the threads * (not even on a per line basis). * * Various design factors are driven by the above, e.g. use of lists instead * of hash tables (at most 3 console handles) and use of global instead of * per thread queues which simplifies lock management particularly because * thread-console relation is not one-one and is likely more performant as * well with fewer locks needing to be obtained. */ /* * The following variable is used to tell whether this module has been * initialized. */ static int initialized = 0; #define CONSOLE_BUFFER_SIZE 8000 // TODO - must be at least 2 :-) /* * Ring buffer for storing data. Actual data is from bufPtr[start]:bufPtr[size-1] * and bufPtr[0]:bufPtr[length - (size-start)]. */ #if TCL_MAJOR_VERSION > 8 typedef ptrdiff_t RingSizeT; /* Tcl9 TODO */ #define RingSizeT_MAX PTRDIFF_MAX #else typedef int RingSizeT; #define RingSizeT_MAX INT_MAX #endif typedef struct RingBuffer { char *bufPtr; /* Pointer to buffer storage */ RingSizeT capacity; /* Size of the buffer in RingBufferChar */ RingSizeT start; /* Start of the data within the buffer. */ RingSizeT length; /* Number of RingBufferChar*/ } RingBuffer; #define RingBufferLength(ringPtr_) ((ringPtr_)->length) #define RingBufferFreeSpace(ringPtr_) ((ringPtr_)->capacity - (ringPtr_)->length) #define RINGBUFFER_ASSERT(ringPtr_) assert(RingBufferCheck(ringPtr_)) /* * The Win32 console API does not support non-blocking I/O in any form. Thus * the actual calls are made on a separate thread. Moreover, separate * threads are needed for each handle because (for example) blocking on user * input on stdin should not prevent output to stdout when non-blocking i/o * is configured at the script level. * * In the input (e.g. stdin) case, the console stdin thread is the producer * writing to the buffer ring buffer. The Tcl interpreter threads are the * consumer. For the output (e.g. stdout/stderr) case, the Tcl interpreter * are the producers while the console stdout/stderr threads are the * consumers. * * Consoles are identified purely by handles and multiple threads may open * them (as stdin/stdout/stderr are shared). * * Note on reference counting - a ConsoleHandleInfo instance has multiple * references to it - one each from every channel that is attached to it * plus one from the console thread itself which also serves as the reference * from gConsoleHandleInfoList. */ typedef struct ConsoleHandleInfo { struct ConsoleHandleInfo *nextPtr; /* Process-global list of consoles */ HANDLE console; /* Console handle */ HANDLE consoleThread; /* Handle to thread doing actual i/o on the console */ SRWLOCK lock; /* Controls access to this structure. * Cheaper than CRITICAL_SECTION but note does not * support recursive locks or Try* style attempts.*/ CONDITION_VARIABLE consoleThreadCV;/* For awakening console thread */ CONDITION_VARIABLE interpThreadCV; /* For awakening interpthread(s) */ RingBuffer buffer; /* Buffer for data transferred between console * threads and Tcl threads. For input consoles, * written by the console thread and read by Tcl * threads. The converse for output threads */ DWORD initMode; /* Initial console mode. */ DWORD lastError; /* An error caused by the last background * operation. Set to 0 if no error has been * detected. */ int numRefs; /* See comments above */ int permissions; /* TCL_READABLE for input consoles, TCL_WRITABLE * for output. Only one or the other can be set. */ } ConsoleHandleInfo; /* * This structure describes per-instance data for a console based channel. * * Note on locking - this structure has no locks because it is accessed * only from the thread owning channel EXCEPT when a console traverses it * looking for a channel that is watching for events on the console. Even * in that case, no locking is required because that access is only under * the consoleLock lock which prevents the channel from being removed from * the gWatchingChannelList which in turn means it will not be deallocated * from under the console thread. Access to individual fields does not need * to be controlled because * - the console thread does not write to any fields * - changes to the nextWatchingChannelPtr field and CONSOLE_EVENT_QUEUE * bit flags are under the gConsoleLock lock * - changes to other fields do not matter because after being read for * queueing events, they are verified again when the event is received * in the interpreter thread (since they could have changed anyways while * the event was in-flight on the event queue) * * Note on reference counting - a structure instance may be referenced from * three places: * - the Tcl channel subsystem. This reference is created when on channel * opening and dropped on channel close. This also covers the reference * from gWatchingChannelList since queueing / dequeuing from that list * happens in conjunction with channel operations. * - the Tcl event queue entries. This reference is added when the event * is queued and dropped on receipt. */ typedef struct ConsoleChannelInfo { HANDLE handle; /* Console handle */ Tcl_ThreadId threadId; /* Id of owning thread */ struct ConsoleChannelInfo *nextWatchingChannelPtr; /* Pointer to next channel watching events. */ Tcl_Channel channel; /* Pointer to channel structure. */ DWORD initMode; /* Initial console mode. */ int numRefs; /* See comments above */ int permissions; /* OR'ed combination of TCL_READABLE, * TCL_WRITABLE, or TCL_EXCEPTION: indicates * which operations are valid on the file. */ int watchMask; /* OR'ed combination of TCL_READABLE, * TCL_WRITABLE, or TCL_EXCEPTION: indicates * which events should be reported. */ int flags; /* State flags */ #define CONSOLE_EVENT_QUEUED (1 << 0) /* Notification event already queued */ #define CONSOLE_ASYNC (1 << 1) /* Channel is non-blocking. */ #define CONSOLE_READ_OPS (1 << 2) /* Channel supports read-related ops. */ } ConsoleChannelInfo; /* * The following structure is what is added to the Tcl event queue when * console events are generated. */ typedef struct { Tcl_Event header; /* Information that is standard for all events. */ ConsoleChannelInfo *chanInfoPtr; /* Pointer to console info structure. Note * that we still have to verify that the * console exists before dereferencing this * pointer. */ } ConsoleEvent; /* * Declarations for functions used only in this file. */ static int ConsoleBlockModeProc(ClientData instanceData, int mode); static void ConsoleCheckProc(ClientData clientData, int flags); static int ConsoleCloseProc(ClientData instanceData, Tcl_Interp *interp, int flags); static int ConsoleEventProc(Tcl_Event *evPtr, int flags); static void ConsoleExitHandler(ClientData clientData); static int ConsoleGetHandleProc(ClientData instanceData, int direction, ClientData *handlePtr); static int ConsoleGetOptionProc(ClientData instanceData, Tcl_Interp *interp, const char *optionName, Tcl_DString *dsPtr); static void ConsoleInit(void); static int ConsoleInputProc(ClientData instanceData, char *buf, int toRead, int *errorCode); static int ConsoleOutputProc(ClientData instanceData, const char *buf, int toWrite, int *errorCode); static int ConsoleSetOptionProc(ClientData instanceData, Tcl_Interp *interp, const char *optionName, const char *value); static void ConsoleSetupProc(ClientData clientData, int flags); static void ConsoleWatchProc(ClientData instanceData, int mask); static void ProcExitHandler(ClientData clientData); static void ConsoleThreadActionProc(ClientData instanceData, int action); static DWORD ReadConsoleChars(HANDLE hConsole, WCHAR *lpBuffer, RingSizeT nChars, RingSizeT *nCharsReadPtr); static DWORD WriteConsoleChars(HANDLE hConsole, const WCHAR *lpBuffer, RingSizeT nChars, RingSizeT *nCharsWritten); static void RingBufferInit(RingBuffer *ringPtr, RingSizeT capacity); static void RingBufferClear(RingBuffer *ringPtr); static char * RingBufferSegment(const RingBuffer *ringPtr, RingSizeT *lenPtr); static int RingBufferCheck(const RingBuffer *ringPtr); static RingSizeT RingBufferIn(RingBuffer *ringPtr, const char *srcPtr, RingSizeT srcLen, int partialCopyOk); static RingSizeT RingBufferOut(RingBuffer *ringPtr, char *dstPtr, RingSizeT dstCapacity, int partialCopyOk); static ConsoleHandleInfo *AllocateConsoleHandleInfo(HANDLE consoleHandle, int permissions); static ConsoleHandleInfo *FindConsoleInfo(const ConsoleChannelInfo *); static DWORD WINAPI ConsoleReaderThread(LPVOID arg); static DWORD WINAPI ConsoleWriterThread(LPVOID arg); #ifdef OBSOLETE static int WaitForRead(ConsoleChannelInfo *infoPtr, int blocking); #endif /* * Static data. */ typedef struct { /* Currently this struct is only used to detect thread initialization */ int notUsed; /* Dummy field */ } ThreadSpecificData; static Tcl_ThreadDataKey dataKey; /* * All access to static data is controlled through a single process-wide * lock. A process can have only a single console at a time, with three * handles for stdin, stdout and stderr. Creation/destruction of consoles is * a relatively rare event (currently only possible during process start), * the number of consoles (as opposed to channels) is small (only stdin, * stdout and stderr), and contention low. More finer-grained locking would * likely not only complicate implementation but be slower due to multiple * locks being held. Note console channels also differ from other Tcl * channel types in that the channel<->OS descriptor mapping is not one-to-one. * * The gConsoleLock locks around access to the initialized variable, and it * is used to protect background threads from being terminated while they * are using APIs that hold locks. TBD - is this still true? */ SRWLOCK gConsoleLock; /* Process-wide list of console handles. Access control through gConsoleLock */ static ConsoleHandleInfo *gConsoleHandleInfoList; /* * Process-wide list of channels that are listening for events. Again access * control through gConsoleLock. Common list for all threads is simplifies * locking and bookkeeping and is workable because in practice multiple * threads are very unlikely to be all waiting on stdin (not workable * because input would be randomly distributed to threads) */ static ConsoleChannelInfo *gWatchingChannelList; /* * This structure describes the channel type structure for command console * based IO. */ static const Tcl_ChannelType consoleChannelType = { "console", /* Type name. */ TCL_CHANNEL_VERSION_5, /* v5 channel */ TCL_CLOSE2PROC, /* Close proc. */ ConsoleInputProc, /* Input proc. */ ConsoleOutputProc, /* Output proc. */ NULL, /* Seek proc. */ ConsoleSetOptionProc, /* Set option proc. */ ConsoleGetOptionProc, /* Get option proc. */ ConsoleWatchProc, /* Set up notifier to watch the channel. */ ConsoleGetHandleProc, /* Get an OS handle from channel. */ ConsoleCloseProc, /* close2proc. */ ConsoleBlockModeProc, /* Set blocking or non-blocking mode. */ NULL, /* Flush proc. */ NULL, /* Handler proc. */ NULL, /* Wide seek proc. */ ConsoleThreadActionProc, /* Thread action proc. */ NULL /* Truncation proc. */ }; /* *------------------------------------------------------------------------ * * RingBufferInit -- * * Initializes the ring buffer to a given size. * * Results: * None. * * Side effects: * Panics on allocation failure. * *------------------------------------------------------------------------ */ static void RingBufferInit(RingBuffer *ringPtr, RingSizeT capacity) { if (capacity <= 0 || capacity > RingSizeT_MAX) { Tcl_Panic("Internal error: invalid ring buffer capacity requested."); } ringPtr->bufPtr = (char *)ckalloc(capacity); ringPtr->capacity = capacity; ringPtr->start = 0; ringPtr->length = 0; } /* *------------------------------------------------------------------------ * * RingBufferClear * * Clears the contents of a ring buffer. * * Results: * None. * * Side effects: * The allocated internal buffer is freed. * *------------------------------------------------------------------------ */ static void RingBufferClear(RingBuffer *ringPtr) { if (ringPtr->bufPtr) { ckfree(ringPtr->bufPtr); ringPtr->bufPtr = NULL; } ringPtr->capacity = 0; ringPtr->start = 0; ringPtr->length = 0; } /* *------------------------------------------------------------------------ * * RingBufferIn -- * * Appends data to the ring buffer. * * Results: * Returns number of bytes copied. * * Side effects: * Internal buffer is updated. * *------------------------------------------------------------------------ */ static RingSizeT RingBufferIn( RingBuffer *ringPtr, const char *srcPtr, /* Source to be copied */ RingSizeT srcLen, /* Length of source */ int partialCopyOk /* If true, partial copy is permitted */ ) { RingSizeT freeSpace; RingSizeT endSpace; RINGBUFFER_ASSERT(ringPtr); freeSpace = ringPtr->capacity - ringPtr->length; if (freeSpace < srcLen) { if (!partialCopyOk) { return 0; } /* Copy only as much as free space allows */ srcLen = freeSpace; } /* Copy as much as possible to the tail */ if (ringPtr->capacity - ringPtr->start > ringPtr->length) { /* There is room at the back */ RingSizeT endSpaceStart = ringPtr->start + ringPtr->length; endSpace = ringPtr->capacity - endSpaceStart; if (endSpace > srcLen) { endSpace = srcLen; } memmove(endSpaceStart + ringPtr->bufPtr, srcPtr, endSpace); } else { endSpace = 0; } /* Wrap around any left over data. Have already copied endSpace bytes */ if (srcLen > endSpace) { memmove(ringPtr->bufPtr, endSpace + srcPtr, srcLen - endSpace); } ringPtr->length += srcLen; RINGBUFFER_ASSERT(ringPtr); return srcLen; } /* *------------------------------------------------------------------------ * * RingBufferOut -- * * Moves data out of the ring buffer. If dstPtr is NULL, the data * is simply removed. * * Results: * Returns number of bytes copied or removed. * * Side effects: * Internal buffer is updated. * *------------------------------------------------------------------------ */ static RingSizeT RingBufferOut(RingBuffer *ringPtr, char *dstPtr, /* Buffer for output data. May be NULL */ RingSizeT dstCapacity, /* Size of buffer */ int partialCopyOk) /* If true, return what's available */ { RingSizeT leadLen; RINGBUFFER_ASSERT(ringPtr); if (dstCapacity > ringPtr->length) { if (dstPtr && !partialCopyOk) { return 0; } dstCapacity = ringPtr->length; } if (ringPtr->start <= (ringPtr->capacity - ringPtr->length)) { /* No content wrap around. So leadLen is entire content */ leadLen = ringPtr->length; } else { /* Content wraps around so lead segment stretches to end of buffer */ leadLen = ringPtr->capacity - ringPtr->start; } if (leadLen >= dstCapacity) { if (dstPtr) { memmove(dstPtr, ringPtr->start + ringPtr->bufPtr, dstCapacity); } ringPtr->start += dstCapacity; } else { RingSizeT wrapLen = dstCapacity - leadLen; if (dstPtr) { memmove(dstPtr, ringPtr->start + ringPtr->bufPtr, leadLen); memmove( leadLen + dstPtr, ringPtr->bufPtr, wrapLen); } ringPtr->start = wrapLen; } ringPtr->length -= dstCapacity; if (ringPtr->start == ringPtr->capacity || ringPtr->length == 0) { ringPtr->start = 0; } RINGBUFFER_ASSERT(ringPtr); return dstCapacity; } /* *------------------------------------------------------------------------ * * RingBufferSegment -- * * Returns a pointer to the leading data segment in the ring buffer. * * Results: * Pointer to start of segment. * * Side effects: * None. * *------------------------------------------------------------------------ */ static char * RingBufferSegment(const RingBuffer *ringPtr, RingSizeT *lengthPtr) { RINGBUFFER_ASSERT(ringPtr); if (ringPtr->length <= (ringPtr->capacity - ringPtr->start)) { /* No content wrap around. */ *lengthPtr = ringPtr->length; } else { /* Content wraps around so lead segment stretches to end of buffer */ *lengthPtr = ringPtr->capacity - ringPtr->start; } return *lengthPtr == 0 ? NULL : ringPtr->start + ringPtr->bufPtr; } static int RingBufferCheck(const RingBuffer *ringPtr) { return (ringPtr->bufPtr != NULL && ringPtr->capacity == CONSOLE_BUFFER_SIZE && ringPtr->start < ringPtr->capacity && ringPtr->length <= ringPtr->capacity); } /* *------------------------------------------------------------------------ * * ReadConsoleChars -- * * Wrapper for ReadConsoleW. * * Results: * Returns 0 on success, else Windows error code. * * Side effects: * On success the number of characters (not bytes) read is stored in * *nCharsReadPtr. This will be 0 if the operation was interrupted by * a Ctrl-C or a CancelIo call. * *------------------------------------------------------------------------ */ static DWORD ReadConsoleChars( HANDLE hConsole, WCHAR *lpBuffer, RingSizeT nChars, RingSizeT *nCharsReadPtr) { DWORD nRead; BOOL result; /* * If user types a Ctrl-Break or Ctrl-C, ReadConsole will return success * with ntchars == 0 and GetLastError() will be ERROR_OPERATION_ABORTED. * If no Ctrl signal handlers have been established, the default signal * OS handler in a separate thread will terminate the program. If a Ctrl * signal handler has been established (through an extension for * example), it will run and take whatever action it deems appropriate. * * If one thread closes its channel, it calls CancelSynchronousIo on the * console handle which results again in success being returned and * GetLastError() being ERROR_OPERATION_ABORTED but ntchars in * unmodified. * * In both cases above we will return success but with nbytesread as 0. * This allows caller to check for thread termination etc. * * See https://bugs.python.org/issue30237 * or https://github.com/microsoft/terminal/issues/12143 */ nRead = (DWORD)-1; result = ReadConsoleW(hConsole, lpBuffer, nChars, &nRead, NULL); if (result) { if ((nRead == 0 || nRead == (DWORD)-1) && GetLastError() == ERROR_OPERATION_ABORTED) { nRead = 0; } *nCharsReadPtr = nRead; return 0; } else return GetLastError(); } /* *------------------------------------------------------------------------ * * WriteConsoleChars -- * * Wrapper for WriteConsoleW. * * Results: * Returns 0 on success, Windows error code on failure. * * Side effects: * On success the number of characters (not bytes) written is stored in * *nCharsWrittenPtr. This will be 0 if the operation was interrupted by * a Ctrl-C or a CancelIo call. * *------------------------------------------------------------------------ */ static DWORD WriteConsoleChars( HANDLE hConsole, const WCHAR *lpBuffer, RingSizeT nChars, RingSizeT *nCharsWrittenPtr) { DWORD nCharsWritten; BOOL result; /* See comments in ReadConsoleChars, not sure that applies here */ nCharsWritten = (DWORD)-1; result = WriteConsoleW(hConsole, lpBuffer, nChars, &nCharsWritten, NULL); if (result) { if (nCharsWritten == (DWORD) -1) { nCharsWritten = 0; } *nCharsWrittenPtr = nCharsWritten; return 0; } else { return GetLastError(); } } /* *---------------------------------------------------------------------- * * ConsoleInit -- * * This function initializes the static variables for this file. * * Results: * None. * * Side effects: * Creates a new event source. * *---------------------------------------------------------------------- */ static void ConsoleInit(void) { /* * Check the initialized flag first, then check again in the mutex. This * is a speed enhancement. */ if (!initialized) { AcquireSRWLockExclusive(&gConsoleLock); if (!initialized) { initialized = 1; Tcl_CreateExitHandler(ProcExitHandler, NULL); } ReleaseSRWLockExclusive(&gConsoleLock); } if (TclThreadDataKeyGet(&dataKey) == NULL) { ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey); tsdPtr->notUsed = 0; Tcl_CreateEventSource(ConsoleSetupProc, ConsoleCheckProc, NULL); Tcl_CreateThreadExitHandler(ConsoleExitHandler, NULL); } } /* *---------------------------------------------------------------------- * * ConsoleExitHandler -- * * This function is called to cleanup the console module before Tcl is * unloaded. * * Results: * None. * * Side effects: * Removes the console event source. * *---------------------------------------------------------------------- */ static void ConsoleExitHandler( TCL_UNUSED(ClientData)) { Tcl_DeleteEventSource(ConsoleSetupProc, ConsoleCheckProc, NULL); } /* *---------------------------------------------------------------------- * * ProcExitHandler -- * * This function is called to cleanup the process list before Tcl is * unloaded. * * Results: * None. * * Side effects: * Resets the process list. * *---------------------------------------------------------------------- */ static void ProcExitHandler( TCL_UNUSED(ClientData)) { AcquireSRWLockExclusive(&gConsoleLock); initialized = 0; ReleaseSRWLockExclusive(&gConsoleLock); } /* *---------------------------------------------------------------------- * * ConsoleSetupProc -- * * This procedure is invoked before Tcl_DoOneEvent blocks waiting for an * event. It walks the channel list and if any input channel has data * available or output channel has space for data, sets the event loop * blocking time to 0 so that it will poll immediately. * * Results: * None. * * Side effects: * Adjusts the block time if needed. * *---------------------------------------------------------------------- */ void ConsoleSetupProc( TCL_UNUSED(ClientData), int flags) /* Event flags as passed to Tcl_DoOneEvent. */ { ConsoleChannelInfo *chanInfoPtr; Tcl_Time blockTime = { 0, 0 }; int block = 1; if (!(flags & TCL_FILE_EVENTS)) { return; } /* * Walk the list of channels. See general comments for struct * ConsoleChannelInfo with regard to locking and field access. */ AcquireSRWLockShared(&gConsoleLock); /* READ lock - no data modification */ for (chanInfoPtr = gWatchingChannelList; block && chanInfoPtr != NULL; chanInfoPtr = chanInfoPtr->nextWatchingChannelPtr) { ConsoleHandleInfo *handleInfoPtr; handleInfoPtr = FindConsoleInfo(chanInfoPtr); if (handleInfoPtr != NULL) { AcquireSRWLockShared(&handleInfoPtr->lock); if ((chanInfoPtr->watchMask & TCL_READABLE) && RingBufferLength(&handleInfoPtr->buffer) > 0) { block = 0; /* Input data available */ } else if (RingBufferFreeSpace(&handleInfoPtr->buffer) > 0) { block = 0; /* Output space available */ } ReleaseSRWLockShared(&handleInfoPtr->lock); } } ReleaseSRWLockShared(&gConsoleLock); if (!block) { /* At least one channel is readable/writable. Set block time to 0 */ Tcl_SetMaxBlockTime(&blockTime); } } /* *---------------------------------------------------------------------- * * ConsoleCheckProc -- * * This procedure is called by Tcl_DoOneEvent to check the console event * source for events. * * Results: * None. * * Side effects: * May queue an event. * *---------------------------------------------------------------------- */ static void ConsoleCheckProc( TCL_UNUSED(ClientData), int flags) /* Event flags as passed to Tcl_DoOneEvent. */ { ConsoleChannelInfo *chanInfoPtr; Tcl_ThreadId me; int needEvent; if (!(flags & TCL_FILE_EVENTS)) { return; } me = Tcl_GetCurrentThread(); /* * Acquire a shared lock. Note this is ok even though we potentially * modify the chanInfoPtr->flags because chanInfoPtr is only modified * when it belongs to this thread and no other thread will write to it. * THe shared lock is intended to protect the global gWatchingChannelList * as we traverse it. */ AcquireSRWLockShared(&gConsoleLock); for (chanInfoPtr = gWatchingChannelList; chanInfoPtr != NULL; chanInfoPtr = chanInfoPtr->nextWatchingChannelPtr) { ConsoleHandleInfo *handleInfoPtr; if (chanInfoPtr->threadId != me) { /* Some other thread owns the channel */ continue; } if (chanInfoPtr->flags & CONSOLE_EVENT_QUEUED) { /* A notification event already queued. No point in another. */ continue; } handleInfoPtr = FindConsoleInfo(chanInfoPtr); /* Pointer is safe to access as we are holding gConsoleLock */ if (handleInfoPtr != NULL) { AcquireSRWLockShared(&handleInfoPtr->lock); if ((chanInfoPtr->watchMask & TCL_READABLE) && RingBufferLength(&handleInfoPtr->buffer) > 0) { needEvent = 1; /* Input data available */ } else if (RingBufferFreeSpace(&handleInfoPtr->buffer) > 0) { needEvent = 1; /* Output space available */ } ReleaseSRWLockShared(&handleInfoPtr->lock); } if (needEvent) { ConsoleEvent *evPtr = (ConsoleEvent *)ckalloc(sizeof(ConsoleEvent)); /* See note above loop why this can be accessed without locks */ chanInfoPtr->flags |= CONSOLE_EVENT_QUEUED; chanInfoPtr->numRefs += 1; /* So it does not go away while event is in queue */ evPtr->header.proc = ConsoleEventProc; evPtr->chanInfoPtr = chanInfoPtr; Tcl_QueueEvent((Tcl_Event *) evPtr, TCL_QUEUE_TAIL); } } ReleaseSRWLockShared(&gConsoleLock); } /* *---------------------------------------------------------------------- * * ConsoleBlockModeProc -- * * Set blocking or non-blocking mode on channel. * * Results: * 0 if successful, errno when failed. * * Side effects: * Sets the device into blocking or non-blocking mode. * *---------------------------------------------------------------------- */ static int ConsoleBlockModeProc( ClientData instanceData, /* Instance data for channel. */ int mode) /* TCL_MODE_BLOCKING or * TCL_MODE_NONBLOCKING. */ { ConsoleChannelInfo *chanInfoPtr = (ConsoleChannelInfo *)instanceData; /* * Consoles on Windows can not be switched between blocking and * nonblocking, hence we have to emulate the behavior. This is done in the * input function by checking against a bit in the state. We set or unset * the bit here to cause the input function to emulate the correct * behavior. */ if (mode == TCL_MODE_NONBLOCKING) { chanInfoPtr->flags |= CONSOLE_ASYNC; } else { chanInfoPtr->flags &= ~CONSOLE_ASYNC; } return 0; } /* *---------------------------------------------------------------------- * * ConsoleCloseProc -- * * Closes a console based IO channel. * * Results: * 0 on success, errno otherwise. * * Side effects: * Closes the physical channel. * *---------------------------------------------------------------------- */ static int ConsoleCloseProc( ClientData instanceData, /* Pointer to ConsoleChannelInfo structure. */ TCL_UNUSED(Tcl_Interp *), int flags) { ConsoleChannelInfo *chanInfoPtr = (ConsoleChannelInfo *)instanceData; ConsoleHandleInfo *handleInfoPtr; int errorCode = 0; ConsoleChannelInfo **nextPtrPtr; if ((flags & (TCL_CLOSE_READ | TCL_CLOSE_WRITE)) != 0) { return EINVAL; } AcquireSRWLockExclusive(&gConsoleLock); /* Remove channel from watchers' list */ for (nextPtrPtr = &gWatchingChannelList; *nextPtrPtr != NULL; nextPtrPtr = &(*nextPtrPtr)->nextWatchingChannelPtr) { if (*nextPtrPtr == (ConsoleChannelInfo *) chanInfoPtr) { *nextPtrPtr = (*nextPtrPtr)->nextWatchingChannelPtr; break; } } handleInfoPtr = FindConsoleInfo(chanInfoPtr); if (handleInfoPtr) { /* * Console thread may be blocked either waiting for console i/o * or waiting on the condition variable for buffer empty/full */ AcquireSRWLockShared(&handleInfoPtr->lock); handleInfoPtr->numRefs -= 1; /* Remove reference from this channel */ /* Break the thread out of blocking console i/o */ CancelSynchronousIo(handleInfoPtr->consoleThread); /* Wake up the console handling thread */ WakeConditionVariable(&handleInfoPtr->consoleThreadCV); ReleaseSRWLockShared(&handleInfoPtr->lock); } ReleaseSRWLockExclusive(&gConsoleLock); chanInfoPtr->channel = NULL; chanInfoPtr->watchMask = 0; chanInfoPtr->permissions = 0; if (chanInfoPtr->handle) { /* * Don't close the Win32 handle if the handle is a standard channel * during the thread exit process. Otherwise, one thread may kill the * stdio of another. TODO - an explicit close in script will still close * it. */ if (!TclInThreadExit() || ((GetStdHandle(STD_INPUT_HANDLE) != chanInfoPtr->handle) && (GetStdHandle(STD_OUTPUT_HANDLE) != chanInfoPtr->handle) && (GetStdHandle(STD_ERROR_HANDLE) != chanInfoPtr->handle))) { if (CloseHandle(chanInfoPtr->handle) == FALSE) { Tcl_WinConvertError(GetLastError()); errorCode = errno; } } chanInfoPtr->handle = NULL; } /* * Note, we can check and manipulate numRefs without a lock because * we have removed it from the watch queue so the console thread cannot * get at it. */ if (chanInfoPtr->numRefs > 1) { /* There may be references already on the event queue */ chanInfoPtr->numRefs -= 1; } else { ckfree(chanInfoPtr); } return errorCode; } /* *---------------------------------------------------------------------- * * ConsoleInputProc -- * * Reads input from the IO channel into the buffer given. Returns count * of how many bytes were actually read, and an error indication. * * Results: * A count of how many bytes were read is returned and an error * indication is returned in an output argument. * * Side effects: * Reads input from the actual channel. * *---------------------------------------------------------------------- */ static int ConsoleInputProc( ClientData instanceData, /* Console state. */ char *bufPtr, /* Where to store data read. */ int bufSize, /* How much space is available in the * buffer? */ int *errorCode) /* Where to store error code. */ { ConsoleChannelInfo *chanInfoPtr = (ConsoleChannelInfo *)instanceData; ConsoleHandleInfo *handleInfoPtr; RingSizeT numRead; *errorCode = 0; AcquireSRWLockShared(&gConsoleLock); handleInfoPtr = FindConsoleInfo(chanInfoPtr); if (handleInfoPtr == NULL) { /* Really shouldn't happen since channel is holding a reference */ ReleaseSRWLockShared(&gConsoleLock); return 0; /* EOF */ } AcquireSRWLockExclusive(&handleInfoPtr->lock); ReleaseSRWLockShared(&gConsoleLock); /* AFTER acquiring handleInfoPtr->lock */ numRead = RingBufferOut(&handleInfoPtr->buffer, bufPtr, bufSize, 1); while (numRead == 0) { /* * No data available. * - If an error was recorded, generate that and reset it. * - If EOF, indicate as much. TODO - can console thread still be * running in that case? * - Otherwise, if non-blocking return EAGAIN or wait for more data. */ if (handleInfoPtr->lastError != 0) { Tcl_WinConvertError(handleInfoPtr->lastError); handleInfoPtr->lastError = 0; *errorCode = Tcl_GetErrno(); numRead = -1; } else if (handleInfoPtr->console == NULL) { /* EOF - break with numRead == 0 */ break; } else { if (chanInfoPtr->flags & CONSOLE_ASYNC) { *errorCode = EAGAIN; numRead = -1; } else { /* * Release the lock and sleep. Note that because the channel * holds a reference count on handleInfoPtr, it will not * be deallocated while the lock is released. */ WakeConditionVariable(&handleInfoPtr->consoleThreadCV); if (SleepConditionVariableSRW(&handleInfoPtr->interpThreadCV, &handleInfoPtr->lock, INFINITE, 0)) { /* * Lock is reacquired. However, in the meanwhile another * thread could have consumed data. So loop continues * with check of numRead value. */ numRead = RingBufferOut( &handleInfoPtr->buffer, bufPtr, bufSize, 1); } else { /* Report the error */ Tcl_WinConvertError(GetLastError()); *errorCode = Tcl_GetErrno(); numRead = -1; } } } } ReleaseSRWLockExclusive(&handleInfoPtr->lock); return numRead; } /* *---------------------------------------------------------------------- * * ConsoleOutputProc -- * * Writes the given output on the IO channel. Returns count of how many * characters were actually written, and an error indication. * * Results: * A count of how many characters were written is returned and an error * indication is returned in an output argument. * * Side effects: * Writes output on the actual channel. * *---------------------------------------------------------------------- */ static int ConsoleOutputProc( ClientData instanceData, /* Console state. */ const char *buf, /* The data buffer. */ int toWrite, /* How many bytes to write? */ int *errorCode) /* Where to store error code. */ { ConsoleChannelInfo *chanInfoPtr = (ConsoleChannelInfo *)instanceData; ConsoleHandleInfo *handleInfoPtr; RingSizeT numWritten; *errorCode = 0; AcquireSRWLockShared(&gConsoleLock); handleInfoPtr = FindConsoleInfo(chanInfoPtr); if (handleInfoPtr == NULL) { /* Really shouldn't happen since channel is holding a reference */ *errorCode = EPIPE; ReleaseSRWLockShared(&gConsoleLock); return -1; } AcquireSRWLockExclusive(&handleInfoPtr->lock); ReleaseSRWLockShared(&gConsoleLock); /* AFTER acquiring handleInfoPtr->lock */ numWritten = RingBufferIn(&handleInfoPtr->buffer, buf, toWrite, 1); while (numWritten < toWrite) { if (handleInfoPtr->lastError != 0) { Tcl_WinConvertError(handleInfoPtr->lastError); *errorCode = Tcl_GetErrno(); numWritten = -1; break; } if (handleInfoPtr->console == NULL) { *errorCode = EPIPE; numWritten = -1; break; } if (chanInfoPtr->flags & CONSOLE_ASYNC) { /* Async, just accept whatever was written */ break; } else { /* * Release the lock and sleep. Note that because the channel * holds a reference count on handleInfoPtr, it will not * be deallocated while the lock is released. */ WakeConditionVariable(&handleInfoPtr->consoleThreadCV); if (SleepConditionVariableSRW(&handleInfoPtr->interpThreadCV, &handleInfoPtr->lock, INFINITE, 0)) { /* Lock is reacquired. Continue loop */ numWritten += RingBufferIn(&handleInfoPtr->buffer, numWritten + buf, toWrite - numWritten, 1); } else { /* Report the error */ Tcl_WinConvertError(GetLastError()); *errorCode = Tcl_GetErrno(); numWritten = -1; break; } } } ReleaseSRWLockExclusive(&handleInfoPtr->lock); WakeConditionVariable(&handleInfoPtr->consoleThreadCV); return numWritten; } /* *---------------------------------------------------------------------- * * ConsoleEventProc -- * * This function is invoked by Tcl_ServiceEvent when a file event reaches * the front of the event queue. This procedure invokes Tcl_NotifyChannel * on the console. * * Results: * Returns 1 if the event was handled, meaning it should be removed from * the queue. Returns 0 if the event was not handled, meaning it should * stay on the queue. The only time the event isn't handled is if the * TCL_FILE_EVENTS flag bit isn't set. * * Side effects: * Whatever the notifier callback does. * *---------------------------------------------------------------------- */ static int ConsoleEventProc( Tcl_Event *evPtr, /* Event to service. */ int flags) /* Flags that indicate what events to handle, * such as TCL_FILE_EVENTS. */ { ConsoleEvent *consoleEvPtr = (ConsoleEvent *) evPtr; ConsoleChannelInfo *chanInfoPtr; int freeChannel; if (!(flags & TCL_FILE_EVENTS)) { return 0; } chanInfoPtr = consoleEvPtr->chanInfoPtr; /* * We know chanInfoPtr is valid because its reference count would have * been incremented when the event was queued. The corresponding release * happens in this function. */ AcquireSRWLockShared(&gConsoleLock); chanInfoPtr->flags &= ~CONSOLE_EVENT_QUEUED; /* * Only handle the event if the Tcl channel has not gone away AND is * still owned by this thread AND is still watching events. */ if (chanInfoPtr->channel && chanInfoPtr->threadId == Tcl_GetCurrentThread() && (chanInfoPtr->watchMask & (TCL_READABLE|TCL_WRITABLE))) { ConsoleHandleInfo *handleInfoPtr; int mask = 0; handleInfoPtr = FindConsoleInfo(chanInfoPtr); if (handleInfoPtr == NULL) { /* Console was closed. EOF->read event only (not write) */ if (chanInfoPtr->watchMask & TCL_READABLE) { mask = TCL_READABLE; } } else { AcquireSRWLockShared(&handleInfoPtr->lock); if (chanInfoPtr->watchMask & TCL_READABLE && RingBufferLength(&handleInfoPtr->buffer)) { mask = TCL_READABLE; } else if (RingBufferFreeSpace(&handleInfoPtr->buffer)) { /* Generate write event space available */ mask = TCL_WRITABLE; } ReleaseSRWLockShared(&handleInfoPtr->lock); } if (mask) { Tcl_NotifyChannel(chanInfoPtr->channel, mask); } } /* Remove the reference to the channel from event record */ if (chanInfoPtr->numRefs > 1) { chanInfoPtr->numRefs -= 1; freeChannel = 0; } else { assert(chanInfoPtr->channel == NULL); freeChannel = 1; ckfree(chanInfoPtr); } ReleaseSRWLockShared(&gConsoleLock); if (freeChannel) ckfree(chanInfoPtr); return 1; } /* *---------------------------------------------------------------------- * * ConsoleWatchProc -- * * Called by the notifier to set up to watch for events on this channel. * * Results: * None. * * Side effects: * None. * *---------------------------------------------------------------------- */ static void ConsoleWatchProc( ClientData instanceData, /* Console state. */ int permissions) /* What events to watch for, OR-ed combination * of TCL_READABLE, TCL_WRITABLE and * TCL_EXCEPTION. */ { ConsoleChannelInfo **nextPtrPtr, *ptr; ConsoleChannelInfo *chanInfoPtr = (ConsoleChannelInfo *)instanceData; int oldMask = chanInfoPtr->watchMask; /* * Since most of the work is handled by the background threads, we just * need to update the watchMask and then force the notifier to poll once. */ chanInfoPtr->watchMask = permissions & chanInfoPtr->permissions; if (chanInfoPtr->watchMask) { Tcl_Time blockTime = { 0, 0 }; if (!oldMask) { /* Add to list of watched channels */ AcquireSRWLockExclusive(&gConsoleLock); chanInfoPtr->nextWatchingChannelPtr = gWatchingChannelList; gWatchingChannelList = chanInfoPtr; ReleaseSRWLockExclusive(&gConsoleLock); } Tcl_SetMaxBlockTime(&blockTime); } else if (oldMask) { /* Remove from list of watched channels */ for (nextPtrPtr = &gWatchingChannelList, ptr = *nextPtrPtr; ptr != NULL; nextPtrPtr = &ptr->nextWatchingChannelPtr, ptr = *nextPtrPtr) { if (chanInfoPtr == ptr) { *nextPtrPtr = ptr->nextWatchingChannelPtr; break; } } } } /* *---------------------------------------------------------------------- * * ConsoleGetHandleProc -- * * Called from Tcl_GetChannelHandle to retrieve OS handles from inside a * command consoleline based channel. * * Results: * Returns TCL_OK with the fd in handlePtr, or TCL_ERROR if there is no * handle for the specified direction. * * Side effects: * None. * *---------------------------------------------------------------------- */ static int ConsoleGetHandleProc( ClientData instanceData, /* The console state. */ TCL_UNUSED(int) /*direction*/, ClientData *handlePtr) /* Where to store the handle. */ { ConsoleChannelInfo *chanInfoPtr = (ConsoleChannelInfo *)instanceData; *handlePtr = chanInfoPtr->handle; return TCL_OK; } #ifdef OBSOLETE /* *---------------------------------------------------------------------- * * WaitForRead -- * * Wait until some data is available, the console is at EOF or the reader * thread is blocked waiting for data (if the channel is in non-blocking * mode). * * Results: * Returns 1 if console is readable. Returns 0 if there is no data on the * console, but there is buffered data. Returns -1 if an error occurred. * If an error occurred, the threads may not be synchronized. * * Side effects: * Updates the shared state flags. If no error occurred, the reader * thread is blocked waiting for a signal from the main thread. * *---------------------------------------------------------------------- */ static int WaitForRead( ConsoleChannelInfo *infoPtr, /* Console state. */ int blocking) /* Indicates whether call should be blocking * or not. */ { DWORD timeout, count; HANDLE *handle = (HANDLE *)infoPtr->handle; ConsoleThreadInfo *threadInfo = &infoPtr->reader; INPUT_RECORD input; while (1) { /* * Synchronize with the reader thread. */ /* avoid blocking if pipe-thread exited */ timeout = (!blocking || !TclPipeThreadIsAlive(&threadInfo->TI) || TclInExit() || TclInThreadExit()) ? 0 : INFINITE; if (WaitForSingleObject(threadInfo->readyEvent, timeout) == WAIT_TIMEOUT) { /* * The reader thread is blocked waiting for data and the channel * is in non-blocking mode. */ errno = EWOULDBLOCK; return -1; } /* * At this point, the two threads are synchronized, so it is safe to * access shared state. */ /* * If the console has hit EOF, it is always readable. */ if (infoPtr->readFlags & CONSOLE_EOF) { return 1; } if (PeekConsoleInputW(handle, &input, 1, &count) == FALSE) { /* * Check to see if the peek failed because of EOF. */ Tcl_WinConvertError(GetLastError()); if (errno == EOF) { infoPtr->readFlags |= CONSOLE_EOF; return 1; } /* * Ignore errors if there is data in the buffer. */ if (infoPtr->readFlags & CONSOLE_BUFFERED) { return 0; } else { return -1; } } /* * If there is data in the buffer, the console must be readable (since * it is a line-oriented device). */ if (infoPtr->readFlags & CONSOLE_BUFFERED) { return 1; } /* * There wasn't any data available, so reset the thread and try again. */ ResetEvent(threadInfo->readyEvent); TclPipeThreadSignal(&threadInfo->TI); } } #endif /* *---------------------------------------------------------------------- * * ConsoleReaderThread -- * * This function runs in a separate thread and waits for input to become * available on a console. * * Results: * Always 0. * * Side effects: * Signals the main thread when input become available. * *---------------------------------------------------------------------- */ static DWORD WINAPI ConsoleReaderThread( LPVOID arg) { ConsoleHandleInfo *handleInfoPtr = (ConsoleHandleInfo *) arg; ConsoleHandleInfo **iterator; BOOL success; char inputChars[200]; /* Temporary buffer */ RingSizeT inputLen = 0; RingSizeT inputOffset = 0; /* * Keep looping until one of the following happens. * * - there are not more channels listening on the console * - the console handle has been closed * * On each iteration, * - if the channel buffer is full, wait for some channel reader to read * - if there is data in our input buffer copy it to the channel buffer * - get more data from the console */ /* This thread is holding a reference so pointer is safe */ AcquireSRWLockExclusive(&handleInfoPtr->lock); while (1) { if (handleInfoPtr->numRefs == 1) { /* Sole reference. That's this thread. Exit since no one clients */ break; } if (RingBufferFreeSpace(&handleInfoPtr->buffer) == 0) { /* No room in buffer. Awaken any reader channels */ WakeConditionVariable(&handleInfoPtr->interpThreadCV); /* XXX - does not wake up fileevent channels! */ /* Release lock and wait for room */ success = SleepConditionVariableSRW(&handleInfoPtr->consoleThreadCV, &handleInfoPtr->lock, INFINITE, 0); /* Note: lock has been acquired again! */ if (!success && GetLastError() != ERROR_TIMEOUT) { /* TODO - what can be done? Should not happen */ /* For now keep going */ } continue; /* Restart loop so we can check for exit conditions */ } /* * The shared buffer now has room. If we had any leftover from last * read, store that. */ if (inputLen > 0) { RingSizeT nStored; HANDLE consoleHandle; ConsoleChannelInfo *chanInfoPtr; nStored = RingBufferIn(&handleInfoPtr->buffer, inputOffset + inputChars, inputLen - inputOffset, 1); inputOffset += nStored; if (inputOffset == inputLen) { /* Temp buffer now empty */ inputOffset = 0; inputLen = 0; } /* Wake up any threads waiting synchronously. */ WakeConditionVariable(&handleInfoPtr->interpThreadCV); /* * Wake up all channels registered for file events. Note in * order to follow the locking hierarchy, we need to release * handleInfoPtr->lock before acquiring gConsoleLock and * relock it. */ consoleHandle = handleInfoPtr->console; ReleaseSRWLockExclusive(&handleInfoPtr->lock); AcquireSRWLockShared(&gConsoleLock); /* Shared-read lock */ for (chanInfoPtr = gWatchingChannelList; chanInfoPtr; chanInfoPtr = chanInfoPtr->nextWatchingChannelPtr) { /* * Notify channels interested in our handle AND that have * a thread attached. * No lock needed for chanInfoPtr. See ConsoleChannelInfo. */ if (chanInfoPtr->handle == consoleHandle && chanInfoPtr->threadId != NULL) { Tcl_ThreadAlert(chanInfoPtr->threadId); } } ReleaseSRWLockShared(&gConsoleLock); AcquireSRWLockExclusive(&handleInfoPtr->lock); continue; /* Restart loop */ } /* * Need to go get more data from console. We only store the last * error. It is up to channel handlers to decide whether to close or * what to do. */ ReleaseSRWLockExclusive(&handleInfoPtr->lock); handleInfoPtr->lastError = ReadConsoleChars(handleInfoPtr->console, (WCHAR *)inputChars, sizeof(inputChars) / sizeof(WCHAR), &inputLen); inputLen *= sizeof(WCHAR); AcquireSRWLockExclusive(&handleInfoPtr->lock); } /* * Exiting: * - remove the console from global list * - close the handle if still valid * - release the structure */ ReleaseSRWLockExclusive(&handleInfoPtr->lock); AcquireSRWLockExclusive(&gConsoleLock); /* Modifying - exclusive lock */ for (iterator = &gConsoleHandleInfoList; *iterator; iterator = &(*iterator)->nextPtr) { if (*iterator == handleInfoPtr) { *iterator = handleInfoPtr->nextPtr; break; } } ReleaseSRWLockExclusive(&gConsoleLock); /* No need for relocking - no other thread should have access to it now */ RingBufferClear(&handleInfoPtr->buffer); if (handleInfoPtr->console) { SetConsoleMode(handleInfoPtr->console, handleInfoPtr->initMode); } /* * NOTE: we do not call CloseHandle(handleInfoPtr->console) * As per the GetStdHandle documentation, it need not be closed. * TODO - what about when application closes and re-opens? - Test */ ckfree(handleInfoPtr); return 0; } /* *---------------------------------------------------------------------- * * ConsoleWriterThread -- * * This function runs in a separate thread and writes data onto a * console. * * Results: * Always returns 0. * * Side effects: * Signals the main thread when an output operation is completed. * *---------------------------------------------------------------------- */ static DWORD WINAPI ConsoleWriterThread(LPVOID arg) { ConsoleHandleInfo *handleInfoPtr = (ConsoleHandleInfo *) arg; ConsoleHandleInfo **iterator; ConsoleChannelInfo *chanInfoPtr = NULL; BOOL success; char buffer[4000]; RingSizeT length; /* * Keep looping until one of the following happens. * * - there are not more channels listening on the console * - the console handle has been closed * * On each iteration, * - if the channel buffer is empty, wait for some channel writer to write * - if there is data in our buffer, write it to the console */ /* This thread is holding a reference so pointer is safe */ AcquireSRWLockExclusive(&handleInfoPtr->lock); while (1) { /* handleInfoPtr->lock must be held on entry to loop */ int offset; HANDLE consoleHandle; /* * Sadly, we need to do another copy because do not want to hold * a lock on handleInfoPtr->buffer while calling WriteConsole as that * might block. Also, we only want to copy an integral number of * WCHAR's, i.e. even number of chars so do some length checks up * front. */ length = RingBufferLength(&handleInfoPtr->buffer); length &= ~1; /* Copy integral number of WCHARs -> even number of bytes */ if (length == 0) { /* No data to write */ if (handleInfoPtr->numRefs == 1) { /* * Sole reference. That's this thread. Exit since no clients * and no buffered output. */ break; } /* Wake up any threads waiting synchronously. */ WakeConditionVariable(&handleInfoPtr->interpThreadCV); success = SleepConditionVariableSRW(&handleInfoPtr->consoleThreadCV, &handleInfoPtr->lock, INFINITE, 0); /* Note: lock has been acquired again! */ if (!success && GetLastError() != ERROR_TIMEOUT) { /* TODO - what can be done? Should not happen */ /* For now keep going */ } continue; } /* We have data to write */ if (length > (sizeof(buffer) / sizeof(buffer[0]))) { length = sizeof(buffer); } /* No need to check result, we already checked length bytes available */ RingBufferOut(&handleInfoPtr->buffer, buffer, length, 0); WakeConditionVariable(&handleInfoPtr->interpThreadCV); ReleaseSRWLockExclusive(&handleInfoPtr->lock); offset = 0; while (length > 0) { RingSizeT numWChars = length / sizeof(WCHAR); DWORD status; status = WriteConsoleChars( handleInfoPtr->console, (WCHAR *) (offset + buffer) , numWChars, &numWChars); if (status != 0) { /* Only overwrite if no previous error */ if (handleInfoPtr->lastError == 0) { handleInfoPtr->lastError = status; } /* Assume this write is done but keep looping in case * it is a transient error. Not sure just closing handle * and exiting thread is a good idea. */ break; } length -= numWChars * sizeof(WCHAR); offset += numWChars * sizeof(WCHAR); } /* * Wake up all channels registered for file events. Note in * order to follow the locking hierarchy, we need to release * handleInfoPtr->lock before acquiring gConsoleLock and * relock it. */ /* Wake up any threads waiting synchronously. */ WakeConditionVariable(&handleInfoPtr->interpThreadCV); AcquireSRWLockShared(&gConsoleLock); /* Shared-read lock */ consoleHandle = handleInfoPtr->console; for (chanInfoPtr = gWatchingChannelList; chanInfoPtr; chanInfoPtr = chanInfoPtr->nextWatchingChannelPtr) { /* * Notify channels interested in our handle AND that have * a thread attached. * No lock needed for chanInfoPtr. See ConsoleChannelInfo. */ if (chanInfoPtr->handle == consoleHandle && chanInfoPtr->threadId != NULL) { Tcl_ThreadAlert(chanInfoPtr->threadId); } } ReleaseSRWLockShared(&gConsoleLock); AcquireSRWLockExclusive(&handleInfoPtr->lock); } /* * Exiting: * - remove the console from global list * - close the handle if still valid * - release the structure */ ReleaseSRWLockExclusive(&handleInfoPtr->lock); AcquireSRWLockExclusive(&gConsoleLock); /* Modifying - exclusive lock */ for (iterator = &gConsoleHandleInfoList; *iterator; iterator = &(*iterator)->nextPtr) { if (*iterator == handleInfoPtr) { *iterator = handleInfoPtr->nextPtr; break; } } ReleaseSRWLockExclusive(&gConsoleLock); RingBufferClear(&handleInfoPtr->buffer); /* * NOTE: we do not call CloseHandle(handleInfoPtr->console) * As per the GetStdHandle documentation, it need not be closed. * TODO - what about when application closes and re-opens? - Test */ ckfree(handleInfoPtr); return 0; } /* *------------------------------------------------------------------------ * * AllocateConsoleHandleInfo -- * * Allocates a ConsoleHandleInfo for the passed console handle. As * a side effect starts a console thread to handle i/o on the handle. * * Important: Caller must be holding an EXCLUSIVE lock on gConsoleLock * when calling this function. The lock continues to be held on return. * * Results: * Pointer to an unlocked ConsoleHandleInfo structure. The reference * count on the structure is 1. This corresponds to the common reference * from the console thread and the gConsoleHandleInfoList. Returns NULL * on error. * * Side effects: * A console reader or writer thread is started. The returned structure * is placed on the active console handler list gConsoleHandleInfoList. * *------------------------------------------------------------------------ */ static ConsoleHandleInfo * AllocateConsoleHandleInfo( HANDLE consoleHandle, int permissions) /* TCL_READABLE or TCL_WRITABLE */ { ConsoleHandleInfo *handleInfoPtr; DWORD consoleMode; handleInfoPtr = (ConsoleHandleInfo *)ckalloc(sizeof(*handleInfoPtr)); handleInfoPtr->console = consoleHandle; InitializeSRWLock(&handleInfoPtr->lock); InitializeConditionVariable(&handleInfoPtr->consoleThreadCV); InitializeConditionVariable(&handleInfoPtr->interpThreadCV); RingBufferInit(&handleInfoPtr->buffer, CONSOLE_BUFFER_SIZE); handleInfoPtr->lastError = 0; handleInfoPtr->permissions = permissions; handleInfoPtr->numRefs = 1; /* See function header */ if (permissions == TCL_READABLE) { GetConsoleMode(consoleHandle, &handleInfoPtr->initMode); consoleMode = handleInfoPtr->initMode; consoleMode &= ~(ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT); consoleMode |= ENABLE_LINE_INPUT; SetConsoleMode(consoleHandle, consoleMode); } handleInfoPtr->consoleThread = CreateThread( NULL, /* default security descriptor */ 8192, /* Stack size - will get rounded up to allocation granularity */ permissions == TCL_READABLE ? ConsoleReaderThread : ConsoleWriterThread, handleInfoPtr, /* Pass to thread */ 0, /* Flags - no special cases */ NULL); /* Don't care about thread id */ if (handleInfoPtr->consoleThread == NULL) { /* Note - SRWLock and condition variables do not need finalization */ RingBufferClear(&handleInfoPtr->buffer); ckfree(handleInfoPtr); return NULL; } /* Chain onto global list */ handleInfoPtr->nextPtr = gConsoleHandleInfoList; gConsoleHandleInfoList = handleInfoPtr; return handleInfoPtr; } /* *------------------------------------------------------------------------ * * FindConsoleInfo -- * * Finds the ConsoleHandleInfo record for a given ConsoleChannelInfo. * The found record must match the console handle. It is the caller's * responsibility to check the permissions (read/write) in the returned * ConsoleHandleInfo match permissions in chanInfoPtr. This function does * not check that. * * Important: Caller must be holding an shared or exclusive lock on * gConsoleMutex. That ensures the returned pointer stays valid on * return without risk of deallocation by other threads. * * Results: * Pointer to the found ConsoleHandleInfo or NULL if not found * * Side effects: * None. * *------------------------------------------------------------------------ */ static ConsoleHandleInfo * FindConsoleInfo(const ConsoleChannelInfo *chanInfoPtr) { ConsoleHandleInfo *handleInfoPtr; for (handleInfoPtr = gConsoleHandleInfoList; handleInfoPtr; handleInfoPtr = handleInfoPtr->nextPtr) { if (handleInfoPtr->console == chanInfoPtr->handle) { return handleInfoPtr; } } return NULL; } /* *---------------------------------------------------------------------- * * TclWinOpenConsoleChannel -- * * Constructs a Console channel for the specified standard OS handle. * This is a helper function to break up the construction of channels * into File, Console, or Serial. * * Results: * Returns the new channel, or NULL. * * Side effects: * May open the channel. * *---------------------------------------------------------------------- */ Tcl_Channel TclWinOpenConsoleChannel( HANDLE handle, char *channelName, int permissions) { ConsoleChannelInfo *chanInfoPtr; ConsoleHandleInfo *handleInfoPtr; /* A console handle can either be input or output, not both */ if (permissions != TCL_READABLE && permissions != TCL_WRITABLE) { return NULL; } ConsoleInit(); chanInfoPtr = (ConsoleChannelInfo *)ckalloc(sizeof(*chanInfoPtr)); memset(chanInfoPtr, 0, sizeof(*chanInfoPtr)); chanInfoPtr->permissions = permissions; chanInfoPtr->handle = handle; chanInfoPtr->channel = (Tcl_Channel) NULL; chanInfoPtr->threadId = Tcl_GetCurrentThread(); /* * Use the pointer for the name of the result channel. This keeps the * channel names unique, since some may share handles (stdin/stdout/stderr * for instance). */ sprintf(channelName, "file%" TCL_Z_MODIFIER "x", (size_t) chanInfoPtr); if (permissions & TCL_READABLE) { /* * Make sure the console input buffer is ready for only character * input notifications and the buffer is set for line buffering. IOW, * we only want to catch when complete lines are ready for reading. */ chanInfoPtr->flags |= CONSOLE_READ_OPS; GetConsoleMode(handle, &chanInfoPtr->initMode); #ifdef OBSOLETE /* Why was priority being set on console input? Code smell */ SetThreadPriority(infoPtr->reader.thread, THREAD_PRIORITY_HIGHEST); #endif } else { /* Already checked permissions is WRITABLE if not READABLE */ /* TODO - enable ansi escape processing? */ } /* * Global lock but that's ok. See comments top of file. Allocations * will happen only a few times in the life of a process and that too * generally at start up where only one thread is active. */ AcquireSRWLockExclusive(&gConsoleLock); /*Allocate needs exclusive lock */ handleInfoPtr = FindConsoleInfo(chanInfoPtr); if (handleInfoPtr == NULL) { /* Not found. Allocate one */ handleInfoPtr = AllocateConsoleHandleInfo(handle, permissions); } else { /* Found. Its direction (read/write) better be the same */ if (handleInfoPtr->permissions != permissions) { handleInfoPtr = NULL; } } if (handleInfoPtr == NULL) { ReleaseSRWLockExclusive(&gConsoleLock); if (permissions == TCL_READABLE) { SetConsoleMode(handle, chanInfoPtr->initMode); } ckfree(chanInfoPtr); return NULL; } /* * There is effectively a reference to this structure from the Tcl * channel subsystem. So record that. This reference will be dropped * when the Tcl channel is closed. */ chanInfoPtr->numRefs = 1; /* * Need to keep track of number of referencing channels for closing. * The pointer is safe since there is a reference held to it from * gConsoleHandleInfoList but still need to lock the structure itself */ AcquireSRWLockExclusive(&handleInfoPtr->lock); handleInfoPtr->numRefs += 1; ReleaseSRWLockExclusive(&handleInfoPtr->lock); ReleaseSRWLockExclusive(&gConsoleLock); /* Note Tcl_CreateChannel never fails other than panic on error */ chanInfoPtr->channel = Tcl_CreateChannel(&consoleChannelType, channelName, chanInfoPtr, permissions); /* * Files have default translation of AUTO and ^Z eof char, which means * that a ^Z will be accepted as EOF when reading. */ Tcl_SetChannelOption(NULL, chanInfoPtr->channel, "-translation", "auto"); Tcl_SetChannelOption(NULL, chanInfoPtr->channel, "-eofchar", "\032 {}"); Tcl_SetChannelOption(NULL, chanInfoPtr->channel, "-encoding", "utf-16"); return chanInfoPtr->channel; } /* *---------------------------------------------------------------------- * * ConsoleThreadActionProc -- * * Insert or remove any thread local refs to this channel. * * Results: * None. * * Side effects: * Changes thread local list of valid channels. * *---------------------------------------------------------------------- */ static void ConsoleThreadActionProc( ClientData instanceData, int action) { ConsoleChannelInfo *chanInfoPtr = (ConsoleChannelInfo *)instanceData; /* No need for any locks as no other thread will be writing to it */ if (action == TCL_CHANNEL_THREAD_INSERT) { ConsoleInit(); /* Needed to set up event source handlers for this thread */ chanInfoPtr->threadId = Tcl_GetCurrentThread(); } else { chanInfoPtr->threadId = NULL; } } /* *---------------------------------------------------------------------- * * ConsoleSetOptionProc -- * * Sets an option on a channel. * * Results: * A standard Tcl result. Also sets the interp's result on error if * interp is not NULL. * * Side effects: * May modify an option on a console. Sets Error message if needed (by * calling Tcl_BadChannelOption). * *---------------------------------------------------------------------- */ static int ConsoleSetOptionProc( ClientData instanceData, /* File state. */ Tcl_Interp *interp, /* For error reporting - can be NULL. */ const char *optionName, /* Which option to set? */ const char *value) /* New value for option. */ { ConsoleChannelInfo *chanInfoPtr = (ConsoleChannelInfo *)instanceData; int len = strlen(optionName); int vlen = strlen(value); /* * Option -inputmode normal|password|raw */ if ((chanInfoPtr->flags & CONSOLE_READ_OPS) && (len > 1) && (strncmp(optionName, "-inputmode", len) == 0)) { DWORD mode; if (GetConsoleMode(chanInfoPtr->handle, &mode) == 0) { Tcl_WinConvertError(GetLastError()); if (interp != NULL) { Tcl_SetObjResult(interp, Tcl_ObjPrintf( "couldn't read console mode: %s", Tcl_PosixError(interp))); } return TCL_ERROR; } if (Tcl_UtfNcasecmp(value, "NORMAL", vlen) == 0) { mode |= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT; } else if (Tcl_UtfNcasecmp(value, "PASSWORD", vlen) == 0) { mode |= ENABLE_LINE_INPUT; mode &= ~ENABLE_ECHO_INPUT; } else if (Tcl_UtfNcasecmp(value, "RAW", vlen) == 0) { mode &= ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT); } else if (Tcl_UtfNcasecmp(value, "RESET", vlen) == 0) { /* * Reset to the initial mode, whatever that is. */ mode = chanInfoPtr->initMode; } else { if (interp) { Tcl_SetObjResult(interp, Tcl_ObjPrintf( "bad mode \"%s\" for -inputmode: must be" " normal, password, raw, or reset", value)); Tcl_SetErrorCode(interp, "TCL", "OPERATION", "FCONFIGURE", "VALUE", NULL); } return TCL_ERROR; } if (SetConsoleMode(chanInfoPtr->handle, mode) == 0) { Tcl_WinConvertError(GetLastError()); if (interp != NULL) { Tcl_SetObjResult(interp, Tcl_ObjPrintf( "couldn't set console mode: %s", Tcl_PosixError(interp))); } return TCL_ERROR; } return TCL_OK; } if (chanInfoPtr->flags & CONSOLE_READ_OPS) { return Tcl_BadChannelOption(interp, optionName, "inputmode"); } else { return Tcl_BadChannelOption(interp, optionName, ""); } } /* *---------------------------------------------------------------------- * * ConsoleGetOptionProc -- * * Gets a mode associated with an IO channel. If the optionName arg is * non-NULL, retrieves the value of that option. If the optionName arg is * NULL, retrieves a list of alternating option names and values for the * given channel. * * Results: * A standard Tcl result. Also sets the supplied DString to the string * value of the option(s) returned. Sets error message if needed * (by calling Tcl_BadChannelOption). * *---------------------------------------------------------------------- */ static int ConsoleGetOptionProc( ClientData instanceData, /* File state. */ Tcl_Interp *interp, /* For error reporting - can be NULL. */ const char *optionName, /* Option to get. */ Tcl_DString *dsPtr) /* Where to store value(s). */ { ConsoleChannelInfo *chanInfoPtr = (ConsoleChannelInfo *)instanceData; int valid = 0; /* Flag if valid option parsed. */ unsigned int len; char buf[TCL_INTEGER_SPACE]; if (optionName == NULL) { len = 0; } else { len = strlen(optionName); } /* * Get option -inputmode * * This is a great simplification of the underlying reality, but actually * represents what almost all scripts really want to know. */ if (chanInfoPtr->flags & CONSOLE_READ_OPS) { if (len == 0) { Tcl_DStringAppendElement(dsPtr, "-inputmode"); } if (len==0 || (len>1 && strncmp(optionName, "-inputmode", len)==0)) { DWORD mode; valid = 1; if (GetConsoleMode(chanInfoPtr->handle, &mode) == 0) { Tcl_WinConvertError(GetLastError()); if (interp != NULL) { Tcl_SetObjResult(interp, Tcl_ObjPrintf( "couldn't read console mode: %s", Tcl_PosixError(interp))); } return TCL_ERROR; } if (mode & ENABLE_LINE_INPUT) { if (mode & ENABLE_ECHO_INPUT) { Tcl_DStringAppendElement(dsPtr, "normal"); } else { Tcl_DStringAppendElement(dsPtr, "password"); } } else { Tcl_DStringAppendElement(dsPtr, "raw"); } } } /* * Get option -winsize * Option is readonly and returned by [fconfigure chan -winsize] but not * returned by [fconfigure chan] without explicit option name. */ if ((len > 1) && (strncmp(optionName, "-winsize", len) == 0)) { CONSOLE_SCREEN_BUFFER_INFO consoleInfo; valid = 1; if (!GetConsoleScreenBufferInfo(chanInfoPtr->handle, &consoleInfo)) { Tcl_WinConvertError(GetLastError()); if (interp != NULL) { Tcl_SetObjResult(interp, Tcl_ObjPrintf( "couldn't read console size: %s", Tcl_PosixError(interp))); } return TCL_ERROR; } sprintf(buf, "%d", consoleInfo.srWindow.Right - consoleInfo.srWindow.Left + 1); Tcl_DStringAppendElement(dsPtr, buf); sprintf(buf, "%d", consoleInfo.srWindow.Bottom - consoleInfo.srWindow.Top + 1); Tcl_DStringAppendElement(dsPtr, buf); } if (valid) { return TCL_OK; } if (chanInfoPtr->flags & CONSOLE_READ_OPS) { return Tcl_BadChannelOption(interp, optionName, "inputmode winsize"); } else { return Tcl_BadChannelOption(interp, optionName, ""); } } /* * Local Variables: * mode: c * c-basic-offset: 4 * fill-column: 78 * End: */