mirror of
https://github.com/systemd/systemd.git
synced 2026-08-09 17:38:42 +00:00
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:
101
src/imds/imdsd.c
101
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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
280
src/test/test-curl-util.c
Normal 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);
|
||||
Reference in New Issue
Block a user