Windows console I/O overhaul
[sbcl.git] / src / runtime / win32-os.c
index f27f5ed..b9b1ebe 100644 (file)
@@ -54,6 +54,7 @@
 #include <float.h>
 
 #include <excpt.h>
+#include <errno.h>
 
 #include "validate.h"
 #include "thread.h"
@@ -377,6 +378,28 @@ void os_preinit()
 
 int os_number_of_processors = 1;
 
+BOOL WINAPI CancelIoEx(HANDLE handle, LPOVERLAPPED overlapped);
+typeof(CancelIoEx) *ptr_CancelIoEx;
+BOOL WINAPI CancelSynchronousIo(HANDLE threadHandle);
+typeof(CancelSynchronousIo) *ptr_CancelSynchronousIo;
+
+#define RESOLVE(hmodule,fn)                     \
+    do {                                        \
+        ptr_##fn = (typeof(ptr_##fn))           \
+            GetProcAddress(hmodule,#fn);        \
+    } while (0)
+
+static void resolve_optional_imports()
+{
+    HMODULE kernel32 = GetModuleHandleA("kernel32");
+    if (kernel32) {
+        RESOLVE(kernel32,CancelIoEx);
+        RESOLVE(kernel32,CancelSynchronousIo);
+    }
+}
+
+#undef RESOLVE
+
 void os_init(char *argv[], char *envp[])
 {
     SYSTEM_INFO system_info;
@@ -389,6 +412,8 @@ void os_init(char *argv[], char *envp[])
     os_number_of_processors = system_info.dwNumberOfProcessors;
 
     base_seh_frame = get_seh_frame();
+
+    resolve_optional_imports();
 }
 
 static inline boolean local_thread_stack_address_p(os_vm_address_t address)
@@ -1102,6 +1127,387 @@ console_handle_p(HANDLE handle)
         ((((int)(intptr_t)handle)&3)==3);
 }
 
