From 87cec65cae656f6ac2e702bd60dad6dd4fdae636 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Fri, 1 May 2026 09:08:35 +0000 Subject: [PATCH] curl-util: bring CurlGlue/CurlSlot in line with sd-bus and qmp-client Refactor curl-util to use the same per-request, refcounted, cancellable slot model as sd-bus, sd-varlink and qmp-client. CurlGlue becomes opaque and refcounted, and dispatches per-slot completion callbacks through CURLOPT_PRIVATE instead of a single g->on_finished demux that every caller had to switch on. The new curl_glue_perform_async(g, easy, cb, userdata, &slot) replaces curl_glue_add + the on_finished/userdata wiring. CurlSlot is the per-request handle: it owns the easy handle, curl_slot_unref does curl_multi_remove_handle + curl_easy_cleanup (which doubles as cancel since remove aborts in-flight transfers without queuing CURLMSG_DONE), and floating slots (ret_slot=NULL) are kept alive in the glue's slot set until the callback fires. Drop the userdata parameter from curl_glue_make: CURLOPT_PRIVATE is now used internally to route completions to the slot. Migrate pull-job and the pull-{oci,raw,tar} drivers, and imdsd, to the new shape. PullJob.curl becomes PullJob.slot; pull_job_curl_on_finished becomes a per-slot callback. imdsd routes its token-vs-data branch off slot identity rather than easy-handle pointer comparison. Both daemons drop the global on_finished/userdata wiring on the glue. pull_job_finish and context_fail{,_full} now return int (always 0) so the callbacks stay in the `return finish(...);` style. Add test-curl-util covering glue lifecycle, easy-handle defaults, floating and non-floating perform paths, cancel-via-slot-unref (verified by a sentinel request that drives the loop to completion), and three concurrent requests on a single glue. Tests fetch local files via file:// URLs so no network is needed; libcurl availability is probed once via dlopen_curl in intro(). --- src/imds/imdsd.c | 101 ++++++------- src/import/pull-job.c | 162 +++++++++------------ src/import/pull-job.h | 5 +- src/import/pull-oci.c | 3 - src/import/pull-raw.c | 3 - src/import/pull-tar.c | 3 - src/shared/curl-util.c | 176 ++++++++++++++++++++--- src/shared/curl-util.h | 45 ++++-- src/shared/shared-forward.h | 2 + src/test/meson.build | 4 + src/test/test-curl-util.c | 280 ++++++++++++++++++++++++++++++++++++ 11 files changed, 585 insertions(+), 199 deletions(-) create mode 100644 src/test/test-curl-util.c diff --git a/src/imds/imdsd.c b/src/imds/imdsd.c index a0c54ad84d7..9c194c09005 100644 --- a/src/imds/imdsd.c +++ b/src/imds/imdsd.c @@ -182,8 +182,8 @@ struct Context { /* Mode 1 "direct": we go directly to the network (this is done if we know the interface index to * use) */ - CURL *curl_token; - CURL *curl_data; + CurlSlot *slot_token; + CurlSlot *slot_data; struct curl_slist *request_header_token, *request_header_data; sd_event_source *retry_source; unsigned n_retry; @@ -247,15 +247,8 @@ static void context_reset_for_refresh(Context *c) { /* Flush out all fields, up to the point we can restart the current request */ - if (c->curl_token) { - curl_glue_remove_and_free(c->glue, c->curl_token); - c->curl_token = NULL; - } - - if (c->curl_data) { - curl_glue_remove_and_free(c->glue, c->curl_data); - c->curl_data = NULL; - } + c->slot_token = curl_slot_unref(c->slot_token); + c->slot_data = curl_slot_unref(c->slot_data); sym_curl_slist_free_all(c->request_header_token); c->request_header_token = NULL; @@ -325,11 +318,12 @@ static void context_done(Context *c) { c->system_bus = sd_bus_flush_close_unref(c->system_bus); } -static void context_fail_full(Context *c, int r, const char *varlink_error) { +static int context_fail_full(Context *c, int r, const char *varlink_error) { assert(c); assert(r != 0); - /* Called whenever the current retrieval fails asynchronously */ + /* Called whenever the current retrieval fails asynchronously. Returns 0 so callers in + * int-returning paths can `return context_fail_full(...)` directly. */ r = -abs(r); @@ -349,10 +343,11 @@ static void context_fail_full(Context *c, int r, const char *varlink_error) { sd_event_exit(c->event, r); context_reset_full(c); + return 0; } -static void context_fail(Context *c, int r) { - context_fail_full(c, r, /* varlink_error= */ NULL); +static int context_fail(Context *c, int r) { + return context_fail_full(c, r, /* varlink_error= */ NULL); } static void context_success(Context *c) { @@ -898,17 +893,12 @@ static int context_save_data(Context *c) { return 0; } -static void curl_glue_on_finished(CurlGlue *g, CURL *curl, CURLcode result) { +static int curl_on_finished(CurlSlot *slot, CURL *curl, CURLcode result, void *userdata) { + Context *c = ASSERT_PTR(userdata); int r; - assert(g); - /* Called whenever libcurl did its thing and reports a download being complete or having failed */ - Context *c = NULL; - if (sym_curl_easy_getinfo(curl, CURLINFO_PRIVATE, (char**) &c) != CURLE_OK) - return; - switch (result) { case CURLE_OK: /* yay! */ @@ -934,7 +924,7 @@ static void curl_glue_on_finished(CurlGlue *g, CURL *curl, CURLcode result) { if (r < 0) return context_fail(c, r); - return; + return 0; default: return context_fail_full( @@ -951,12 +941,12 @@ static void curl_glue_on_finished(CurlGlue *g, CURL *curl, CURLcode result) { return context_fail(c, r); if (r == 0) { /* We shall retry */ (void) context_schedule_retry(c); - return; + return 0; } if (result != CURLE_OK) /* if getting the HTTP status didn't work, propagate a generic error */ return context_fail(c, SYNTHETIC_ERRNO(ENOTRECOVERABLE)); - if (curl == c->curl_token) { + if (slot == c->slot_token) { r = context_validate_token_http_status(c, status); if (r < 0) return context_fail(c, r); @@ -975,7 +965,7 @@ static void curl_glue_on_finished(CurlGlue *g, CURL *curl, CURLcode result) { if (r < 0) return context_fail(c, r); - } else if (curl == c->curl_data) { + } else if (slot == c->slot_data) { r = context_validate_data_http_status(c, status); if (r == -ENOENT) @@ -983,7 +973,7 @@ static void curl_glue_on_finished(CurlGlue *g, CURL *curl, CURLcode result) { if (r < 0) return context_fail(c, r); if (r == 0) /* Immediately restarted */ - return; + return 0; context_log(c, LOG_DEBUG, "Data download successful."); @@ -994,6 +984,8 @@ static void curl_glue_on_finished(CurlGlue *g, CURL *curl, CURLcode result) { context_success(c); } else assert_not_reached(); + + return 0; } static int context_acquire_glue(Context *c) { @@ -1010,9 +1002,6 @@ static int context_acquire_glue(Context *c) { if (r < 0) return context_log_errno(c, LOG_ERR, r, "Failed to allocate curl glue: %m"); - c->glue->on_finished = curl_glue_on_finished; - c->glue->userdata = c; - return 0; } @@ -1028,13 +1017,13 @@ static size_t data_write_callback(void *contents, size_t size, size_t nmemb, voi (void) context_save_ifname(c); /* Before we use the acquired data, let's verify the HTTP status, if there's a failure or we need to - * restart, abort the write here. Note that the curl_glue_on_finished() call will then check the HTTP + * restart, abort the write here. Note that the curl_on_finished() call will then check the HTTP * status again and act on it. */ long status; - r = context_acquire_http_status(c, c->curl_data, &status); + r = context_acquire_http_status(c, curl_slot_get_easy(c->slot_data), &status); if (r <= 0) - return 0; /* fail the thing, so that curl_glue_on_finished() can handle this failure or retry request */ - if (status >= 300) /* any status equal or above 300 needs to be handled by curl_glue_on_finished() too */ + return 0; /* fail the thing, so that curl_on_finished() can handle this failure or retry request */ + if (status >= 300) /* any status equal or above 300 needs to be handled by curl_on_finished() too */ return 0; if (sz > UINT64_MAX - c->data_size || @@ -1103,7 +1092,8 @@ static int context_acquire_data(Context *c) { if (!url) return context_log_oom(c); - r = curl_glue_make(&c->curl_data, url, c); + _cleanup_(curl_easy_cleanupp) CURL *easy = NULL; + r = curl_glue_make(&easy, url); if (r < 0) return context_log_errno(c, LOG_ERR, r, "Failed to create CURL request for data: %m"); @@ -1122,30 +1112,31 @@ static int context_acquire_data(Context *c) { return context_log_errno(c, LOG_ERR, r, "Failed to create curl header: %m"); if (c->request_header_data) - if (sym_curl_easy_setopt(c->curl_data, CURLOPT_HTTPHEADER, c->request_header_data) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_HTTPHEADER, c->request_header_data) != CURLE_OK) return context_log_errno(c, LOG_ERR, SYNTHETIC_ERRNO(EIO), "Failed to set HTTP request header."); - if (sym_curl_easy_setopt(c->curl_data, CURLOPT_WRITEFUNCTION, data_write_callback) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, data_write_callback) != CURLE_OK) return context_log_errno(c, LOG_ERR, SYNTHETIC_ERRNO(EIO), "Failed to set CURL write function."); - if (sym_curl_easy_setopt(c->curl_data, CURLOPT_WRITEDATA, c) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_WRITEDATA, c) != CURLE_OK) return context_log_errno(c, LOG_ERR, SYNTHETIC_ERRNO(EIO), "Failed to set CURL write function userdata."); - if (sym_curl_easy_setopt(c->curl_data, CURLOPT_SOCKOPTFUNCTION, setsockopt_callback) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_SOCKOPTFUNCTION, setsockopt_callback) != CURLE_OK) return context_log_errno(c, LOG_ERR, SYNTHETIC_ERRNO(EIO), "Failed to set CURL setsockopt function."); - if (sym_curl_easy_setopt(c->curl_data, CURLOPT_SOCKOPTDATA, c) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_SOCKOPTDATA, c) != CURLE_OK) return context_log_errno(c, LOG_ERR, SYNTHETIC_ERRNO(EIO), "Failed to set CURL setsockopt function userdata."); - if (sym_curl_easy_setopt(c->curl_data, CURLOPT_LOCALPORT, 1L) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_LOCALPORT, 1L) != CURLE_OK) return context_log_errno(c, LOG_ERR, SYNTHETIC_ERRNO(EIO), "Failed to set CURL setsockopt local port"); - if (sym_curl_easy_setopt(c->curl_data, CURLOPT_LOCALPORTRANGE, 1023L) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_LOCALPORTRANGE, 1023L) != CURLE_OK) return context_log_errno(c, LOG_ERR, SYNTHETIC_ERRNO(EIO), "Failed to set CURL setsockopt local port range"); - r = curl_glue_add(c->glue, c->curl_data); + r = curl_glue_perform_async(c->glue, easy, curl_on_finished, c, &c->slot_data); if (r < 0) return context_log_errno(c, LOG_ERR, r, "Failed to add CURL request to glue: %m"); + TAKE_PTR(easy); return 0; } @@ -1163,10 +1154,10 @@ static size_t token_write_callback(void *contents, size_t size, size_t nmemb, vo /* Before we use acquired data, let's verify the HTTP status */ long status; - r = context_acquire_http_status(c, c->curl_token, &status); + r = context_acquire_http_status(c, curl_slot_get_easy(c->slot_token), &status); if (r <= 0) - return 0; /* fail the thing, so that curl_glue_on_finished() can handle this failure or retry request */ - if (status >= 300) /* any status equal or above 300 needs to be handled by curl_glue_on_finished() */ + return 0; /* fail the thing, so that curl_on_finished() can handle this failure or retry request */ + if (status >= 300) /* any status equal or above 300 needs to be handled by curl_on_finished() */ return 0; if (sz > SIZE_MAX - c->token.iov_len || @@ -1199,7 +1190,8 @@ static int context_acquire_token(Context *c) { if (r < 0) return r; - r = curl_glue_make(&c->curl_token, arg_token_url, c); + _cleanup_(curl_easy_cleanupp) CURL *easy = NULL; + r = curl_glue_make(&easy, arg_token_url); if (r < 0) return context_log_errno(c, LOG_ERR, r, "Failed to create CURL request for API token: %m"); @@ -1216,27 +1208,28 @@ static int context_acquire_token(Context *c) { return context_log_oom(c); } - if (sym_curl_easy_setopt(c->curl_token, CURLOPT_HTTPHEADER, c->request_header_token) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_HTTPHEADER, c->request_header_token) != CURLE_OK) return context_log_errno(c, LOG_ERR, SYNTHETIC_ERRNO(EIO), "Failed to set HTTP request header."); - if (sym_curl_easy_setopt(c->curl_token, CURLOPT_CUSTOMREQUEST, "PUT") != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_CUSTOMREQUEST, "PUT") != CURLE_OK) return context_log_errno(c, LOG_ERR, SYNTHETIC_ERRNO(EIO), "Failed to set HTTP request method."); - if (sym_curl_easy_setopt(c->curl_token, CURLOPT_WRITEFUNCTION, token_write_callback) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, token_write_callback) != CURLE_OK) return context_log_errno(c, LOG_ERR, SYNTHETIC_ERRNO(EIO), "Failed to set CURL write function."); - if (sym_curl_easy_setopt(c->curl_token, CURLOPT_WRITEDATA, c) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_WRITEDATA, c) != CURLE_OK) return context_log_errno(c, LOG_ERR, SYNTHETIC_ERRNO(EIO), "Failed to set CURL write function userdata."); - if (sym_curl_easy_setopt(c->curl_token, CURLOPT_SOCKOPTFUNCTION, setsockopt_callback) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_SOCKOPTFUNCTION, setsockopt_callback) != CURLE_OK) return context_log_errno(c, LOG_ERR, SYNTHETIC_ERRNO(EIO), "Failed to set CURL setsockopt function."); - if (sym_curl_easy_setopt(c->curl_token, CURLOPT_SOCKOPTDATA, c) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_SOCKOPTDATA, c) != CURLE_OK) return context_log_errno(c, LOG_ERR, SYNTHETIC_ERRNO(EIO), "Failed to set CURL setsockopt function userdata."); - r = curl_glue_add(c->glue, c->curl_token); + r = curl_glue_perform_async(c->glue, easy, curl_on_finished, c, &c->slot_token); if (r < 0) return context_log_errno(c, LOG_ERR, r, "Failed to add CURL request to glue: %m"); + TAKE_PTR(easy); return 0; } diff --git a/src/import/pull-job.c b/src/import/pull-job.c index 4c3fb05dd35..5b8aa6da269 100644 --- a/src/import/pull-job.c +++ b/src/import/pull-job.c @@ -53,7 +53,7 @@ PullJob* pull_job_unref(PullJob *j) { pull_job_close_disk_fd(j); - curl_glue_remove_and_free(j->glue, j->curl); + curl_slot_unref(j->slot); sym_curl_slist_free_all(j->request_header); j->compress = compressor_free(j->compress); @@ -83,11 +83,13 @@ static const char* pull_job_description(PullJob *j) { return j->description ?: j->url; } -static void pull_job_finish(PullJob *j, int ret) { +static int pull_job_finish(PullJob *j, int ret) { assert(j); + /* Returns 0 so callers in int-returning paths can `return pull_job_finish(...)` directly. */ + if (IN_SET(j->state, PULL_JOB_DONE, PULL_JOB_FAILED)) - return; + return 0; if (ret == 0) { j->state = PULL_JOB_DONE; @@ -100,6 +102,8 @@ static void pull_job_finish(PullJob *j, int ret) { if (j->on_finished) j->on_finished(j); + + return 0; } int pull_job_restart(PullJob *j, const char *new_url) { @@ -134,8 +138,7 @@ int pull_job_restart(PullJob *j, const char *new_url) { j->expected_content_length = UINT64_MAX; } - curl_glue_remove_and_free(j->glue, j->curl); - j->curl = NULL; + j->slot = curl_slot_unref(j->slot); j->compress = compressor_free(j->compress); @@ -160,23 +163,18 @@ static uint64_t pull_job_content_length_effective(PullJob *j) { return j->content_length; } -void pull_job_curl_on_finished(CurlGlue *g, CURL *curl, CURLcode result) { - PullJob *j = NULL; +static int pull_job_curl_on_finished(CurlSlot *slot, CURL *curl, CURLcode result, void *userdata) { + PullJob *j = ASSERT_PTR(userdata); char *scheme = NULL; CURLcode code; int r; - if (sym_curl_easy_getinfo(curl, CURLINFO_PRIVATE, (char **)&j) != CURLE_OK) - return; - - if (!j || IN_SET(j->state, PULL_JOB_DONE, PULL_JOB_FAILED)) - return; + if (IN_SET(j->state, PULL_JOB_DONE, PULL_JOB_FAILED)) + return 0; code = sym_curl_easy_getinfo(curl, CURLINFO_SCHEME, &scheme); - if (code != CURLE_OK || !scheme) { - r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to retrieve URL scheme."); - goto finish; - } + if (code != CURLE_OK || !scheme) + return pull_job_finish(j, log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to retrieve URL scheme.")); if (strcaseeq(scheme, "FILE") && result == CURLE_FILE_COULDNT_READ_FILE && j->on_not_found) { _cleanup_free_ char *new_url = NULL; @@ -184,43 +182,37 @@ void pull_job_curl_on_finished(CurlGlue *g, CURL *curl, CURLcode result) { /* This resource wasn't found, but the implementer wants to maybe let us know a new URL, query for it. */ r = j->on_not_found(j, &new_url); if (r < 0) - goto finish; + return pull_job_finish(j, r); if (r > 0) { /* A new url to use */ assert(new_url); r = pull_job_restart(j, new_url); if (r < 0) - goto finish; + return pull_job_finish(j, r); - return; + return 0; } /* if this didn't work, handle like any other error below */ } - if (result != CURLE_OK) { - r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Transfer failed: %s", sym_curl_easy_strerror(result)); - goto finish; - } + if (result != CURLE_OK) + return pull_job_finish(j, log_error_errno(SYNTHETIC_ERRNO(EIO), "Transfer failed: %s", sym_curl_easy_strerror(result))); if (STRCASE_IN_SET(scheme, "HTTP", "HTTPS")) { long status; code = sym_curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status); - if (code != CURLE_OK) { - r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to retrieve response code: %s", sym_curl_easy_strerror(code)); - goto finish; - } + if (code != CURLE_OK) + return pull_job_finish(j, log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to retrieve response code: %s", sym_curl_easy_strerror(code))); if (http_status_etag_exists(status)) { log_info("Image already downloaded. Skipping download."); j->etag_exists = true; - r = 0; - goto finish; + return pull_job_finish(j, 0); } else if (http_status_need_authentication(status)) { log_info("Access to image requires authentication."); - r = -ENOKEY; - goto finish; + return pull_job_finish(j, -ENOKEY); } else if (status >= 300) { if (status == 404 && j->on_not_found) { @@ -229,81 +221,64 @@ void pull_job_curl_on_finished(CurlGlue *g, CURL *curl, CURLcode result) { /* This resource wasn't found, but the implementer wants to maybe let us know a new URL, query for it. */ r = j->on_not_found(j, &new_url); if (r < 0) - goto finish; + return pull_job_finish(j, r); if (r > 0) { /* A new url to use */ assert(new_url); r = pull_job_restart(j, new_url); if (r < 0) - goto finish; + return pull_job_finish(j, r); - code = sym_curl_easy_getinfo(j->curl, CURLINFO_RESPONSE_CODE, &status); - if (code != CURLE_OK) { - r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to retrieve response code: %s", sym_curl_easy_strerror(code)); - goto finish; - } + code = sym_curl_easy_getinfo(curl_slot_get_easy(j->slot), CURLINFO_RESPONSE_CODE, &status); + if (code != CURLE_OK) + return pull_job_finish(j, log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to retrieve response code: %s", sym_curl_easy_strerror(code))); if (status == 0) - return; + return 0; } } - r = log_notice_errno( + return pull_job_finish(j, log_notice_errno( status == 404 ? SYNTHETIC_ERRNO(ENOMEDIUM) : SYNTHETIC_ERRNO(EIO), /* Make the most common error recognizable */ - "HTTP request to %s failed with code %li.", j->url, status); - goto finish; - } else if (status < 200) { - r = log_error_errno(SYNTHETIC_ERRNO(EIO), "HTTP request to %s finished with unexpected code %li.", j->url, status); - goto finish; - } + "HTTP request to %s failed with code %li.", j->url, status)); + } else if (status < 200) + return pull_job_finish(j, log_error_errno(SYNTHETIC_ERRNO(EIO), "HTTP request to %s finished with unexpected code %li.", j->url, status)); } - if (j->state != PULL_JOB_RUNNING) { - r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Premature connection termination."); - goto finish; - } + if (j->state != PULL_JOB_RUNNING) + return pull_job_finish(j, log_error_errno(SYNTHETIC_ERRNO(EIO), "Premature connection termination.")); uint64_t cl = pull_job_content_length_effective(j); if (cl != UINT64_MAX && - cl != j->written_compressed) { - r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Download truncated."); - goto finish; - } + cl != j->written_compressed) + return pull_job_finish(j, log_error_errno(SYNTHETIC_ERRNO(EIO), "Download truncated.")); if (j->checksum_ctx) { unsigned checksum_len; iovec_done(&j->checksum); j->checksum.iov_base = malloc(EVP_MAX_MD_SIZE); - if (!j->checksum.iov_base) { - r = log_oom(); - goto finish; - } + if (!j->checksum.iov_base) + return pull_job_finish(j, log_oom()); r = sym_EVP_DigestFinal_ex(j->checksum_ctx, j->checksum.iov_base, &checksum_len); - if (r == 0) { - r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get checksum."); - goto finish; - } + if (r == 0) + return pull_job_finish(j, log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get checksum.")); assert(checksum_len <= EVP_MAX_MD_SIZE); j->checksum.iov_len = checksum_len; if (DEBUG_LOGGING) { _cleanup_free_ char *h = hexmem(j->checksum.iov_base, j->checksum.iov_len); - if (!h) { - r = log_oom(); - goto finish; - } + if (!h) + return pull_job_finish(j, log_oom()); log_debug("%s of %s is %s.", sym_EVP_MD_CTX_get0_name(j->checksum_ctx), pull_job_description(j), h); } if (iovec_is_set(&j->expected_checksum) && - !iovec_equal(&j->checksum, &j->expected_checksum)) { - r = log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Checksum of downloaded resource does not match expected checksum, yikes."); - goto finish; - } + !iovec_equal(&j->checksum, &j->expected_checksum)) + return pull_job_finish(j, log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Checksum of downloaded resource does not match expected checksum, yikes.")); } /* Do a couple of finishing disk operations, but only if we are the sole owner of the file (i.e. no @@ -318,10 +293,8 @@ void pull_job_curl_on_finished(CurlGlue *g, CURL *curl, CURLcode result) { if (j->written_compressed > 0) { /* Make sure the file size is right, in case the file was sparse and * we just moved to the last part. */ - if (ftruncate(j->disk_fd, j->written_uncompressed) < 0) { - r = log_error_errno(errno, "Failed to truncate file: %m"); - goto finish; - } + if (ftruncate(j->disk_fd, j->written_uncompressed) < 0) + return pull_job_finish(j, log_error_errno(errno, "Failed to truncate file: %m")); } if (j->etag) @@ -345,27 +318,20 @@ void pull_job_curl_on_finished(CurlGlue *g, CURL *curl, CURLcode result) { if (j->sync) { r = fsync_full(j->disk_fd); - if (r < 0) { - log_error_errno(r, "Failed to synchronize file to disk: %m"); - goto finish; - } + if (r < 0) + return pull_job_finish(j, log_error_errno(r, "Failed to synchronize file to disk: %m")); } } else if (S_ISBLK(j->disk_stat.st_mode) && j->sync) { - if (fsync(j->disk_fd) < 0) { - r = log_error_errno(errno, "Failed to synchronize block device: %m"); - goto finish; - } + if (fsync(j->disk_fd) < 0) + return pull_job_finish(j, log_error_errno(errno, "Failed to synchronize block device: %m")); } } log_info("Acquired %s for %s.", FORMAT_BYTES(j->written_uncompressed), pull_job_description(j)); - r = 0; - -finish: - pull_job_finish(j, r); + return pull_job_finish(j, 0); } static int pull_job_write_uncompressed(const void *p, size_t sz, void *userdata) { @@ -595,7 +561,7 @@ static size_t pull_job_header_callback(void *contents, size_t size, size_t nmemb assert(j->state == PULL_JOB_ANALYZING); - code = sym_curl_easy_getinfo(j->curl, CURLINFO_RESPONSE_CODE, &status); + code = sym_curl_easy_getinfo(curl_slot_get_easy(j->slot), CURLINFO_RESPONSE_CODE, &status); if (code != CURLE_OK) { r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to retrieve response code: %s", sym_curl_easy_strerror(code)); goto fail; @@ -809,7 +775,8 @@ int pull_job_begin(PullJob *j) { if (j->state != PULL_JOB_INIT) return -EBUSY; - r = curl_glue_make(&j->curl, j->url, j); + _cleanup_(curl_easy_cleanupp) CURL *easy = NULL; + r = curl_glue_make(&easy, j->url); if (r < 0) return r; @@ -830,34 +797,35 @@ int pull_job_begin(PullJob *j) { } if (j->request_header) { - if (sym_curl_easy_setopt(j->curl, CURLOPT_HTTPHEADER, j->request_header) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_HTTPHEADER, j->request_header) != CURLE_OK) return -EIO; } - if (sym_curl_easy_setopt(j->curl, CURLOPT_WRITEFUNCTION, pull_job_write_callback) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, pull_job_write_callback) != CURLE_OK) return -EIO; - if (sym_curl_easy_setopt(j->curl, CURLOPT_WRITEDATA, j) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_WRITEDATA, j) != CURLE_OK) return -EIO; - if (sym_curl_easy_setopt(j->curl, CURLOPT_HEADERFUNCTION, pull_job_header_callback) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, pull_job_header_callback) != CURLE_OK) return -EIO; - if (sym_curl_easy_setopt(j->curl, CURLOPT_HEADERDATA, j) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_HEADERDATA, j) != CURLE_OK) return -EIO; - if (sym_curl_easy_setopt(j->curl, CURLOPT_XFERINFOFUNCTION, pull_job_progress_callback) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_XFERINFOFUNCTION, pull_job_progress_callback) != CURLE_OK) return -EIO; - if (sym_curl_easy_setopt(j->curl, CURLOPT_XFERINFODATA, j) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_XFERINFODATA, j) != CURLE_OK) return -EIO; - if (sym_curl_easy_setopt(j->curl, CURLOPT_NOPROGRESS, 0L) != CURLE_OK) + if (sym_curl_easy_setopt(easy, CURLOPT_NOPROGRESS, 0L) != CURLE_OK) return -EIO; - r = curl_glue_add(j->glue, j->curl); + r = curl_glue_perform_async(j->glue, easy, pull_job_curl_on_finished, j, &j->slot); if (r < 0) return r; + TAKE_PTR(easy); j->state = PULL_JOB_ANALYZING; diff --git a/src/import/pull-job.h b/src/import/pull-job.h index 0b878292f09..00d001680ff 100644 --- a/src/import/pull-job.h +++ b/src/import/pull-job.h @@ -7,7 +7,6 @@ #include "shared-forward.h" -typedef struct CurlGlue CurlGlue; typedef struct PullJob PullJob; typedef void (*PullJobFinished)(PullJob *job); @@ -46,7 +45,7 @@ typedef struct PullJob { PullJobNotFound on_not_found; CurlGlue *glue; - CURL *curl; + CurlSlot *slot; struct curl_slist *request_header; char *etag; @@ -95,8 +94,6 @@ PullJob* pull_job_unref(PullJob *job); int pull_job_begin(PullJob *j); -void pull_job_curl_on_finished(CurlGlue *g, CURL *curl, CURLcode result); - void pull_job_close_disk_fd(PullJob *j); int pull_job_add_request_header(PullJob *j, const char *hdr); diff --git a/src/import/pull-oci.c b/src/import/pull-oci.c index acea93b09de..c1c76fc8980 100644 --- a/src/import/pull-oci.c +++ b/src/import/pull-oci.c @@ -195,9 +195,6 @@ int oci_pull_new( .userns_fd = -EBADF, }; - i->glue->on_finished = pull_job_curl_on_finished; - i->glue->userdata = i; - *ret = TAKE_PTR(i); return 0; diff --git a/src/import/pull-raw.c b/src/import/pull-raw.c index 0ddde7c0919..c63a453177c 100644 --- a/src/import/pull-raw.c +++ b/src/import/pull-raw.c @@ -149,9 +149,6 @@ int raw_pull_new( .offset = UINT64_MAX, }; - p->glue->on_finished = pull_job_curl_on_finished; - p->glue->userdata = p; - *ret = TAKE_PTR(p); return 0; diff --git a/src/import/pull-tar.c b/src/import/pull-tar.c index fe18636eb7d..453ad1187cf 100644 --- a/src/import/pull-tar.c +++ b/src/import/pull-tar.c @@ -153,9 +153,6 @@ int tar_pull_new( .progress_ratelimit = { 100 * USEC_PER_MSEC, 1 }, }; - p->glue->on_finished = pull_job_curl_on_finished; - p->glue->userdata = p; - *ret = TAKE_PTR(p); return 0; diff --git a/src/shared/curl-util.c b/src/shared/curl-util.c index 0a6bdeefe55..e438ddf61a4 100644 --- a/src/shared/curl-util.c +++ b/src/shared/curl-util.c @@ -12,6 +12,7 @@ #include "dlfcn-util.h" #include "fd-util.h" #include "hashmap.h" +#include "set.h" #include "string-util.h" #include "strv.h" #include "time-util.h" @@ -42,6 +43,77 @@ DLSYM_PROTOTYPE(curl_slist_free_all) = NULL; DEFINE_TRIVIAL_CLEANUP_FUNC_FULL_RENAME(CURLM*, sym_curl_multi_cleanup, curl_multi_cleanupp, NULL); +struct CurlGlue { + unsigned n_ref; + sd_event *event; + CURLM *curl; + sd_event_source *timer; + Hashmap *ios; + sd_event_source *defer; + Set *slots; /* CurlSlot* — back-pointer set; floating slots are kept alive here */ +}; + +struct CurlSlot { + unsigned n_ref; + CurlGlue *glue; /* NULL once disconnected (callback fired, cancelled, or glue died) */ + CURL *easy; /* owned; cleared once the easy handle has been freed */ + bool floating; + curl_finished_t callback; + void *userdata; +}; + +static void curl_slot_disconnect(CurlSlot *slot, bool unref) { + assert(slot); + + /* Tear down the slot's connection to the glue: pull the easy handle out of the multi, + * curl_easy_cleanup() it, and remove the slot from the glue's lookup set. Floating + * slots are owned by that set, so on disconnect we drop the implicit ref (when + * unref=true; the recursive call from curl_slot_free passes false to avoid infinite + * recursion). Non-floating slots release the back-ref they held on the glue. + * + * Idempotent: once slot->glue is NULL, subsequent calls are no-ops. */ + + if (!slot->glue) + return; + + CurlGlue *glue = slot->glue; + + if (slot->easy) { + if (glue->curl) + (void) sym_curl_multi_remove_handle(glue->curl, slot->easy); + sym_curl_easy_cleanup(slot->easy); + slot->easy = NULL; + } + + set_remove(glue->slots, slot); + slot->glue = NULL; + + if (!slot->floating) + curl_glue_unref(glue); + else if (unref) + curl_slot_unref(slot); +} + +static CurlSlot* curl_slot_free(CurlSlot *slot) { + if (!slot) + return NULL; + + curl_slot_disconnect(slot, /* unref= */ false); + return mfree(slot); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(CurlSlot, curl_slot, curl_slot_free); + +CURL* curl_slot_get_easy(CurlSlot *slot) { + assert(slot); + return slot->easy; +} + +CurlGlue* curl_slot_get_glue(CurlSlot *slot) { + assert(slot); + return slot->glue; +} + static void curl_glue_check_finished(CurlGlue *g) { int r; @@ -60,8 +132,27 @@ static void curl_glue_check_finished(CurlGlue *g) { if (!msg) return; - if (msg->msg == CURLMSG_DONE && g->on_finished) - g->on_finished(g, msg->easy_handle, msg->data.result); + if (msg->msg == CURLMSG_DONE) { + CURL *easy = msg->easy_handle; + CURLcode code = msg->data.result; + CurlSlot *slot = NULL; + + if (sym_curl_easy_getinfo(easy, CURLINFO_PRIVATE, (char**) &slot) == CURLE_OK && slot) { + /* Pin the slot across the callback: a floating slot's only + * reference is the one held via the glue's slots set, and + * disconnect drops it. */ + curl_slot_ref(slot); + + if (slot->callback) { + r = slot->callback(slot, easy, code, slot->userdata); + if (r < 0) + log_debug_errno(r, "Curl finished callback returned error, ignoring: %m"); + } + + curl_slot_disconnect(slot, /* unref= */ true); + curl_slot_unref(slot); + } + } /* This is a queue, process another item soon, but do so in a later event loop iteration. */ (void) sd_event_source_set_enabled(g->defer, SD_EVENT_ONESHOT); @@ -212,12 +303,22 @@ static int curl_glue_on_defer(sd_event_source *s, void *userdata) { return 0; } -CurlGlue *curl_glue_unref(CurlGlue *g) { +static CurlGlue* curl_glue_free(CurlGlue *g) { sd_event_source *io; + CurlSlot *slot; if (!g) return NULL; + /* Drain any slots still hanging off us. By construction only floating slots can + * be here: connected non-floating slots hold a glue back-ref, so glue's last ref + * couldn't have dropped while one was attached. disconnect(unref=true) does the + * floating slot's free as part of its work. set_steal_first() pops up front so + * forward progress doesn't depend on disconnect's internal set_remove(). */ + while ((slot = set_steal_first(g->slots))) + curl_slot_disconnect(slot, /* unref= */ true); + g->slots = set_free(g->slots); + if (g->curl) sym_curl_multi_cleanup(g->curl); @@ -232,6 +333,8 @@ CurlGlue *curl_glue_unref(CurlGlue *g) { return mfree(g); } +DEFINE_TRIVIAL_REF_UNREF_FUNC(CurlGlue, curl_glue, curl_glue_free); + int curl_glue_new(CurlGlue **glue, sd_event *event) { _cleanup_(curl_glue_unrefp) CurlGlue *g = NULL; _cleanup_(curl_multi_cleanupp) CURLM *c = NULL; @@ -261,6 +364,7 @@ int curl_glue_new(CurlGlue **glue, sd_event *event) { return -ENOMEM; *g = (CurlGlue) { + .n_ref = 1, .event = TAKE_PTR(e), .curl = TAKE_PTR(c), }; @@ -288,7 +392,7 @@ int curl_glue_new(CurlGlue **glue, sd_event *event) { return 0; } -int curl_glue_make(CURL **ret, const char *url, void *userdata) { +int curl_glue_make(CURL **ret, const char *url) { _cleanup_(curl_easy_cleanupp) CURL *c = NULL; const char *useragent; int r; @@ -310,9 +414,6 @@ int curl_glue_make(CURL **ret, const char *url, void *userdata) { if (sym_curl_easy_setopt(c, CURLOPT_URL, url) != CURLE_OK) return -EIO; - if (sym_curl_easy_setopt(c, CURLOPT_PRIVATE, userdata) != CURLE_OK) - return -EIO; - useragent = strjoina(program_invocation_short_name, "/" GIT_VERSION); if (sym_curl_easy_setopt(c, CURLOPT_USERAGENT, useragent) != CURLE_OK) return -EIO; @@ -342,26 +443,61 @@ int curl_glue_make(CURL **ret, const char *url, void *userdata) { return 0; } -int curl_glue_add(CurlGlue *g, CURL *c) { +int curl_glue_perform_async( + CurlGlue *g, + CURL *easy, + curl_finished_t cb, + void *userdata, + CurlSlot **ret_slot) { + + int r; + assert(g); - assert(c); + assert(easy); - if (sym_curl_multi_add_handle(g->curl, c) != CURLM_OK) - return -EIO; + _cleanup_(curl_slot_unrefp) CurlSlot *slot = new(CurlSlot, 1); + if (!slot) + return -ENOMEM; - return 0; -} + *slot = (CurlSlot) { + .n_ref = 1, + .glue = NULL, /* wired up below, after we've committed to the multi */ + .easy = easy, + .floating = !ret_slot, + .callback = cb, + .userdata = userdata, + }; -void curl_glue_remove_and_free(CurlGlue *g, CURL *c) { - assert(g); + r = set_ensure_put(&g->slots, &trivial_hash_ops, slot); + if (r < 0) + return r; + assert(r > 0); - if (!c) - return; + if (sym_curl_multi_add_handle(g->curl, easy) != CURLM_OK) { + set_remove(g->slots, slot); + return -EIO; + } - if (g->curl) - sym_curl_multi_remove_handle(g->curl, c); + /* Stash the slot pointer on the easy handle so curl_glue_check_finished() can recover + * it on completion. Set this only after we've fully committed to the multi, so that + * error paths above don't leave a dangling pointer on the easy handle. */ + if (sym_curl_easy_setopt(easy, CURLOPT_PRIVATE, slot) != CURLE_OK) { + sym_curl_multi_remove_handle(g->curl, easy); + set_remove(g->slots, slot); + return -EIO; + } + + slot->glue = g; + if (!slot->floating) + curl_glue_ref(g); - sym_curl_easy_cleanup(c); + /* Transfer the slot's single reference: to the caller for non-floating slots, or to + * the glue's slot set (implicitly, until disconnect drops it) for floating ones. */ + if (ret_slot) + *ret_slot = slot; + + TAKE_PTR(slot); + return 0; } struct curl_slist *curl_slist_new(const char *first, ...) { diff --git a/src/shared/curl-util.h b/src/shared/curl-util.h index 33ab0a5fb20..3436188952f 100644 --- a/src/shared/curl-util.h +++ b/src/shared/curl-util.h @@ -30,27 +30,42 @@ extern DLSYM_PROTOTYPE(curl_slist_free_all); code == CURLE_OK; \ }) -typedef struct CurlGlue CurlGlue; - -typedef struct CurlGlue { - sd_event *event; - CURLM *curl; - sd_event_source *timer; - Hashmap *ios; - sd_event_source *defer; - - void (*on_finished)(CurlGlue *g, CURL *curl, CURLcode code); - void *userdata; -} CurlGlue; +typedef int (*curl_finished_t)(CurlSlot *slot, CURL *curl, CURLcode code, void *userdata); int curl_glue_new(CurlGlue **glue, sd_event *event); +CurlGlue* curl_glue_ref(CurlGlue *glue); CurlGlue* curl_glue_unref(CurlGlue *glue); DEFINE_TRIVIAL_CLEANUP_FUNC(CurlGlue*, curl_glue_unref); -int curl_glue_make(CURL **ret, const char *url, void *userdata); -int curl_glue_add(CurlGlue *g, CURL *c); -void curl_glue_remove_and_free(CurlGlue *g, CURL *c); +/* Build a CURL easy handle with sane defaults. The caller configures any + * additional options (headers, write callbacks, …) before handing it off to + * curl_glue_perform_async(). */ +int curl_glue_make(CURL **ret, const char *url); + +/* Hand a configured CURL easy handle off to the multi for execution. The slot + * takes ownership of the easy handle: once the slot is released (the callback + * has fired, the caller has dropped its last ref, or the glue is being freed), + * the handle is removed from the multi and freed. + * + * If ret_slot is NULL the slot is allocated as floating: the glue keeps it + * alive until the callback fires or the glue is torn down. Otherwise a + * reference is returned to the caller; releasing that reference cancels the + * call. */ +int curl_glue_perform_async( + CurlGlue *g, + CURL *easy, + curl_finished_t cb, + void *userdata, + CurlSlot **ret_slot); + +CURL* curl_slot_get_easy(CurlSlot *slot); +CurlGlue* curl_slot_get_glue(CurlSlot *slot); + +CurlSlot* curl_slot_ref(CurlSlot *slot); +CurlSlot* curl_slot_unref(CurlSlot *slot); + +DEFINE_TRIVIAL_CLEANUP_FUNC(CurlSlot*, curl_slot_unref); struct curl_slist *curl_slist_new(const char *first, ...) _sentinel_; int curl_header_strdup(const void *contents, size_t sz, const char *field, char **value); diff --git a/src/shared/shared-forward.h b/src/shared/shared-forward.h index e850d8982bd..751a6f71dc3 100644 --- a/src/shared/shared-forward.h +++ b/src/shared/shared-forward.h @@ -57,6 +57,8 @@ typedef struct Condition Condition; typedef struct ConfigSection ConfigSection; typedef struct ConfigTableItem ConfigTableItem; typedef struct CPUSet CPUSet; +typedef struct CurlGlue CurlGlue; +typedef struct CurlSlot CurlSlot; typedef struct DissectedImage DissectedImage; typedef struct DnsAnswer DnsAnswer; typedef struct DnsPacket DnsPacket; diff --git a/src/test/meson.build b/src/test/meson.build index f4288119f94..ba890e73410 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -348,6 +348,10 @@ executables += [ 'sources' : files('test-kexec.c'), 'link_with' : [libshared], }, + test_template + { + 'sources' : files('test-curl-util.c'), + 'conditions' : ['HAVE_LIBCURL'], + }, test_template + { 'sources' : files('test-libcrypt-util.c'), 'conditions' : ['HAVE_LIBCRYPT'], diff --git a/src/test/test-curl-util.c b/src/test/test-curl-util.c new file mode 100644 index 00000000000..fb3d2782006 --- /dev/null +++ b/src/test/test-curl-util.c @@ -0,0 +1,280 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "sd-event.h" + +#include "alloc-util.h" +#include "curl-util.h" +#include "fd-util.h" +#include "fs-util.h" +#include "io-util.h" +#include "string-util.h" +#include "tests.h" +#include "tmpfile-util.h" + +#define ASSERT_CURL_OK(expr) \ + ({ \ + CURLcode _code = (expr); \ + if (_code != CURLE_OK) \ + log_test_failed("Expected \"%s\" to be CURLE_OK, but got %d/%s",\ + #expr, (int) _code, sym_curl_easy_strerror(_code)); \ + }) + +/* Per-request context: the write callback appends bytes to ->body, and the + * on_finished callback stashes the CURLcode plus a "fired" flag. Each test + * uses one or more of these and cleans them up via context_done(). */ +typedef struct Context { + sd_event *event; + char *body; + size_t body_len; + bool finished; + CURLcode result; +} Context; + +static void context_done(Context *f) { + f->event = sd_event_unref(f->event); + f->body = mfree(f->body); +} + +static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userdata) { + Context *f = ASSERT_PTR(userdata); + size_t sz = size * nmemb; + + if (!GREEDY_REALLOC(f->body, f->body_len + sz + 1)) + return 0; + memcpy(f->body + f->body_len, contents, sz); + f->body[f->body_len + sz] = 0; + f->body_len += sz; + return sz; +} + +static int on_finished(CurlSlot *slot, CURL *curl, CURLcode code, void *userdata) { + Context *f = ASSERT_PTR(userdata); + + f->finished = true; + f->result = code; + + return sd_event_exit(f->event, 0); +} + +static int make_tmp_url(char **ret_path, char **ret_url, const char *body) { + const char *t; + ASSERT_OK(tmp_dir(&t)); + + _cleanup_(unlink_and_freep) char *path = ASSERT_NOT_NULL(strjoin(t, "/test-curl-util.XXXXXX")); + + _cleanup_close_ int fd = ASSERT_OK(mkostemp_safe(path)); + ASSERT_OK(loop_write(fd, body, strlen(body))); + + char *url = ASSERT_NOT_NULL(strjoin("file://", path)); + + *ret_url = url; + *ret_path = TAKE_PTR(path); + return 0; +} + +static int build_easy(const char *url, Context *f, CURL **ret) { + _cleanup_(curl_easy_cleanupp) CURL *easy = NULL; + ASSERT_OK(curl_glue_make(&easy, url)); + + ASSERT_CURL_OK(sym_curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_callback)); + ASSERT_CURL_OK(sym_curl_easy_setopt(easy, CURLOPT_WRITEDATA, f)); + + *ret = TAKE_PTR(easy); + return 0; +} + +TEST(curl_glue_lifecycle) { + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + ASSERT_OK(sd_event_default(&event)); + + _cleanup_(curl_glue_unrefp) CurlGlue *g = NULL; + ASSERT_OK(curl_glue_new(&g, event)); + + /* ref/unref roundtrip */ + ASSERT_PTR_EQ(curl_glue_ref(g), g); + ASSERT_NULL(curl_glue_unref(g)); +} + +TEST(curl_glue_make) { + _cleanup_(curl_easy_cleanupp) CURL *easy = NULL; + ASSERT_OK(curl_glue_make(&easy, "file:///dev/null")); + ASSERT_NOT_NULL(easy); +} + +TEST(curl_perform_floating) { + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + ASSERT_OK(sd_event_default(&event)); + + _cleanup_(curl_glue_unrefp) CurlGlue *g = NULL; + ASSERT_OK(curl_glue_new(&g, event)); + + _cleanup_(unlink_and_freep) char *path = NULL; + _cleanup_free_ char *url = NULL; + ASSERT_OK(make_tmp_url(&path, &url, "hello world")); + + _cleanup_(context_done) Context f = { .event = sd_event_ref(event) }; + + _cleanup_(curl_easy_cleanupp) CURL *easy = NULL; + ASSERT_OK(build_easy(url, &f, &easy)); + + /* Floating: pass NULL for ret_slot. The glue owns the slot until completion. */ + ASSERT_OK(curl_glue_perform_async(g, easy, on_finished, &f, /* ret_slot= */ NULL)); + TAKE_PTR(easy); + + ASSERT_OK(sd_event_loop(event)); + + ASSERT_TRUE(f.finished); + ASSERT_CURL_OK(f.result); + ASSERT_STREQ(f.body, "hello world"); +} + +TEST(curl_perform_slot) { + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + ASSERT_OK(sd_event_default(&event)); + + _cleanup_(curl_glue_unrefp) CurlGlue *g = NULL; + ASSERT_OK(curl_glue_new(&g, event)); + + _cleanup_(unlink_and_freep) char *path = NULL; + _cleanup_free_ char *url = NULL; + ASSERT_OK(make_tmp_url(&path, &url, "slot test")); + + _cleanup_(context_done) Context f = { .event = sd_event_ref(event) }; + + _cleanup_(curl_easy_cleanupp) CURL *easy = NULL; + ASSERT_OK(build_easy(url, &f, &easy)); + + _cleanup_(curl_slot_unrefp) CurlSlot *slot = NULL; + ASSERT_OK(curl_glue_perform_async(g, easy, on_finished, &f, &slot)); + TAKE_PTR(easy); + + ASSERT_NOT_NULL(slot); + ASSERT_NOT_NULL(curl_slot_get_easy(slot)); + ASSERT_PTR_EQ(curl_slot_get_glue(slot), g); + + ASSERT_OK(sd_event_loop(event)); + + ASSERT_TRUE(f.finished); + ASSERT_CURL_OK(f.result); + ASSERT_STREQ(f.body, "slot test"); + + /* After completion, disconnect has cleared the slot's back-pointers; the slot itself + * is still alive because we hold a ref. Releasing it must be a clean no-op. */ + ASSERT_NULL(curl_slot_get_easy(slot)); + ASSERT_NULL(curl_slot_get_glue(slot)); +} + +TEST(curl_perform_cancel) { + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + ASSERT_OK(sd_event_default(&event)); + + _cleanup_(curl_glue_unrefp) CurlGlue *g = NULL; + ASSERT_OK(curl_glue_new(&g, event)); + + _cleanup_(unlink_and_freep) char *path = NULL; + _cleanup_free_ char *url = NULL; + ASSERT_OK(make_tmp_url(&path, &url, "payload")); + + /* Two requests: cancelled is unref'd before we run the loop; sentinel runs to + * completion and exits the loop. After the loop returns we know the dispatcher had + * an opportunity to fire any pending completion — so cancelled.finished staying false + * means our cancel actually prevented the callback from running, not just outraced it. */ + _cleanup_(context_done) Context cancelled = { .event = sd_event_ref(event) }; + _cleanup_(context_done) Context sentinel = { .event = sd_event_ref(event) }; + + _cleanup_(curl_easy_cleanupp) CURL *easy_cancelled = NULL, *easy_sentinel = NULL; + ASSERT_OK(build_easy(url, &cancelled, &easy_cancelled)); + ASSERT_OK(build_easy(url, &sentinel, &easy_sentinel)); + + _cleanup_(curl_slot_unrefp) CurlSlot *slot = NULL; + ASSERT_OK(curl_glue_perform_async(g, easy_cancelled, on_finished, &cancelled, &slot)); + TAKE_PTR(easy_cancelled); + + /* Cancel by dropping our only reference: removes the easy handle from the multi and + * cleans it up. The callback must not fire afterwards. */ + slot = curl_slot_unref(slot); + + /* The sentinel runs as floating; its callback will exit the loop on completion. */ + ASSERT_OK(curl_glue_perform_async(g, easy_sentinel, on_finished, &sentinel, /* ret_slot= */ NULL)); + TAKE_PTR(easy_sentinel); + + ASSERT_OK(sd_event_loop(event)); + + ASSERT_TRUE(sentinel.finished); + ASSERT_FALSE(cancelled.finished); +} + +typedef struct ConcurrentReq { + Context ctx; + const char *expected; + unsigned *remaining; +} ConcurrentReq; + +static int concurrent_on_finished(CurlSlot *slot, CURL *curl, CURLcode code, void *userdata) { + ConcurrentReq *cr = ASSERT_PTR(userdata); + + cr->ctx.finished = true; + cr->ctx.result = code; + + (*cr->remaining)--; + if (*cr->remaining == 0) + return sd_event_exit(cr->ctx.event, 0); + return 0; +} + +TEST(curl_concurrent) { + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + ASSERT_OK(sd_event_default(&event)); + + _cleanup_(curl_glue_unrefp) CurlGlue *g = NULL; + ASSERT_OK(curl_glue_new(&g, event)); + + _cleanup_(unlink_and_freep) char *path_a = NULL, *path_b = NULL, *path_c = NULL; + _cleanup_free_ char *url_a = NULL, *url_b = NULL, *url_c = NULL; + ASSERT_OK(make_tmp_url(&path_a, &url_a, "alpha")); + ASSERT_OK(make_tmp_url(&path_b, &url_b, "bravo")); + ASSERT_OK(make_tmp_url(&path_c, &url_c, "charlie")); + + unsigned remaining = 3; + ConcurrentReq reqs[3] = { + { .ctx = { .event = sd_event_ref(event) }, .expected = "alpha", .remaining = &remaining }, + { .ctx = { .event = sd_event_ref(event) }, .expected = "bravo", .remaining = &remaining }, + { .ctx = { .event = sd_event_ref(event) }, .expected = "charlie", .remaining = &remaining }, + }; + + _cleanup_(curl_easy_cleanupp) CURL *ea = NULL, *eb = NULL, *ec = NULL; + ASSERT_OK(build_easy(url_a, &reqs[0].ctx, &ea)); + ASSERT_OK(build_easy(url_b, &reqs[1].ctx, &eb)); + ASSERT_OK(build_easy(url_c, &reqs[2].ctx, &ec)); + + /* All three fire as floating slots; the only way the loop exits is through the + * remaining-counter hitting zero, which means every callback fired with the right + * userdata routed to its respective body. */ + ASSERT_OK(curl_glue_perform_async(g, ea, concurrent_on_finished, &reqs[0], NULL)); + TAKE_PTR(ea); + ASSERT_OK(curl_glue_perform_async(g, eb, concurrent_on_finished, &reqs[1], NULL)); + TAKE_PTR(eb); + ASSERT_OK(curl_glue_perform_async(g, ec, concurrent_on_finished, &reqs[2], NULL)); + TAKE_PTR(ec); + + ASSERT_OK(sd_event_loop(event)); + + ASSERT_EQ(remaining, 0u); + + FOREACH_ARRAY(r, reqs, ELEMENTSOF(reqs)) { + ASSERT_TRUE(r->ctx.finished); + ASSERT_CURL_OK(r->ctx.result); + ASSERT_STREQ(r->ctx.body, r->expected); + context_done(&r->ctx); + } +} + +static int intro(void) { + if (dlopen_curl(LOG_DEBUG) < 0) + return log_tests_skipped("libcurl not available"); + return EXIT_SUCCESS; +} + +DEFINE_TEST_MAIN_WITH_INTRO(LOG_DEBUG, intro); -- 2.47.3