From e142124730d4be14b738833c42c80a2a2bc4e723 Mon Sep 17 00:00:00 2001 From: Justine Tunney Date: Thu, 12 Sep 2024 23:01:20 -0700 Subject: [PATCH] Rewrite Windows connect() Our old code wasn't working with projects like Qt that call connect() in O_NONBLOCK mode multiple times. This change overhauls connect() to use a simpler WSAConnect() API and follows the same pattern as cosmo accept(). This change also reduces the binary footprint of read(), which no longer needs to depend on our enormous clock_gettime() function. --- libc/calls/clock_gettime_monotonic_nt.c | 26 +++ libc/calls/poll-nt.c | 11 +- libc/calls/read-nt.c | 6 +- libc/calls/struct/timespec.internal.h | 1 + libc/intrin/fds.h | 2 +- libc/sock/BUILD.mk | 1 + libc/sock/accept-nt.c | 15 +- libc/sock/accept.c | 3 + libc/sock/accept4.c | 11 + libc/sock/closesocket-nt.c | 5 - libc/sock/connect-nt.c | 236 +++++++++++---------- libc/sock/connect.c | 19 +- libc/sock/getsockname.c | 7 +- libc/sock/getsockopt-nt.c | 13 ++ libc/sock/recv-nt.c | 5 +- libc/sock/recvfrom-nt.c | 2 +- libc/sock/send-nt.c | 2 +- libc/sock/sendfile.c | 3 +- libc/sock/sendto-nt.c | 2 +- libc/sock/syscall_fd.internal.h | 1 - libc/sysv/consts.sh | 4 + test/libc/calls/sched_getcpu_test.c | 113 ---------- test/libc/sock/connect_test.c | 6 - test/posix/connect_nonblock_test.c | 260 ++++++++++++++++++++++++ test/posix/listen_timeout_test.c | 79 +++++++ 25 files changed, 556 insertions(+), 277 deletions(-) create mode 100644 libc/calls/clock_gettime_monotonic_nt.c delete mode 100644 test/libc/calls/sched_getcpu_test.c create mode 100644 test/posix/connect_nonblock_test.c create mode 100644 test/posix/listen_timeout_test.c diff --git a/libc/calls/clock_gettime_monotonic_nt.c b/libc/calls/clock_gettime_monotonic_nt.c new file mode 100644 index 000000000..f1371d27d --- /dev/null +++ b/libc/calls/clock_gettime_monotonic_nt.c @@ -0,0 +1,26 @@ +/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│ +│ vi: set et ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi │ +╞══════════════════════════════════════════════════════════════════════════════╡ +│ Copyright 2024 Justine Alexandra Roberts Tunney │ +│ │ +│ Permission to use, copy, modify, and/or distribute this software for │ +│ any purpose with or without fee is hereby granted, provided that the │ +│ above copyright notice and this permission notice appear in all copies. │ +│ │ +│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │ +│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │ +│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │ +│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │ +│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │ +│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │ +│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ +│ PERFORMANCE OF THIS SOFTWARE. │ +╚─────────────────────────────────────────────────────────────────────────────*/ +#include "libc/calls/struct/timespec.internal.h" +#include "libc/nt/time.h" + +textwindows struct timespec sys_clock_gettime_monotonic_nt(void) { + uint64_t hectons; + QueryUnbiasedInterruptTimePrecise(&hectons); + return timespec_fromnanos(hectons * 100); +} diff --git a/libc/calls/poll-nt.c b/libc/calls/poll-nt.c index 4d0000b46..1f82dc9ae 100644 --- a/libc/calls/poll-nt.c +++ b/libc/calls/poll-nt.c @@ -21,6 +21,7 @@ #include "libc/calls/struct/sigset.h" #include "libc/calls/struct/sigset.internal.h" #include "libc/calls/struct/timespec.h" +#include "libc/calls/struct/timespec.internal.h" #include "libc/calls/syscall_support-nt.internal.h" #include "libc/intrin/atomic.h" #include "libc/intrin/fds.h" @@ -58,14 +59,8 @@ #define POLLPRI_ 0x0400 // MSDN unsupported // -textwindows dontinline static struct timespec sys_poll_nt_now(void) { - uint64_t hectons; - QueryUnbiasedInterruptTimePrecise(&hectons); - return timespec_fromnanos(hectons * 100); -} - textwindows static uint32_t sys_poll_nt_waitms(struct timespec deadline) { - struct timespec now = sys_poll_nt_now(); + struct timespec now = sys_clock_gettime_monotonic_nt(); if (timespec_cmp(now, deadline) < 0) { struct timespec remain = timespec_sub(deadline, now); int64_t millis = timespec_tomillis(remain); @@ -340,7 +335,7 @@ textwindows int sys_poll_nt(struct pollfd *fds, uint64_t nfds, uint32_t *ms, int rc; struct timespec now, timeout, deadline; BLOCK_SIGNALS; - now = ms ? sys_poll_nt_now() : timespec_zero; + now = ms ? sys_clock_gettime_monotonic_nt() : timespec_zero; timeout = ms ? timespec_frommillis(*ms) : timespec_max; deadline = timespec_add(now, timeout); rc = sys_poll_nt_impl(fds, nfds, deadline, sigmask ? *sigmask : _SigMask); diff --git a/libc/calls/read-nt.c b/libc/calls/read-nt.c index 31eea8e87..141bc07df 100644 --- a/libc/calls/read-nt.c +++ b/libc/calls/read-nt.c @@ -24,6 +24,7 @@ #include "libc/calls/struct/iovec.h" #include "libc/calls/struct/sigset.internal.h" #include "libc/calls/struct/timespec.h" +#include "libc/calls/struct/timespec.internal.h" #include "libc/calls/syscall_support-nt.internal.h" #include "libc/cosmo.h" #include "libc/ctype.h" @@ -837,7 +838,8 @@ textwindows static int CountConsoleInputBytesBlockingImpl(uint32_t ms, uint32_t wi; struct timespec now, deadline; InitConsole(); - deadline = timespec_add(timespec_mono(), timespec_frommillis(ms)); + deadline = + timespec_add(sys_clock_gettime_monotonic_nt(), timespec_frommillis(ms)); RestartOperation: if (_check_cancel() == -1) return -1; @@ -870,7 +872,7 @@ RestartOperation: // this can happen for multiple reasons. first our driver controls // user interactions in canonical mode. secondly we could lose the // race with another thread that's reading input. - now = timespec_mono(); + now = sys_clock_gettime_monotonic_nt(); if (timespec_cmp(now, deadline) >= 0) return etimedout(); ms = timespec_tomillis(timespec_sub(deadline, now)); diff --git a/libc/calls/struct/timespec.internal.h b/libc/calls/struct/timespec.internal.h index bc4ff4d47..4eaa08004 100644 --- a/libc/calls/struct/timespec.internal.h +++ b/libc/calls/struct/timespec.internal.h @@ -25,6 +25,7 @@ int sys_sem_timedwait(int64_t, const struct timespec *); int sys_utimensat(int, const char *, const struct timespec[2], int); int sys_utimensat_nt(int, const char *, const struct timespec[2], int); int sys_utimensat_old(int, const char *, const struct timespec[2], int); +struct timespec sys_clock_gettime_monotonic_nt(void); const char *_DescribeTimespec(char[45], int, const struct timespec *); #define DescribeTimespec(rc, ts) _DescribeTimespec(alloca(45), rc, ts) diff --git a/libc/intrin/fds.h b/libc/intrin/fds.h index 59569ff28..2cfccc771 100644 --- a/libc/intrin/fds.h +++ b/libc/intrin/fds.h @@ -28,6 +28,7 @@ struct Cursor { struct Fd { char kind; bool isbound; + char connecting; unsigned flags; unsigned mode; long handle; @@ -38,7 +39,6 @@ struct Fd { unsigned sndtimeo; /* millis; 0 means wait forever */ void *connect_op; struct Cursor *cursor; - struct sockaddr_storage peer; }; struct Fds { diff --git a/libc/sock/BUILD.mk b/libc/sock/BUILD.mk index faf19f971..3b8982eae 100644 --- a/libc/sock/BUILD.mk +++ b/libc/sock/BUILD.mk @@ -34,6 +34,7 @@ LIBC_SOCK_A_DIRECTDEPS = \ LIBC_NT_IPHLPAPI \ LIBC_NT_KERNEL32 \ LIBC_NT_NTDLL \ + LIBC_NT_REALTIME \ LIBC_NT_WS2_32 \ LIBC_RUNTIME \ LIBC_STDIO \ diff --git a/libc/sock/accept-nt.c b/libc/sock/accept-nt.c index b05963fc6..5490c3560 100644 --- a/libc/sock/accept-nt.c +++ b/libc/sock/accept-nt.c @@ -18,15 +18,12 @@ ╚─────────────────────────────────────────────────────────────────────────────*/ #include "libc/calls/internal.h" #include "libc/calls/struct/sigset.internal.h" -#include "libc/calls/syscall_support-nt.internal.h" #include "libc/errno.h" -#include "libc/intrin/kprintf.h" #include "libc/nt/errors.h" #include "libc/nt/struct/pollfd.h" #include "libc/nt/thunk/msabi.h" #include "libc/nt/winsock.h" #include "libc/sock/internal.h" -#include "libc/sock/struct/sockaddr.h" #include "libc/sock/syscall_fd.internal.h" #include "libc/sysv/consts/fio.h" #include "libc/sysv/consts/o.h" @@ -38,8 +35,6 @@ #define POLL_INTERVAL_MS 10 -__msabi extern typeof(__sys_setsockopt_nt) *const __imp_setsockopt; -__msabi extern typeof(__sys_closesocket_nt) *const __imp_closesocket; __msabi extern typeof(__sys_ioctlsocket_nt) *const __imp_ioctlsocket; textwindows static int sys_accept_nt_impl(struct Fd *f, @@ -61,7 +56,6 @@ textwindows static int sys_accept_nt_impl(struct Fd *f, for (;;) { // perform non-blocking accept - // we assume listen() put f->handle in non-blocking mode int32_t addrsize = sizeof(*addr); struct sockaddr *paddr = (struct sockaddr *)addr; if ((handle = WSAAccept(f->handle, paddr, &addrsize, 0, 0)) != -1) @@ -76,7 +70,7 @@ textwindows static int sys_accept_nt_impl(struct Fd *f, return -1; } - // we're done if user wants non-blocking + // check for non-blocking if (f->flags & O_NONBLOCK) return eagain(); @@ -91,13 +85,6 @@ textwindows static int sys_accept_nt_impl(struct Fd *f, return __winsockerr(); } - // inherit properties of listening socket - // errors ignored as if f->handle was created before forking - // this fails with WSAENOTSOCK, see - // https://github.com/jart/cosmopolitan/issues/1174 - __imp_setsockopt(handle, SOL_SOCKET, kNtSoUpdateAcceptContext, &f->handle, - sizeof(f->handle)); - // create file descriptor for new socket // don't inherit the file open mode bits int oflags = 0; diff --git a/libc/sock/accept.c b/libc/sock/accept.c index 882f38752..02f5fba31 100644 --- a/libc/sock/accept.c +++ b/libc/sock/accept.c @@ -22,6 +22,9 @@ /** * Creates client socket file descriptor for incoming connection. * + * On Windows, when this function blocks, there may be a 10 millisecond + * delay on the handling of signals or thread cancelation. + * * @param fd is the server socket file descriptor * @param opt_out_addr will receive the remote address * @param opt_inout_addrsize provides and receives addr's byte length diff --git a/libc/sock/accept4.c b/libc/sock/accept4.c index 9b44071cc..ded323600 100644 --- a/libc/sock/accept4.c +++ b/libc/sock/accept4.c @@ -30,12 +30,23 @@ /** * Creates client socket file descriptor for incoming connection. * + * When `fd` is in `O_NONBLOCK` mode, this function will raise `EAGAIN` + * when no client is available to accept. To wait until a client exists + * the poll() function may be called using `POLLIN`. + * + * On Linux, your `SO_RCVTIMEO` will timeout accept4(). Other OSes (i.e. + * Windows, MacOS, and BSDs) do not support this and will block forever. + * + * On Windows, when this function blocks, there may be a 10 millisecond + * delay on the handling of signals or thread cancelation. + * * @param fd is the server socket file descriptor * @param opt_out_addr will receive the remote address * @param opt_inout_addrsize provides and receives out_addr's byte length * @param flags can have SOCK_{CLOEXEC,NONBLOCK}, which may apply to * both the newly created socket and the server one * @return client fd which needs close(), or -1 w/ errno + * @raise EAGAIN if `O_NONBLOCK` and no clients pending * @cancelationpoint * @asyncsignalsafe * @restartable (unless SO_RCVTIMEO) diff --git a/libc/sock/closesocket-nt.c b/libc/sock/closesocket-nt.c index ed6d41e97..1084ac363 100644 --- a/libc/sock/closesocket-nt.c +++ b/libc/sock/closesocket-nt.c @@ -16,8 +16,6 @@ │ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ │ PERFORMANCE OF THIS SOFTWARE. │ ╚─────────────────────────────────────────────────────────────────────────────*/ -#include "libc/intrin/weaken.h" -#include "libc/mem/mem.h" #include "libc/nt/thunk/msabi.h" #include "libc/nt/winsock.h" #include "libc/sock/internal.h" @@ -32,9 +30,6 @@ __msabi extern typeof(__sys_closesocket_nt) *const __imp_closesocket; * This function should only be called by close(). */ textwindows int sys_closesocket_nt(struct Fd *f) { - if (_weaken(sys_connect_nt_cleanup)) { - _weaken(sys_connect_nt_cleanup)(f, true); - } if (!__imp_closesocket(f->handle)) { return 0; } else { diff --git a/libc/sock/connect-nt.c b/libc/sock/connect-nt.c index 1bcc1ced1..4bbc19059 100644 --- a/libc/sock/connect-nt.c +++ b/libc/sock/connect-nt.c @@ -16,92 +16,47 @@ │ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ │ PERFORMANCE OF THIS SOFTWARE. │ ╚─────────────────────────────────────────────────────────────────────────────*/ -#include "libc/assert.h" -#include "libc/atomic.h" +#include "libc/calls/internal.h" +#include "libc/calls/struct/sigset.h" #include "libc/calls/struct/sigset.internal.h" -#include "libc/cosmo.h" #include "libc/errno.h" -#include "libc/intrin/fds.h" #include "libc/macros.h" -#include "libc/mem/mem.h" -#include "libc/nt/enum/wsaid.h" #include "libc/nt/errors.h" -#include "libc/nt/struct/guid.h" -#include "libc/nt/struct/overlapped.h" -#include "libc/nt/thread.h" +#include "libc/nt/struct/fdset.h" +#include "libc/nt/struct/pollfd.h" +#include "libc/nt/struct/timeval.h" #include "libc/nt/thunk/msabi.h" #include "libc/nt/winsock.h" #include "libc/sock/internal.h" #include "libc/sock/struct/sockaddr.h" #include "libc/sock/syscall_fd.internal.h" -#include "libc/sock/wsaid.internal.h" -#include "libc/sysv/consts/af.h" +#include "libc/sysv/consts/fio.h" #include "libc/sysv/consts/o.h" +#include "libc/sysv/consts/poll.h" +#include "libc/sysv/consts/so.h" #include "libc/sysv/consts/sol.h" #include "libc/sysv/errfuns.h" - #ifdef __x86_64__ -#include "libc/sock/yoink.inc" -__msabi extern typeof(__sys_setsockopt_nt) *const __imp_setsockopt; +#define UNCONNECTED 0 +#define CONNECTING 1 +#define CONNECTED 2 -struct ConnectArgs { - const void *addr; - uint32_t addrsize; -}; +#define POLL_INTERVAL_MS 10 -static struct { - atomic_uint once; - bool32 (*__msabi lpConnectEx)(int64_t hSocket, const struct sockaddr *name, - int namelen, const void *opt_lpSendBuffer, - uint32_t dwSendDataLength, - uint32_t *opt_out_lpdwBytesSent, - struct NtOverlapped *lpOverlapped); -} g_connectex; +__msabi extern typeof(__sys_getsockopt_nt) *const __imp_getsockopt; +__msabi extern typeof(__sys_ioctlsocket_nt) *const __imp_ioctlsocket; +__msabi extern typeof(__sys_select_nt) *const __imp_select; -static void connectex_init(void) { - static struct NtGuid ConnectExGuid = WSAID_CONNECTEX; - g_connectex.lpConnectEx = __get_wsaid(&ConnectExGuid); -} +textwindows static int sys_connect_nt_impl(struct Fd *f, const void *addr, + uint32_t addrsize, + sigset_t waitmask) { -void sys_connect_nt_cleanup(struct Fd *f, bool cancel) { - struct NtOverlapped *overlap; - if ((overlap = f->connect_op)) { - uint32_t got, flags; - if (cancel) - CancelIoEx(f->handle, overlap); - if (WSAGetOverlappedResult(f->handle, overlap, &got, cancel, &flags) || - WSAGetLastError() != kNtErrorIoIncomplete) { - WSACloseEvent(overlap->hEvent); - free(overlap); - f->connect_op = 0; - } - } -} + // check if already connected + if (f->connecting == 2) + return eisconn(); -static int sys_connect_nt_start(int64_t hSocket, - struct NtOverlapped *lpOverlapped, - uint32_t *flags, void *arg) { - struct ConnectArgs *args = arg; - if (g_connectex.lpConnectEx(hSocket, args->addr, args->addrsize, 0, 0, 0, - lpOverlapped)) { - return 0; - } else { - return -1; - } -} - -static textwindows int sys_connect_nt_impl(struct Fd *f, const void *addr, - uint32_t addrsize, sigset_t mask) { - - // get connect function from winsock api - cosmo_once(&g_connectex.once, connectex_init); - - // fail if previous connect() is still in progress - if (f->connect_op) - return ealready(); - - // ConnectEx() requires bind() be called beforehand + // winsock requires bind() be called beforehand if (!f->isbound) { struct sockaddr_storage ss = {0}; ss.ss_family = ((struct sockaddr *)addr)->sa_family; @@ -109,60 +64,121 @@ static textwindows int sys_connect_nt_impl(struct Fd *f, const void *addr, return -1; } - // perform normal connect - if (!(f->flags & O_NONBLOCK)) { - f->peer.ss_family = AF_UNSPEC; - ssize_t rc = __winsock_block(f->handle, 0, false, f->sndtimeo, mask, - sys_connect_nt_start, - &(struct ConnectArgs){addr, addrsize}); - if (rc == -1 && errno == EAGAIN) { - // return ETIMEDOUT if SO_SNDTIMEO elapsed - // note that Linux will return EINPROGRESS - errno = etimedout(); - } else if (!rc) { - __imp_setsockopt(f->handle, SOL_SOCKET, kNtSoUpdateConnectContext, 0, 0); + if (f->connecting == UNCONNECTED) { + + // make sure winsock is in non-blocking mode + uint32_t mode = 1; + if (__imp_ioctlsocket(f->handle, FIONBIO, &mode)) + return __winsockerr(); + + // perform non-blocking connect + if (!WSAConnect(f->handle, addr, addrsize, 0, 0, 0, 0)) { + f->connecting = CONNECTED; + return 0; + } + + // check for errors + switch (WSAGetLastError()) { + case WSAEISCONN: + f->connecting = CONNECTED; + return eisconn(); + case WSAEALREADY: + f->connecting = CONNECTING; + break; + case WSAEWOULDBLOCK: + break; + default: + return __winsockerr(); + } + + // handle non-blocking + if (f->flags & O_NONBLOCK) { + if (f->connecting == UNCONNECTED) { + f->connecting = CONNECTING; + return einprogress(); + } else { + return ealready(); + } + } else { + f->connecting = CONNECTING; } - return rc; } - // win32 getpeername() stops working in non-blocking connect mode - if (addrsize) - memcpy(&f->peer, addr, MIN(addrsize, sizeof(struct sockaddr_storage))); + for (;;) { - // perform nonblocking connect(), i.e. - // 1. connect(O_NONBLOCK) → EINPROGRESS - // 2. poll(POLLOUT) - bool32 ok; - struct NtOverlapped *overlap = calloc(1, sizeof(struct NtOverlapped)); - if (!overlap) - return -1; - overlap->hEvent = WSACreateEvent(); - ok = g_connectex.lpConnectEx(f->handle, addr, addrsize, 0, 0, 0, overlap); - if (ok) { - uint32_t dwBytes, dwFlags; - ok = WSAGetOverlappedResult(f->handle, overlap, &dwBytes, false, &dwFlags); - WSACloseEvent(overlap->hEvent); - free(overlap); - if (!ok) { - return __winsockerr(); + // check for signals and thread cancelation + // connect() will restart if SA_RESTART is used + if (!(f->flags & O_NONBLOCK)) + if (__sigcheck(waitmask, true) == -1) + return -1; + + // + // "Use select to determine the completion of the connection request + // by checking if the socket is writable." + // + // —Quoth MSDN § WSAConnect function + // + // "If a socket is processing a connect call (nonblocking), failure + // of the connect attempt is indicated in exceptfds (application + // must then call getsockopt SO_ERROR to determine the error value + // to describe why the failure occurred). This document does not + // define which other errors will be included." + // + // —Quoth MSDN § select function + // + struct NtFdSet wrfds; + struct NtFdSet exfds; + struct NtTimeval timeout; + wrfds.fd_count = 1; + wrfds.fd_array[0] = f->handle; + exfds.fd_count = 1; + exfds.fd_array[0] = f->handle; + if (f->flags & O_NONBLOCK) { + timeout.tv_sec = 0; + timeout.tv_usec = 0; + } else { + timeout.tv_sec = POLL_INTERVAL_MS / 1000; + timeout.tv_usec = POLL_INTERVAL_MS % 1000 * 1000; } - __imp_setsockopt(f->handle, SOL_SOCKET, kNtSoUpdateConnectContext, 0, 0); + int ready = __imp_select(1, 0, &wrfds, &exfds, &timeout); + if (ready == -1) + return __winsockerr(); + + // check if we still need more time + if (!ready) { + if (f->flags & O_NONBLOCK) { + return ealready(); + } else { + continue; + } + } + + // check if connect failed + if (exfds.fd_count) { + int err; + uint32_t len = sizeof(err); + if (__imp_getsockopt(f->handle, SOL_SOCKET, SO_ERROR, &err, &len) == -1) + return __winsockerr(); + if (!err) + return eio(); // should be impossible + errno = __dos2errno(err); + return -1; + } + + // handle successful connection + if (!wrfds.fd_count) + return eio(); // should be impossible + f->connecting = CONNECTED; return 0; - } else if (WSAGetLastError() == kNtErrorIoPending) { - f->connect_op = overlap; - return einprogress(); - } else { - WSACloseEvent(overlap->hEvent); - free(overlap); - return __winsockerr(); } } textwindows int sys_connect_nt(struct Fd *f, const void *addr, uint32_t addrsize) { - sigset_t mask = __sig_block(); - int rc = sys_connect_nt_impl(f, addr, addrsize, mask); - __sig_unblock(mask); + int rc; + BLOCK_SIGNALS; + rc = sys_connect_nt_impl(f, addr, addrsize, _SigMask); + ALLOW_SIGNALS; return rc; } diff --git a/libc/sock/connect.c b/libc/sock/connect.c index c8a08db36..8426c2102 100644 --- a/libc/sock/connect.c +++ b/libc/sock/connect.c @@ -31,12 +31,23 @@ /** * Connects socket to remote end. * - * ProTip: Connectionless sockets, e.g. UDP, can be connected too. The - * benefit is not needing to specify the remote address on each send. It - * also means getsockname() can be called to retrieve routing details. + * When `fd` is in `O_NONBLOCK` mode, this raises `EINPROGRESS`. To wait + * for establishment poll() function may be called using `POLLOUT`. Then + * `SO_ERROR` may be used to check for errors. + * + * Connectionless sockets, e.g. UDP, can be connected too. The benefit + * is not needing to specify the remote address on each send. It also + * means getsockname() can be called to retrieve routing details. + * + * On Linux, your `SO_SNDTIMEO` will timeout connect(). Other OSes (i.e. + * Windows, MacOS, and BSDs) do not support this and will block forever. + * + * On Windows, when this function blocks, there may be a 10 millisecond + * delay on the handling of signals or thread cancelation. * * @return 0 on success or -1 w/ errno - * @raise EALREADY if a non-blocking connection request already happened + * @raise EINPROGRESS if `O_NONBLOCK` and connecting process initiated + * @raise EALREADY if a `O_NONBLOCK` connecting already in flight * @raise EADDRINUSE if local address is already in use * @raise EINTR if a signal handler was called instead * @raise ENETUNREACH if network is unreachable diff --git a/libc/sock/getsockname.c b/libc/sock/getsockname.c index cc596d4d0..58012d056 100644 --- a/libc/sock/getsockname.c +++ b/libc/sock/getsockname.c @@ -46,12 +46,7 @@ static int __getsockpeername(int fd, struct sockaddr *out_addr, if (IsWindows()) { if (__isfdkind(fd, kFdSocket)) { if ((rc = impl_win32(g_fds.p[fd].handle, &ss, &size))) { - if (impl_win32 == __imp_getpeername && - g_fds.p[fd].peer.ss_family != AF_UNSPEC) { - ss = g_fds.p[fd].peer; - rc = 0; - } else if (impl_win32 == __imp_getsockname && - WSAGetLastError() == WSAEINVAL) { + if (impl_win32 == __imp_getsockname && WSAGetLastError() == WSAEINVAL) { // The socket has not been bound to an address with bind, or // ADDR_ANY is specified in bind but connection has not yet // occurred. -MSDN diff --git a/libc/sock/getsockopt-nt.c b/libc/sock/getsockopt-nt.c index a5b3e3dd8..7fb573757 100644 --- a/libc/sock/getsockopt-nt.c +++ b/libc/sock/getsockopt-nt.c @@ -47,6 +47,19 @@ textwindows int sys_getsockopt_nt(struct Fd *fd, int level, int optname, in_optlen = 0; } + if (level == SOL_SOCKET && optname == SO_ERROR) { + if (in_optlen >= sizeof(int)) { + int err; + uint32_t len = sizeof(err); + if (__imp_getsockopt(fd->handle, SOL_SOCKET, SO_ERROR, &err, &len) == -1) + return __winsockerr(); + *(int *)out_opt_optval = __dos2errno(err); + *inout_optlen = sizeof(int); + } else { + return einval(); + } + } + if (level == SOL_SOCKET && (optname == SO_RCVTIMEO || optname == SO_SNDTIMEO)) { if (in_optlen >= sizeof(struct timeval)) { diff --git a/libc/sock/recv-nt.c b/libc/sock/recv-nt.c index 1652c113e..5457a335d 100644 --- a/libc/sock/recv-nt.c +++ b/libc/sock/recv-nt.c @@ -17,9 +17,9 @@ │ PERFORMANCE OF THIS SOFTWARE. │ ╚─────────────────────────────────────────────────────────────────────────────*/ #include "libc/calls/internal.h" -#include "libc/intrin/fds.h" #include "libc/calls/struct/iovec.h" #include "libc/calls/struct/sigset.internal.h" +#include "libc/intrin/fds.h" #include "libc/nt/struct/iovec.h" #include "libc/nt/winsock.h" #include "libc/sock/internal.h" @@ -50,9 +50,8 @@ static textwindows int sys_recv_nt_start(int64_t handle, textwindows ssize_t sys_recv_nt(int fd, const struct iovec *iov, size_t iovlen, uint32_t flags) { - if (flags & ~(_MSG_DONTWAIT | _MSG_OOB | _MSG_PEEK | _MSG_WAITALL)) { + if (flags & ~(_MSG_DONTWAIT | _MSG_OOB | _MSG_PEEK | _MSG_WAITALL)) return einval(); - } ssize_t rc; struct Fd *f = g_fds.p + fd; sigset_t m = __sig_block(); diff --git a/libc/sock/recvfrom-nt.c b/libc/sock/recvfrom-nt.c index 69f436d07..d1f6f5572 100644 --- a/libc/sock/recvfrom-nt.c +++ b/libc/sock/recvfrom-nt.c @@ -17,9 +17,9 @@ │ PERFORMANCE OF THIS SOFTWARE. │ ╚─────────────────────────────────────────────────────────────────────────────*/ #include "libc/calls/internal.h" -#include "libc/intrin/fds.h" #include "libc/calls/struct/iovec.h" #include "libc/calls/struct/sigset.internal.h" +#include "libc/intrin/fds.h" #include "libc/nt/struct/iovec.h" #include "libc/nt/winsock.h" #include "libc/sock/internal.h" diff --git a/libc/sock/send-nt.c b/libc/sock/send-nt.c index 63cf646cf..bccb7cdc7 100644 --- a/libc/sock/send-nt.c +++ b/libc/sock/send-nt.c @@ -17,9 +17,9 @@ │ PERFORMANCE OF THIS SOFTWARE. │ ╚─────────────────────────────────────────────────────────────────────────────*/ #include "libc/calls/internal.h" -#include "libc/intrin/fds.h" #include "libc/calls/struct/iovec.h" #include "libc/calls/struct/sigset.internal.h" +#include "libc/intrin/fds.h" #include "libc/nt/struct/iovec.h" #include "libc/nt/winsock.h" #include "libc/sock/internal.h" diff --git a/libc/sock/sendfile.c b/libc/sock/sendfile.c index e10611006..17fc36b6a 100644 --- a/libc/sock/sendfile.c +++ b/libc/sock/sendfile.c @@ -38,6 +38,7 @@ #include "libc/nt/winsock.h" #include "libc/sock/internal.h" #include "libc/sock/sendfile.internal.h" +#include "libc/sock/syscall_fd.internal.h" #include "libc/sock/wsaid.internal.h" #include "libc/stdio/sysparam.h" #include "libc/sysv/errfuns.h" @@ -58,7 +59,7 @@ static void transmitfile_init(void) { g_transmitfile.lpTransmitFile = __get_wsaid(&TransmitfileGuid); } -static dontinline textwindows ssize_t sys_sendfile_nt( +textwindows dontinline static ssize_t sys_sendfile_nt( int outfd, int infd, int64_t *opt_in_out_inoffset, uint32_t uptobytes) { ssize_t rc; uint32_t flags = 0; diff --git a/libc/sock/sendto-nt.c b/libc/sock/sendto-nt.c index 2daf9badc..c7cdcf225 100644 --- a/libc/sock/sendto-nt.c +++ b/libc/sock/sendto-nt.c @@ -17,9 +17,9 @@ │ PERFORMANCE OF THIS SOFTWARE. │ ╚─────────────────────────────────────────────────────────────────────────────*/ #include "libc/calls/internal.h" -#include "libc/intrin/fds.h" #include "libc/calls/struct/iovec.h" #include "libc/calls/struct/sigset.internal.h" +#include "libc/intrin/fds.h" #include "libc/nt/struct/iovec.h" #include "libc/nt/winsock.h" #include "libc/sock/internal.h" diff --git a/libc/sock/syscall_fd.internal.h b/libc/sock/syscall_fd.internal.h index 0433ff13d..51f7f8082 100644 --- a/libc/sock/syscall_fd.internal.h +++ b/libc/sock/syscall_fd.internal.h @@ -6,7 +6,6 @@ #include "libc/sock/struct/sockaddr.h" COSMOPOLITAN_C_START_ -void sys_connect_nt_cleanup(struct Fd *, bool); int sys_accept_nt(struct Fd *, struct sockaddr_storage *, int); int sys_bind_nt(struct Fd *, const void *, uint32_t); int sys_closesocket_nt(struct Fd *); diff --git a/libc/sysv/consts.sh b/libc/sysv/consts.sh index 156699952..41dec4c43 100755 --- a/libc/sysv/consts.sh +++ b/libc/sysv/consts.sh @@ -19,6 +19,10 @@ dir=libc/sysv/consts . libc/sysv/gen.sh +# syscon errno EALREADY 114 114 37 37 37 37 37 10037 # connection already in progress; bsd consensus; WSAEALREADY; raised by connect(2), send(2), ip(7) +# syscon errno EINPROGRESS 115 115 36 36 36 36 36 10036 # bsd consensus; WSAEINPROGRESS; raised by connect(2) w/ O_NONBLOCK +# syscon errno EISCONN 106 106 56 56 56 56 56 10056 # socket is connected; bsd consensus; WSAEISCONN; raised by connect(2), send(2), unix(7), ip(7) + # The Fifth Bell System, Community Edition # » catalogue of carnage # diff --git a/test/libc/calls/sched_getcpu_test.c b/test/libc/calls/sched_getcpu_test.c deleted file mode 100644 index 687d4b982..000000000 --- a/test/libc/calls/sched_getcpu_test.c +++ /dev/null @@ -1,113 +0,0 @@ -/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│ -│ vi: set et ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi │ -╞══════════════════════════════════════════════════════════════════════════════╡ -│ Copyright 2024 Justine Alexandra Roberts Tunney │ -│ │ -│ Permission to use, copy, modify, and/or distribute this software for │ -│ any purpose with or without fee is hereby granted, provided that the │ -│ above copyright notice and this permission notice appear in all copies. │ -│ │ -│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │ -│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │ -│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │ -│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │ -│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │ -│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │ -│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ -│ PERFORMANCE OF THIS SOFTWARE. │ -╚─────────────────────────────────────────────────────────────────────────────*/ -#include "libc/atomic.h" -#include "libc/calls/calls.h" -#include "libc/dce.h" -#include "libc/intrin/atomic.h" -#include "libc/macros.h" -#include "libc/runtime/runtime.h" -#include "libc/testlib/subprocess.h" -#include "libc/testlib/testlib.h" -#include "libc/thread/thread.h" -#include "libc/thread/thread2.h" - -int cpu_count; - -void SetUpOnce(void) { - cpu_count = __get_cpu_count(); -} - -//////////////////////////////////////////////////////////////////////////////// -// AFFINITY TEST - -TEST(sched_getcpu, affinity_test) { - - if (IsXnu()) - return; - if (IsNetbsd()) - return; - if (IsOpenbsd()) - return; - - SPAWN(fork); - int n = cpu_count; - for (int i = 0; i < n; ++i) { - cpu_set_t affinity; - CPU_ZERO(&affinity); - CPU_SET(i, &affinity); - ASSERT_EQ( - 0, pthread_setaffinity_np(pthread_self(), sizeof(affinity), &affinity)); - EXPECT_EQ(i, sched_getcpu()); - } - EXITS(0); -} - -//////////////////////////////////////////////////////////////////////////////// -// KLUDGE TEST - -#define THREADS 2 -#define ITERATIONS 100000 - -int g_hits[256]; -atomic_int g_sync; - -int call_sched_getcpu(void) { - int res = sched_getcpu(); - ASSERT_NE(-1, res); - ASSERT_GE(res, 0); - ASSERT_LT(res, cpu_count); - return res; -} - -void *worker(void *arg) { - int ith = (long)arg; - int nth = THREADS; - for (int i = 0; i < ITERATIONS; ++i) { - // help execution of threads be interleaved - int sync = atomic_fetch_add(&g_sync, 1); - if (sync % nth == ith) { - g_hits[call_sched_getcpu() % ARRAYLEN(g_hits)]++; - } - } - return 0; -} - -TEST(sched_getcpu, kludge_test) { - -#ifdef __x86_64__ - if (IsXnu()) - return; -#endif - if (IsNetbsd()) - return; - if (IsOpenbsd()) - return; - - if (cpu_count < THREADS) - return; - pthread_t th[THREADS]; - for (int i = 0; i < THREADS; ++i) - ASSERT_EQ(0, pthread_create(th + i, 0, worker, (void *)(long)i)); - for (int i = 0; i < THREADS; ++i) - ASSERT_EQ(0, pthread_join(th[i], 0)); - int hit = 0; - for (int i = 0; i < ARRAYLEN(g_hits); ++i) - hit += !!g_hits[i]; - ASSERT_GE(hit, THREADS); -} diff --git a/test/libc/sock/connect_test.c b/test/libc/sock/connect_test.c index 7ea5d1f51..c7e33f567 100644 --- a/test/libc/sock/connect_test.c +++ b/test/libc/sock/connect_test.c @@ -121,12 +121,6 @@ TEST(connect, nonblocking) { ASSERT_SYS(0, 3, socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP)); ASSERT_SYS(EINPROGRESS, -1, connect(3, (struct sockaddr *)&addr, sizeof(addr))); - if (!IsLinux() && !IsNetbsd() && !IsXnu()) { - // this doens't work on linux and netbsd - // on MacOS this can EISCONN before accept() is called - ASSERT_SYS(EALREADY, -1, - connect(3, (struct sockaddr *)&addr, sizeof(addr))); - } ASSERT_SYS(EAGAIN, -1, read(3, buf, 16)); *sem = 1; { // wait until connected diff --git a/test/posix/connect_nonblock_test.c b/test/posix/connect_nonblock_test.c new file mode 100644 index 000000000..7655ef1a0 --- /dev/null +++ b/test/posix/connect_nonblock_test.c @@ -0,0 +1,260 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main() { + char buffer[1024]; + fd_set wset; + int listenfd, connfd, sockfd; + int s, error; + pid_t pid; + socklen_t len; + struct sockaddr_in serv_addr, cli_addr; + uint16_t port; + + printf("\n"); + + /* Create listening socket */ + listenfd = socket(AF_INET, SOCK_STREAM, 0); + if (listenfd < 0) { + perror("socket() failed"); + exit(1); + } + + /* Initialize server address */ + memset(&serv_addr, 0, sizeof(serv_addr)); + serv_addr.sin_family = AF_INET; + serv_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + serv_addr.sin_port = htons(0); + + /* Bind socket */ + if (bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { + perror("bind"); + exit(2); + } + + /* Get the assigned port number */ + len = sizeof(serv_addr); + if (getsockname(listenfd, (struct sockaddr *)&serv_addr, &len) < 0) { + perror("getsockname"); + exit(3); + } + port = ntohs(serv_addr.sin_port); + + /* Listen on the socket */ + if (listen(listenfd, 1) < 0) { + perror("listen"); + exit(4); + } + + /* Fork a child process */ + pid = fork(); + if (pid < 0) { + perror("fork"); + exit(5); + } else if (pid == 0) { + /* Child process: acts as the client */ + close(listenfd); /* Close the listening socket in the child */ + + /* Create socket */ + sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); + if (sockfd < 0) { + perror("socket"); + exit(6); + } + + /* Initialize server address */ + memset(&serv_addr, 0, sizeof(serv_addr)); + serv_addr.sin_family = AF_INET; + serv_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); /* 127.0.0.1 */ + serv_addr.sin_port = htons(port); /* Assigned port */ + + /* Try calling read() before connection is established */ + s = read(sockfd, buffer, sizeof(buffer)); + if (s < 0) { + if (errno == ENOTCONN) { + printf("read #1 enotconn\n"); + /* good */ + } else { + perror("read #1"); + exit(6); + } + } else { + printf("read #1 succeeded\n"); + exit(6); + } + +#if 0 + /* Try calling read() before connection is established */ + s = write(sockfd, buffer, sizeof(buffer)); + if (s < 0) { + if (errno == ENOTCONN) { + /* good */ + } else { + perror("write"); + } + } else { + printf("Wrote %d bytes: %.*s\n", s, s, buffer); + } +#endif + + /* Attempt to connect */ + s = connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); + if (s == 0) { + printf("connect #1 success\n"); + } else if (s < 0 && errno == EINPROGRESS) { + printf("connect #1 einprogress\n"); + } else { + perror("connect #1"); + exit(10); + } + + /* Try calling read() before connection is established */ + s = read(sockfd, buffer, sizeof(buffer)); + if (s < 0) { + if (errno == EAGAIN) { + printf("read #2 eagain\n"); + } else { + perror("read #2"); + exit(10); + } + } else { + printf("read #2 succeeded\n"); + exit(10); + } + + /* Try calling connect() again to trigger EALREADY */ + s = connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); + if (!s) { + printf("connect #2 succeeded\n"); + } else if (s < 0 && errno == EALREADY) { + printf("connect #2 ealready\n"); + } else if (s < 0 && errno == EISCONN) { + printf("connect #2 eisconn\n"); + } else if (s < 0) { + perror("connect #2"); + exit(11); + } + + /* Try calling connect() again to trigger EALREADY */ + s = connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); + if (!s) { + printf("connect #3 succeeded\n"); + } else if (errno == EALREADY) { + printf("connect #3 ealready\n"); + } else if (errno == EISCONN) { + printf("connect #3 eisconn\n"); + } else { + perror("connect"); + exit(11); + } + + /* Try calling read() before connection is established */ + s = read(sockfd, buffer, sizeof(buffer)); + if (s < 0) { + if (errno == EAGAIN) { + /* good */ + } else { + perror("read"); + } + } else { + printf("Read %d bytes: %.*s\n", s, s, buffer); + } + + /* Use select() to wait for the socket to be writable */ + FD_ZERO(&wset); + FD_SET(sockfd, &wset); + + s = select(sockfd + 1, NULL, &wset, NULL, 0); + if (s == 0) { + printf("not possible\n"); + exit(11); + } else if (s < 0) { + perror("select"); + exit(12); + } + + /* Check if socket is writable */ + if (FD_ISSET(sockfd, &wset)) { + /* Check for error */ + len = sizeof(error); + if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0) + exit(13); + if (error) { + printf("connection failed after select(): %s\n", strerror(error)); + exit(14); + } + } else { + exit(16); + } + + /* Try calling connect() again to trigger EISCONN */ + s = connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); + if (!s) { + printf("connect #4 succeeded\n"); + } else if (s < 0 && errno == EISCONN) { + printf("connect #4 eisconn\n"); + } else if (s < 0) { + exit(17); + } + + if (close(sockfd)) + exit(15); + exit(0); + } else { + /* Accept connection */ + len = sizeof(cli_addr); + connfd = accept(listenfd, (struct sockaddr *)&cli_addr, &len); + if (connfd < 0) { + close(listenfd); + wait(NULL); + exit(18); + } + + /* Read data from client */ + s = read(connfd, buffer, sizeof(buffer)); + if (s < 0) { + exit(51); + } else if (!s) { + /* got close */ + } else { + exit(50); + } + + /* Close connected socket */ + if (close(connfd)) { + close(listenfd); + wait(NULL); + exit(19); + } + + /* Close listening socket */ + if (close(listenfd)) { + wait(NULL); + exit(20); + } + + /* Wait for child process to finish */ + int status; + if (waitpid(pid, &status, 0) < 0) + exit(21); + + printf("\n"); + if (WIFEXITED(status)) { + exit(WEXITSTATUS(status)); /* Return child's exit status */ + } else { + exit(22); + } + } + + exit(23); +} diff --git a/test/posix/listen_timeout_test.c b/test/posix/listen_timeout_test.c new file mode 100644 index 000000000..952c8a83a --- /dev/null +++ b/test/posix/listen_timeout_test.c @@ -0,0 +1,79 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main() { + int listenfd, connfd; + struct sockaddr_in serv_addr; + struct timeval timeout; + socklen_t len; + struct sockaddr_in cli_addr; + + // only linux really does this + if (!IsLinux()) + return 0; + + // create listening socket + listenfd = socket(AF_INET, SOCK_STREAM, 0); + if (listenfd < 0) { + perror("socket"); + exit(1); + } + + // initialize server address + memset(&serv_addr, 0, sizeof(serv_addr)); + serv_addr.sin_family = AF_INET; + serv_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + serv_addr.sin_port = htons(0); + + // bind socket + if (bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { + perror("bind"); + close(listenfd); + exit(2); + } + + // listen on the socket + if (listen(listenfd, 5) < 0) { + perror("listen"); + close(listenfd); + exit(3); + } + + // accept for 200ms + timeout.tv_sec = 0; + timeout.tv_usec = 200e3; + if (setsockopt(listenfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, + sizeof(timeout))) { + perror("setsockopt"); + close(listenfd); + exit(4); + } + + // Accept connection + len = sizeof(cli_addr); + connfd = accept(listenfd, (struct sockaddr *)&cli_addr, &len); + if (connfd < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + /* printf("accept() timed out\n"); */ + } else { + perror("accept"); + } + } else { + printf("Connection accepted from client.\n"); + // Close connected socket + close(connfd); + } + + // Close listening socket + close(listenfd); +}