summaryrefslogtreecommitdiff
path: root/libraries/base/cbits/inputReady.c
diff options
context:
space:
mode:
Diffstat (limited to 'libraries/base/cbits/inputReady.c')
-rw-r--r--libraries/base/cbits/inputReady.c518
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