summaryrefslogtreecommitdiffstats
path: root/win/tclWinConsole.c
diff options
context:
space:
mode:
authorapnadkarni <apnmbx-wits@yahoo.com>2022-06-27 16:02:37 (GMT)
committerapnadkarni <apnmbx-wits@yahoo.com>2022-06-27 16:02:37 (GMT)
commit59571d7b3fc9705ea9f18a0cff6cc2f37ebfa0f9 (patch)
tree7e7cd1c2e71e82cbe59836af4e5fca224cda9472 /win/tclWinConsole.c
parent31b740c312532d91ceed2453d5678b9350c3bf32 (diff)
downloadtcl-59571d7b3fc9705ea9f18a0cff6cc2f37ebfa0f9.zip
tcl-59571d7b3fc9705ea9f18a0cff6cc2f37ebfa0f9.tar.gz
tcl-59571d7b3fc9705ea9f18a0cff6cc2f37ebfa0f9.tar.bz2
Ticket #21280817 - Windows console rewrite to only create one thread per stdio channel.
Functional, but not complete.
Diffstat (limited to 'win/tclWinConsole.c')
-rw-r--r--win/tclWinConsole.c1961
1 files changed, 1294 insertions, 667 deletions
diff --git a/win/tclWinConsole.c b/win/tclWinConsole.c
index c3ba814..14cc6e5 100644
--- a/win/tclWinConsole.c
+++ b/win/tclWinConsole.c
@@ -2,15 +2,51 @@
* tclWinConsole.c --
*
* This file implements the Windows-specific console functions, and the
- * "console" channel driver.
+ * "console" channel driver. Windows 7 or later required.
*
- * Copyright © 1999 Scriptics Corp.
+ * 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 <assert.h>
+#include <ctype.h>
+
+/*
+ * 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
@@ -19,106 +55,119 @@
static int initialized = 0;
-/*
- * The consoleMutex 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.
- */
-
-TCL_DECLARE_MUTEX(consoleMutex)
+#define CONSOLE_BUFFER_SIZE 8000 // TODO - must be at least 2 :-)
/*
- * Bit masks used in the flags field of the ConsoleInfo structure below.
+ * Ring buffer for storing data. Actual data is from bufPtr[start]:bufPtr[size-1]
+ * and bufPtr[0]:bufPtr[length - (size-start)].
*/
-
-#define CONSOLE_PENDING (1<<0) /* Message is pending in the queue. */
-#define CONSOLE_ASYNC (1<<1) /* Channel is non-blocking. */
-#define CONSOLE_READ_OPS (1<<4) /* Channel supports read-related ops. */
-#define CONSOLE_RESET (1<<5) /* Console mode needs to be reset. */
+#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_))
/*
- * Bit masks used in the sharedFlags field of the ConsoleInfo structure below.
+ * 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.
*/
-
-#define CONSOLE_EOF (1<<2) /* Console has reached EOF. */
-#define CONSOLE_BUFFERED (1<<3) /* Data was read into a buffer by the reader
- * thread. */
-
-#define CONSOLE_BUFFER_SIZE (8*1024)
-
-/*
- * Structure containing handles associated with one of the special console
- * threads.
- */
-
-typedef struct {
- HANDLE thread; /* Handle to reader or writer thread. */
- HANDLE readyEvent; /* Manual-reset event to signal _to_ the main
- * thread when the worker thread has finished
- * waiting for its normal work to happen. */
- TclPipeThreadInfo *TI; /* Thread info structure of writer and reader. */
-} ConsoleThreadInfo;
+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 ConsoleInfo {
- HANDLE handle;
- int type;
- struct ConsoleInfo *nextPtr;/* Pointer to next registered console. */
+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. */
- int validMask; /* OR'ed combination of TCL_READABLE,
+ 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, see above for a list. */
- Tcl_ThreadId threadId; /* Thread to which events should be reported.
- * This value is used by the reader/writer
- * threads. */
- ConsoleThreadInfo writer; /* A specialized thread for handling
- * asynchronous writes to the console; the
- * waiting starts when a control event is sent,
- * and a reset event is sent back to the main
- * thread when the write is done. */
- ConsoleThreadInfo reader; /* A specialized thread for handling
- * asynchronous reads from the console; the
- * waiting starts when a control event is sent,
- * and a reset event is sent back to the main
- * thread when input is available. */
- DWORD writeError; /* An error caused by the last background
- * write. Set to 0 if no error has been
- * detected. This word is shared with the
- * writer thread so access must be
- * synchronized with the writable object. */
- char *writeBuf; /* Current background output buffer. Access is
- * synchronized with the writable object. */
- int writeBufLen; /* Size of write buffer. Access is
- * synchronized with the writable object. */
- int toWrite; /* Current amount to be written. Access is
- * synchronized with the writable object. */
- int readFlags; /* Flags that are shared with the reader
- * thread. Access is synchronized with the
- * readable object. */
- int bytesRead; /* Number of bytes in the buffer. */
- int offset; /* Number of bytes read out of the buffer. */
- DWORD initMode; /* Initial console mode. */
- char buffer[CONSOLE_BUFFER_SIZE];
- /* Data consumed by reader thread. */
-} ConsoleInfo;
-
-typedef struct {
- /*
- * The following pointer refers to the head of the list of consoles that
- * are being watched for file events.
- */
-
- ConsoleInfo *firstConsolePtr;
-} ThreadSpecificData;
-
-static Tcl_ThreadDataKey dataKey;
+ 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
@@ -126,51 +175,101 @@ static Tcl_ThreadDataKey dataKey;
*/
typedef struct {
- Tcl_Event header; /* Information that is standard for all
- * events. */
- ConsoleInfo *infoPtr; /* Pointer to console info structure. Note
- * that we still have to verify that the
- * console exists before dereferencing this
- * pointer. */
+ 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 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 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 DWORD WINAPI ConsoleWriterThread(LPVOID arg);
-static void ProcExitHandler(ClientData clientData);
-static int WaitForRead(ConsoleInfo *infoPtr, int blocking);
-static void ConsoleThreadActionProc(ClientData instanceData,
- int action);
-static BOOL ReadConsoleBytes(HANDLE hConsole, LPVOID lpBuffer,
- DWORD nbytes, LPDWORD nbytesread);
-static BOOL WriteConsoleBytes(HANDLE hConsole,
- const void *lpBuffer, DWORD nbytes,
- LPDWORD nbyteswritten);
+#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
@@ -178,82 +277,350 @@ static BOOL WriteConsoleBytes(HANDLE hConsole,
*/
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. */
+ "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;
+}
/*
- *----------------------------------------------------------------------
+ *------------------------------------------------------------------------
*
- * ReadConsoleBytes, WriteConsoleBytes --
+ * RingBufferClear
*
- * Wrapper for ReadConsoleW, that takes and returns number of bytes
- * instead of number of WCHARS.
+ * 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);
+}
-static BOOL
-ReadConsoleBytes(
+/*
+ *------------------------------------------------------------------------
+ *
+ * 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,
- LPVOID lpBuffer,
- DWORD nbytes,
- LPDWORD nbytesread)
+ WCHAR *lpBuffer,
+ RingSizeT nChars,
+ RingSizeT *nCharsReadPtr)
{
- DWORD ntchars;
+ 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. We do not want to treat this case
- * as EOF so we will loop around again. 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 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
*/
- do {
- result = ReadConsoleW(hConsole, lpBuffer, nbytes / sizeof(WCHAR), &ntchars,
- NULL);
- } while (result && ntchars == 0 && GetLastError() == ERROR_OPERATION_ABORTED);
- if (nbytesread != NULL) {
- *nbytesread = ntchars * sizeof(WCHAR);
- }
- return result;
+ 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 BOOL
-WriteConsoleBytes(
+static DWORD
+WriteConsoleChars(
HANDLE hConsole,
- const void *lpBuffer,
- DWORD nbytes,
- LPDWORD nbyteswritten)
+ const WCHAR *lpBuffer,
+ RingSizeT nChars,
+ RingSizeT *nCharsWrittenPtr)
{
- DWORD ntchars;
+ DWORD nCharsWritten;
BOOL result;
- result = WriteConsoleW(hConsole, lpBuffer, nbytes / sizeof(WCHAR), &ntchars,
- NULL);
- if (nbyteswritten != NULL) {
- *nbyteswritten = ntchars * sizeof(WCHAR);
+ /* 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();
}
- return result;
}
/*
@@ -281,18 +648,18 @@ ConsoleInit(void)
*/
if (!initialized) {
- Tcl_MutexLock(&consoleMutex);
+ AcquireSRWLockExclusive(&gConsoleLock);
if (!initialized) {
initialized = 1;
Tcl_CreateExitHandler(ProcExitHandler, NULL);
}
- Tcl_MutexUnlock(&consoleMutex);
+ ReleaseSRWLockExclusive(&gConsoleLock);
}
if (TclThreadDataKeyGet(&dataKey) == NULL) {
ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
- tsdPtr->firstConsolePtr = NULL;
+ tsdPtr->notUsed = 0;
Tcl_CreateEventSource(ConsoleSetupProc, ConsoleCheckProc, NULL);
Tcl_CreateThreadExitHandler(ConsoleExitHandler, NULL);
}
@@ -343,9 +710,9 @@ static void
ProcExitHandler(
TCL_UNUSED(ClientData))
{
- Tcl_MutexLock(&consoleMutex);
+ AcquireSRWLockExclusive(&gConsoleLock);
initialized = 0;
- Tcl_MutexUnlock(&consoleMutex);
+ ReleaseSRWLockExclusive(&gConsoleLock);
}
/*
@@ -354,7 +721,9 @@ ProcExitHandler(
* ConsoleSetupProc --
*
* This procedure is invoked before Tcl_DoOneEvent blocks waiting for an
- * event.
+ * 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.
@@ -370,34 +739,40 @@ ConsoleSetupProc(
TCL_UNUSED(ClientData),
int flags) /* Event flags as passed to Tcl_DoOneEvent. */
{
- ConsoleInfo *infoPtr;
+ ConsoleChannelInfo *chanInfoPtr;
Tcl_Time blockTime = { 0, 0 };
int block = 1;
- ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
if (!(flags & TCL_FILE_EVENTS)) {
return;
}
/*
- * Look to see if any events are already pending. If they are, poll.
+ * Walk the list of channels. See general comments for struct
+ * ConsoleChannelInfo with regard to locking and field access.
*/
-
- for (infoPtr = tsdPtr->firstConsolePtr; infoPtr != NULL;
- infoPtr = infoPtr->nextPtr) {
- if (infoPtr->watchMask & TCL_WRITABLE) {
- if (WaitForSingleObject(infoPtr->writer.readyEvent,
- 0) != WAIT_TIMEOUT) {
- block = 0;
+ 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 */
}
- }
- if (infoPtr->watchMask & TCL_READABLE) {
- if (WaitForRead(infoPtr, 0) >= 0) {
- block = 0;
+ 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);
}
}
@@ -424,54 +799,69 @@ ConsoleCheckProc(
TCL_UNUSED(ClientData),
int flags) /* Event flags as passed to Tcl_DoOneEvent. */
{
- ConsoleInfo *infoPtr;
+ ConsoleChannelInfo *chanInfoPtr;
+ Tcl_ThreadId me;
int needEvent;
- ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
if (!(flags & TCL_FILE_EVENTS)) {
return;
}
+ me = Tcl_GetCurrentThread();
+
/*
- * Queue events for any ready consoles that don't already have events
- * queued.
+ * 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;
- for (infoPtr = tsdPtr->firstConsolePtr; infoPtr != NULL;
- infoPtr = infoPtr->nextPtr) {
- if (infoPtr->flags & CONSOLE_PENDING) {
+ 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;
}
- /*
- * Queue an event if the console is signaled for reading or writing.
- */
+ handleInfoPtr = FindConsoleInfo(chanInfoPtr);
+ /* Pointer is safe to access as we are holding gConsoleLock */
- needEvent = 0;
- if (infoPtr->watchMask & TCL_WRITABLE) {
- if (WaitForSingleObject(infoPtr->writer.readyEvent,
- 0) != WAIT_TIMEOUT) {
- needEvent = 1;
+ if (handleInfoPtr != NULL) {
+ AcquireSRWLockShared(&handleInfoPtr->lock);
+ if ((chanInfoPtr->watchMask & TCL_READABLE)
+ && RingBufferLength(&handleInfoPtr->buffer) > 0) {
+ needEvent = 1; /* Input data available */
}
- }
-
- if (infoPtr->watchMask & TCL_READABLE) {
- if (WaitForRead(infoPtr, 0) >= 0) {
- needEvent = 1;
+ else if (RingBufferFreeSpace(&handleInfoPtr->buffer) > 0) {
+ needEvent = 1; /* Output space available */
}
+ ReleaseSRWLockShared(&handleInfoPtr->lock);
}
if (needEvent) {
ConsoleEvent *evPtr = (ConsoleEvent *)ckalloc(sizeof(ConsoleEvent));
- infoPtr->flags |= CONSOLE_PENDING;
+ /* 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->infoPtr = infoPtr;
+ evPtr->chanInfoPtr = chanInfoPtr;
Tcl_QueueEvent((Tcl_Event *) evPtr, TCL_QUEUE_TAIL);
}
}
+
+ ReleaseSRWLockShared(&gConsoleLock);
}
-
+
/*
*----------------------------------------------------------------------
*
@@ -494,7 +884,7 @@ ConsoleBlockModeProc(
int mode) /* TCL_MODE_BLOCKING or
* TCL_MODE_NONBLOCKING. */
{
- ConsoleInfo *infoPtr = (ConsoleInfo *)instanceData;
+ ConsoleChannelInfo *chanInfoPtr = (ConsoleChannelInfo *)instanceData;
/*
* Consoles on Windows can not be switched between blocking and
@@ -505,9 +895,9 @@ ConsoleBlockModeProc(
*/
if (mode == TCL_MODE_NONBLOCKING) {
- infoPtr->flags |= CONSOLE_ASYNC;
+ chanInfoPtr->flags |= CONSOLE_ASYNC;
} else {
- infoPtr->flags &= ~CONSOLE_ASYNC;
+ chanInfoPtr->flags &= ~CONSOLE_ASYNC;
}
return 0;
}
@@ -530,102 +920,86 @@ ConsoleBlockModeProc(
static int
ConsoleCloseProc(
- ClientData instanceData, /* Pointer to ConsoleInfo structure. */
+ ClientData instanceData, /* Pointer to ConsoleChannelInfo structure. */
TCL_UNUSED(Tcl_Interp *),
int flags)
{
- ConsoleInfo *consolePtr = (ConsoleInfo *)instanceData;
+ ConsoleChannelInfo *chanInfoPtr = (ConsoleChannelInfo *)instanceData;
+ ConsoleHandleInfo *handleInfoPtr;
int errorCode = 0;
- ConsoleInfo *infoPtr, **nextPtrPtr;
- ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
+ ConsoleChannelInfo **nextPtrPtr;
if ((flags & (TCL_CLOSE_READ | TCL_CLOSE_WRITE)) != 0) {
return EINVAL;
}
- /*
- * Clean up the background thread if necessary. Note that this must be
- * done before we can close the file, since the thread may be blocking
- * trying to read from the console.
- */
+ AcquireSRWLockExclusive(&gConsoleLock);
- if (consolePtr->reader.thread) {
- TclPipeThreadStop(&consolePtr->reader.TI, consolePtr->reader.thread);
- CloseHandle(consolePtr->reader.thread);
- CloseHandle(consolePtr->reader.readyEvent);
- consolePtr->reader.thread = NULL;
+ /* Remove channel from watchers' list */
+ for (nextPtrPtr = &gWatchingChannelList; *nextPtrPtr != NULL;
+ nextPtrPtr = &(*nextPtrPtr)->nextWatchingChannelPtr) {
+ if (*nextPtrPtr == (ConsoleChannelInfo *) chanInfoPtr) {
+ *nextPtrPtr = (*nextPtrPtr)->nextWatchingChannelPtr;
+ break;
+ }
}
- consolePtr->validMask &= ~TCL_READABLE;
- /*
- * Wait for the writer thread to finish the current buffer, then terminate
- * the thread and close the handles. If the channel is nonblocking, there
- * should be no pending write operations.
- */
-
- if (consolePtr->writer.thread) {
- if (consolePtr->toWrite) {
- /*
- * We only need to wait if there is something to write. This may
- * prevent infinite wait on exit. [Python Bug 216289]
- */
+ 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);
- WaitForSingleObject(consolePtr->writer.readyEvent, 5000);
- }
+ handleInfoPtr->numRefs -= 1; /* Remove reference from this channel */
- TclPipeThreadStop(&consolePtr->writer.TI, consolePtr->writer.thread);
- CloseHandle(consolePtr->writer.thread);
- CloseHandle(consolePtr->writer.readyEvent);
- consolePtr->writer.thread = NULL;
- }
- consolePtr->validMask &= ~TCL_WRITABLE;
+ /* Break the thread out of blocking console i/o */
+ CancelSynchronousIo(handleInfoPtr->consoleThread);
- /*
- * If the user has been tinkering with the mode, reset it now. We ignore
- * any errors from this; we're quite possibly about to close or exit
- * anyway.
- */
+ /* Wake up the console handling thread */
+ WakeConditionVariable(&handleInfoPtr->consoleThreadCV);
- if ((consolePtr->flags & CONSOLE_READ_OPS) &&
- (consolePtr->flags & CONSOLE_RESET)) {
- SetConsoleMode(consolePtr->handle, consolePtr->initMode);
+ ReleaseSRWLockShared(&handleInfoPtr->lock);
}
- /*
- * 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.
- */
+ ReleaseSRWLockExclusive(&gConsoleLock);
- if (!TclInThreadExit()
- || ((GetStdHandle(STD_INPUT_HANDLE) != consolePtr->handle)
- && (GetStdHandle(STD_OUTPUT_HANDLE) != consolePtr->handle)
- && (GetStdHandle(STD_ERROR_HANDLE) != consolePtr->handle))) {
- if (CloseHandle(consolePtr->handle) == FALSE) {
- Tcl_WinConvertError(GetLastError());
- errorCode = errno;
+ 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;
}
- consolePtr->watchMask &= consolePtr->validMask;
-
/*
- * Remove the file from the list of watched files.
+ * 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.
*/
-
- for (nextPtrPtr = &tsdPtr->firstConsolePtr, infoPtr = *nextPtrPtr;
- infoPtr != NULL;
- nextPtrPtr = &infoPtr->nextPtr, infoPtr = *nextPtrPtr) {
- if (infoPtr == (ConsoleInfo *) consolePtr) {
- *nextPtrPtr = infoPtr->nextPtr;
- break;
- }
+ if (chanInfoPtr->numRefs > 1) {
+ /* There may be references already on the event queue */
+ chanInfoPtr->numRefs -= 1;
}
- if (consolePtr->writeBuf != NULL) {
- ckfree(consolePtr->writeBuf);
- consolePtr->writeBuf = 0;
+ else {
+ ckfree(chanInfoPtr);
}
- ckfree(consolePtr);
return errorCode;
}
@@ -647,80 +1021,85 @@ ConsoleCloseProc(
*
*----------------------------------------------------------------------
*/
-
static int
ConsoleInputProc(
ClientData instanceData, /* Console state. */
- char *buf, /* Where to store data read. */
+ char *bufPtr, /* Where to store data read. */
int bufSize, /* How much space is available in the
* buffer? */
int *errorCode) /* Where to store error code. */
{
- ConsoleInfo *infoPtr = (ConsoleInfo *)instanceData;
- DWORD count, bytesRead = 0;
- int result;
+ ConsoleChannelInfo *chanInfoPtr = (ConsoleChannelInfo *)instanceData;
+ ConsoleHandleInfo *handleInfoPtr;
+ RingSizeT numRead;
*errorCode = 0;
- /*
- * Synchronize with the reader thread.
- */
-
- result = WaitForRead(infoPtr, (infoPtr->flags & CONSOLE_ASYNC) ? 0 : 1);
-
- /*
- * If an error occurred, return immediately.
- */
-
- if (result == -1) {
- *errorCode = errno;
- return -1;
+ 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 */
- if (infoPtr->readFlags & CONSOLE_BUFFERED) {
+ numRead = RingBufferOut(&handleInfoPtr->buffer, bufPtr, bufSize, 1);
+ while (numRead == 0) {
/*
- * Data is stored in the buffer.
+ * 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 (bufSize < (infoPtr->bytesRead - infoPtr->offset)) {
- memcpy(buf, &infoPtr->buffer[infoPtr->offset], bufSize);
- bytesRead = bufSize;
- infoPtr->offset += bufSize;
- } else {
- memcpy(buf, &infoPtr->buffer[infoPtr->offset], bufSize);
- bytesRead = infoPtr->bytesRead - infoPtr->offset;
-
- /*
- * Reset the buffer.
- */
-
- infoPtr->readFlags &= ~CONSOLE_BUFFERED;
- infoPtr->offset = 0;
+ 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;
+ }
+ }
}
-
- return bytesRead;
- }
-
- /*
- * Attempt to read bufSize bytes. The read will return immediately if
- * there is any data available. Otherwise it will block until at least one
- * byte is available or an EOF occurs.
- */
-
- if (ReadConsoleBytes(infoPtr->handle, (LPVOID) buf, (DWORD) bufSize,
- &count) == TRUE) {
- /*
- * TODO: This potentially writes beyond the limits specified
- * by the caller. In practice this is harmless, since all writes
- * are into ChannelBuffers, and those have padding, but still
- * ought to remove this, unless some Windows wizard can give
- * a reason not to.
- */
- buf[count] = '\0';
- return count;
}
- return -1;
+ ReleaseSRWLockExclusive(&handleInfoPtr->lock);
+ return numRead;
}
/*
@@ -740,7 +1119,6 @@ ConsoleInputProc(
*
*----------------------------------------------------------------------
*/
-
static int
ConsoleOutputProc(
ClientData instanceData, /* Console state. */
@@ -748,74 +1126,70 @@ ConsoleOutputProc(
int toWrite, /* How many bytes to write? */
int *errorCode) /* Where to store error code. */
{
- ConsoleInfo *infoPtr = (ConsoleInfo *)instanceData;
- ConsoleThreadInfo *threadInfo = &infoPtr->writer;
- DWORD bytesWritten, timeout;
+ ConsoleChannelInfo *chanInfoPtr = (ConsoleChannelInfo *)instanceData;
+ ConsoleHandleInfo *handleInfoPtr;
+ RingSizeT numWritten;
*errorCode = 0;
- /* avoid blocking if pipe-thread exited */
- timeout = (infoPtr->flags & CONSOLE_ASYNC) || !TclPipeThreadIsAlive(&threadInfo->TI)
- || TclInExit() || TclInThreadExit() ? 0 : INFINITE;
- if (WaitForSingleObject(threadInfo->readyEvent, timeout) == WAIT_TIMEOUT) {
- /*
- * The writer thread is blocked waiting for a write to complete and
- * the channel is in non-blocking mode.
- */
-
- errno = EWOULDBLOCK;
- goto error;
- }
-
- /*
- * Check for a background error on the last write.
- */
-
- if (infoPtr->writeError) {
- Tcl_WinConvertError(infoPtr->writeError);
- infoPtr->writeError = 0;
- goto error;
+ AcquireSRWLockShared(&gConsoleLock);
+ handleInfoPtr = FindConsoleInfo(chanInfoPtr);
+ if (handleInfoPtr == NULL) {
+ /* Really shouldn't happen since channel is holding a reference */
+ *errorCode = EPIPE;
+ ReleaseSRWLockShared(&gConsoleLock);
+ return -1;
}
-
- if (infoPtr->flags & CONSOLE_ASYNC) {
- /*
- * The console is non-blocking, so copy the data into the output
- * buffer and restart the writer thread.
- */
-
- if (toWrite > infoPtr->writeBufLen) {
+ 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 {
/*
- * Reallocate the buffer to be large enough to hold the data.
+ * 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.
*/
-
- if (infoPtr->writeBuf) {
- ckfree(infoPtr->writeBuf);
+ 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;
}
- infoPtr->writeBufLen = toWrite;
- infoPtr->writeBuf = (char *)ckalloc(toWrite);
- }
- memcpy(infoPtr->writeBuf, buf, toWrite);
- infoPtr->toWrite = toWrite;
- ResetEvent(threadInfo->readyEvent);
- TclPipeThreadSignal(&threadInfo->TI);
- bytesWritten = toWrite;
- } else {
- /*
- * In the blocking case, just try to write the buffer directly. This
- * avoids an unnecessary copy.
- */
-
- if (WriteConsoleBytes(infoPtr->handle, buf, (DWORD) toWrite,
- &bytesWritten) == FALSE) {
- Tcl_WinConvertError(GetLastError());
- goto error;
}
}
- return bytesWritten;
- error:
- *errorCode = errno;
- return -1;
+ ReleaseSRWLockExclusive(&handleInfoPtr->lock);
+ WakeConditionVariable(&handleInfoPtr->consoleThreadCV);
+ return numWritten;
}
/*
@@ -846,66 +1220,69 @@ ConsoleEventProc(
* such as TCL_FILE_EVENTS. */
{
ConsoleEvent *consoleEvPtr = (ConsoleEvent *) evPtr;
- ConsoleInfo *infoPtr;
- int mask;
- ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
+ ConsoleChannelInfo *chanInfoPtr;
+ int freeChannel;
if (!(flags & TCL_FILE_EVENTS)) {
return 0;
}
+ chanInfoPtr = consoleEvPtr->chanInfoPtr;
/*
- * Search through the list of watched consoles for the one whose handle
- * matches the event. We do this rather than simply dereferencing the
- * handle in the event so that consoles can be deleted while the event is
- * in the queue.
- */
-
- for (infoPtr = tsdPtr->firstConsolePtr; infoPtr != NULL;
- infoPtr = infoPtr->nextPtr) {
- if (consoleEvPtr->infoPtr == infoPtr) {
- infoPtr->flags &= ~CONSOLE_PENDING;
- break;
- }
- }
-
- /*
- * Remove stale events.
+ * 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.
*/
- if (!infoPtr) {
- return 1;
- }
+ AcquireSRWLockShared(&gConsoleLock);
+ chanInfoPtr->flags &= ~CONSOLE_EVENT_QUEUED;
/*
- * Check to see if the console is readable. Note that we can't tell if a
- * console is writable, so we always report it as being writable unless we
- * have detected EOF.
+ * Only handle the event if the Tcl channel has not gone away AND is
+ * still owned by this thread AND is still watching events.
*/
-
- mask = 0;
- if (infoPtr->watchMask & TCL_WRITABLE) {
- if (WaitForSingleObject(infoPtr->writer.readyEvent,
- 0) != WAIT_TIMEOUT) {
- mask = TCL_WRITABLE;
+ 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;
+ }
}
- }
-
- if (infoPtr->watchMask & TCL_READABLE) {
- if (WaitForRead(infoPtr, 0) >= 0) {
- if (infoPtr->readFlags & CONSOLE_EOF) {
+ else {
+ AcquireSRWLockShared(&handleInfoPtr->lock);
+ if (chanInfoPtr->watchMask & TCL_READABLE
+ && RingBufferLength(&handleInfoPtr->buffer)) {
mask = TCL_READABLE;
- } else {
- 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);
- /*
- * Inform the channel of the events.
- */
+ if (freeChannel)
+ ckfree(chanInfoPtr);
- Tcl_NotifyChannel(infoPtr->channel, infoPtr->watchMask & mask);
return 1;
}
@@ -928,39 +1305,39 @@ ConsoleEventProc(
static void
ConsoleWatchProc(
ClientData instanceData, /* Console state. */
- int mask) /* What events to watch for, OR-ed combination
+ int permissions) /* What events to watch for, OR-ed combination
* of TCL_READABLE, TCL_WRITABLE and
* TCL_EXCEPTION. */
{
- ConsoleInfo **nextPtrPtr, *ptr;
- ConsoleInfo *infoPtr = (ConsoleInfo *)instanceData;
- int oldMask = infoPtr->watchMask;
- ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
+ 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.
*/
- infoPtr->watchMask = mask & infoPtr->validMask;
- if (infoPtr->watchMask) {
+ chanInfoPtr->watchMask = permissions & chanInfoPtr->permissions;
+ if (chanInfoPtr->watchMask) {
Tcl_Time blockTime = { 0, 0 };
if (!oldMask) {
- infoPtr->nextPtr = tsdPtr->firstConsolePtr;
- tsdPtr->firstConsolePtr = infoPtr;
+ /* Add to list of watched channels */
+ AcquireSRWLockExclusive(&gConsoleLock);
+ chanInfoPtr->nextWatchingChannelPtr = gWatchingChannelList;
+ gWatchingChannelList = chanInfoPtr;
+ ReleaseSRWLockExclusive(&gConsoleLock);
}
Tcl_SetMaxBlockTime(&blockTime);
} else if (oldMask) {
- /*
- * Remove the console from the list of watched consoles.
- */
+ /* Remove from list of watched channels */
- for (nextPtrPtr = &(tsdPtr->firstConsolePtr), ptr = *nextPtrPtr;
+ for (nextPtrPtr = &gWatchingChannelList, ptr = *nextPtrPtr;
ptr != NULL;
- nextPtrPtr = &ptr->nextPtr, ptr = *nextPtrPtr) {
- if (infoPtr == ptr) {
- *nextPtrPtr = ptr->nextPtr;
+ nextPtrPtr = &ptr->nextWatchingChannelPtr, ptr = *nextPtrPtr) {
+ if (chanInfoPtr == ptr) {
+ *nextPtrPtr = ptr->nextWatchingChannelPtr;
break;
}
}
@@ -991,12 +1368,13 @@ ConsoleGetHandleProc(
TCL_UNUSED(int) /*direction*/,
ClientData *handlePtr) /* Where to store the handle. */
{
- ConsoleInfo *infoPtr = (ConsoleInfo *)instanceData;
+ ConsoleChannelInfo *chanInfoPtr = (ConsoleChannelInfo *)instanceData;
- *handlePtr = infoPtr->handle;
+ *handlePtr = chanInfoPtr->handle;
return TCL_OK;
}
+#ifdef OBSOLETE
/*
*----------------------------------------------------------------------
*
@@ -1020,7 +1398,7 @@ ConsoleGetHandleProc(
static int
WaitForRead(
- ConsoleInfo *infoPtr, /* Console state. */
+ ConsoleChannelInfo *infoPtr, /* Console state. */
int blocking) /* Indicates whether call should be blocking
* or not. */
{
@@ -1100,6 +1478,7 @@ WaitForRead(
TclPipeThreadSignal(&threadInfo->TI);
}
}
+#endif
/*
*----------------------------------------------------------------------
@@ -1110,12 +1489,10 @@ WaitForRead(
* available on a console.
*
* Results:
- * None.
+ * Always 0.
*
* Side effects:
- * Signals the main thread when input become available. May cause the
- * main thread to wake up by posting a message. May one line from the
- * console for each wait operation.
+ * Signals the main thread when input become available.
*
*----------------------------------------------------------------------
*/
@@ -1124,76 +1501,147 @@ static DWORD WINAPI
ConsoleReaderThread(
LPVOID arg)
{
- TclPipeThreadInfo *pipeTI = (TclPipeThreadInfo *)arg;
- ConsoleInfo *infoPtr = NULL; /* access info only after success init/wait */
- HANDLE *handle = NULL;
- ConsoleThreadInfo *threadInfo = NULL;
- int done = 0;
+ ConsoleHandleInfo *handleInfoPtr = (ConsoleHandleInfo *) arg;
+ ConsoleHandleInfo **iterator;
+ BOOL success;
+ char inputChars[200]; /* Temporary buffer */
+ RingSizeT inputLen = 0;
+ RingSizeT inputOffset = 0;
- while (!done) {
- /*
- * Wait for the main thread to signal before attempting to read.
- */
+ /*
+ * 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);
- if (!TclPipeThreadWaitForSignal(&pipeTI)) {
- /* exit */
+ while (1) {
+
+ if (handleInfoPtr->numRefs == 1) {
+ /* Sole reference. That's this thread. Exit since no one clients */
break;
}
- if (!infoPtr) {
- infoPtr = (ConsoleInfo *)pipeTI->clientData;
- handle = (HANDLE *)infoPtr->handle;
- threadInfo = &infoPtr->reader;
- }
+ 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 */
+ }
/*
- * Look for data on the console, but first ignore any events that are
- * not KEY_EVENTs.
+ * 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);
- if (ReadConsoleBytes(handle, infoPtr->buffer, CONSOLE_BUFFER_SIZE,
- (LPDWORD) &infoPtr->bytesRead) != FALSE) {
/*
- * Data was stored in the buffer.
+ * 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.
*/
-
- infoPtr->readFlags |= CONSOLE_BUFFERED;
- } else {
- DWORD err = GetLastError();
-
- if (err == (DWORD) EOF) {
- infoPtr->readFlags = CONSOLE_EOF;
+ 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);
+ }
}
- done = 1;
+ ReleaseSRWLockShared(&gConsoleLock);
+ AcquireSRWLockExclusive(&handleInfoPtr->lock);
+ continue; /* Restart loop */
}
/*
- * Signal the main thread by signalling the readable event and then
- * waking up the notifier thread.
+ * 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);
+ }
- SetEvent(threadInfo->readyEvent);
-
- /*
- * Alert the foreground thread. Note that we need to treat this like a
- * critical section so the foreground thread does not terminate this
- * thread while we are holding a mutex in the notifier code.
- */
+ /*
+ * 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);
- Tcl_MutexLock(&consoleMutex);
- if (infoPtr->threadId != NULL) {
- /*
- * TIP #218. When in flight ignore the event, no one will receive
- * it anyway.
- */
+ /* No need for relocking - no other thread should have access to it now */
+ RingBufferClear(&handleInfoPtr->buffer);
- Tcl_ThreadAlert(infoPtr->threadId);
- }
- Tcl_MutexUnlock(&consoleMutex);
+ 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
+ */
- /* Worker exit, so inform the main thread or free TI-structure (if owned) */
- TclPipeThreadExit(&pipeTI);
+ ckfree(handleInfoPtr);
return 0;
}
@@ -1210,89 +1658,262 @@ ConsoleReaderThread(
* Always returns 0.
*
* Side effects:
-
- * Signals the main thread when an output operation is completed. May
- * cause the main thread to wake up by posting a message.
+ * Signals the main thread when an output operation is completed.
*
*----------------------------------------------------------------------
*/
-
static DWORD WINAPI
-ConsoleWriterThread(
- LPVOID arg)
+ConsoleWriterThread(LPVOID arg)
{
- TclPipeThreadInfo *pipeTI = (TclPipeThreadInfo *)arg;
- ConsoleInfo *infoPtr = NULL; /* access info only after success init/wait */
- HANDLE *handle = NULL;
- ConsoleThreadInfo *threadInfo = NULL;
- DWORD count, toWrite;
- char *buf;
- int done = 0;
-
- while (!done) {
- /*
- * Wait for the main thread to signal before attempting to write.
- */
- if (!TclPipeThreadWaitForSignal(&pipeTI)) {
- /* exit */
- break;
- }
- if (!infoPtr) {
- infoPtr = (ConsoleInfo *)pipeTI->clientData;
- handle = (HANDLE *)infoPtr->handle;
- threadInfo = &infoPtr->writer;
- }
+ 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
+ */
- buf = infoPtr->writeBuf;
- toWrite = infoPtr->toWrite;
+ /* 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;
/*
- * Loop until all of the bytes are written or an error occurs.
+ * 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.
*/
-
- while (toWrite > 0) {
- if (WriteConsoleBytes(handle, buf, (DWORD) toWrite,
- &count) == FALSE) {
- infoPtr->writeError = GetLastError();
- done = 1;
+ 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;
}
- toWrite -= count;
- buf += count;
+ /* 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;
}
- /*
- * Signal the main thread by signalling the writable event and then
- * waking up the notifier thread.
- */
-
- SetEvent(threadInfo->readyEvent);
+ /* 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);
+ }
/*
- * Alert the foreground thread. Note that we need to treat this like a
- * critical section so the foreground thread does not terminate this
- * thread while we are holding a mutex in the notifier code.
+ * 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.
*/
-
- Tcl_MutexLock(&consoleMutex);
- if (infoPtr->threadId != NULL) {
+ /* Wake up any threads waiting synchronously. */
+ WakeConditionVariable(&handleInfoPtr->interpThreadCV);
+ AcquireSRWLockShared(&gConsoleLock); /* Shared-read lock */
+ consoleHandle = handleInfoPtr->console;
+ for (chanInfoPtr = gWatchingChannelList; chanInfoPtr;
+ chanInfoPtr = chanInfoPtr->nextWatchingChannelPtr) {
/*
- * TIP #218. When in flight ignore the event, no one will receive
- * it anyway.
+ * 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);
+ }
- Tcl_ThreadAlert(infoPtr->threadId);
+ /*
+ * 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;
}
- Tcl_MutexUnlock(&consoleMutex);
}
+ 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
+ */
- /* Worker exit, so inform the main thread or free TI-structure (if owned) */
- TclPipeThreadExit(&pipeTI);
+ 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 --
@@ -1309,33 +1930,30 @@ ConsoleWriterThread(
*
*----------------------------------------------------------------------
*/
-
Tcl_Channel
TclWinOpenConsoleChannel(
HANDLE handle,
char *channelName,
int permissions)
{
- char encoding[4 + TCL_INTEGER_SPACE];
- ConsoleInfo *infoPtr;
- DWORD modes;
+ ConsoleChannelInfo *chanInfoPtr;
+ ConsoleHandleInfo *handleInfoPtr;
- ConsoleInit();
-
- /*
- * See if a channel with this handle already exists.
- */
+ /* A console handle can either be input or output, not both */
+ if (permissions != TCL_READABLE && permissions != TCL_WRITABLE) {
+ return NULL;
+ }
- infoPtr = (ConsoleInfo *)ckalloc(sizeof(ConsoleInfo));
- memset(infoPtr, 0, sizeof(ConsoleInfo));
+ ConsoleInit();
- infoPtr->validMask = permissions;
- infoPtr->handle = handle;
- infoPtr->channel = (Tcl_Channel) NULL;
+ chanInfoPtr = (ConsoleChannelInfo *)ckalloc(sizeof(*chanInfoPtr));
+ memset(chanInfoPtr, 0, sizeof(*chanInfoPtr));
- wsprintfA(encoding, "cp%d", GetConsoleCP());
+ chanInfoPtr->permissions = permissions;
+ chanInfoPtr->handle = handle;
+ chanInfoPtr->channel = (Tcl_Channel) NULL;
- infoPtr->threadId = Tcl_GetCurrentThread();
+ chanInfoPtr->threadId = Tcl_GetCurrentThread();
/*
* Use the pointer for the name of the result channel. This keeps the
@@ -1343,10 +1961,7 @@ TclWinOpenConsoleChannel(
* for instance).
*/
- sprintf(channelName, "file%" TCL_Z_MODIFIER "x", (size_t) infoPtr);
-
- infoPtr->channel = Tcl_CreateChannel(&consoleChannelType, channelName,
- infoPtr, permissions);
+ sprintf(channelName, "file%" TCL_Z_MODIFIER "x", (size_t) chanInfoPtr);
if (permissions & TCL_READABLE) {
/*
@@ -1355,38 +1970,78 @@ TclWinOpenConsoleChannel(
* we only want to catch when complete lines are ready for reading.
*/
- infoPtr->flags |= CONSOLE_READ_OPS;
- GetConsoleMode(infoPtr->handle, &infoPtr->initMode);
- modes = infoPtr->initMode;
- modes &= ~(ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT);
- modes |= ENABLE_LINE_INPUT;
- SetConsoleMode(infoPtr->handle, modes);
-
- infoPtr->reader.readyEvent = CreateEventW(NULL, TRUE, TRUE, NULL);
- infoPtr->reader.thread = CreateThread(NULL, 256, ConsoleReaderThread,
- TclPipeThreadCreateTI(&infoPtr->reader.TI, infoPtr,
- infoPtr->reader.readyEvent), 0, NULL);
+ 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? */
}
- if (permissions & TCL_WRITABLE) {
+ /*
+ * 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 */
- infoPtr->writer.readyEvent = CreateEventW(NULL, TRUE, TRUE, NULL);
- infoPtr->writer.thread = CreateThread(NULL, 256, ConsoleWriterThread,
- TclPipeThreadCreateTI(&infoPtr->writer.TI, infoPtr,
- infoPtr->writer.readyEvent), 0, NULL);
- SetThreadPriority(infoPtr->writer.thread, THREAD_PRIORITY_HIGHEST);
+ 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, infoPtr->channel, "-translation", "auto");
- Tcl_SetChannelOption(NULL, infoPtr->channel, "-eofchar", "\032 {}");
- Tcl_SetChannelOption(NULL, infoPtr->channel, "-encoding", "utf-16");
- return infoPtr->channel;
+ 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;
}
/*
@@ -1410,33 +2065,16 @@ ConsoleThreadActionProc(
ClientData instanceData,
int action)
{
- ConsoleInfo *infoPtr = (ConsoleInfo *)instanceData;
-
- /*
- * We do not access firstConsolePtr in the thread structures. This is not
- * for all serials managed by the thread, but only those we are watching.
- * Removal of the filevent handlers before transfer thus takes care of
- * this structure.
- */
+ ConsoleChannelInfo *chanInfoPtr = (ConsoleChannelInfo *)instanceData;
- Tcl_MutexLock(&consoleMutex);
+ /* No need for any locks as no other thread will be writing to it */
if (action == TCL_CHANNEL_THREAD_INSERT) {
- /*
- * We can't copy the thread information from the channel when the
- * channel is created. At this time the channel back pointer has not
- * been set yet. However in that case the threadId has already been
- * set by TclpCreateCommandChannel itself, so the structure is still
- * good.
- */
-
- ConsoleInit();
- if (infoPtr->channel != NULL) {
- infoPtr->threadId = Tcl_GetChannelThread(infoPtr->channel);
- }
- } else {
- infoPtr->threadId = NULL;
+ ConsoleInit(); /* Needed to set up event source handlers for this thread */
+ chanInfoPtr->threadId = Tcl_GetCurrentThread();
+ }
+ else {
+ chanInfoPtr->threadId = NULL;
}
- Tcl_MutexUnlock(&consoleMutex);
}
/*
@@ -1456,7 +2094,6 @@ ConsoleThreadActionProc(
*
*----------------------------------------------------------------------
*/
-
static int
ConsoleSetOptionProc(
ClientData instanceData, /* File state. */
@@ -1464,7 +2101,7 @@ ConsoleSetOptionProc(
const char *optionName, /* Which option to set? */
const char *value) /* New value for option. */
{
- ConsoleInfo *infoPtr = (ConsoleInfo *)instanceData;
+ ConsoleChannelInfo *chanInfoPtr = (ConsoleChannelInfo *)instanceData;
int len = strlen(optionName);
int vlen = strlen(value);
@@ -1472,11 +2109,11 @@ ConsoleSetOptionProc(
* Option -inputmode normal|password|raw
*/
- if ((infoPtr->flags & CONSOLE_READ_OPS) && (len > 1) &&
+ if ((chanInfoPtr->flags & CONSOLE_READ_OPS) && (len > 1) &&
(strncmp(optionName, "-inputmode", len) == 0)) {
DWORD mode;
- if (GetConsoleMode(infoPtr->handle, &mode) == 0) {
+ if (GetConsoleMode(chanInfoPtr->handle, &mode) == 0) {
Tcl_WinConvertError(GetLastError());
if (interp != NULL) {
Tcl_SetObjResult(interp, Tcl_ObjPrintf(
@@ -1496,8 +2133,7 @@ ConsoleSetOptionProc(
/*
* Reset to the initial mode, whatever that is.
*/
-
- mode = infoPtr->initMode;
+ mode = chanInfoPtr->initMode;
} else {
if (interp) {
Tcl_SetObjResult(interp, Tcl_ObjPrintf(
@@ -1508,7 +2144,7 @@ ConsoleSetOptionProc(
}
return TCL_ERROR;
}
- if (SetConsoleMode(infoPtr->handle, mode) == 0) {
+ if (SetConsoleMode(chanInfoPtr->handle, mode) == 0) {
Tcl_WinConvertError(GetLastError());
if (interp != NULL) {
Tcl_SetObjResult(interp, Tcl_ObjPrintf(
@@ -1518,19 +2154,10 @@ ConsoleSetOptionProc(
return TCL_ERROR;
}
- /*
- * If we've changed the mode from default, schedule a reset later.
- */
-
- if (mode == infoPtr->initMode) {
- infoPtr->flags &= ~CONSOLE_RESET;
- } else {
- infoPtr->flags |= CONSOLE_RESET;
- }
return TCL_OK;
}
- if (infoPtr->flags & CONSOLE_READ_OPS) {
+ if (chanInfoPtr->flags & CONSOLE_READ_OPS) {
return Tcl_BadChannelOption(interp, optionName, "inputmode");
} else {
return Tcl_BadChannelOption(interp, optionName, "");
@@ -1562,7 +2189,7 @@ ConsoleGetOptionProc(
const char *optionName, /* Option to get. */
Tcl_DString *dsPtr) /* Where to store value(s). */
{
- ConsoleInfo *infoPtr = (ConsoleInfo *)instanceData;
+ ConsoleChannelInfo *chanInfoPtr = (ConsoleChannelInfo *)instanceData;
int valid = 0; /* Flag if valid option parsed. */
unsigned int len;
char buf[TCL_INTEGER_SPACE];
@@ -1580,7 +2207,7 @@ ConsoleGetOptionProc(
* represents what almost all scripts really want to know.
*/
- if (infoPtr->flags & CONSOLE_READ_OPS) {
+ if (chanInfoPtr->flags & CONSOLE_READ_OPS) {
if (len == 0) {
Tcl_DStringAppendElement(dsPtr, "-inputmode");
}
@@ -1588,7 +2215,7 @@ ConsoleGetOptionProc(
DWORD mode;
valid = 1;
- if (GetConsoleMode(infoPtr->handle, &mode) == 0) {
+ if (GetConsoleMode(chanInfoPtr->handle, &mode) == 0) {
Tcl_WinConvertError(GetLastError());
if (interp != NULL) {
Tcl_SetObjResult(interp, Tcl_ObjPrintf(
@@ -1619,7 +2246,7 @@ ConsoleGetOptionProc(
CONSOLE_SCREEN_BUFFER_INFO consoleInfo;
valid = 1;
- if (!GetConsoleScreenBufferInfo(infoPtr->handle, &consoleInfo)) {
+ if (!GetConsoleScreenBufferInfo(chanInfoPtr->handle, &consoleInfo)) {
Tcl_WinConvertError(GetLastError());
if (interp != NULL) {
Tcl_SetObjResult(interp, Tcl_ObjPrintf(
@@ -1639,7 +2266,7 @@ ConsoleGetOptionProc(
if (valid) {
return TCL_OK;
}
- if (infoPtr->flags & CONSOLE_READ_OPS) {
+ if (chanInfoPtr->flags & CONSOLE_READ_OPS) {
return Tcl_BadChannelOption(interp, optionName, "inputmode winsize");
} else {
return Tcl_BadChannelOption(interp, optionName, "");