Commit 81c41dbf authored by Victor Stinner's avatar Victor Stinner

Issue #23618: socket.socket.connect() now waits until the connection completes

instead of raising InterruptedError if the connection is interrupted by
signals, signal handlers don't raise an exception and the socket is blocking or
has a timeout.

socket.socket.connect() still raise InterruptedError for non-blocking sockets.
parent 708d9ba5
...@@ -830,6 +830,19 @@ to sockets. ...@@ -830,6 +830,19 @@ to sockets.
Connect to a remote socket at *address*. (The format of *address* depends on the Connect to a remote socket at *address*. (The format of *address* depends on the
address family --- see above.) address family --- see above.)
If the connection is interrupted by a signal, the method waits until the
connection completes, or raise a :exc:`socket.timeout` on timeout, if the
signal handler doesn't raise an exception and the socket is blocking or has
a timeout. For non-blocking sockets, the method raises an
:exc:`InterruptedError` exception if the connection is interrupted by a
signal (or the exception raised by the signal handler).
.. versionchanged:: 3.5
The method now waits until the connection completes instead of raising an
:exc:`InterruptedError` exception if the connection is interrupted by a
signal, the signal handler doesn't raise an exception and the socket is
blocking or has a timeout (see the :pep:`475` for the rationale).
.. method:: socket.connect_ex(address) .. method:: socket.connect_ex(address)
......
...@@ -643,6 +643,7 @@ Changes in the Python API ...@@ -643,6 +643,7 @@ Changes in the Python API
- :func:`socket.socket` methods: - :func:`socket.socket` methods:
* :meth:`~socket.socket.accept` * :meth:`~socket.socket.accept`
* :meth:`~socket.socket.connect` (except for non-blocking sockets)
* :meth:`~socket.socket.recv` * :meth:`~socket.socket.recv`
* :meth:`~socket.socket.recvfrom` * :meth:`~socket.socket.recvfrom`
* :meth:`~socket.socket.recvmsg` * :meth:`~socket.socket.recvmsg`
......
...@@ -16,6 +16,12 @@ Core and Builtins ...@@ -16,6 +16,12 @@ Core and Builtins
Library Library
------- -------
- Issue #23618: :meth:`socket.socket.connect` now waits until the connection
completes instead of raising :exc:`InterruptedError` if the connection is
interrupted by signals, signal handlers don't raise an exception and the
socket is blocking or has a timeout. :meth:`socket.socket.connect` still
raise :exc:`InterruptedError` for non-blocking sockets.
- Issue #21526: Tkinter now supports new boolean type in Tcl 8.5. - Issue #21526: Tkinter now supports new boolean type in Tcl 8.5.
- Issue #23836: Fix the faulthandler module to handle reentrant calls to - Issue #23836: Fix the faulthandler module to handle reentrant calls to
......
...@@ -481,6 +481,19 @@ select_error(void) ...@@ -481,6 +481,19 @@ select_error(void)
(errno == expected) (errno == expected)
#endif #endif
#ifdef MS_WINDOWS
# define GET_SOCK_ERROR WSAGetLastError()
# define SET_SOCK_ERROR(err) WSASetLastError(err)
# define SOCK_TIMEOUT_ERR WSAEWOULDBLOCK
# define SOCK_INPROGRESS_ERR WSAEWOULDBLOCK
#else
# define GET_SOCK_ERROR errno
# define SET_SOCK_ERROR(err) do { errno = err; } while (0)
# define SOCK_TIMEOUT_ERR EWOULDBLOCK
# define SOCK_INPROGRESS_ERR EINPROGRESS
#endif
#ifdef MS_WINDOWS #ifdef MS_WINDOWS
/* Does WSASocket() support the WSA_FLAG_NO_HANDLE_INHERIT flag? */ /* Does WSASocket() support the WSA_FLAG_NO_HANDLE_INHERIT flag? */
static int support_wsa_no_inherit = -1; static int support_wsa_no_inherit = -1;
...@@ -592,8 +605,8 @@ internal_setblocking(PySocketSockObject *s, int block) ...@@ -592,8 +605,8 @@ internal_setblocking(PySocketSockObject *s, int block)
} }
static int static int
internal_select_impl(PySocketSockObject *s, int writing, _PyTime_t interval, internal_select(PySocketSockObject *s, int writing, _PyTime_t interval,
int error) int connect)
{ {
int n; int n;
#ifdef HAVE_POLL #ifdef HAVE_POLL
...@@ -610,43 +623,64 @@ internal_select_impl(PySocketSockObject *s, int writing, _PyTime_t interval, ...@@ -610,43 +623,64 @@ internal_select_impl(PySocketSockObject *s, int writing, _PyTime_t interval,
#endif #endif
/* Error condition is for output only */ /* Error condition is for output only */
assert(!(error && !writing)); assert(!(connect && !writing));
/* Nothing to do unless we're in timeout mode (not non-blocking) */
if (s->sock_timeout <= 0)
return 0;
/* Guard against closed socket */ /* Guard against closed socket */
if (s->sock_fd < 0) if (s->sock_fd < 0)
return 0; return 0;
/* Handling this condition here simplifies the select loops */ /* for connect(), we want to poll even if the socket is blocking */
if (interval < 0) if (!connect) {
return 1; /* Nothing to do unless we're in timeout mode (not non-blocking) */
if (s->sock_timeout <= 0)
return 0;
/* Handling this condition here simplifies the select loops */
if (interval < 0)
return 1;
}
/* Prefer poll, if available, since you can poll() any fd /* Prefer poll, if available, since you can poll() any fd
* which can't be done with select(). */ * which can't be done with select(). */
#ifdef HAVE_POLL #ifdef HAVE_POLL
pollfd.fd = s->sock_fd; pollfd.fd = s->sock_fd;
pollfd.events = writing ? POLLOUT : POLLIN; pollfd.events = writing ? POLLOUT : POLLIN;
if (error) if (connect) {
/* On Windows, the socket becomes writable on connection success,
but a connection failure is notified as an error. On POSIX, the
socket becomes writable on connection success or on connection
failure. */
pollfd.events |= POLLERR; pollfd.events |= POLLERR;
}
/* s->sock_timeout is in seconds, timeout in ms */ /* s->sock_timeout is in seconds, timeout in ms */
ms = _PyTime_AsMilliseconds(interval, _PyTime_ROUND_CEILING); if (interval >= 0)
ms = _PyTime_AsMilliseconds(interval, _PyTime_ROUND_CEILING);
else
ms = -1;
assert(ms <= INT_MAX); assert(ms <= INT_MAX);
Py_BEGIN_ALLOW_THREADS; Py_BEGIN_ALLOW_THREADS;
n = poll(&pollfd, 1, (int)ms); n = poll(&pollfd, 1, (int)ms);
Py_END_ALLOW_THREADS; Py_END_ALLOW_THREADS;
#else #else
_PyTime_AsTimeval_noraise(interval, &tv, _PyTime_ROUND_CEILING); if (interval >= 0)
_PyTime_AsTimeval_noraise(interval, &tv, _PyTime_ROUND_CEILING);
else {
tv.tv_sec = -1;
tv.tv_sec = 0;
}
FD_ZERO(&fds); FD_ZERO(&fds);
FD_SET(s->sock_fd, &fds); FD_SET(s->sock_fd, &fds);
FD_ZERO(&efds); FD_ZERO(&efds);
if (error) if (connect) {
/* On Windows, the socket becomes writable on connection success,
but a connection failure is notified as an error. On POSIX, the
socket becomes writable on connection success or on connection
failure. */
FD_SET(s->sock_fd, &efds); FD_SET(s->sock_fd, &efds);
}
/* See if the socket is ready */ /* See if the socket is ready */
Py_BEGIN_ALLOW_THREADS; Py_BEGIN_ALLOW_THREADS;
...@@ -666,43 +700,32 @@ internal_select_impl(PySocketSockObject *s, int writing, _PyTime_t interval, ...@@ -666,43 +700,32 @@ internal_select_impl(PySocketSockObject *s, int writing, _PyTime_t interval,
return 0; return 0;
} }
/* Do a select()/poll() on the socket, if necessary (sock_timeout > 0).
The argument writing indicates the direction.
This does not raise an exception; we'll let our caller do that
after they've reacquired the interpreter lock.
Returns 1 on timeout, -1 on error, 0 otherwise. */
static int
internal_select(PySocketSockObject *s, int writing, _PyTime_t interval)
{
return internal_select_impl(s, writing, interval, 0);
}
static int
internal_connect_select(PySocketSockObject *s)
{
return internal_select_impl(s, 1, s->sock_timeout, 1);
}
/* Call a socket function. /* Call a socket function.
Raise an exception and return -1 on error, return 0 on success. On error, raise an exception and return -1 if err is set, or fill err and
return -1 otherwise. If a signal was received and the signal handler raised
an exception, return -1, and set err to -1 if err is set.
On success, return 0, and set err to 0 if err is set.
If the socket has a timeout, wait until the socket is ready before calling If the socket has a timeout, wait until the socket is ready before calling
the function: wait until the socket is writable if writing is nonzero, wait the function: wait until the socket is writable if writing is nonzero, wait
until the socket received data otherwise. until the socket received data otherwise.
If the function func is interrupted by a signal (failed with EINTR): retry If the socket function is interrupted by a signal (failed with EINTR): retry
the function, except if the signal handler raised an exception (PEP 475). the function, except if the signal handler raised an exception (PEP 475).
When the function is retried, recompute the timeout using a monotonic clock. When the function is retried, recompute the timeout using a monotonic clock.
sock_call() must be called with the GIL held. The function func is called sock_call_ex() must be called with the GIL held. The socket function is
with the GIL released. */ called with the GIL released. */
static int static int
sock_call(PySocketSockObject *s, sock_call_ex(PySocketSockObject *s,
int writing, int writing,
int (*func) (PySocketSockObject *s, void *data), int (*sock_func) (PySocketSockObject *s, void *data),
void *data) void *data,
int connect,
int *err)
{ {
int has_timeout = (s->sock_timeout > 0); int has_timeout = (s->sock_timeout > 0);
_PyTime_t deadline = 0; _PyTime_t deadline = 0;
...@@ -713,27 +736,39 @@ sock_call(PySocketSockObject *s, ...@@ -713,27 +736,39 @@ sock_call(PySocketSockObject *s,
assert(PyGILState_Check()); assert(PyGILState_Check());
/* outer loop to retry select() when select() is interrupted by a signal /* outer loop to retry select() when select() is interrupted by a signal
or to retry select()+func() on false positive (see above) */ or to retry select()+sock_func() on false positive (see above) */
while (1) { while (1) {
if (has_timeout) { /* For connect(), poll even for blocking socket. The connection
runs asynchronously. */
if (has_timeout || connect) {
_PyTime_t interval; _PyTime_t interval;
if (deadline_initialized) { if (has_timeout) {
/* recompute the timeout */ if (deadline_initialized) {
interval = deadline - _PyTime_GetMonotonicClock(); /* recompute the timeout */
} interval = deadline - _PyTime_GetMonotonicClock();
else { }
deadline_initialized = 1; else {
deadline = _PyTime_GetMonotonicClock() + s->sock_timeout; deadline_initialized = 1;
interval = s->sock_timeout; deadline = _PyTime_GetMonotonicClock() + s->sock_timeout;
interval = s->sock_timeout;
}
} }
else
interval = -1;
res = internal_select(s, writing, interval); res = internal_select(s, writing, interval, connect);
if (res == -1) { if (res == -1) {
if (err)
*err = GET_SOCK_ERROR;
if (CHECK_ERRNO(EINTR)) { if (CHECK_ERRNO(EINTR)) {
/* select() was interrupted by a signal */ /* select() was interrupted by a signal */
if (PyErr_CheckSignals()) if (PyErr_CheckSignals()) {
if (err)
*err = -1;
return -1; return -1;
}
/* retry select() */ /* retry select() */
continue; continue;
...@@ -745,37 +780,49 @@ sock_call(PySocketSockObject *s, ...@@ -745,37 +780,49 @@ sock_call(PySocketSockObject *s,
} }
if (res == 1) { if (res == 1) {
PyErr_SetString(socket_timeout, "timed out"); if (err)
*err = SOCK_TIMEOUT_ERR;
else
PyErr_SetString(socket_timeout, "timed out");
return -1; return -1;
} }
/* the socket is ready */ /* the socket is ready */
} }
/* inner loop to retry func() when func() is interrupted by a signal */ /* inner loop to retry sock_func() when sock_func() is interrupted
by a signal */
while (1) { while (1) {
Py_BEGIN_ALLOW_THREADS Py_BEGIN_ALLOW_THREADS
res = func(s, data); res = sock_func(s, data);
Py_END_ALLOW_THREADS Py_END_ALLOW_THREADS
if (res) { if (res) {
/* func() succeeded */ /* sock_func() succeeded */
if (err)
*err = 0;
return 0; return 0;
} }
if (err)
*err = GET_SOCK_ERROR;
if (!CHECK_ERRNO(EINTR)) if (!CHECK_ERRNO(EINTR))
break; break;
/* func() was interrupted by a signal */ /* sock_func() was interrupted by a signal */
if (PyErr_CheckSignals()) if (PyErr_CheckSignals()) {
if (err)
*err = -1;
return -1; return -1;
}
/* retry func() */ /* retry sock_func() */
} }
if (s->sock_timeout > 0 if (s->sock_timeout > 0
&& (CHECK_ERRNO(EWOULDBLOCK) || CHECK_ERRNO(EAGAIN))) { && (CHECK_ERRNO(EWOULDBLOCK) || CHECK_ERRNO(EAGAIN))) {
/* False positive: func() failed with EWOULDBLOCK or EAGAIN. /* False positive: sock_func() failed with EWOULDBLOCK or EAGAIN.
For example, select() could indicate a socket is ready for For example, select() could indicate a socket is ready for
reading, but the data then discarded by the OS because of a reading, but the data then discarded by the OS because of a
...@@ -785,12 +832,23 @@ sock_call(PySocketSockObject *s, ...@@ -785,12 +832,23 @@ sock_call(PySocketSockObject *s,
continue; continue;
} }
/* func() failed */ /* sock_func() failed */
s->errorhandler(); if (!err)
s->errorhandler();
/* else: err was already set before */
return -1; return -1;
} }
} }
static int
sock_call(PySocketSockObject *s,
int writing,
int (*func) (PySocketSockObject *s, void *data),
void *data)
{
return sock_call_ex(s, writing, func, data, 0, NULL);
}
/* Initialize a new socket object. */ /* Initialize a new socket object. */
...@@ -2519,23 +2577,26 @@ The object cannot be used after this call, but the file descriptor\n\ ...@@ -2519,23 +2577,26 @@ The object cannot be used after this call, but the file descriptor\n\
can be reused for other purposes. The file descriptor is returned."); can be reused for other purposes. The file descriptor is returned.");
static int static int
internal_connect(PySocketSockObject *s, struct sockaddr *addr, int addrlen, sock_connect_impl(PySocketSockObject *s, void* Py_UNUSED(data))
int *timeoutp)
{ {
#ifdef MS_WINDOWS int err;
# define GET_ERROR WSAGetLastError() socklen_t size = sizeof err;
# define IN_PROGRESS_ERR WSAEWOULDBLOCK
# define TIMEOUT_ERR WSAEWOULDBLOCK
#else
# define GET_ERROR errno
# define IN_PROGRESS_ERR EINPROGRESS
# define TIMEOUT_ERR EWOULDBLOCK
#endif
int res, err, wait_connect, timeout; if (getsockopt(s->sock_fd, SOL_SOCKET, SO_ERROR, (void *)&err, &size)) {
socklen_t res_size; /* getsockopt() failed */
return 0;
}
if (err == EISCONN)
return 1;
return (err == 0);
}
*timeoutp = 0; static int
internal_connect(PySocketSockObject *s, struct sockaddr *addr, int addrlen,
int raise)
{
int res, err, wait_connect;
Py_BEGIN_ALLOW_THREADS Py_BEGIN_ALLOW_THREADS
res = connect(s->sock_fd, addr, addrlen); res = connect(s->sock_fd, addr, addrlen);
...@@ -2546,44 +2607,53 @@ internal_connect(PySocketSockObject *s, struct sockaddr *addr, int addrlen, ...@@ -2546,44 +2607,53 @@ internal_connect(PySocketSockObject *s, struct sockaddr *addr, int addrlen,
return 0; return 0;
} }
err = GET_ERROR; /* connect() failed */
if (CHECK_ERRNO(EINTR) && PyErr_CheckSignals())
return -1;
wait_connect = (s->sock_timeout > 0 && err == IN_PROGRESS_ERR
&& IS_SELECTABLE(s));
if (!wait_connect)
return err;
timeout = internal_connect_select(s); /* save error, PyErr_CheckSignals() can replace it */
if (timeout == -1) { err = GET_SOCK_ERROR;
/* select() failed */ if (CHECK_ERRNO(EINTR)) {
err = GET_ERROR; if (PyErr_CheckSignals())
if (CHECK_ERRNO(EINTR) && PyErr_CheckSignals())
return -1; return -1;
return err;
}
if (timeout == 1) { /* Issue #23618: when connect() fails with EINTR, the connection is
/* select() timed out */ running asynchronously.
*timeoutp = 1;
return TIMEOUT_ERR;
}
res_size = sizeof res; If the socket is blocking or has a timeout, wait until the
if (getsockopt(s->sock_fd, SOL_SOCKET, SO_ERROR, connection completes, fails or timed out using select(), and then
(void *)&res, &res_size)) { get the connection status using getsockopt(SO_ERROR).
/* getsockopt() failed */
return GET_ERROR; If the socket is non-blocking, raise InterruptedError. The caller is
responsible to wait until the connection completes, fails or timed
out (it's the case in asyncio for example). */
wait_connect = (s->sock_timeout != 0 && IS_SELECTABLE(s));
}
else {
wait_connect = (s->sock_timeout > 0 && err == SOCK_INPROGRESS_ERR
&& IS_SELECTABLE(s));
} }
if (res == EISCONN) if (!wait_connect) {
return 0; if (raise) {
return res; /* restore error, maybe replaced by PyErr_CheckSignals() */
SET_SOCK_ERROR(err);
s->errorhandler();
return -1;
}
else
return err;
}
#undef GET_ERROR if (raise) {
#undef IN_PROGRESS_ERR /* socket.connect() raises an exception on error */
#undef TIMEOUT_ERR if (sock_call_ex(s, 1, sock_connect_impl, NULL, 1, NULL) < 0)
return -1;
}
else {
/* socket.connect_ex() returns the error code on error */
if (sock_call_ex(s, 1, sock_connect_impl, NULL, 1, &err) < 0)
return err;
}
return 0;
} }
/* s.connect(sockaddr) method */ /* s.connect(sockaddr) method */
...@@ -2594,29 +2664,14 @@ sock_connect(PySocketSockObject *s, PyObject *addro) ...@@ -2594,29 +2664,14 @@ sock_connect(PySocketSockObject *s, PyObject *addro)
sock_addr_t addrbuf; sock_addr_t addrbuf;
int addrlen; int addrlen;
int res; int res;
int timeout;
if (!getsockaddrarg(s, addro, SAS2SA(&addrbuf), &addrlen)) if (!getsockaddrarg(s, addro, SAS2SA(&addrbuf), &addrlen))
return NULL; return NULL;
res = internal_connect(s, SAS2SA(&addrbuf), addrlen, &timeout); res = internal_connect(s, SAS2SA(&addrbuf), addrlen, 1);
if (res < 0) if (res < 0)
return NULL; return NULL;
if (timeout == 1) {
PyErr_SetString(socket_timeout, "timed out");
return NULL;
}
if (res != 0) {
#ifdef MS_WINDOWS
WSASetLastError(res);
#else
errno = res;
#endif
return s->errorhandler();
}
Py_RETURN_NONE; Py_RETURN_NONE;
} }
...@@ -2635,12 +2690,11 @@ sock_connect_ex(PySocketSockObject *s, PyObject *addro) ...@@ -2635,12 +2690,11 @@ sock_connect_ex(PySocketSockObject *s, PyObject *addro)
sock_addr_t addrbuf; sock_addr_t addrbuf;
int addrlen; int addrlen;
int res; int res;
int timeout;
if (!getsockaddrarg(s, addro, SAS2SA(&addrbuf), &addrlen)) if (!getsockaddrarg(s, addro, SAS2SA(&addrbuf), &addrlen))
return NULL; return NULL;
res = internal_connect(s, SAS2SA(&addrbuf), addrlen, &timeout); res = internal_connect(s, SAS2SA(&addrbuf), addrlen, 0);
if (res < 0) if (res < 0)
return NULL; return NULL;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment