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().
This commit is contained in:
Daan De Meyer
2026-05-01 09:08:35 +00:00
parent 0a5e09404d
commit 87cec65cae
11 changed files with 588 additions and 202 deletions

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,28 +443,63 @@ int curl_glue_make(CURL **ret, const char *url, void *userdata) {
return 0;
}
int curl_glue_add(CurlGlue *g, CURL *c) {
assert(g);
assert(c);
int curl_glue_perform_async(
CurlGlue *g,
CURL *easy,
curl_finished_t cb,
void *userdata,
CurlSlot **ret_slot) {
if (sym_curl_multi_add_handle(g->curl, c) != CURLM_OK)
int r;
assert(g);
assert(easy);
_cleanup_(curl_slot_unrefp) CurlSlot *slot = new(CurlSlot, 1);
if (!slot)
return -ENOMEM;
*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,
};
r = set_ensure_put(&g->slots, &trivial_hash_ops, slot);
if (r < 0)
return r;
assert(r > 0);
if (sym_curl_multi_add_handle(g->curl, easy) != CURLM_OK) {
set_remove(g->slots, slot);
return -EIO;
}
/* 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);
/* 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;
}
void curl_glue_remove_and_free(CurlGlue *g, CURL *c) {
assert(g);
if (!c)
return;
if (g->curl)
sym_curl_multi_remove_handle(g->curl, c);
sym_curl_easy_cleanup(c);
}
struct curl_slist *curl_slist_new(const char *first, ...) {
struct curl_slist *l;
va_list ap;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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'],

280
src/test/test-curl-util.c Normal file
View File

@@ -0,0 +1,280 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <unistd.h>
#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);