+/* Atomically mark current thread as (probably) doing synchronous I/O
+ * on handle, if no cancellation is requested yet (and return TRUE),
+ * otherwise clear thread's I/O cancellation flag and return false.
+ */
+static
+boolean io_begin_interruptible(HANDLE handle)
+{
+    /* No point in doing it unless OS supports cancellation from other
+     * threads */
+    if (!ptr_CancelIoEx)
+        return 1;
+
+    if (!__sync_bool_compare_and_swap(&this_thread->synchronous_io_handle_and_flag,
+                                      0, handle)) {
+        ResetEvent(this_thread->private_events.events[0]);
+        this_thread->synchronous_io_handle_and_flag = 0;
+        return 0;
+    }
+    return 1;
+}
+
+/* Unmark current thread as (probably) doing synchronous I/O; if an
+ * I/O cancellation was requested, postpone it until next
+ * io_begin_interruptible */
+static void
+io_end_interruptible(HANDLE handle)
+{
+    if (!ptr_CancelIoEx)
+        return;
+    __sync_bool_compare_and_swap(&this_thread->synchronous_io_handle_and_flag,
+                                 handle, 0);
+}
+
+/* Documented limit for ReadConsole/WriteConsole is 64K bytes.
+   Real limit observed on W2K-SP3 is somewhere in between 32KiB and 64Kib...
+*/
+#define MAX_CONSOLE_TCHARS 16384
+
+int
+win32_write_unicode_console(HANDLE handle, void * buf, int count)
+{
+    DWORD written = 0;
+    DWORD nchars;
+    BOOL result;
+    nchars = count>>1;
+    if (nchars>MAX_CONSOLE_TCHARS) nchars = MAX_CONSOLE_TCHARS;
+
+    if (!io_begin_interruptible(handle)) {
+        errno = EINTR;
+        return -1;
+    }
+    result = WriteConsoleW(handle,buf,nchars,&written,NULL);
+    io_end_interruptible(handle);
+
+    if (result) {
+        if (!written) {
+            errno = EINTR;
+            return -1;
+        } else {
+            return 2*written;
+        }
+    } else {
+        DWORD err = GetLastError();
+        odxprint(io,"WriteConsole fails => %u\n", err);
+        errno = (err==ERROR_OPERATION_ABORTED ? EINTR : EIO);
+        return -1;
+    }
+}
+
+/*
+ * (AK writes:)
+ *
+ * It may be unobvious, but (probably) the most straightforward way of
+ * providing some sane CL:LISTEN semantics for line-mode console
+ * channel requires _dedicated input thread_.
+ *
+ * LISTEN should return true iff the next (READ-CHAR) won't have to
+ * wait. As our console may be shared with another process, entirely
+ * out of our control, looking at the events in PeekConsoleEvent
+ * result (and searching for #\Return) doesn't cut it.
+ *
+ * We decided that console input thread must do something smarter than
+ * a bare loop of continuous ReadConsoleW(). On Unix, user experience
+ * with the terminal is entirely unaffected by the fact that some
+ * process does (or doesn't) call read(); the situation on MS Windows
+ * is different.
+ *
+ * Echo output and line editing present on MS Windows while some
+ * process is waiting in ReadConsole(); otherwise all input events are
+ * buffered. If our thread were calling ReadConsole() all the time, it
+ * would feel like Unix cooked mode.
+ *
+ * But we don't write a Unix emulator here, even if it sometimes feels
+ * like that; therefore preserving this aspect of console I/O seems a
+ * good thing to us.
+ *
+ * LISTEN itself becomes trivial with dedicated input thread, but the
+ * goal stated above -- provide `native' user experience with blocked
+ * console -- don't play well with this trivial implementation.
+ *
+ * What's currently implemented is a compromise, looking as something
+ * in between Unix cooked mode and Win32 line mode.
+ *
+ * 1. As long as no console I/O function is called (incl. CL:LISTEN),
+ * console looks `blocked': no echo, no line editing.
+ *
+ * 2. (READ-CHAR), (READ-SEQUENCE) and other functions doing real
+ * input result in the ReadConsole request (in a dedicated thread);
+ *
+ * 3. Once ReadConsole is called, it is not cancelled in the
+ * middle. In line mode, it returns when <Enter> key is hit (or
+ * something like that happens). Therefore, if line editing and echo
+ * output had a chance to happen, console won't look `blocked' until
+ * the line is entered (even if line input was triggered by
+ * (READ-CHAR)).
+ *
+ * 4. LISTEN may request ReadConsole too (if no other thread is
+ * reading the console and no data are queued). It's the only case
+ * when the console becomes `unblocked' without any actual input
+ * requested by Lisp code.  LISTEN check if there is at least one
+ * input event in PeekConsole queue; unless there is such an event,
+ * ReadConsole is not triggered by LISTEN.
+ *
+ * 5. Console-reading Lisp thread now may be interrupted immediately;
+ * ReadConsole call itself, however, continues until the line is
+ * entered.
+ */
+
+struct {
+    WCHAR buffer[MAX_CONSOLE_TCHARS];
+    DWORD head, tail;
+    pthread_mutex_t lock;
+    pthread_cond_t cond_has_data;
+    pthread_cond_t cond_has_client;
+    pthread_t thread;
+    boolean initialized;
+    HANDLE handle;
+    boolean in_progress;
+} ttyinput = {.lock = PTHREAD_MUTEX_INITIALIZER};
+
+static void*
+tty_read_line_server()
+{
+    pthread_mutex_lock(&ttyinput.lock);
+    while (ttyinput.handle) {
+        DWORD nchars;
+        BOOL ok;
+
+        while (!ttyinput.in_progress)
+            pthread_cond_wait(&ttyinput.cond_has_client,&ttyinput.lock);
+
+        pthread_mutex_unlock(&ttyinput.lock);
+
+        ok = ReadConsoleW(ttyinput.handle,
+                          &ttyinput.buffer[ttyinput.tail],
+                          MAX_CONSOLE_TCHARS-ttyinput.tail,
+                          &nchars,NULL);
+
+        pthread_mutex_lock(&ttyinput.lock);
+
+        if (ok) {
+            ttyinput.tail += nchars;
+            pthread_cond_broadcast(&ttyinput.cond_has_data);
+        }
+        ttyinput.in_progress = 0;
+    }
+    pthread_mutex_unlock(&ttyinput.lock);
+    return NULL;
+}
+
+static boolean
+tty_maybe_initialize_unlocked(HANDLE handle)
+{
+    if (!ttyinput.initialized) {
+        if (!DuplicateHandle(GetCurrentProcess(),handle,
+                             GetCurrentProcess(),&ttyinput.handle,
+                             0,FALSE,DUPLICATE_SAME_ACCESS)) {
+            return 0;
+        }
+        pthread_cond_init(&ttyinput.cond_has_data,NULL);
+        pthread_cond_init(&ttyinput.cond_has_client,NULL);
+        pthread_create(&ttyinput.thread,NULL,tty_read_line_server,NULL);
+        ttyinput.initialized = 1;
+    }
+    return 1;
+}
+
+boolean
+win32_tty_listen(HANDLE handle)
+{
+    boolean result = 0;
+    INPUT_RECORD ir;
+    DWORD nevents;
+    pthread_mutex_lock(&ttyinput.lock);
+    if (!tty_maybe_initialize_unlocked(handle))
+        result = 0;
+
+    if (ttyinput.in_progress) {
+        result = 0;
+    } else {
+        if (ttyinput.head != ttyinput.tail) {
+            result = 1;
+        } else {
+            if (PeekConsoleInput(ttyinput.handle,&ir,1,&nevents) && nevents) {
+                ttyinput.in_progress = 1;
+                pthread_cond_broadcast(&ttyinput.cond_has_client);
+            }
+        }
+    }
+    pthread_mutex_unlock(&ttyinput.lock);
+    return result;
+}
+
+static int
+tty_read_line_client(HANDLE handle, void* buf, int count)
+{
+    int result = 0;
+    int nchars = count / sizeof(WCHAR);
+    sigset_t pendset;
+
+    if (!nchars)
+        return 0;
+    if (nchars>MAX_CONSOLE_TCHARS)
+        nchars=MAX_CONSOLE_TCHARS;
+
+    count = nchars*sizeof(WCHAR);
+
+    pthread_mutex_lock(&ttyinput.lock);
+
+    if (!tty_maybe_initialize_unlocked(handle)) {
+        result = -1;
+        errno = EIO;
+        goto unlock;
+    }
+
+    while (!result) {
+        while (ttyinput.head == ttyinput.tail) {
+            if (!io_begin_interruptible(ttyinput.handle)) {
+                ttyinput.in_progress = 0;
+                result = -1;
+                errno = EINTR;
+                goto unlock;
+            } else {
+                if (!ttyinput.in_progress) {
+                    /* We are to wait */
+                    ttyinput.in_progress=1;
+                    /* wake console reader */
+                    pthread_cond_broadcast(&ttyinput.cond_has_client);
+                }
+                pthread_cond_wait(&ttyinput.cond_has_data, &ttyinput.lock);
+                io_end_interruptible(ttyinput.handle);
+            }
+        }
+        result = sizeof(WCHAR)*(ttyinput.tail-ttyinput.head);
+        if (result > count) {
+            result = count;
+        }
+        if (result) {
+            if (result > 0) {
+                DWORD nch,offset = 0;
+                LPWSTR ubuf = buf;
+
+                memcpy(buf,&ttyinput.buffer[ttyinput.head],count);
+                ttyinput.head += (result / sizeof(WCHAR));
+                if (ttyinput.head == ttyinput.tail)
+                    ttyinput.head = ttyinput.tail = 0;
+
+                for (nch=0;nch<result/sizeof(WCHAR);++nch) {
+                    if (ubuf[nch]==13) {
+                        ++offset;
+                    } else {
+                        ubuf[nch-offset]=ubuf[nch];
+                    }
+                }
+                result-=offset*sizeof(WCHAR);
+
+            }
+        } else {
+            result = -1;
+            ttyinput.head = ttyinput.tail = 0;
+            errno = EIO;
+        }
+    }
+unlock:
+    pthread_mutex_unlock(&ttyinput.lock);
+    return result;
+}
+
+int
+win32_read_unicode_console(HANDLE handle, void* buf, int count)
+{
+
+    int result;
+    result = tty_read_line_client(handle,buf,count);
+    return result;
+}
+
+boolean
+win32_maybe_interrupt_io(void* thread)
+{
+    struct thread *th = thread;
+    boolean done = 0;
+    /* Kludge. (?)
+     *
+     * ICBW about all of this.  But it seems to me that this procedure is
+     * a race condition.  In theory.  One that is hard produce (I can't
+     * come up with a test case that exploits it), and might only be a bug
+     * if users are doing weird things with I/O, possibly from FFI.  But a
+     * race is a race, so shouldn't this function and io_end_interruptible
+     * cooperate more?
+     *
+     * Here's my thinking:
+     *
+     * A.. <interruptee thread>
+     *     ... stuffs its handle into its structure.
+     * B.. <interrupter thread>
+     *     ... calls us to wake the thread, finds the handle.
+     *     But just before we actually call CancelSynchronousIo/CancelIoEx,
+     *     something weird happens in the scheduler and the system is
+     *     so extremely busy that the interrupter doesn't get scheduled
+     *     for a while, giving the interruptee lots of time to continue.
+     * A.. Didn't actually have to block, calls io_end_interruptible (in
+     *     which the handle flag already invalid, but it doesn't care
+     *     about that and still continues).
+     *     ... Proceeds to do unrelated I/O, e.g. goes into FFI code
+     *     (possible, because the CSP page hasn't been armed yet), which
+     *     does I/O from a C library, completely unrelated to SBCL's
+     *     routines.
+     * B.. The scheduler gives us time for the interrupter again.
+     *     We call CancelSynchronousIo/CancelIoEx.
+     * A.. Interruptee gets an expected error in unrelated I/O during FFI.
+     *     Interruptee's C code is unhappy and dies.
+     *
+     * Note that CancelSynchronousIo and CancelIoEx have a rather different
+     * effect here.  In the normal (CancelIoEx) case, we only ever kill
+     * I/O on the file handle in question.  I think we could ask users
+     * to please not both use Lisp streams (unix-read/write) _and_ FFI code
+     * on the same file handle in quick succession.
+     *
+     * CancelSynchronousIo seems more dangerous though.  Here we interrupt
+     * I/O on any other handle, even ones we're not actually responsible for,
+     * because this functions deals with the thread handle, not the file
+     * handle.
+     *
+     * Options:
+     *  - Use mutexes.  Somewhere, somehow.  Presumably one mutex per
+     *    target thread, acquired around win32_maybe_interrupt_io and
+     *    io_end_interruptible.  (That's one mutex use per I/O
+     *    operation, but I can't imagine that compared to our FFI overhead
+     *    that's much of a problem.)
+     *  - In io_end_interruptible, detect that the flag has been
+     *    invalidated, and in that case, do something clever (what?) to
+     *    wait for the imminent gc_stop_the_world, which implicitly tells
+     *    us that win32_maybe_interrupt_io must have exited.  Except if
+     *    some _third_ thread is also beginning to call interrupt-thread
+     *    and wake_thread at the same time...?
+     *  - Revert the whole CancelSynchronousIo business after all.
+     *  - I'm wrong and everything is OK already.
+     */
+    if (ptr_CancelIoEx) {
+        HANDLE h = (HANDLE)
+            InterlockedExchangePointer((volatile LPVOID *)
+                                       &th->synchronous_io_handle_and_flag,
+                                       (LPVOID)INVALID_HANDLE_VALUE);
+        if (h && (h!=INVALID_HANDLE_VALUE)) {
+            if (console_handle_p(h)) {
+                pthread_mutex_lock(&ttyinput.lock);
+                pthread_cond_broadcast(&ttyinput.cond_has_data);
+                pthread_mutex_unlock(&ttyinput.lock);
+            }
+            if (ptr_CancelSynchronousIo) {
+                pthread_mutex_lock(&th->os_thread->fiber_lock);
+                done = ptr_CancelSynchronousIo(th->os_thread->fiber_group->handle);
+                pthread_mutex_unlock(&th->os_thread->fiber_lock);
+            }
+            return (!!done)|(!!ptr_CancelIoEx(h,NULL));
+        }
+    }
+    return 0;
+}
+
 static const LARGE_INTEGER zero_large_offset = {.QuadPart = 0LL};
 
 int
