diff options
Diffstat (limited to 'libraries/base/cbits/inputReady.c')
-rw-r--r-- | libraries/base/cbits/inputReady.c | 518 |
1 files changed, 391 insertions, 127 deletions
diff --git a/libraries/base/cbits/inputReady.c b/libraries/base/cbits/inputReady.c index 0a84668689..cfbced914f 100644 --- a/libraries/base/cbits/inputReady.c +++ b/libraries/base/cbits/inputReady.c @@ -4,76 +4,265 @@ * hWaitForInput Runtime Support */ +/* FD_SETSIZE defaults to 64 on Windows, which makes even the most basic + * programs break that use select() on a socket FD. + * Thus we raise it here (before any #include of network-related headers) + * to 1024 so that at least those programs would work that would work on + * Linux if that used select() (luckily it uses poll() by now). + * See https://ghc.haskell.org/trac/ghc/ticket/13497#comment:23 + * The real solution would be to remove all uses of select() + * on Windows, too, and use IO Completion Ports instead. + * Note that on Windows, one can simply define FD_SETSIZE to the desired + * size before including Winsock2.h, as described here: + * https://msdn.microsoft.com/en-us/library/windows/desktop/ms740141(v=vs.85).aspx + */ +#if defined(_WIN32) +#define FD_SETSIZE 1024 +#endif + /* select and supporting types is not Posix */ /* #include "PosixSource.h" */ +#include <limits.h> +#include <stdbool.h> #include "HsBase.h" +#include "Rts.h" #if !defined(_WIN32) #include <poll.h> -#include <sys/time.h> +#endif + +/* + * Returns a timeout suitable to be passed into poll(). + * + * If `remaining` contains a fractional milliseconds part that cannot be passed + * to poll(), this function will return the next larger value that can, so + * that the timeout passed to poll() would always be `>= remaining`. + * + * If `infinite`, `remaining` is ignored. + */ +static inline +int +compute_poll_timeout(bool infinite, Time remaining) +{ + if (infinite) return -1; + + if (remaining < 0) return 0; + + if (remaining > MSToTime(INT_MAX)) return INT_MAX; + + int remaining_ms = TimeToMS(remaining); + + if (remaining != MSToTime(remaining_ms)) return remaining_ms + 1; + + return remaining_ms; +} + +#if defined(_WIN32) +/* + * Returns a timeout suitable to be passed into select() on Windows. + * + * The given `remaining_tv` serves as a storage for the timeout + * when needed, but callers should use the returned value instead + * as it will not be filled in all cases. + * + * If `infinite`, `remaining` is ignored and `remaining_tv` not touched + * (and may be passed as NULL in that case). + */ +static inline +struct timeval * +compute_windows_select_timeout(bool infinite, Time remaining, + /* out */ struct timeval * remaining_tv) +{ + if (infinite) { + return NULL; + } + + ASSERT(remaining_tv); + + if (remaining < 0) { + remaining_tv->tv_sec = 0; + remaining_tv->tv_usec = 0; + } else if (remaining > MSToTime(LONG_MAX)) { + remaining_tv->tv_sec = LONG_MAX; + remaining_tv->tv_usec = LONG_MAX; + } else { + remaining_tv->tv_sec = TimeToMS(remaining) / 1000; + remaining_tv->tv_usec = TimeToUS(remaining) % 1000000; + } + + return remaining_tv; +} + +/* + * Returns a timeout suitable to be passed into WaitForSingleObject() on + * Windows. + * + * If `remaining` contains a fractional milliseconds part that cannot be passed + * to WaitForSingleObject(), this function will return the next larger value + * that can, so that the timeout passed to WaitForSingleObject() would + * always be `>= remaining`. + * + * If `infinite`, `remaining` is ignored. + */ +static inline +DWORD +compute_WaitForSingleObject_timeout(bool infinite, Time remaining) +{ + // WaitForSingleObject() has the fascinating delicacy behaviour + // that it waits indefinitely if the `DWORD dwMilliseconds` + // is set to 0xFFFFFFFF (the maximum DWORD value), which is + // 4294967295 seconds == ~49.71 days + // (the Windows API calls this constant INFINITE...). + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms687032(v=vs.85).aspx + // + // We ensure that if accidentally `remaining == 4294967295`, it does + // NOT wait forever, by never passing that value to + // WaitForSingleObject() (so, never returning it from this function), + // unless `infinite`. + + if (infinite) return INFINITE; + + if (remaining < 0) return 0; + + if (remaining >= MSToTime(INFINITE)) return INFINITE - 1; + + DWORD remaining_ms = TimeToMS(remaining); + + if (remaining != MSToTime(remaining_ms)) return remaining_ms + 1; + + return remaining_ms; +} #endif /* * inputReady(fd) checks to see whether input is available on the file * descriptor 'fd' within 'msecs' milliseconds (or indefinitely if 'msecs' is * negative). "Input is available" is defined as 'can I safely read at least a - * *character* from this file object without blocking?' + * *character* from this file object without blocking?' (this does not work + * reliably on Linux when the fd is a not-O_NONBLOCK socket, so if you pass + * socket fds to this function, ensure they have O_NONBLOCK; + * see `man 2 poll` and `man 2 select`, and + * https://ghc.haskell.org/trac/ghc/ticket/13497#comment:26). + * + * This function blocks until either `msecs` have passed, or input is + * available. + * + * Returns: + * 1 => Input ready, 0 => not ready, -1 => error + * On error, sets `errno`. */ int -fdReady(int fd, int write, int msecs, int isSock) +fdReady(int fd, bool write, int64_t msecs, bool isSock) { + bool infinite = msecs < 0; -#if !defined(_WIN32) - struct pollfd fds[1]; - - // if we need to track the then record the current time in case we are + // if we need to track the time then record the end time in case we are // interrupted. - struct timeval tv0; + Time endTime = 0; if (msecs > 0) { - if (gettimeofday(&tv0, NULL) != 0) { - fprintf(stderr, "fdReady: gettimeofday failed: %s\n", - strerror(errno)); - abort(); - } + endTime = getProcessElapsedTime() + MSToTime(msecs); } + // Invariant of all code below: + // If `infinite`, then `remaining` and `endTime` are never used. + + Time remaining = MSToTime(msecs); + + // Note [Guaranteed syscall time spent] + // + // The implementation ensures that if fdReady() is called with N `msecs`, + // it will not return before an FD-polling syscall *returns* + // with `endTime` having passed. + // + // Consider the following scenario: + // + // 1 int ready = poll(..., msecs); + // 2 if (EINTR happened) { + // 3 Time now = getProcessElapsedTime(); + // 4 if (now >= endTime) return 0; + // 5 remaining = endTime - now; + // 6 } + // + // If `msecs` is 5 seconds, but in line 1 poll() returns with EINTR after + // only 10 ms due to a signal, and if at line 2 the machine starts + // swapping for 10 seconds, then line 4 will return that there's no + // data ready, even though by now there may be data ready now, and we have + // not actually checked after up to `msecs` = 5 seconds whether there's + // data ready as promised. + // + // Why is this important? + // Assume you call the pizza man to bring you a pizza. + // You arrange that you won't pay if he doesn't ring your doorbell + // in under 10 minutes delivery time. + // At 9:58 fdReady() gets woken by EINTR and then your computer swaps + // for 3 seconds. + // At 9:59 the pizza man rings. + // At 10:01 fdReady() will incorrectly tell you that the pizza man hasn't + // rung within 10 minutes, when in fact he has. + // + // If the pizza man is some watchdog service or dead man's switch program, + // this is problematic. + // + // To avoid it, we ensure that in the timeline diagram: + // + // endTime + // | + // time ----+----------+-------+----> + // | | + // syscall starts syscall returns + // + // the "syscall returns" event is always >= the "endTime" time. + // + // In the code this means that we never check whether to `return 0` + // after a `Time now = getProcessElapsedTime();`, and instead always + // let the branch marked [we waited the full msecs] handle that case. + +#if !defined(_WIN32) + struct pollfd fds[1]; + fds[0].fd = fd; fds[0].events = write ? POLLOUT : POLLIN; fds[0].revents = 0; - int res; - while ((res = poll(fds, 1, msecs)) < 0) { - if (errno == EINTR) { - if (msecs > 0) { - struct timeval tv; - if (gettimeofday(&tv, NULL) != 0) { - fprintf(stderr, "fdReady: gettimeofday failed: %s\n", - strerror(errno)); - abort(); - } + // The code below tries to make as few syscalls as possible; + // in particular, it eschews getProcessElapsedTime() calls + // when `infinite` or `msecs == 0`. - int elapsed = 1000 * (tv.tv_sec - tv0.tv_sec) - + (tv.tv_usec - tv0.tv_usec) / 1000; - msecs -= elapsed; - if (msecs <= 0) return 0; - tv0 = tv; - } - } else { - return (-1); + // We need to wait in a loop because poll() accepts `int` but `msecs` is + // `int64_t`, and because signals can interrupt it. + + while (true) { + int res = poll(fds, 1, compute_poll_timeout(infinite, remaining)); + + if (res < 0 && errno != EINTR) + return (-1); // real error; errno is preserved + + if (res > 0) + return 1; // FD has new data + + if (res == 0 && !infinite && remaining <= MSToTime(INT_MAX)) + return 0; // FD has no new data and [we waited the full msecs] + + // Non-exit cases + CHECK( ( res < 0 && errno == EINTR ) || // EINTR happened + // need to wait more + ( res == 0 && (infinite || + remaining > MSToTime(INT_MAX)) ) ); + + if (!infinite) { + Time now = getProcessElapsedTime(); + remaining = endTime - now; } } - // res is the number of FDs with events - return (res > 0); - #else if (isSock) { - int maxfd, ready; + int maxfd; fd_set rfd, wfd; - struct timeval tv; + struct timeval remaining_tv; + if ((fd >= (int)FD_SETSIZE) || (fd < 0)) { - fprintf(stderr, "fdReady: fd is too big"); - abort(); + barf("fdReady: fd is too big: %d but FD_SETSIZE is %d", fd, (int)FD_SETSIZE); } FD_ZERO(&rfd); FD_ZERO(&wfd); @@ -87,52 +276,110 @@ fdReady(int fd, int write, int msecs, int isSock) * (maxfd-1) */ maxfd = fd + 1; - tv.tv_sec = msecs / 1000; - tv.tv_usec = (msecs % 1000) * 1000; - while ((ready = select(maxfd, &rfd, &wfd, NULL, &tv)) < 0 ) { - if (errno != EINTR ) { - return -1; + // We need to wait in a loop because the `timeval` `tv_*` members + // passed into select() accept are `long` (which is 32 bits on 32-bit + // and 64-bit Windows), but `msecs` is `int64_t`, and because signals + // can interrupt it. + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms740560(v=vs.85).aspx + // https://stackoverflow.com/questions/384502/what-is-the-bit-size-of-long-on-64-bit-windows#384672 + + while (true) { + int res = select(maxfd, &rfd, &wfd, NULL, + compute_windows_select_timeout(infinite, remaining, + &remaining_tv)); + + if (res < 0 && errno != EINTR) + return (-1); // real error; errno is preserved + + if (res > 0) + return 1; // FD has new data + + if (res == 0 && !infinite && remaining <= MSToTime(INT_MAX)) + return 0; // FD has no new data and [we waited the full msecs] + + // Non-exit cases + CHECK( ( res < 0 && errno == EINTR ) || // EINTR happened + // need to wait more + ( res == 0 && (infinite || + remaining > MSToTime(INT_MAX)) ) ); + + if (!infinite) { + Time now = getProcessElapsedTime(); + remaining = endTime - now; } } - /* 1 => Input ready, 0 => not ready, -1 => error */ - return (ready); - } - else { + } else { DWORD rc; HANDLE hFile = (HANDLE)_get_osfhandle(fd); - DWORD avail; + DWORD avail = 0; switch (GetFileType(hFile)) { - case FILE_TYPE_CHAR: - { - INPUT_RECORD buf[1]; - DWORD count; + case FILE_TYPE_CHAR: + { + INPUT_RECORD buf[1]; + DWORD count; - // nightmare. A Console Handle will appear to be ready - // (WaitForSingleObject() returned WAIT_OBJECT_0) when - // it has events in its input buffer, but these events might - // not be keyboard events, so when we read from the Handle the - // read() will block. So here we try to discard non-keyboard - // events from a console handle's input buffer and then try - // the WaitForSingleObject() again. + // nightmare. A Console Handle will appear to be ready + // (WaitForSingleObject() returned WAIT_OBJECT_0) when + // it has events in its input buffer, but these events might + // not be keyboard events, so when we read from the Handle the + // read() will block. So here we try to discard non-keyboard + // events from a console handle's input buffer and then try + // the WaitForSingleObject() again. - while (1) // keep trying until we find a real key event + while (1) // keep trying until we find a real key event { - rc = WaitForSingleObject( hFile, msecs ); + rc = WaitForSingleObject( + hFile, + compute_WaitForSingleObject_timeout(infinite, remaining)); switch (rc) { - case WAIT_TIMEOUT: return 0; - case WAIT_OBJECT_0: break; - default: /* WAIT_FAILED */ maperrno(); return -1; + case WAIT_TIMEOUT: + // We need to use < here because if remaining + // was INFINITE, we'll have waited for + // `INFINITE - 1` as per + // compute_WaitForSingleObject_timeout(), + // so that's 1 ms too little. Wait again then. + if (!infinite && remaining < MSToTime(INFINITE)) + return 0; // real complete or [we waited the full msecs] + goto waitAgain; + case WAIT_OBJECT_0: break; + default: /* WAIT_FAILED */ maperrno(); return -1; } while (1) // discard non-key events + { + BOOL success = PeekConsoleInput(hFile, buf, 1, &count); + // printf("peek, rc=%d, count=%d, type=%d\n", rc, count, buf[0].EventType); + if (!success) { + rc = GetLastError(); + if (rc == ERROR_INVALID_HANDLE || rc == ERROR_INVALID_FUNCTION) { + return 1; + } else { + maperrno(); + return -1; + } + } + + if (count == 0) break; // no more events => wait again + + // discard console events that are not "key down", because + // these will also be discarded by ReadFile(). + if (buf[0].EventType == KEY_EVENT && + buf[0].Event.KeyEvent.bKeyDown && + buf[0].Event.KeyEvent.uChar.AsciiChar != '\0') + { + // it's a proper keypress: + return 1; + } + else { - rc = PeekConsoleInput(hFile, buf, 1, &count); - // printf("peek, rc=%d, count=%d, type=%d\n", rc, count, buf[0].EventType); - if (rc == 0) { + // it's a non-key event, a key up event, or a + // non-character key (e.g. shift). discard it. + BOOL success = ReadConsoleInput(hFile, buf, 1, &count); + if (!success) { rc = GetLastError(); if (rc == ERROR_INVALID_HANDLE || rc == ERROR_INVALID_FUNCTION) { return 1; @@ -141,75 +388,92 @@ fdReady(int fd, int write, int msecs, int isSock) return -1; } } + } + } - if (count == 0) break; // no more events => wait again + Time now; + waitAgain: + now = getProcessElapsedTime(); + remaining = endTime - now; + } + } - // discard console events that are not "key down", because - // these will also be discarded by ReadFile(). - if (buf[0].EventType == KEY_EVENT && - buf[0].Event.KeyEvent.bKeyDown && - buf[0].Event.KeyEvent.uChar.AsciiChar != '\0') - { - // it's a proper keypress: - return 1; - } - else - { - // it's a non-key event, a key up event, or a - // non-character key (e.g. shift). discard it. - rc = ReadConsoleInput(hFile, buf, 1, &count); - if (rc == 0) { - rc = GetLastError(); - if (rc == ERROR_INVALID_HANDLE || rc == ERROR_INVALID_FUNCTION) { - return 1; - } else { - maperrno(); - return -1; - } - } - } + case FILE_TYPE_DISK: + // assume that disk files are always ready: + return 1; + + case FILE_TYPE_PIPE: { + // WaitForMultipleObjects() doesn't work for pipes (it + // always returns WAIT_OBJECT_0 even when no data is + // available). If the HANDLE is a pipe, therefore, we try + // PeekNamedPipe(): + // + // PeekNamedPipe() does not block, so if it returns that + // there is no new data, we have to sleep and try again. + + // Because PeekNamedPipe() doesn't block, we have to track + // manually whether we've called it one more time after `endTime` + // to fulfill Note [Guaranteed syscall time spent]. + bool endTimeReached = false; + while (avail == 0) { + BOOL success = PeekNamedPipe( hFile, NULL, 0, NULL, &avail, NULL ); + if (success) { + if (avail != 0) { + return 1; + } else { // no new data + if (infinite) { + Sleep(1); // 1 millisecond (smallest possible time on Windows) + continue; + } else if (msecs == 0) { + return 0; + } else { + if (endTimeReached) return 0; // [we waited the full msecs] + Time now = getProcessElapsedTime(); + if (now >= endTime) endTimeReached = true; + Sleep(1); // 1 millisecond (smallest possible time on Windows) + continue; } + } + } else { + rc = GetLastError(); + if (rc == ERROR_BROKEN_PIPE) { + return 1; // this is probably what we want + } + if (rc != ERROR_INVALID_HANDLE && rc != ERROR_INVALID_FUNCTION) { + maperrno(); + return -1; + } } - } - - case FILE_TYPE_DISK: - // assume that disk files are always ready: - return 1; - - case FILE_TYPE_PIPE: - // WaitForMultipleObjects() doesn't work for pipes (it - // always returns WAIT_OBJECT_0 even when no data is - // available). If the HANDLE is a pipe, therefore, we try - // PeekNamedPipe: - // - rc = PeekNamedPipe( hFile, NULL, 0, NULL, &avail, NULL ); - if (rc != 0) { - if (avail != 0) { - return 1; - } else { - return 0; - } - } else { - rc = GetLastError(); - if (rc == ERROR_BROKEN_PIPE) { - return 1; // this is probably what we want - } - if (rc != ERROR_INVALID_HANDLE && rc != ERROR_INVALID_FUNCTION) { - maperrno(); - return -1; } } /* PeekNamedPipe didn't work - fall through to the general case */ - default: - rc = WaitForSingleObject( hFile, msecs ); + default: + while (true) { + rc = WaitForSingleObject( + hFile, + compute_WaitForSingleObject_timeout(infinite, remaining)); - /* 1 => Input ready, 0 => not ready, -1 => error */ - switch (rc) { - case WAIT_TIMEOUT: return 0; - case WAIT_OBJECT_0: return 1; - default: /* WAIT_FAILED */ maperrno(); return -1; - } + switch (rc) { + case WAIT_TIMEOUT: + // We need to use < here because if remaining + // was INFINITE, we'll have waited for + // `INFINITE - 1` as per + // compute_WaitForSingleObject_timeout(), + // so that's 1 ms too little. Wait again then. + if (!infinite && remaining < MSToTime(INFINITE)) + return 0; // real complete or [we waited the full msecs] + break; + case WAIT_OBJECT_0: return 1; + default: /* WAIT_FAILED */ maperrno(); return -1; + } + + // EINTR or a >(INFINITE - 1) timeout completed + if (!infinite) { + Time now = getProcessElapsedTime(); + remaining = endTime - now; + } + } } } #endif |