Skip to content

Commit

Permalink
Emulate Linux socket timeout signaling on Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
jart committed Sep 17, 2024
1 parent 65e425f commit b14dddc
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 29 deletions.
38 changes: 16 additions & 22 deletions libc/sock/getsockopt-nt.c
Original file line number Diff line number Diff line change
Expand Up @@ -48,40 +48,34 @@ textwindows int sys_getsockopt_nt(struct Fd *fd, int level, int optname,
}

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 {
if (in_optlen < sizeof(int))
return einval();
}
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);
}

if (level == SOL_SOCKET &&
(optname == SO_RCVTIMEO || optname == SO_SNDTIMEO)) {
if (in_optlen >= sizeof(struct timeval)) {
if (optname == SO_RCVTIMEO) {
ms = fd->rcvtimeo;
} else {
ms = fd->sndtimeo;
}
((struct timeval *)out_opt_optval)->tv_sec = ms / 1000;
((struct timeval *)out_opt_optval)->tv_usec = ms % 1000 * 1000;
*inout_optlen = sizeof(struct timeval);
return 0;
} else {
if (in_optlen < sizeof(struct timeval))
return einval();
if (optname == SO_RCVTIMEO) {
ms = fd->rcvtimeo;
} else {
ms = fd->sndtimeo;
}
*(struct timeval *)out_opt_optval = timeval_frommillis(ms);
*inout_optlen = sizeof(struct timeval);
return 0;
}

// TODO(jart): Use WSAIoctl?
if (__imp_getsockopt(fd->handle, level, optname, out_opt_optval,
inout_optlen) == -1) {
inout_optlen) == -1)
return __winsockerr();
}

