From 820c014578b56b9fa4d2c472e90b5060198dd404 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 12 Jul 2026 17:52:16 +0100 Subject: [PATCH] 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 --- lib/cfilters.c | 24 ++- lib/connect.c | 11 +- lib/connect.h | 2 +- lib/cshutdn.c | 4 +- lib/transfer.c | 2 +- tests/data/Makefile.am | 2 +- tests/data/test2606 | 18 ++ tests/http/test_19_shutdown.py | 34 ++++ tests/libtest/Makefile.inc | 1 + tests/libtest/cli_ev_download.c | 313 ++++++++++++++++++++++++++++++++ tests/unit/Makefile.inc | 1 + tests/unit/unit2606.c | 221 ++++++++++++++++++++++ 12 files changed, 612 insertions(+), 21 deletions(-) create mode 100644 tests/data/test2606 create mode 100644 tests/libtest/cli_ev_download.c create mode 100644 tests/unit/unit2606.c diff --git a/lib/cfilters.c b/lib/cfilters.c index fb1914d133..c5ee51a1b4 100644 --- a/lib/cfilters.c +++ b/lib/cfilters.c @@ -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; } diff --git a/lib/connect.c b/lib/connect.c index 6d6ddd2e91..86e97b955a 100644 --- a/lib/connect.c +++ b/lib/connect.c @@ -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); } /* diff --git a/lib/connect.h b/lib/connect.h index a5b7042d6e..20eedaf10e 100644 --- a/lib/connect.h +++ b/lib/connect.h @@ -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 diff --git a/lib/cshutdn.c b/lib/cshutdn.c index cc77e401fd..30cbaa552a 100644 --- a/lib/cshutdn.c +++ b/lib/cshutdn.c @@ -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; diff --git a/lib/transfer.c b/lib/transfer.c index 2eab7cabad..b7902de9a1 100644 --- a/lib/transfer.c +++ b/lib/transfer.c @@ -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) diff --git a/tests/data/Makefile.am b/tests/data/Makefile.am index 40e7c7da5c..e88472728b 100644 --- a/tests/data/Makefile.am +++ b/tests/data/Makefile.am @@ -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 \ diff --git a/tests/data/test2606 b/tests/data/test2606 new file mode 100644 index 0000000000..1ce80013a7 --- /dev/null +++ b/tests/data/test2606 @@ -0,0 +1,18 @@ + + + + +unittest + + + +# Client-side + + +unittest + + +Curl_conn_adjust_pollset unit test + + + diff --git a/tests/http/test_19_shutdown.py b/tests/http/test_19_shutdown.py index f8a948acd6..4ac447d758 100644 --- a/tests/http/test_19_shutdown.py +++ b/tests/http/test_19_shutdown.py @@ -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}' diff --git a/tests/libtest/Makefile.inc b/tests/libtest/Makefile.inc index 6fd8469dfa..07d78b5192 100644 --- a/tests/libtest/Makefile.inc +++ b/tests/libtest/Makefile.inc @@ -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 \ diff --git a/tests/libtest/cli_ev_download.c b/tests/libtest/cli_ev_download.c new file mode 100644 index 0000000000..e3ca6ad406 --- /dev/null +++ b/tests/libtest/cli_ev_download.c @@ -0,0 +1,313 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , 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; +} diff --git a/tests/unit/Makefile.inc b/tests/unit/Makefile.inc index c292e7a198..4fe2dc645b 100644 --- a/tests/unit/Makefile.inc +++ b/tests/unit/Makefile.inc @@ -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 \ diff --git a/tests/unit/unit2606.c b/tests/unit/unit2606.c new file mode 100644 index 0000000000..ebe8b275fd --- /dev/null +++ b/tests/unit/unit2606.c @@ -0,0 +1,221 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , 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)) +}