/* 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;
/* 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;
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);
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) {
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! */
if (r < 0)
return context_fail(c, r);
- return;
+ return 0;
default:
return context_fail_full(
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);
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)
if (r < 0)
return context_fail(c, r);
if (r == 0) /* Immediately restarted */
- return;
+ return 0;
context_log(c, LOG_DEBUG, "Data download successful.");
context_success(c);
} else
assert_not_reached();
+
+ return 0;
}
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;
}
(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 ||
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");
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;
}
/* 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 ||
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");
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;
}
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);
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;
if (j->on_finished)
j->on_finished(j);
+
+ return 0;
}
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);
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;
/* 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) {
/* 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
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)
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) {
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;
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;
}
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;
#include "shared-forward.h"
-typedef struct CurlGlue CurlGlue;
typedef struct PullJob PullJob;
typedef void (*PullJobFinished)(PullJob *job);
PullJobNotFound on_not_found;
CurlGlue *glue;
- CURL *curl;
+ CurlSlot *slot;
struct curl_slist *request_header;
char *etag;
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);
.userns_fd = -EBADF,
};
- i->glue->on_finished = pull_job_curl_on_finished;
- i->glue->userdata = i;
-
*ret = TAKE_PTR(i);
return 0;
.offset = UINT64_MAX,
};
- p->glue->on_finished = pull_job_curl_on_finished;
- p->glue->userdata = p;
-
*ret = TAKE_PTR(p);
return 0;
.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;
#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"
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;
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);
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);
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;
return -ENOMEM;
*g = (CurlGlue) {
+ .n_ref = 1,
.event = TAKE_PTR(e),
.curl = TAKE_PTR(c),
};
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;
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;
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, ...) {
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);
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;
'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'],
--- /dev/null
+/* 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);