if (level == SOL_SOCKET) {
if (optname == SO_LINGER && in_optlen == sizeof(struct linger)) {
Expand Down
2 changes: 1 addition & 1 deletion libc/sock/recv.c
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
* EPIPE (if MSG_NOSIGNAL), EMSGSIZE, ENOTSOCK, EFAULT, etc.
* @cancelationpoint
* @asyncsignalsafe
* @restartable (unless SO_RCVTIMEO)
* @restartable (unless SO_RCVTIMEO on Linux or Windows)
*/
ssize_t recv(int fd, void *buf, size_t size, int flags) {
ssize_t rc;
Expand Down
2 changes: 1 addition & 1 deletion libc/sock/recvfrom.c
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
* EPIPE (if MSG_NOSIGNAL), EMSGSIZE, ENOTSOCK, EFAULT, etc.
* @cancelationpoint
* @asyncsignalsafe
* @restartable (unless SO_RCVTIMEO)
* @restartable (unless SO_RCVTIMEO on Linux or Windows)
*/
ssize_t recvfrom(int fd, void *buf, size_t size, int flags,
struct sockaddr *opt_out_srcaddr,
Expand Down
2 changes: 1 addition & 1 deletion libc/sock/send.c
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
* EPIPE (if MSG_NOSIGNAL), EMSGSIZE, ENOTSOCK, EFAULT, etc.
* @cancelationpoint
* @asyncsignalsafe
* @restartable (unless SO_RCVTIMEO)
* @restartable (unless SO_SNDTIMEO on Linux or Windows)
*/
ssize_t send(int fd, const void *buf, size_t size, int flags) {
ssize_t rc;
Expand Down
2 changes: 1 addition & 1 deletion libc/sock/sendto.c
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
* EPIPE (if MSG_NOSIGNAL), EMSGSIZE, ENOTSOCK, EFAULT, etc.
* @cancelationpoint
* @asyncsignalsafe
* @restartable (unless SO_RCVTIMEO)
* @restartable (unless SO_SNDTIMEO on Linux or Windows)
*/
ssize_t sendto(int fd, const void *buf, size_t size, int flags,
const struct sockaddr *opt_addr, uint32_t addrsize) {
Expand Down
9 changes: 6 additions & 3 deletions libc/sock/setsockopt-nt.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,25 @@ textwindows int sys_setsockopt_nt(struct Fd *fd, int level, int optname,
const void *optval, uint32_t optlen) {

// socket read/write timeouts
// timeout of zero means wait forever (default)
if (level == SOL_SOCKET &&
(optname == SO_RCVTIMEO || optname == SO_SNDTIMEO)) {
if (!(optval && optlen == sizeof(struct timeval)))
if (!optval)
return einval();
if (optlen < sizeof(struct timeval))
return einval();
const struct timeval *tv = optval;
int64_t ms = timeval_tomillis(*tv);
if (ms > -1u)
ms = 0; // wait forever (default) yes zero actually means this
ms = -1u;
if (optname == SO_RCVTIMEO)
fd->rcvtimeo = ms;
if (optname == SO_SNDTIMEO)
fd->sndtimeo = ms;
return 0; // we want to handle this on our own
}

// how to make close() a blocking i/o call
// how to make close() a blocking i/o call lool
union {
uint32_t millis;
struct linger_nt linger;
Expand Down
5 changes: 5 additions & 0 deletions libc/sock/winsockblock.c
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ __winsock_block(int64_t handle, uint32_t flags, int nonblock,
// check if signal handler without SA_RESTART was called
if (handler_was_called & SIG_HANDLED_NO_RESTART)
return eintr();

// emulates linux behavior of having timeouts @norestart
if (handler_was_called & SIG_HANDLED_SA_RESTART)
if (srwtimeout)
return eintr();
}

// otherwise try the i/o operation again
Expand Down
215 changes: 215 additions & 0 deletions test/posix/socket_timeout_signal_test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
#include <arpa/inet.h>
#include <cosmo.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include "libc/limits.h"

/**
* @fileoverview SO_RCVTIMEO + SA_RESTART interaction test
*
* This code tests that setting a read timeout on a socket will cause
* read() to change its signal handling behavior from @restartable to
* @norestart. This is currently the case on GNU/Systemd and Windows.
*/

struct sockaddr_in serv_addr;
atomic_bool g_ready_for_conn;
atomic_bool g_ready_for_data;
atomic_bool g_ready_for_more;
atomic_bool g_ready_for_exit;
atomic_bool got_sigusr1;
atomic_bool got_sigusr2;

void on_sigusr1(int sig) {
got_sigusr1 = true;
}

void on_sigusr2(int sig) {
got_sigusr2 = true;
}

void *server_thread(void *arg) {
int server, client;
struct timeval timeout;
socklen_t len;
struct sockaddr_in cli_addr;

// create listening socket
server = socket(AF_INET, SOCK_STREAM, 0);
if (server == -1) {
perror("socket");
exit(31);
}

// 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(server, (struct sockaddr *)&serv_addr, sizeof(serv_addr))) {
perror("bind");
exit(32);
}

// get assigned port
len = sizeof(serv_addr);
if (getsockname(server, (struct sockaddr *)&serv_addr, &len)) {
perror("getsockname");
exit(30);
}

// listen on the socket
if (listen(server, SOMAXCONN)) {
perror("listen");
exit(33);
}

// wake main thread
g_ready_for_conn = true;

// accept connection
len = sizeof(cli_addr);
client = accept(server, (struct sockaddr *)&cli_addr, &len);
if (client == -1) {
perror("accept");
exit(35);
}

// wake main thread
g_ready_for_data = true;

// check read() has @restartable behavior
char buf[1];
int rc = read(client, buf, 1);
if (rc != -1)
exit(35);
if (errno != EINTR)
exit(36);
if (!got_sigusr1)
exit(37);
if (!got_sigusr2)
exit(38);
got_sigusr1 = false;
got_sigusr2 = false;

// install a socket receive timeout
timeout.tv_sec = 5000000;
timeout.tv_usec = 0;
if (setsockopt(client, SOL_SOCKET, SO_RCVTIMEO, &timeout,
sizeof(timeout) + !IsNetbsd())) {
perror("setsockopt");
exit(34);
}

// wake main thread
g_ready_for_more = true;

// check read() has @norestart behavior
rc = read(client, buf, 1);
if (rc != -1)
exit(35);
if (errno != EINTR)
exit(36);
if (!got_sigusr1)
exit(37);

// here's the whammy
if (IsLinux() || IsWindows()) {
if (got_sigusr2)
exit(38);
} else {
if (!got_sigusr2)
exit(38);
}

// wait for main thread
for (;;)
if (g_ready_for_exit)
break;

// close listening socket
if (close(server))
exit(40);
if (close(client))
exit(39);
return 0;
}

int main() {

// handle signals
struct sigaction sa = {0};
sa.sa_handler = on_sigusr1;
sa.sa_flags = SA_RESTART;
sigaction(SIGUSR1, &sa, 0);
sa.sa_handler = on_sigusr2;
sa.sa_flags = 0;
sigaction(SIGUSR2, &sa, 0);

// create server thread
pthread_t th;
if (pthread_create(&th, 0, server_thread, 0))
return 1;

// wait for thread
for (;;)
if (g_ready_for_conn)
break;

// create socket
int client = socket(AF_INET, SOCK_STREAM, 0);
if (client == -1) {
perror("socket");
return 2;
}

// connect to server
if (connect(client, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
perror("connect");
return 3;
}

// wait for thread
for (;;)
if (g_ready_for_data)
break;

usleep(100e3);
if (pthread_kill(th, SIGUSR1))
return 4;

usleep(100e3);
if (pthread_kill(th, SIGUSR2))
return 5;

// wait for thread
for (;;)
if (g_ready_for_more)
break;

usleep(100e3);
if (pthread_kill(th, SIGUSR1))
return 4;

usleep(400e3);
if (pthread_kill(th, SIGUSR2))
return 5;

g_ready_for_exit = true;

if (pthread_join(th, 0))
return 20;
}

0 comments on commit b14dddc

Please sign in to comment.