cfilters: fix event-based connection shutdown
No binary (build failed)
—
(log yok)
| @@ -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 \ | |
| @@ -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 \ | |
| @@ -0,0 +1,313 @@ | |
| + | /*************************************************************************** |
| + | * _ _ ____ _ |
| + | * Project ___| | | | _ \| | |
| + | * / __| | | | |_) | | |
| + | * | (__| |_| | _ <| |___ |
| + | * \___|\___/|_| \_\_____| |
| + | * |
| + | * Copyright (C) Daniel Stenberg, <[email protected]>, 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 \ | |
| @@ -0,0 +1,221 @@ | |
| + | /*************************************************************************** |
| + | * _ _ ____ _ |
| + | * Project ___| | | | _ \| | |
| + | * / __| | | | |_) | | |
| + | * | (__| |_| | _ <| |___ |
| + | * \___|\___/|_| \_\_____| |
| + | * |
| + | * Copyright (C) Daniel Stenberg, <[email protected]>, 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)) |
| + | } |