@@ -1118,7 +1524,7 @@ win32_unix_write(FDTYPE fd, void * buf, int count)
 
     handle =(HANDLE)maybe_get_osfhandle(fd);
     if (console_handle_p(handle))
-        return write(fd, buf, count);
+        return win32_write_unicode_console(handle,buf,count);
 
     overlapped.hEvent = self->private_events.events[0];
     seekable = SetFilePointerEx(handle,
@@ -1132,12 +1538,23 @@ win32_unix_write(FDTYPE fd, void * buf, int count)
         overlapped.Offset = 0;
         overlapped.OffsetHigh = 0;
     }
+    if (!io_begin_interruptible(handle)) {
+        errno = EINTR;
+        return -1;
+    }
     ok = WriteFile(handle, buf, count, &written_bytes, &overlapped);
+    io_end_interruptible(handle);
 
     if (ok) {
         goto done_something;
     } else {
-        if (GetLastError()!=ERROR_IO_PENDING) {
+        DWORD errorCode = GetLastError();
+        if (errorCode==ERROR_OPERATION_ABORTED) {
+            GetOverlappedResult(handle,&overlapped,&written_bytes,FALSE);
+            errno = EINTR;
+            return -1;
+        }
+        if (errorCode!=ERROR_IO_PENDING) {
             errno = EIO;
             return -1;
         } else {
@@ -1183,13 +1600,9 @@ win32_unix_read(FDTYPE fd, void * buf, int count)
 
     handle = (HANDLE)maybe_get_osfhandle(fd);
 
-    if (console_handle_p(handle)) {
-        /* 1. Console is a singleton.
-           2. The only way to cancel console handle I/O is to close it.
-        */
     if (console_handle_p(handle))
-        return read(fd, buf, count);
-    }
+        return win32_read_unicode_console(handle,buf,count);
+
     overlapped.hEvent = self->private_events.events[0];
     /* If it has a position, we won't try overlapped */
     seekable = SetFilePointerEx(handle,
@@ -1203,7 +1616,12 @@ win32_unix_read(FDTYPE fd, void * buf, int count)
         overlapped.Offset = 0;
         overlapped.OffsetHigh = 0;
     }
+    if (!io_begin_interruptible(handle)) {
+        errno = EINTR;
+        return -1;
+    }
     ok = ReadFile(handle,buf,count,&read_bytes, &overlapped);
+    io_end_interruptible(handle);
     if (ok) {
         /* immediately */
         goto done_something;
@@ -1215,6 +1633,11 @@ win32_unix_read(FDTYPE fd, void * buf, int count)
             read_bytes = 0;
             goto done_something;
         }
+        if (errorCode==ERROR_OPERATION_ABORTED) {
+            GetOverlappedResult(handle,&overlapped,&read_bytes,FALSE);
+            errno = EINTR;
+            return -1;
+        }
         if (errorCode!=ERROR_IO_PENDING) {
             /* is it some _real_ error? */
             errno = EIO;
@@ -1304,6 +1727,7 @@ void scratch(void)
     UnmapViewOfFile(0);
     FlushViewOfFile(0,0);
     SetFilePointerEx(0, la, 0, 0);
+    DuplicateHandle(0, 0, 0, 0, 0, 0, 0);
     #ifndef LISP_FEATURE_SB_UNICODE
       CreateDirectoryA(0,0);
       CreateFileMappingA(0,0,0,0,0,0);