mirror of
https://github.com/curl/curl.git
synced 2026-07-23 07:27:16 +03:00
cfilters: fix event-based connection shutdown
- Adjust pollset during connection shutdown. - Separate the FIRSTSOCKET and SECONDSOCKET check so that one being in connect or shutdown no longer adds poll events for the other. - Fix shutdown state evaluation (as detailed below). - Add a unit test for Curl_conn_adjust_pollset. - Add a client for event testing. - Test that shutdown sockets stay with the socket callback until done. The pollset predicate now reads the connection's own shutdown state rather than going through data->conn, and the EXPIRE_SHUTDOWN arming in cshutdn_perform() works again: next_expire_ms started at 0 and could never be set, so a peer that never sends a close_notify would still park its connection forever, timeout or not. Reported-by: pszemus@users.noreply.github.com Fixes https://github.com/curl/curl/issues/22282 Closes https://github.com/curl/curl/pull/22304
This commit is contained in:
parent
33dc64fd0e
commit
820c014578
12 changed files with 612 additions and 21 deletions
|
|
@ -206,7 +206,7 @@ CURLcode Curl_conn_shutdown(struct Curl_easy *data, int sockindex, bool *done)
|
|||
}
|
||||
|
||||
*done = FALSE;
|
||||
if(!Curl_shutdown_started(data, sockindex)) {
|
||||
if(!Curl_shutdown_started(data->conn, sockindex)) {
|
||||
Curl_shutdown_start(data, sockindex, 0);
|
||||
}
|
||||
else {
|
||||
|
|
@ -748,23 +748,29 @@ CURLcode Curl_conn_adjust_pollset(struct Curl_easy *data,
|
|||
struct easy_pollset *ps)
|
||||
{
|
||||
CURLcode result = CURLE_OK;
|
||||
bool want_io = !!ps->n;
|
||||
int i;
|
||||
|
||||
DEBUGASSERT(data);
|
||||
DEBUGASSERT(conn);
|
||||
/* During connect time, connection filters may add sockets to the pollset
|
||||
* even when the transfer neither wants to send nor receive. And those
|
||||
* sockets, when having events, are served.
|
||||
* Once connected however, a transfer that neither wants to send nor receive
|
||||
* sockets, when having events, are served. The same applies to a
|
||||
* filter chain whose shutdown has started.
|
||||
* Once a filter chain is connected however and before its shutdown
|
||||
* starts, a transfer that neither wants to send nor receive
|
||||
* will never call the connection filters. Any sockets added by the filters
|
||||
* will not change state and POLLIN/POLLOUT events will trigger forever,
|
||||
* making us busy loop. See #21671 */
|
||||
if(ps->n || !Curl_conn_is_connected(conn, FIRSTSOCKET) ||
|
||||
(conn->cfilter[SECONDARYSOCKET] &&
|
||||
!Curl_conn_is_connected(conn, SECONDARYSOCKET))) {
|
||||
for(i = 0; (i < 2) && !result && conn; ++i) {
|
||||
* making us busy loop. See #21671.
|
||||
* Gate each filter chain on its own state, so that one chain being in
|
||||
* connect or shutdown does not add poll events for the other. Check
|
||||
* against the transfer's own interest, before any chain added sockets
|
||||
* of its own. */
|
||||
for(i = 0; (i < 2) && !result; ++i) {
|
||||
if(conn->cfilter[i] &&
|
||||
(want_io || !Curl_conn_is_connected(conn, i) ||
|
||||
Curl_shutdown_started(conn, i)))
|
||||
result = Curl_conn_cf_adjust_pollset(conn->cfilter[i], data, ps);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ UNITTEST timediff_t timeleft_now_ms(struct Curl_easy *data,
|
|||
timediff_t timeleft_ms = 0;
|
||||
timediff_t ctimeleft_ms = 0;
|
||||
|
||||
if(Curl_shutdown_started(data, FIRSTSOCKET))
|
||||
if(data->conn && Curl_shutdown_started(data->conn, FIRSTSOCKET))
|
||||
return Curl_shutdown_timeleft(data, data->conn, FIRSTSOCKET);
|
||||
else if(Curl_is_connecting(data)) {
|
||||
timediff_t ctimeout_ms = (data->set.connecttimeout > 0) ?
|
||||
|
|
@ -164,13 +164,10 @@ void Curl_shutdown_clear(struct Curl_easy *data, int sockindex)
|
|||
memset(pt, 0, sizeof(*pt));
|
||||
}
|
||||
|
||||
bool Curl_shutdown_started(struct Curl_easy *data, int sockindex)
|
||||
bool Curl_shutdown_started(struct connectdata *conn, int sockindex)
|
||||
{
|
||||
if(data->conn) {
|
||||
struct curltime *pt = &data->conn->shutdown.start[sockindex];
|
||||
return (pt->tv_sec > 0) || (pt->tv_usec > 0);
|
||||
}
|
||||
return FALSE;
|
||||
const struct curltime *pt = &conn->shutdown.start[sockindex];
|
||||
return (pt->tv_sec > 0) || (pt->tv_usec > 0);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ timediff_t Curl_conn_shutdown_timeleft(struct Curl_easy *data,
|
|||
void Curl_shutdown_clear(struct Curl_easy *data, int sockindex);
|
||||
|
||||
/* TRUE iff shutdown has been started */
|
||||
bool Curl_shutdown_started(struct Curl_easy *data, int sockindex);
|
||||
bool Curl_shutdown_started(struct connectdata *conn, int sockindex);
|
||||
|
||||
/*
|
||||
* Used to extract socket and connectdata struct for the most recent
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ static void cshutdn_run_once(struct Curl_easy *data,
|
|||
/* We expect to be attached when called */
|
||||
DEBUGASSERT(data->conn == conn);
|
||||
|
||||
if(!Curl_shutdown_started(data, FIRSTSOCKET)) {
|
||||
if(!Curl_shutdown_started(conn, FIRSTSOCKET)) {
|
||||
Curl_shutdown_start(data, FIRSTSOCKET, 0);
|
||||
}
|
||||
|
||||
|
|
@ -253,7 +253,7 @@ static void cshutdn_perform(struct cshutdn *cshutdn,
|
|||
/* idata has one timer list, but maybe more than one connection.
|
||||
* Set EXPIRE_SHUTDOWN to the smallest time left for all. */
|
||||
ms = Curl_conn_shutdown_timeleft(data, conn);
|
||||
if(ms && ms < next_expire_ms)
|
||||
if(ms && (!next_expire_ms || (ms < next_expire_ms)))
|
||||
next_expire_ms = ms;
|
||||
}
|
||||
e = enext;
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ static bool xfer_recv_shutdown_started(struct Curl_easy *data)
|
|||
{
|
||||
if(!data || !data->conn)
|
||||
return FALSE;
|
||||
return Curl_shutdown_started(data, data->conn->recv_idx);
|
||||
return Curl_shutdown_started(data->conn, data->conn->recv_idx);
|
||||
}
|
||||
|
||||
CURLcode Curl_xfer_send_shutdown(struct Curl_easy *data, bool *done)
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ test2408 test2409 test2410 test2411 test2412 test2413 \
|
|||
\
|
||||
test2500 test2501 test2502 test2503 test2504 test2505 test2506 \
|
||||
\
|
||||
test2600 test2601 test2602 test2603 test2604 test2605 \
|
||||
test2600 test2601 test2602 test2603 test2604 test2605 test2606 \
|
||||
\
|
||||
test2700 test2701 test2702 test2703 test2704 test2705 test2706 test2707 \
|
||||
test2708 test2709 test2710 test2711 test2712 test2713 test2714 test2715 \
|
||||
|
|
|
|||
18
tests/data/test2606
Normal file
18
tests/data/test2606
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="US-ASCII"?>
|
||||
<testcase>
|
||||
<info>
|
||||
<keywords>
|
||||
unittest
|
||||
</keywords>
|
||||
</info>
|
||||
|
||||
# Client-side
|
||||
<client>
|
||||
<features>
|
||||
unittest
|
||||
</features>
|
||||
<name>
|
||||
Curl_conn_adjust_pollset unit test
|
||||
</name>
|
||||
</client>
|
||||
</testcase>
|
||||
|
|
@ -211,3 +211,37 @@ class TestShutdown:
|
|||
if re.match(r'.*SHUTDOWN] shutdown, done=1', line)]
|
||||
# we see less clean shutdowns as total limit forces early closes
|
||||
assert len(shutdowns) < count, f'{shutdowns}'
|
||||
|
||||
# Strictly event-based transfers, no connection reuse. Connections
|
||||
# whose graceful shutdown cannot finish right away must have their
|
||||
# socket stay registered with the application's socket callback,
|
||||
# or they never make progress and leak until multi cleanup.
|
||||
@pytest.mark.parametrize("proto", ['http/1.1'])
|
||||
def test_19_08_event_shutdown_watched(self, env: Env, httpd, proto):
|
||||
if not env.curl_is_debug():
|
||||
pytest.skip('only works for curl debug builds')
|
||||
count = 5
|
||||
docname = 'data.json'
|
||||
url = f'https://localhost:{env.https_port}/{docname}'
|
||||
client = LocalClient(name='cli_ev_download', env=env, run_env={
|
||||
# make socket receives block often, so the TLS shutdown
|
||||
# cannot finish on its first attempt
|
||||
'CURL_DBG_SOCK_RBLOCK': '90',
|
||||
'CURL_DEBUG': 'ssl,multi'
|
||||
})
|
||||
if not client.exists():
|
||||
pytest.skip(f'example client not built: {client.name}')
|
||||
r = client.run(args=[
|
||||
'-n', f'{count}', '-C', env.ca.cert_file, url
|
||||
])
|
||||
r.check_exit_code(0)
|
||||
m = None
|
||||
for line in r.stderr.splitlines():
|
||||
m = re.match(r'.*\[ev] final: watched=(\d+) socks_left=(\d+)', line)
|
||||
if m:
|
||||
break
|
||||
assert m, f'no client event summary found: {r.stderr}'
|
||||
# shutdown sockets were watched after transfers finished and
|
||||
# all shutdowns finished within the event loop
|
||||
assert int(m.group(1)) > 0, f'{r.stderr}'
|
||||
assert int(m.group(2)) == 0, f'{r.stderr}'
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ TOOLX_C = \
|
|||
|
||||
# All libtest programs
|
||||
TESTS_C = \
|
||||
cli_ev_download.c \
|
||||
cli_ftp_upload.c \
|
||||
cli_h2_pausing.c \
|
||||
cli_h2_serverpush.c \
|
||||
|
|
|
|||
313
tests/libtest/cli_ev_download.c
Normal file
313
tests/libtest/cli_ev_download.c
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
/***************************************************************************
|
||||
* _ _ ____ _
|
||||
* Project ___| | | | _ \| |
|
||||
* / __| | | | |_) | |
|
||||
* | (__| |_| | _ <| |___
|
||||
* \___|\___/|_| \_\_____|
|
||||
*
|
||||
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
|
||||
*
|
||||
* This software is licensed as described in the file COPYING, which
|
||||
* you should have received as part of this distribution. The terms
|
||||
* are also available at https://curl.se/docs/copyright.html.
|
||||
*
|
||||
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
|
||||
* copies of the Software, and permit persons to whom the Software is
|
||||
* furnished to do so, under the terms of the COPYING file.
|
||||
*
|
||||
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
|
||||
* KIND, either express or implied.
|
||||
*
|
||||
* SPDX-License-Identifier: curl
|
||||
*
|
||||
***************************************************************************/
|
||||
#include "first.h"
|
||||
|
||||
/* An event-based download client, driving transfers purely via
|
||||
* curl_multi_socket_action() with its own socket/timer callbacks, the
|
||||
* way an event-based application does. It runs its transfers serially
|
||||
* with CURLOPT_FORBID_REUSE, so every transfer's connection enters
|
||||
* shutdown when done, and reports whether the connection sockets
|
||||
* remain under event supervision until their shutdown finished. */
|
||||
|
||||
#define EV_DL_MAX_SOCKS 8
|
||||
|
||||
struct ev_dl_ctx {
|
||||
CURLM *multi;
|
||||
struct curltime timer_started;
|
||||
curl_socket_t fds[EV_DL_MAX_SOCKS];
|
||||
int actions[EV_DL_MAX_SOCKS];
|
||||
size_t nsocks;
|
||||
long timer_ms; /* last timeout set, -1 for none */
|
||||
};
|
||||
|
||||
static int ev_dl_socket_cb(CURL *easy, curl_socket_t fd, int what,
|
||||
void *clientp, void *socketp)
|
||||
{
|
||||
struct ev_dl_ctx *ctx = clientp;
|
||||
size_t i;
|
||||
|
||||
(void)easy;
|
||||
(void)socketp;
|
||||
for(i = 0; i < ctx->nsocks; ++i) {
|
||||
if(ctx->fds[i] == fd)
|
||||
break;
|
||||
}
|
||||
if(what == CURL_POLL_REMOVE) {
|
||||
if(i < ctx->nsocks) {
|
||||
ctx->nsocks--;
|
||||
ctx->fds[i] = ctx->fds[ctx->nsocks];
|
||||
ctx->actions[i] = ctx->actions[ctx->nsocks];
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(i == ctx->nsocks) {
|
||||
if(ctx->nsocks >= EV_DL_MAX_SOCKS) {
|
||||
curl_mfprintf(stderr, "[ev] too many sockets to track\n");
|
||||
return -1;
|
||||
}
|
||||
ctx->nsocks++;
|
||||
}
|
||||
ctx->fds[i] = fd;
|
||||
ctx->actions[i] = what;
|
||||
}
|
||||
curl_mfprintf(stderr, "[ev] socket fd=%ld what=%d, tracking %zu\n",
|
||||
(long)fd, what, ctx->nsocks);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int ev_dl_timer_cb(CURLM *multi, long timeout_ms, void *clientp)
|
||||
{
|
||||
struct ev_dl_ctx *ctx = clientp;
|
||||
|
||||
(void)multi;
|
||||
ctx->timer_ms = timeout_ms;
|
||||
if(timeout_ms >= 0)
|
||||
ctx->timer_started = curlx_now();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static size_t ev_dl_discard_cb(char *ptr, size_t size, size_t nmemb,
|
||||
void *userdata)
|
||||
{
|
||||
(void)ptr;
|
||||
(void)userdata;
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
/* wait for socket events or the timer, then feed libcurl. */
|
||||
static CURLMcode ev_dl_roundtrip(struct ev_dl_ctx *ctx)
|
||||
{
|
||||
curl_socket_t fds[EV_DL_MAX_SOCKS];
|
||||
int actions[EV_DL_MAX_SOCKS];
|
||||
struct timeval tv;
|
||||
fd_set rd, wr;
|
||||
size_t i, nsocks;
|
||||
long wait_ms = 100;
|
||||
int maxfd = -1;
|
||||
int running = 0;
|
||||
CURLMcode mres = CURLM_OK;
|
||||
|
||||
FD_ZERO(&rd);
|
||||
FD_ZERO(&wr);
|
||||
for(i = 0; i < ctx->nsocks; ++i) {
|
||||
if(ctx->actions[i] & CURL_POLL_IN)
|
||||
FD_SET(ctx->fds[i], &rd);
|
||||
if(ctx->actions[i] & CURL_POLL_OUT)
|
||||
FD_SET(ctx->fds[i], &wr);
|
||||
if((int)ctx->fds[i] > maxfd)
|
||||
maxfd = (int)ctx->fds[i];
|
||||
}
|
||||
if(ctx->timer_ms >= 0) {
|
||||
long left = ctx->timer_ms -
|
||||
(long)curlx_timediff_ms(curlx_now(), ctx->timer_started);
|
||||
if(left < 0)
|
||||
left = 0;
|
||||
if(left < wait_ms)
|
||||
wait_ms = left;
|
||||
}
|
||||
tv.tv_sec = wait_ms / 1000;
|
||||
tv.tv_usec = (int)(wait_ms % 1000) * 1000;
|
||||
select_wrapper(maxfd + 1, &rd, &wr, NULL, &tv);
|
||||
|
||||
/* the callbacks change our socket table while we dispatch */
|
||||
nsocks = ctx->nsocks;
|
||||
memcpy(fds, ctx->fds, sizeof(fds));
|
||||
memcpy(actions, ctx->actions, sizeof(actions));
|
||||
for(i = 0; i < nsocks; ++i) {
|
||||
int ev_bitmask = 0;
|
||||
if((actions[i] & CURL_POLL_IN) && FD_ISSET(fds[i], &rd))
|
||||
ev_bitmask |= CURL_CSELECT_IN;
|
||||
if((actions[i] & CURL_POLL_OUT) && FD_ISSET(fds[i], &wr))
|
||||
ev_bitmask |= CURL_CSELECT_OUT;
|
||||
if(ev_bitmask) {
|
||||
mres = curl_multi_socket_action(ctx->multi, fds[i], ev_bitmask,
|
||||
&running);
|
||||
if(mres)
|
||||
return mres;
|
||||
}
|
||||
}
|
||||
|
||||
if((ctx->timer_ms >= 0) &&
|
||||
(curlx_timediff_ms(curlx_now(), ctx->timer_started) >= ctx->timer_ms)) {
|
||||
ctx->timer_ms = -1;
|
||||
mres = curl_multi_socket_action(ctx->multi, CURL_SOCKET_TIMEOUT, 0,
|
||||
&running);
|
||||
}
|
||||
return mres;
|
||||
}
|
||||
|
||||
static void usage_ev_download(const char *msg)
|
||||
{
|
||||
if(msg)
|
||||
curl_mfprintf(stderr, "%s\n", msg);
|
||||
curl_mfprintf(stderr,
|
||||
"usage: [options] url\n"
|
||||
" event-based downloads with connection reuse forbidden\n"
|
||||
" -n number of transfers to do serially (default: 5)\n"
|
||||
" -C certfile for CA verification\n"
|
||||
);
|
||||
}
|
||||
|
||||
static CURLcode test_cli_ev_download(const char *URL)
|
||||
{
|
||||
struct ev_dl_ctx ctx;
|
||||
CURLM *multi = NULL;
|
||||
char *cafile = NULL;
|
||||
const char *url;
|
||||
size_t transfer_count = 5;
|
||||
size_t i;
|
||||
int watched = 0;
|
||||
struct curltime started;
|
||||
CURLcode result = CURLE_OK;
|
||||
int ch;
|
||||
|
||||
(void)URL;
|
||||
memset(&ctx, 0, sizeof(ctx));
|
||||
ctx.timer_ms = -1;
|
||||
|
||||
while((ch = cgetopt(test_argc, test_argv, "hn:C:")) != -1) {
|
||||
switch(ch) {
|
||||
case 'h':
|
||||
usage_ev_download(NULL);
|
||||
result = (CURLcode)2;
|
||||
goto optcleanup;
|
||||
case 'n': {
|
||||
const char *opt = coptarg;
|
||||
curl_off_t num;
|
||||
if(!curlx_str_number(&opt, &num, LONG_MAX))
|
||||
transfer_count = (size_t)num;
|
||||
break;
|
||||
}
|
||||
case 'C':
|
||||
curlx_free(cafile);
|
||||
cafile = curlx_strdup(coptarg);
|
||||
break;
|
||||
default:
|
||||
usage_ev_download("invalid option");
|
||||
result = (CURLcode)1;
|
||||
goto optcleanup;
|
||||
}
|
||||
}
|
||||
test_argc -= coptind;
|
||||
test_argv += coptind;
|
||||
|
||||
if(test_argc != 1) {
|
||||
usage_ev_download("not enough arguments");
|
||||
result = (CURLcode)2;
|
||||
goto optcleanup;
|
||||
}
|
||||
url = test_argv[0];
|
||||
|
||||
if(curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK) {
|
||||
curl_mfprintf(stderr, "curl_global_init() failed\n");
|
||||
result = (CURLcode)3;
|
||||
goto optcleanup;
|
||||
}
|
||||
curl_global_trace("ids,time");
|
||||
|
||||
multi = curl_multi_init();
|
||||
if(!multi) {
|
||||
curl_mfprintf(stderr, "curl_multi_init() failed\n");
|
||||
result = (CURLcode)3;
|
||||
goto cleanup;
|
||||
}
|
||||
ctx.multi = multi;
|
||||
curl_multi_setopt(multi, CURLMOPT_SOCKETFUNCTION, ev_dl_socket_cb);
|
||||
curl_multi_setopt(multi, CURLMOPT_SOCKETDATA, &ctx);
|
||||
curl_multi_setopt(multi, CURLMOPT_TIMERFUNCTION, ev_dl_timer_cb);
|
||||
curl_multi_setopt(multi, CURLMOPT_TIMERDATA, &ctx);
|
||||
|
||||
for(i = 0; i < transfer_count && !result; ++i) {
|
||||
CURL *easy = curl_easy_init();
|
||||
int done = 0;
|
||||
int running = 0;
|
||||
|
||||
if(!easy) {
|
||||
curl_mfprintf(stderr, "[t-%zu] FAILED setup\n", i);
|
||||
result = (CURLcode)1;
|
||||
goto cleanup;
|
||||
}
|
||||
curl_easy_setopt(easy, CURLOPT_URL, url);
|
||||
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, ev_dl_discard_cb);
|
||||
curl_easy_setopt(easy, CURLOPT_FORBID_REUSE, 1L);
|
||||
curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
|
||||
if(cafile)
|
||||
curl_easy_setopt(easy, CURLOPT_CAINFO, cafile);
|
||||
|
||||
curl_multi_add_handle(multi, easy);
|
||||
curl_mfprintf(stderr, "[t-%zu] STARTED\n", i);
|
||||
curl_multi_socket_action(multi, CURL_SOCKET_TIMEOUT, 0, &running);
|
||||
|
||||
started = curlx_now();
|
||||
while(!done) {
|
||||
struct CURLMsg *m;
|
||||
int msgq = 0;
|
||||
|
||||
if(ev_dl_roundtrip(&ctx)) {
|
||||
curl_mfprintf(stderr, "[t-%zu] multi failure\n", i);
|
||||
result = (CURLcode)1;
|
||||
goto cleanup;
|
||||
}
|
||||
m = curl_multi_info_read(multi, &msgq);
|
||||
if(m && (m->msg == CURLMSG_DONE)) {
|
||||
done = 1;
|
||||
result = m->data.result;
|
||||
curl_mfprintf(stderr, "[t-%zu] FINISHED with result %d\n",
|
||||
i, (int)result);
|
||||
}
|
||||
else if(curlx_timediff_ms(curlx_now(), started) > (timediff_t)30000) {
|
||||
curl_mfprintf(stderr, "[t-%zu] transfer timed out\n", i);
|
||||
result = (CURLcode)1;
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
curl_multi_remove_handle(multi, easy);
|
||||
curl_easy_cleanup(easy);
|
||||
|
||||
/* The transfer is over and its connection may not be reused. A
|
||||
* graceful shutdown that could not finish right away needs its
|
||||
* socket watched by us, or it can never make progress. */
|
||||
curl_mfprintf(stderr, "[t-%zu] sockets tracked after done: %zu\n",
|
||||
i, ctx.nsocks);
|
||||
if(ctx.nsocks)
|
||||
watched++;
|
||||
}
|
||||
|
||||
/* drive the remaining shutdowns via socket events */
|
||||
started = curlx_now();
|
||||
while(ctx.nsocks &&
|
||||
(curlx_timediff_ms(curlx_now(), started) < (timediff_t)5000)) {
|
||||
if(ev_dl_roundtrip(&ctx))
|
||||
break;
|
||||
}
|
||||
curl_mfprintf(stderr, "[ev] final: watched=%d socks_left=%zu\n",
|
||||
watched, ctx.nsocks);
|
||||
|
||||
cleanup:
|
||||
curl_multi_cleanup(multi);
|
||||
curl_global_cleanup();
|
||||
optcleanup:
|
||||
curlx_free(cafile);
|
||||
return result;
|
||||
}
|
||||
|
|
@ -46,6 +46,7 @@ TESTS_C = \
|
|||
unit1961.c unit1979.c unit1980.c \
|
||||
unit2413.c \
|
||||
unit2600.c unit2601.c unit2602.c unit2603.c unit2604.c unit2605.c \
|
||||
unit2606.c \
|
||||
unit3200.c unit3205.c \
|
||||
unit3211.c unit3212.c unit3213.c unit3214.c unit3216.c unit3219.c \
|
||||
unit3227.c \
|
||||
|
|
|
|||
221
tests/unit/unit2606.c
Normal file
221
tests/unit/unit2606.c
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
/***************************************************************************
|
||||
* _ _ ____ _
|
||||
* Project ___| | | | _ \| |
|
||||
* / __| | | | |_) | |
|
||||
* | (__| |_| | _ <| |___
|
||||
* \___|\___/|_| \_\_____|
|
||||
*
|
||||
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
|
||||
*
|
||||
* This software is licensed as described in the file COPYING, which
|
||||
* you should have received as part of this distribution. The terms
|
||||
* are also available at https://curl.se/docs/copyright.html.
|
||||
*
|
||||
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
|
||||
* copies of the Software, and permit persons to whom the Software is
|
||||
* furnished to do so, under the terms of the COPYING file.
|
||||
*
|
||||
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
|
||||
* KIND, either express or implied.
|
||||
*
|
||||
* SPDX-License-Identifier: curl
|
||||
*
|
||||
***************************************************************************/
|
||||
#include "unitcheck.h"
|
||||
|
||||
#include "urldata.h"
|
||||
#include "cfilters.h"
|
||||
#include "curl_trc.h"
|
||||
#include "select.h"
|
||||
|
||||
/* Test the gating in Curl_conn_adjust_pollset(): each filter chain
|
||||
* of a connection must only get to adjust the pollset when the transfer
|
||||
* wants to send or receive, or when that chain itself is connecting or
|
||||
* shutting down. One chain being in connect or shutdown must not add
|
||||
* poll events for the other, see #21671. */
|
||||
|
||||
struct t2606_ctx {
|
||||
int calls;
|
||||
bool add_socket;
|
||||
curl_socket_t sock;
|
||||
};
|
||||
|
||||
static void t2606_cf_destroy(struct Curl_cfilter *cf,
|
||||
struct Curl_easy *data)
|
||||
{
|
||||
(void)data;
|
||||
/* ctx is owned by the test case, nothing to free */
|
||||
cf->ctx = NULL;
|
||||
}
|
||||
|
||||
static CURLcode t2606_cf_connect(struct Curl_cfilter *cf,
|
||||
struct Curl_easy *data,
|
||||
bool *done)
|
||||
{
|
||||
(void)cf;
|
||||
(void)data;
|
||||
*done = TRUE;
|
||||
return CURLE_OK;
|
||||
}
|
||||
|
||||
static CURLcode t2606_cf_adjust_pollset(struct Curl_cfilter *cf,
|
||||
struct Curl_easy *data,
|
||||
struct easy_pollset *ps)
|
||||
{
|
||||
struct t2606_ctx *ctx = cf->ctx;
|
||||
ctx->calls++;
|
||||
if(ctx->add_socket)
|
||||
return Curl_pollset_add_in(data, ps, ctx->sock);
|
||||
return CURLE_OK;
|
||||
}
|
||||
|
||||
static const struct Curl_cftype t2606_cft = {
|
||||
"TEST-PS",
|
||||
CF_TYPE_IP_CONNECT,
|
||||
CURL_LOG_LVL_NONE,
|
||||
t2606_cf_destroy,
|
||||
t2606_cf_connect,
|
||||
Curl_cf_def_shutdown,
|
||||
t2606_cf_adjust_pollset,
|
||||
Curl_cf_def_data_pending,
|
||||
Curl_cf_def_send,
|
||||
Curl_cf_def_recv,
|
||||
Curl_cf_def_cntrl,
|
||||
Curl_cf_def_conn_is_alive,
|
||||
Curl_cf_def_conn_keep_alive,
|
||||
Curl_cf_def_query,
|
||||
};
|
||||
|
||||
struct t2606_case {
|
||||
int id;
|
||||
bool has_c1; /* a SECONDARYSOCKET chain exists */
|
||||
bool c0_connected;
|
||||
bool c1_connected;
|
||||
bool c0_shutdown; /* shutdown started on the chain */
|
||||
bool c1_shutdown;
|
||||
bool xfer_io; /* the pollset holds a transfer socket already */
|
||||
bool c0_adds; /* chain 0 adds a socket when called */
|
||||
int exp_c0_calls;
|
||||
int exp_c1_calls;
|
||||
};
|
||||
|
||||
static CURLcode t2606_setup(CURL **easy)
|
||||
{
|
||||
CURLcode result = CURLE_OK;
|
||||
|
||||
global_init(CURL_GLOBAL_ALL);
|
||||
*easy = curl_easy_init();
|
||||
if(!*easy) {
|
||||
curl_global_cleanup();
|
||||
return CURLE_OUT_OF_MEMORY;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void t2606_stop(CURL *easy)
|
||||
{
|
||||
curl_easy_cleanup(easy);
|
||||
curl_global_cleanup();
|
||||
}
|
||||
|
||||
static void t2606_run(struct Curl_easy *data, const struct t2606_case *tc)
|
||||
{
|
||||
struct connectdata *conn;
|
||||
struct Curl_cfilter *cf0 = NULL, *cf1 = NULL;
|
||||
struct t2606_ctx ctx0, ctx1;
|
||||
struct easy_pollset ps;
|
||||
CURLcode result;
|
||||
char msg[128];
|
||||
|
||||
conn = curlx_calloc(1, sizeof(*conn));
|
||||
abort_if(!conn, "could not allocate a connection");
|
||||
|
||||
memset(&ctx0, 0, sizeof(ctx0));
|
||||
memset(&ctx1, 0, sizeof(ctx1));
|
||||
ctx0.sock = 42;
|
||||
ctx0.add_socket = tc->c0_adds;
|
||||
ctx1.sock = 43;
|
||||
|
||||
result = Curl_cf_create(&cf0, &t2606_cft, &ctx0);
|
||||
fail_unless(!result, "creating filter chain 0 failed");
|
||||
if(!result) {
|
||||
Curl_conn_cf_add(data, conn, FIRSTSOCKET, cf0);
|
||||
cf0->connected = tc->c0_connected;
|
||||
if(tc->c0_shutdown)
|
||||
conn->shutdown.start[FIRSTSOCKET] = curlx_now();
|
||||
}
|
||||
|
||||
if(tc->has_c1) {
|
||||
result = Curl_cf_create(&cf1, &t2606_cft, &ctx1);
|
||||
fail_unless(!result, "creating filter chain 1 failed");
|
||||
if(!result) {
|
||||
Curl_conn_cf_add(data, conn, SECONDARYSOCKET, cf1);
|
||||
cf1->connected = tc->c1_connected;
|
||||
if(tc->c1_shutdown)
|
||||
conn->shutdown.start[SECONDARYSOCKET] = curlx_now();
|
||||
}
|
||||
}
|
||||
|
||||
Curl_pollset_init(&ps);
|
||||
if(tc->xfer_io) {
|
||||
result = Curl_pollset_add_in(data, &ps, 7);
|
||||
fail_unless(!result, "preloading the pollset failed");
|
||||
}
|
||||
|
||||
result = Curl_conn_adjust_pollset(data, conn, &ps);
|
||||
|
||||
curl_msprintf(msg, "case %d: adjust_pollset failed", tc->id);
|
||||
fail_unless(!result, msg);
|
||||
curl_msprintf(msg, "case %d: chain 0 called %d times, expected %d",
|
||||
tc->id, ctx0.calls, tc->exp_c0_calls);
|
||||
fail_unless(ctx0.calls == tc->exp_c0_calls, msg);
|
||||
curl_msprintf(msg, "case %d: chain 1 called %d times, expected %d",
|
||||
tc->id, ctx1.calls, tc->exp_c1_calls);
|
||||
fail_unless(ctx1.calls == tc->exp_c1_calls, msg);
|
||||
if(tc->c0_adds && tc->exp_c0_calls) {
|
||||
curl_msprintf(msg, "case %d: chain 0 did not add its socket", tc->id);
|
||||
fail_unless(ps.n > 0, msg);
|
||||
}
|
||||
|
||||
Curl_pollset_cleanup(&ps);
|
||||
Curl_conn_cf_discard_all(data, conn, SECONDARYSOCKET);
|
||||
Curl_conn_cf_discard_all(data, conn, FIRSTSOCKET);
|
||||
unit_test_abort:
|
||||
curlx_free(conn);
|
||||
}
|
||||
|
||||
static CURLcode test_unit2606(const char *arg)
|
||||
{
|
||||
CURL *easy;
|
||||
|
||||
UNITTEST_BEGIN(t2606_setup(&easy))
|
||||
|
||||
static const struct t2606_case TEST_CASES[] = {
|
||||
/* both chains connected and quiet, transfer idle: nothing runs */
|
||||
{ 1, TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, 0, 0 },
|
||||
/* transfer wants IO: both chains run */
|
||||
{ 2, TRUE, TRUE, TRUE, FALSE, FALSE, TRUE, FALSE, 1, 1 },
|
||||
/* chain 0 connecting, chain 1 connected and quiet: only chain 0 */
|
||||
{ 3, TRUE, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, 1, 0 },
|
||||
/* chain 1 connecting, chain 0 connected and quiet: only chain 1 */
|
||||
{ 4, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, 0, 1 },
|
||||
/* chain 0 shutting down, chain 1 connected and quiet: only chain 0 */
|
||||
{ 5, TRUE, TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, 1, 0 },
|
||||
/* chain 1 shutting down, chain 0 connected and quiet: only chain 1 */
|
||||
{ 6, TRUE, TRUE, TRUE, FALSE, TRUE, FALSE, FALSE, 0, 1 },
|
||||
/* single connected quiet chain, transfer idle: nothing runs */
|
||||
{ 7, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, 0, 0 },
|
||||
/* single chain shutting down: runs */
|
||||
{ 8, FALSE, TRUE, FALSE, TRUE, FALSE, FALSE, FALSE, 1, 0 },
|
||||
/* a socket added by connecting chain 0 must not run quiet chain 1 */
|
||||
{ 9, TRUE, FALSE, TRUE, FALSE, FALSE, FALSE, TRUE, 1, 0 },
|
||||
};
|
||||
|
||||
size_t i;
|
||||
|
||||
for(i = 0; i < CURL_ARRAYSIZE(TEST_CASES); ++i) {
|
||||
t2606_run(easy, &TEST_CASES[i]);
|
||||
}
|
||||
|
||||
UNITTEST_END(t2606_stop(easy))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue