Skip to content

Commit

Permalink
esp32/modsocket: Implement socket connect timeout.
Browse files Browse the repository at this point in the history
LWIP doesn't implement a timeout for blocking connect(), and such a timeout
is not required by POSIX.  However, CPython will use the socket timeout for
blocking connect on most platforms.  The "principle of least surprise"
suggests we should support it on ESP32 as well (not to mention it's
useful!).

This provides the additional improvement that external exceptions (like
KeyboardInterrupt) are now handled immediately if they happen during
connect().  Previously Ctrl-C would not terminate a blocking connect until
connect() returned, but now it will.

Fixes issue micropython#8326.

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
  • Loading branch information
projectgus authored and dpgeorge committed Nov 20, 2023
1 parent 6117aa6 commit 57cce79
Showing 1 changed file with 87 additions and 5 deletions.
92 changes: 87 additions & 5 deletions ports/esp32/modsocket.c
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ typedef struct _socket_obj_t {
#endif
} socket_obj_t;

STATIC const char *TAG = "modsocket";

void _socket_settimeout(socket_obj_t *sock, uint64_t timeout_ms);

#if MICROPY_PY_SOCKET_EVENTS
Expand Down Expand Up @@ -364,16 +366,96 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_1(socket_accept_obj, socket_accept);
STATIC mp_obj_t socket_connect(const mp_obj_t arg0, const mp_obj_t arg1) {
socket_obj_t *self = MP_OBJ_TO_PTR(arg0);
struct addrinfo *res;
bool blocking = false;
int flags;
int raise_err = 0;

_socket_getaddrinfo(arg1, &res);
MP_THREAD_GIL_EXIT();
self->state = SOCKET_STATE_CONNECTED;
int r = lwip_connect(self->fd, res->ai_addr, res->ai_addrlen);
MP_THREAD_GIL_ENTER();

flags = fcntl(self->fd, F_GETFL);

blocking = (flags & O_NONBLOCK) == 0;

if (blocking) {
// For blocking sockets, make the socket temporarily non-blocking and emulate
// blocking using select.
//
// This has two benefits:
//
// - Allows handling external exceptions while waiting for connect.
//
// - Allows emulating a connect timeout, which is not supported by LWIP or
// required by POSIX but is normal behaviour for CPython.
if (fcntl(self->fd, F_SETFL, flags | O_NONBLOCK) < 0) {
ESP_LOGE(TAG, "fcntl set failed %d", errno); // Unexpected internal failure
raise_err = errno;
}
}

if (raise_err == 0) {
// Try performing the actual connect. Expected to always return immediately.
int r = lwip_connect(self->fd, res->ai_addr, res->ai_addrlen);
if (r != 0) {
raise_err = errno;
}
}

if (blocking) {
// Set the socket back to blocking. We can still pass it to select() in this state.
int r = fcntl(self->fd, F_SETFL, flags);
if (r != 0 && (raise_err == 0 || raise_err == EINPROGRESS)) {
ESP_LOGE(TAG, "fcntl restore failed %d", errno); // Unexpected internal failure
raise_err = errno;
}
}

lwip_freeaddrinfo(res);
if (r != 0) {
mp_raise_OSError(errno);

if (blocking && raise_err == EINPROGRESS) {
// Keep calling select() until the socket is marked writable (i.e. connected),
// or an error or a timeout occurs

// Note: _socket_settimeout() always sets self->retries != 0 on blocking sockets.

for (unsigned int i = 0; i <= self->retries; i++) {
struct timeval timeout = {
.tv_sec = 0,
.tv_usec = SOCKET_POLL_US,
};
fd_set wfds;
FD_ZERO(&wfds);
FD_SET(self->fd, &wfds);

int r = select(self->fd + 1, NULL, &wfds, NULL, &timeout);
if (r < 0) {
// Error condition
raise_err = errno;
break;
} else if (r > 0) {
// Select indicated the socket is writable. Check for any error.
socklen_t socklen = sizeof(raise_err);
r = getsockopt(self->fd, SOL_SOCKET, SO_ERROR, &raise_err, &socklen);
if (r < 0) {
raise_err = errno;
}
break;
} else {
// Select timed out
raise_err = ETIMEDOUT;

MP_THREAD_GIL_ENTER();
check_for_exceptions();
MP_THREAD_GIL_EXIT();
}
}
}

MP_THREAD_GIL_ENTER();
if (raise_err) {
mp_raise_OSError(raise_err);
}
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(socket_connect_obj, socket_connect);
Expand Down Expand Up @@ -847,7 +929,7 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(esp_socket_getaddrinfo_obj, 2, 6, esp
STATIC mp_obj_t esp_socket_initialize() {
static int initialized = 0;
if (!initialized) {
ESP_LOGI("modsocket", "Initializing");
ESP_LOGI(TAG, "Initializing");
esp_netif_init();
initialized = 1;
}
Expand Down

0 comments on commit 57cce79

Please sign in to comment.