From c15f36cb8bf5be17504edde18ad40b14dce1c147 Mon Sep 17 00:00:00 2001 From: Eric Curtin Date: Tue, 9 Sep 2025 18:16:11 +0100 Subject: [PATCH 1/4] Add resumable downloads for llama-server model loading - Implement resumable downloads in common_download_file_single function - Add detection of partial download files (.downloadInProgress) - Check server support for HTTP Range requests via Accept-Ranges header - Implement HTTP Range request with "bytes=-" header - Open files in append mode when resuming vs create mode for new downloads Signed-off-by: Eric Curtin --- common/arg.cpp | 442 ++++++++++++++++++++++++++----------------------- 1 file changed, 239 insertions(+), 203 deletions(-) diff --git a/common/arg.cpp b/common/arg.cpp index c15008fe79b4d..36315b9d0c558 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -57,12 +57,18 @@ static std::string read_file(const std::string & fname) { } static void write_file(const std::string & fname, const std::string & content) { - std::ofstream file(fname); + const std::string fname_tmp = fname + ".tmp"; + std::ofstream file(fname_tmp); if (!file) { throw std::runtime_error(string_format("error: failed to open file '%s'\n", fname.c_str())); } file << content; file.close(); + + // Makes write atomic + if (rename(fname_tmp.c_str(), fname.c_str()) != 0) { + LOG_ERR("%s: unable to rename file: %s to %s\n", __func__, fname_tmp.c_str(), fname.c_str()); + } } common_arg & common_arg::set_examples(std::initializer_list examples) { @@ -217,250 +223,280 @@ struct curl_slist_ptr { } }; -#define CURL_MAX_RETRY 3 -#define CURL_RETRY_DELAY_SECONDS 2 +static CURLcode curl_perform(CURL * curl) { + CURLcode res = curl_easy_perform(curl); + if (res != CURLE_OK) { + LOG_ERR("%s: curl_easy_perform() failed\n", __func__); + } -static bool curl_perform_with_retry(const std::string & url, CURL * curl, int max_attempts, int retry_delay_seconds, const char * method_name) { - int remaining_attempts = max_attempts; + return res; +} - while (remaining_attempts > 0) { - LOG_INF("%s: %s %s (attempt %d of %d)...\n", __func__ , method_name, url.c_str(), max_attempts - remaining_attempts + 1, max_attempts); +// Send a HEAD request to retrieve the etag and last-modified headers +struct common_load_model_from_url_headers { + std::string etag; + std::string last_modified; + std::string accept_ranges; +}; - CURLcode res = curl_easy_perform(curl); - if (res == CURLE_OK) { - return true; - } +struct FILE_deleter { + void operator()(FILE * f) const { fclose(f); } +}; - int exponential_backoff_delay = std::pow(retry_delay_seconds, max_attempts - remaining_attempts) * 1000; - LOG_WRN("%s: curl_easy_perform() failed: %s, retrying after %d milliseconds...\n", __func__, curl_easy_strerror(res), exponential_backoff_delay); +static size_t common_header_callback(char * buffer, size_t, size_t n_items, void * userdata) { + common_load_model_from_url_headers * headers = (common_load_model_from_url_headers *) userdata; + static std::regex header_regex("([^:]+): (.*)\r\n"); + static std::regex etag_regex("ETag", std::regex_constants::icase); + static std::regex last_modified_regex("Last-Modified", std::regex_constants::icase); + static std::regex accept_ranges_regex("Accept-Ranges", std::regex_constants::icase); + std::string header(buffer, n_items); + std::smatch match; + if (std::regex_match(header, match, header_regex)) { + const std::string & key = match[1]; + const std::string & value = match[2]; + if (std::regex_match(key, match, etag_regex)) { + headers->etag = value; + } else if (std::regex_match(key, match, last_modified_regex)) { + headers->last_modified = value; + } else if (std::regex_match(key, match, accept_ranges_regex)) { + headers->accept_ranges = value; + } + } + + return n_items; +} + +static size_t common_write_callback(void * data, size_t size, size_t nmemb, void * fd) { + return std::fwrite(data, size, nmemb, static_cast(fd)); +} - remaining_attempts--; - if (remaining_attempts == 0) break; - std::this_thread::sleep_for(std::chrono::milliseconds(exponential_backoff_delay)); +// helper function to hide password in URL +static std::string llama_download_hide_password_in_url(const std::string & url) { + std::size_t protocol_pos = url.find("://"); + if (protocol_pos == std::string::npos) { + return url; // Malformed URL } - LOG_ERR("%s: curl_easy_perform() failed after %d attempts\n", __func__, max_attempts); + std::size_t at_pos = url.find('@', protocol_pos + 3); + if (at_pos == std::string::npos) { + return url; // No password in URL + } - return false; + return url.substr(0, protocol_pos + 3) + "********" + url.substr(at_pos); } // download one single file from remote URL to local path -static bool common_download_file_single(const std::string & url, const std::string & path, const std::string & bearer_token, bool offline) { - // Check if the file already exists locally - auto file_exists = std::filesystem::exists(path); - +static bool common_download_file_single(const std::string & url, + const std::string & path, + const std::string & bearer_token, + bool offline) { // If the file exists, check its JSON metadata companion file. std::string metadata_path = path + ".json"; - nlohmann::json metadata; // TODO @ngxson : get rid of this json, use regex instead - std::string etag; - std::string last_modified; + static const int max_attempts = 3; + static const int retry_delay_seconds = 2; - if (file_exists) { - if (offline) { - LOG_INF("%s: using cached file (offline mode): %s\n", __func__, path.c_str()); - return true; // skip verification/downloading - } - // Try and read the JSON metadata file (note: stream autoclosed upon exiting this block). - std::ifstream metadata_in(metadata_path); - if (metadata_in.good()) { - try { - metadata_in >> metadata; - LOG_DBG("%s: previous metadata file found %s: %s\n", __func__, metadata_path.c_str(), metadata.dump().c_str()); - if (metadata.contains("etag") && metadata.at("etag").is_string()) { - etag = metadata.at("etag"); - } - if (metadata.contains("lastModified") && metadata.at("lastModified").is_string()) { - last_modified = metadata.at("lastModified"); + for (int i = 0; i < 3; ++i) { + nlohmann::json metadata; // TODO @ngxson : get rid of this json, use regex instead + std::string etag; + std::string last_modified; + + // Check if the file already exists locally + const auto file_exists = std::filesystem::exists(path); + if (file_exists) { + if (offline) { + LOG_INF("%s: using cached file (offline mode): %s\n", __func__, path.c_str()); + return true; // skip verification/downloading + } + // Try and read the JSON metadata file (note: stream autoclosed upon exiting this block). + std::ifstream metadata_in(metadata_path); + if (metadata_in.good()) { + try { + metadata_in >> metadata; + LOG_DBG("%s: previous metadata file found %s: %s\n", __func__, metadata_path.c_str(), + metadata.dump().c_str()); + if (metadata.contains("etag") && metadata.at("etag").is_string()) { + etag = metadata.at("etag"); + } + if (metadata.contains("lastModified") && metadata.at("lastModified").is_string()) { + last_modified = metadata.at("lastModified"); + } + } catch (const nlohmann::json::exception & e) { + LOG_ERR("%s: error reading metadata file %s: %s\n", __func__, metadata_path.c_str(), e.what()); } - } catch (const nlohmann::json::exception & e) { - LOG_ERR("%s: error reading metadata file %s: %s\n", __func__, metadata_path.c_str(), e.what()); } + // if we cannot open the metadata file, we assume that the downloaded file is not valid (etag and last-modified are left empty, so we will download it again) + } else { + if (offline) { + LOG_ERR("%s: required file is not available in cache (offline mode): %s\n", __func__, path.c_str()); + return false; + } + LOG_INF("%s: no previous model file found %s\n", __func__, path.c_str()); } - // if we cannot open the metadata file, we assume that the downloaded file is not valid (etag and last-modified are left empty, so we will download it again) - } else { - if (offline) { - LOG_ERR("%s: required file is not available in cache (offline mode): %s\n", __func__, path.c_str()); - return false; - } - LOG_INF("%s: no previous model file found %s\n", __func__, path.c_str()); - } - - // Send a HEAD request to retrieve the etag and last-modified headers - struct common_load_model_from_url_headers { - std::string etag; - std::string last_modified; - }; - common_load_model_from_url_headers headers; - bool head_request_ok = false; - bool should_download = !file_exists; // by default, we should download if the file does not exist - - // Initialize libcurl - curl_ptr curl(curl_easy_init(), &curl_easy_cleanup); - curl_slist_ptr http_headers; - if (!curl) { - LOG_ERR("%s: error initializing libcurl\n", __func__); - return false; - } + common_load_model_from_url_headers headers; + bool head_request_ok = false; + bool should_download = !file_exists; // by default, we should download if the file does not exist - // Set the URL, allow to follow http redirection - curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl.get(), CURLOPT_FOLLOWLOCATION, 1L); + // Initialize libcurl + curl_ptr curl(curl_easy_init(), &curl_easy_cleanup); + curl_slist_ptr http_headers; + if (!curl) { + LOG_ERR("%s: error initializing libcurl\n", __func__); + return false; + } - http_headers.ptr = curl_slist_append(http_headers.ptr, "User-Agent: llama-cpp"); - // Check if hf-token or bearer-token was specified - if (!bearer_token.empty()) { - std::string auth_header = "Authorization: Bearer " + bearer_token; - http_headers.ptr = curl_slist_append(http_headers.ptr, auth_header.c_str()); - } - curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, http_headers.ptr); + // Set the URL, allow to follow http redirection + curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl.get(), CURLOPT_FOLLOWLOCATION, 1L); -#if defined(_WIN32) - // CURLSSLOPT_NATIVE_CA tells libcurl to use standard certificate store of - // operating system. Currently implemented under MS-Windows. - curl_easy_setopt(curl.get(), CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); -#endif + http_headers.ptr = curl_slist_append(http_headers.ptr, "User-Agent: llama-cpp"); + // Check if hf-token or bearer-token was specified + if (!bearer_token.empty()) { + std::string auth_header = "Authorization: Bearer " + bearer_token; + http_headers.ptr = curl_slist_append(http_headers.ptr, auth_header.c_str()); + } + curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, http_headers.ptr); - typedef size_t(*CURLOPT_HEADERFUNCTION_PTR)(char *, size_t, size_t, void *); - auto header_callback = [](char * buffer, size_t /*size*/, size_t n_items, void * userdata) -> size_t { - common_load_model_from_url_headers * headers = (common_load_model_from_url_headers *) userdata; +# if defined(_WIN32) + // CURLSSLOPT_NATIVE_CA tells libcurl to use standard certificate store of + // operating system. Currently implemented under MS-Windows. + curl_easy_setopt(curl.get(), CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); +# endif - static std::regex header_regex("([^:]+): (.*)\r\n"); - static std::regex etag_regex("ETag", std::regex_constants::icase); - static std::regex last_modified_regex("Last-Modified", std::regex_constants::icase); + curl_easy_setopt(curl.get(), CURLOPT_NOBODY, 1L); // will trigger the HEAD verb + curl_easy_setopt(curl.get(), CURLOPT_NOPROGRESS, 1L); // hide head request progress + curl_easy_setopt(curl.get(), CURLOPT_HEADERFUNCTION, common_header_callback); + curl_easy_setopt(curl.get(), CURLOPT_HEADERDATA, &headers); - std::string header(buffer, n_items); - std::smatch match; - if (std::regex_match(header, match, header_regex)) { - const std::string & key = match[1]; - const std::string & value = match[2]; - if (std::regex_match(key, match, etag_regex)) { - headers->etag = value; - } else if (std::regex_match(key, match, last_modified_regex)) { - headers->last_modified = value; - } + const bool was_perform_successful = curl_perform(curl.get()) == CURLE_OK; + if (!was_perform_successful) { + head_request_ok = false; } - return n_items; - }; - - curl_easy_setopt(curl.get(), CURLOPT_NOBODY, 1L); // will trigger the HEAD verb - curl_easy_setopt(curl.get(), CURLOPT_NOPROGRESS, 1L); // hide head request progress - curl_easy_setopt(curl.get(), CURLOPT_HEADERFUNCTION, static_cast(header_callback)); - curl_easy_setopt(curl.get(), CURLOPT_HEADERDATA, &headers); - // we only allow retrying once for HEAD requests - // this is for the use case of using running offline (no internet), retrying can be annoying - bool was_perform_successful = curl_perform_with_retry(url, curl.get(), 1, 0, "HEAD"); - if (!was_perform_successful) { - head_request_ok = false; - } + long http_code = 0; + curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &http_code); + if (http_code == 200) { + head_request_ok = true; + } else { + LOG_WRN("%s: HEAD invalid http status code received: %ld\n", __func__, http_code); + head_request_ok = false; + } + + // if head_request_ok is false, we don't have the etag or last-modified headers + // we leave should_download as-is, which is true if the file does not exist + bool should_download_from_scratch = false; + if (head_request_ok) { + // check if ETag or Last-Modified headers are different + // if it is, we need to download the file again + if (!etag.empty() && etag != headers.etag) { + LOG_WRN("%s: ETag header is different (%s != %s): triggering a new download\n", __func__, etag.c_str(), + headers.etag.c_str()); + should_download = true; + should_download_from_scratch = true; + } else if (!last_modified.empty() && last_modified != headers.last_modified) { + LOG_WRN("%s: Last-Modified header is different (%s != %s): triggering a new download\n", __func__, + last_modified.c_str(), headers.last_modified.c_str()); + should_download = true; + should_download_from_scratch = true; + } + } + + const bool accept_ranges_supported = !headers.accept_ranges.empty() && headers.accept_ranges != "none"; + if (should_download) { + if (file_exists && + !accept_ranges_supported) { // Resumable downloads not supported, delete and start again. + LOG_WRN("%s: deleting previous downloaded file: %s\n", __func__, path.c_str()); + if (remove(path.c_str()) != 0) { + LOG_ERR("%s: unable to delete file: %s\n", __func__, path.c_str()); + return false; + } + } - long http_code = 0; - curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &http_code); - if (http_code == 200) { - head_request_ok = true; - } else { - LOG_WRN("%s: HEAD invalid http status code received: %ld\n", __func__, http_code); - head_request_ok = false; - } + const std::string path_temporary = path + ".downloadInProgress"; + if (should_download_from_scratch) { + if (std::filesystem::exists(path_temporary)) { + if (remove(path_temporary.c_str()) != 0) { + LOG_ERR("%s: unable to delete file: %s\n", __func__, path_temporary.c_str()); + return false; + } + } - // if head_request_ok is false, we don't have the etag or last-modified headers - // we leave should_download as-is, which is true if the file does not exist - if (head_request_ok) { - // check if ETag or Last-Modified headers are different - // if it is, we need to download the file again - if (!etag.empty() && etag != headers.etag) { - LOG_WRN("%s: ETag header is different (%s != %s): triggering a new download\n", __func__, etag.c_str(), headers.etag.c_str()); - should_download = true; - } else if (!last_modified.empty() && last_modified != headers.last_modified) { - LOG_WRN("%s: Last-Modified header is different (%s != %s): triggering a new download\n", __func__, last_modified.c_str(), headers.last_modified.c_str()); - should_download = true; - } - } + if (std::filesystem::exists(path)) { + if (remove(path.c_str()) != 0) { + LOG_ERR("%s: unable to delete file: %s\n", __func__, path.c_str()); + return false; + } + } + } - if (should_download) { - std::string path_temporary = path + ".downloadInProgress"; - if (file_exists) { - LOG_WRN("%s: deleting previous downloaded file: %s\n", __func__, path.c_str()); - if (remove(path.c_str()) != 0) { - LOG_ERR("%s: unable to delete file: %s\n", __func__, path.c_str()); + // Always open file in append mode could be resuming + std::unique_ptr outfile(fopen(path_temporary.c_str(), "ab")); + if (!outfile) { + LOG_ERR("%s: error opening local file for writing: %s\n", __func__, path.c_str()); return false; } - } - - // Set the output file - struct FILE_deleter { - void operator()(FILE * f) const { - fclose(f); - } - }; + curl_easy_setopt(curl.get(), CURLOPT_NOBODY, 0L); + curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, common_write_callback); + curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, outfile.get()); - std::unique_ptr outfile(fopen(path_temporary.c_str(), "wb")); - if (!outfile) { - LOG_ERR("%s: error opening local file for writing: %s\n", __func__, path.c_str()); - return false; - } + // display download progress + curl_easy_setopt(curl.get(), CURLOPT_NOPROGRESS, 0L); - typedef size_t(*CURLOPT_WRITEFUNCTION_PTR)(void * data, size_t size, size_t nmemb, void * fd); - auto write_callback = [](void * data, size_t size, size_t nmemb, void * fd) -> size_t { - return fwrite(data, size, nmemb, (FILE *)fd); - }; - curl_easy_setopt(curl.get(), CURLOPT_NOBODY, 0L); - curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, static_cast(write_callback)); - curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, outfile.get()); + // start the download + LOG_INF("%s: trying to download model from %s to %s (server_etag:%s, server_last_modified:%s)...\n", + __func__, llama_download_hide_password_in_url(url).c_str(), path.c_str(), headers.etag.c_str(), + headers.last_modified.c_str()); - // display download progress - curl_easy_setopt(curl.get(), CURLOPT_NOPROGRESS, 0L); + // Write the updated JSON metadata file. + metadata.update({ + { "url", url }, + { "etag", headers.etag }, + { "lastModified", headers.last_modified } + }); + write_file(metadata_path, metadata.dump(4)); + LOG_DBG("%s: file metadata saved: %s\n", __func__, metadata_path.c_str()); + + if (std::filesystem::exists(path_temporary)) { + const std::string partial_size = std::to_string(std::filesystem::file_size(path_temporary)); + LOG_INF("%s: server supports range requests, resuming download from byte %s\n", __func__, + partial_size.c_str()); + const std::string range_str = partial_size + "-"; + curl_easy_setopt(curl.get(), CURLOPT_RANGE, range_str.c_str()); + } + + const bool was_perform_successful = curl_perform(curl.get()) == CURLE_OK; + if (!was_perform_successful) { + if (i + 1 < max_attempts) { + const int exponential_backoff_delay = std::pow(retry_delay_seconds, i) * 1000; + LOG_WRN("%s: retrying after %d milliseconds...\n", __func__, exponential_backoff_delay); + std::this_thread::sleep_for(std::chrono::milliseconds(exponential_backoff_delay)); + } else { + LOG_ERR("%s: curl_easy_perform() failed after %d attempts\n", __func__, max_attempts); + } - // helper function to hide password in URL - auto llama_download_hide_password_in_url = [](const std::string & url) -> std::string { - std::size_t protocol_pos = url.find("://"); - if (protocol_pos == std::string::npos) { - return url; // Malformed URL + continue; } - std::size_t at_pos = url.find('@', protocol_pos + 3); - if (at_pos == std::string::npos) { - return url; // No password in URL + long http_code = 0; + curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &http_code); + if (http_code < 200 || http_code >= 400) { + LOG_ERR("%s: invalid http status code received: %ld\n", __func__, http_code); + return false; } - return url.substr(0, protocol_pos + 3) + "********" + url.substr(at_pos); - }; - - // start the download - LOG_INF("%s: trying to download model from %s to %s (server_etag:%s, server_last_modified:%s)...\n", __func__, - llama_download_hide_password_in_url(url).c_str(), path.c_str(), headers.etag.c_str(), headers.last_modified.c_str()); - bool was_perform_successful = curl_perform_with_retry(url, curl.get(), CURL_MAX_RETRY, CURL_RETRY_DELAY_SECONDS, "GET"); - if (!was_perform_successful) { - return false; - } + // Causes file to be closed explicitly here before we rename it. + outfile.reset(); - long http_code = 0; - curl_easy_getinfo (curl.get(), CURLINFO_RESPONSE_CODE, &http_code); - if (http_code < 200 || http_code >= 400) { - LOG_ERR("%s: invalid http status code received: %ld\n", __func__, http_code); - return false; + if (rename(path_temporary.c_str(), path.c_str()) != 0) { + LOG_ERR("%s: unable to rename file: %s to %s\n", __func__, path_temporary.c_str(), path.c_str()); + return false; + } + } else { + LOG_INF("%s: using cached file: %s\n", __func__, path.c_str()); } - // Causes file to be closed explicitly here before we rename it. - outfile.reset(); - - // Write the updated JSON metadata file. - metadata.update({ - {"url", url}, - {"etag", headers.etag}, - {"lastModified", headers.last_modified} - }); - write_file(metadata_path, metadata.dump(4)); - LOG_DBG("%s: file metadata saved: %s\n", __func__, metadata_path.c_str()); - - if (rename(path_temporary.c_str(), path.c_str()) != 0) { - LOG_ERR("%s: unable to rename file: %s to %s\n", __func__, path_temporary.c_str(), path.c_str()); - return false; - } - } else { - LOG_INF("%s: using cached file: %s\n", __func__, path.c_str()); + break; } return true; @@ -770,7 +806,7 @@ static std::string common_docker_get_token(const std::string & repo) { } static std::string common_docker_resolve_model(const std::string & docker) { - // Parse ai/smollm2:135M-Q4_K_M + // Parse ai/smollm2:135M-Q4_0 size_t colon_pos = docker.find(':'); std::string repo, tag; if (colon_pos != std::string::npos) { From 8f55cff3e06559ee04aaafbd665162e1f7e26103 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 12:35:17 +0000 Subject: [PATCH 2/4] Initial plan From 476cdfbedf013d2ff24867b5a49f599f2176eceb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 12:50:33 +0000 Subject: [PATCH 3/4] Create curl class and refactor common_download_file_single function Co-authored-by: ericcurtin <1694275+ericcurtin@users.noreply.github.com> --- common/CMakeLists.txt | 2 + common/arg.cpp | 249 +++++++-------------------------- common/curl.cpp | 318 ++++++++++++++++++++++++++++++++++++++++++ common/curl.h | 77 ++++++++++ 4 files changed, 447 insertions(+), 199 deletions(-) create mode 100644 common/curl.cpp create mode 100644 common/curl.h diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 0ae4d698f080c..7dfeecbd91997 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -56,6 +56,8 @@ add_library(${TARGET} STATIC common.h console.cpp console.h + curl.cpp + curl.h json-partial.cpp json-partial.h json-schema-to-grammar.cpp diff --git a/common/arg.cpp b/common/arg.cpp index 36315b9d0c558..acdc96febbf19 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -2,6 +2,7 @@ #include "chat.h" #include "common.h" +#include "curl.h" #include "gguf.h" // for reading GGUF splits #include "json-schema-to-grammar.h" #include "log.h" @@ -33,12 +34,6 @@ //#define LLAMA_USE_CURL -#if defined(LLAMA_USE_CURL) -#include -#include -#include -#endif - using json = nlohmann::ordered_json; std::initializer_list mmproj_examples = { @@ -191,7 +186,7 @@ struct common_hf_file_res { #ifdef LLAMA_USE_CURL bool common_has_curl() { - return true; + return common_curl_available(); } #ifdef __linux__ @@ -211,65 +206,6 @@ bool common_has_curl() { // CURL utils // -using curl_ptr = std::unique_ptr; - -// cannot use unique_ptr for curl_slist, because we cannot update without destroying the old one -struct curl_slist_ptr { - struct curl_slist * ptr = nullptr; - ~curl_slist_ptr() { - if (ptr) { - curl_slist_free_all(ptr); - } - } -}; - -static CURLcode curl_perform(CURL * curl) { - CURLcode res = curl_easy_perform(curl); - if (res != CURLE_OK) { - LOG_ERR("%s: curl_easy_perform() failed\n", __func__); - } - - return res; -} - -// Send a HEAD request to retrieve the etag and last-modified headers -struct common_load_model_from_url_headers { - std::string etag; - std::string last_modified; - std::string accept_ranges; -}; - -struct FILE_deleter { - void operator()(FILE * f) const { fclose(f); } -}; - -static size_t common_header_callback(char * buffer, size_t, size_t n_items, void * userdata) { - common_load_model_from_url_headers * headers = (common_load_model_from_url_headers *) userdata; - static std::regex header_regex("([^:]+): (.*)\r\n"); - static std::regex etag_regex("ETag", std::regex_constants::icase); - static std::regex last_modified_regex("Last-Modified", std::regex_constants::icase); - static std::regex accept_ranges_regex("Accept-Ranges", std::regex_constants::icase); - std::string header(buffer, n_items); - std::smatch match; - if (std::regex_match(header, match, header_regex)) { - const std::string & key = match[1]; - const std::string & value = match[2]; - if (std::regex_match(key, match, etag_regex)) { - headers->etag = value; - } else if (std::regex_match(key, match, last_modified_regex)) { - headers->last_modified = value; - } else if (std::regex_match(key, match, accept_ranges_regex)) { - headers->accept_ranges = value; - } - } - - return n_items; -} - -static size_t common_write_callback(void * data, size_t size, size_t nmemb, void * fd) { - return std::fwrite(data, size, nmemb, static_cast(fd)); -} - // helper function to hide password in URL static std::string llama_download_hide_password_in_url(const std::string & url) { std::size_t protocol_pos = url.find("://"); @@ -333,52 +269,22 @@ static bool common_download_file_single(const std::string & url, LOG_INF("%s: no previous model file found %s\n", __func__, path.c_str()); } - common_load_model_from_url_headers headers; - bool head_request_ok = false; + common_curl curl_client; + common_curl_params curl_params; + curl_params.bearer_token = bearer_token; + curl_params.show_progress = false; // hide head request progress + + bool head_request_ok = false; bool should_download = !file_exists; // by default, we should download if the file does not exist - // Initialize libcurl - curl_ptr curl(curl_easy_init(), &curl_easy_cleanup); - curl_slist_ptr http_headers; - if (!curl) { - LOG_ERR("%s: error initializing libcurl\n", __func__); - return false; - } - - // Set the URL, allow to follow http redirection - curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl.get(), CURLOPT_FOLLOWLOCATION, 1L); - - http_headers.ptr = curl_slist_append(http_headers.ptr, "User-Agent: llama-cpp"); - // Check if hf-token or bearer-token was specified - if (!bearer_token.empty()) { - std::string auth_header = "Authorization: Bearer " + bearer_token; - http_headers.ptr = curl_slist_append(http_headers.ptr, auth_header.c_str()); - } - curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, http_headers.ptr); - -# if defined(_WIN32) - // CURLSSLOPT_NATIVE_CA tells libcurl to use standard certificate store of - // operating system. Currently implemented under MS-Windows. - curl_easy_setopt(curl.get(), CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); -# endif - - curl_easy_setopt(curl.get(), CURLOPT_NOBODY, 1L); // will trigger the HEAD verb - curl_easy_setopt(curl.get(), CURLOPT_NOPROGRESS, 1L); // hide head request progress - curl_easy_setopt(curl.get(), CURLOPT_HEADERFUNCTION, common_header_callback); - curl_easy_setopt(curl.get(), CURLOPT_HEADERDATA, &headers); - - const bool was_perform_successful = curl_perform(curl.get()) == CURLE_OK; - if (!was_perform_successful) { - head_request_ok = false; - } - - long http_code = 0; - curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &http_code); - if (http_code == 200) { + // Perform HEAD request to get metadata + // we only allow retrying once for HEAD requests + // this is for the use case of using running offline (no internet), retrying can be annoying + auto head_response = curl_client.head_request(url, curl_params); + if (head_response.http_code == 200) { head_request_ok = true; } else { - LOG_WRN("%s: HEAD invalid http status code received: %ld\n", __func__, http_code); + LOG_WRN("%s: HEAD invalid http status code received: %ld\n", __func__, head_response.http_code); head_request_ok = false; } @@ -388,20 +294,20 @@ static bool common_download_file_single(const std::string & url, if (head_request_ok) { // check if ETag or Last-Modified headers are different // if it is, we need to download the file again - if (!etag.empty() && etag != headers.etag) { - LOG_WRN("%s: ETag header is different (%s != %s): triggering a new download\n", __func__, etag.c_str(), - headers.etag.c_str()); + if (!etag.empty() && etag != head_response.etag) { + LOG_WRN("%s: ETag header is different (%s != %s): triggering a new download\n", __func__, + etag.c_str(), head_response.etag.c_str()); should_download = true; should_download_from_scratch = true; - } else if (!last_modified.empty() && last_modified != headers.last_modified) { + } else if (!last_modified.empty() && last_modified != head_response.last_modified) { LOG_WRN("%s: Last-Modified header is different (%s != %s): triggering a new download\n", __func__, - last_modified.c_str(), headers.last_modified.c_str()); + last_modified.c_str(), head_response.last_modified.c_str()); should_download = true; should_download_from_scratch = true; } } - const bool accept_ranges_supported = !headers.accept_ranges.empty() && headers.accept_ranges != "none"; + const bool accept_ranges_supported = !head_response.accept_ranges.empty() && head_response.accept_ranges != "none"; if (should_download) { if (file_exists && !accept_ranges_supported) { // Resumable downloads not supported, delete and start again. @@ -429,65 +335,38 @@ static bool common_download_file_single(const std::string & url, } } - // Always open file in append mode could be resuming - std::unique_ptr outfile(fopen(path_temporary.c_str(), "ab")); - if (!outfile) { - LOG_ERR("%s: error opening local file for writing: %s\n", __func__, path.c_str()); - return false; - } - - curl_easy_setopt(curl.get(), CURLOPT_NOBODY, 0L); - curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, common_write_callback); - curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, outfile.get()); - - // display download progress - curl_easy_setopt(curl.get(), CURLOPT_NOPROGRESS, 0L); - - // start the download - LOG_INF("%s: trying to download model from %s to %s (server_etag:%s, server_last_modified:%s)...\n", - __func__, llama_download_hide_password_in_url(url).c_str(), path.c_str(), headers.etag.c_str(), - headers.last_modified.c_str()); - // Write the updated JSON metadata file. metadata.update({ - { "url", url }, - { "etag", headers.etag }, - { "lastModified", headers.last_modified } + { "url", url }, + { "etag", head_response.etag }, + { "lastModified", head_response.last_modified } }); write_file(metadata_path, metadata.dump(4)); LOG_DBG("%s: file metadata saved: %s\n", __func__, metadata_path.c_str()); - if (std::filesystem::exists(path_temporary)) { - const std::string partial_size = std::to_string(std::filesystem::file_size(path_temporary)); - LOG_INF("%s: server supports range requests, resuming download from byte %s\n", __func__, - partial_size.c_str()); - const std::string range_str = partial_size + "-"; - curl_easy_setopt(curl.get(), CURLOPT_RANGE, range_str.c_str()); - } - - const bool was_perform_successful = curl_perform(curl.get()) == CURLE_OK; - if (!was_perform_successful) { + // start the download + LOG_INF("%s: trying to download model from %s to %s (server_etag:%s, server_last_modified:%s)...\n", + __func__, llama_download_hide_password_in_url(url).c_str(), path.c_str(), + head_response.etag.c_str(), head_response.last_modified.c_str()); + + curl_params.show_progress = true; // show download progress + + bool download_successful = false; + + // Use the curl class download method which handles resume automatically + download_successful = curl_client.download_file(url, path_temporary, curl_params); + + if (!download_successful) { if (i + 1 < max_attempts) { const int exponential_backoff_delay = std::pow(retry_delay_seconds, i) * 1000; LOG_WRN("%s: retrying after %d milliseconds...\n", __func__, exponential_backoff_delay); std::this_thread::sleep_for(std::chrono::milliseconds(exponential_backoff_delay)); } else { - LOG_ERR("%s: curl_easy_perform() failed after %d attempts\n", __func__, max_attempts); + LOG_ERR("%s: download failed after %d attempts\n", __func__, max_attempts); } - continue; } - long http_code = 0; - curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &http_code); - if (http_code < 200 || http_code >= 400) { - LOG_ERR("%s: invalid http status code received: %ld\n", __func__, http_code); - return false; - } - - // Causes file to be closed explicitly here before we rename it. - outfile.reset(); - if (rename(path_temporary.c_str(), path.c_str()) != 0) { LOG_ERR("%s: unable to rename file: %s to %s\n", __func__, path_temporary.c_str(), path.c_str()); return false; @@ -599,47 +478,19 @@ static bool common_download_model( } std::pair> common_remote_get_content(const std::string & url, const common_remote_params & params) { - curl_ptr curl(curl_easy_init(), &curl_easy_cleanup); - curl_slist_ptr http_headers; - std::vector res_buffer; - - curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl.get(), CURLOPT_NOPROGRESS, 1L); - curl_easy_setopt(curl.get(), CURLOPT_FOLLOWLOCATION, 1L); - typedef size_t(*CURLOPT_WRITEFUNCTION_PTR)(void * ptr, size_t size, size_t nmemb, void * data); - auto write_callback = [](void * ptr, size_t size, size_t nmemb, void * data) -> size_t { - auto data_vec = static_cast *>(data); - data_vec->insert(data_vec->end(), (char *)ptr, (char *)ptr + size * nmemb); - return size * nmemb; - }; - curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, static_cast(write_callback)); - curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &res_buffer); -#if defined(_WIN32) - curl_easy_setopt(curl.get(), CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); -#endif - if (params.timeout > 0) { - curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, params.timeout); - } - if (params.max_size > 0) { - curl_easy_setopt(curl.get(), CURLOPT_MAXFILESIZE, params.max_size); - } - http_headers.ptr = curl_slist_append(http_headers.ptr, "User-Agent: llama-cpp"); - for (const auto & header : params.headers) { - http_headers.ptr = curl_slist_append(http_headers.ptr, header.c_str()); - } - curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, http_headers.ptr); - - CURLcode res = curl_easy_perform(curl.get()); - - if (res != CURLE_OK) { - std::string error_msg = curl_easy_strerror(res); - throw std::runtime_error("error: cannot make GET request: " + error_msg); + common_curl curl_client; + + // Convert common_remote_params to common_curl_params + common_curl_params curl_params; + curl_params.headers = params.headers; + curl_params.timeout = params.timeout; + curl_params.max_size = params.max_size; + + try { + return curl_client.get_content(url, curl_params); + } catch (const std::exception& e) { + throw std::runtime_error("error: cannot make GET request: " + std::string(e.what())); } - - long res_code; - curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &res_code); - - return { res_code, std::move(res_buffer) }; } /** @@ -745,7 +596,7 @@ static struct common_hf_file_res common_get_hf_file(const std::string & hf_repo_ #else bool common_has_curl() { - return false; + return common_curl_available(); } static bool common_download_file_single(const std::string &, const std::string &, const std::string &, bool) { diff --git a/common/curl.cpp b/common/curl.cpp new file mode 100644 index 0000000000000..195b8cee5d2cd --- /dev/null +++ b/common/curl.cpp @@ -0,0 +1,318 @@ +#include "curl.h" +#include "log.h" + +#include +#include +#include +#include +#include +#include + +#ifdef LLAMA_USE_CURL +#include +#endif + +#ifdef LLAMA_USE_CURL + +bool common_curl_available() { + return true; +} + +common_curl::common_curl() : curl(nullptr), headers_list(nullptr) { + curl = curl_easy_init(); + if (!curl) { + LOG_ERR("%s: failed to initialize curl\n", __func__); + } +} + +common_curl::~common_curl() { + cleanup_headers(); + if (curl) { + curl_easy_cleanup(curl); + } +} + +common_curl::common_curl(common_curl&& other) noexcept + : curl(other.curl), headers_list(other.headers_list) { + other.curl = nullptr; + other.headers_list = nullptr; +} + +common_curl& common_curl::operator=(common_curl&& other) noexcept { + if (this != &other) { + cleanup_headers(); + if (curl) { + curl_easy_cleanup(curl); + } + + curl = other.curl; + headers_list = other.headers_list; + + other.curl = nullptr; + other.headers_list = nullptr; + } + return *this; +} + +void common_curl::reset() { + if (curl) { + curl_easy_reset(curl); + } + cleanup_headers(); +} + +void common_curl::cleanup_headers() { + if (headers_list) { + curl_slist_free_all(headers_list); + headers_list = nullptr; + } +} + +void common_curl::setup_common_options(const common_curl_params& params) { + if (!curl) return; + + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, params.follow_redirects ? 1L : 0L); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, params.show_progress ? 0L : 1L); + +#if defined(_WIN32) + if (params.ssl_verify) { + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); + } +#endif + + if (params.timeout > 0) { + curl_easy_setopt(curl, CURLOPT_TIMEOUT, params.timeout); + } + + if (params.max_size > 0) { + curl_easy_setopt(curl, CURLOPT_MAXFILESIZE, params.max_size); + } +} + +void common_curl::setup_headers(const common_curl_params& params) { + cleanup_headers(); + + // Always add User-Agent + headers_list = curl_slist_append(headers_list, "User-Agent: llama-cpp"); + + // Add bearer token if provided + if (!params.bearer_token.empty()) { + std::string auth_header = "Authorization: Bearer " + params.bearer_token; + headers_list = curl_slist_append(headers_list, auth_header.c_str()); + } + + // Add custom headers + for (const auto& header : params.headers) { + headers_list = curl_slist_append(headers_list, header.c_str()); + } + + if (headers_list) { + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers_list); + } +} + +CURLcode common_curl::perform_internal() { + if (!curl) { + return CURLE_FAILED_INIT; + } + return curl_easy_perform(curl); +} + +size_t common_curl::header_callback(char* buffer, size_t /*size*/, size_t nitems, void* userdata) { + auto* response = static_cast(userdata); + + static std::regex header_regex("([^:]+): (.*)\r\n"); + static std::regex etag_regex("ETag", std::regex_constants::icase); + static std::regex last_modified_regex("Last-Modified", std::regex_constants::icase); + static std::regex accept_ranges_regex("Accept-Ranges", std::regex_constants::icase); + + std::string header(buffer, nitems); + std::smatch match; + + if (std::regex_match(header, match, header_regex)) { + const std::string& key = match[1]; + const std::string& value = match[2]; + + if (std::regex_match(key, match, etag_regex)) { + response->etag = value; + } else if (std::regex_match(key, match, last_modified_regex)) { + response->last_modified = value; + } else if (std::regex_match(key, match, accept_ranges_regex)) { + response->accept_ranges = value; + } + } + + return nitems; +} + +size_t common_curl::write_callback_file(void* data, size_t size, size_t nmemb, void* userdata) { + return std::fwrite(data, size, nmemb, static_cast(userdata)); +} + +size_t common_curl::write_callback_memory(void* data, size_t size, size_t nmemb, void* userdata) { + auto* buffer = static_cast*>(userdata); + buffer->insert(buffer->end(), static_cast(data), static_cast(data) + size * nmemb); + return size * nmemb; +} + +common_curl_response common_curl::head_request(const std::string& url, const common_curl_params& params) { + common_curl_response response; + + if (!curl) { + LOG_ERR("%s: curl not initialized\n", __func__); + return response; + } + + reset(); + setup_common_options(params); + setup_headers(params); + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); // HEAD request + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response); + + CURLcode res = perform_internal(); + if (res == CURLE_OK) { + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response.http_code); + } else { + LOG_ERR("%s: curl_easy_perform() failed: %s\n", __func__, curl_easy_strerror(res)); + } + + return response; +} + +bool common_curl::download_file(const std::string& url, const std::string& path, const common_curl_params& params) { + if (!curl) { + LOG_ERR("%s: curl not initialized\n", __func__); + return false; + } + + reset(); + setup_common_options(params); + setup_headers(params); + + // Check if we need to resume + size_t existing_size = 0; + if (std::filesystem::exists(path)) { + existing_size = std::filesystem::file_size(path); + if (existing_size > 0) { + curl_easy_setopt(curl, CURLOPT_RESUME_FROM_LARGE, static_cast(existing_size)); + } + } + + // Open file for writing (append mode for resume support) + FILE* file = fopen(path.c_str(), existing_size > 0 ? "ab" : "wb"); + if (!file) { + LOG_ERR("%s: failed to open file for writing: %s\n", __func__, path.c_str()); + return false; + } + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback_file); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, file); + + CURLcode res = perform_internal(); + fclose(file); + + if (res != CURLE_OK) { + LOG_ERR("%s: curl_easy_perform() failed: %s\n", __func__, curl_easy_strerror(res)); + return false; + } + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + if (http_code < 200 || http_code >= 400) { + LOG_ERR("%s: HTTP error: %ld\n", __func__, http_code); + return false; + } + + return true; +} + +std::pair> common_curl::get_content(const std::string& url, const common_curl_params& params) { + std::vector buffer; + long http_code = 0; + + if (!curl) { + LOG_ERR("%s: curl not initialized\n", __func__); + return {0, std::move(buffer)}; + } + + reset(); + setup_common_options(params); + setup_headers(params); + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback_memory); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); + + CURLcode res = perform_internal(); + if (res != CURLE_OK) { + LOG_ERR("%s: curl_easy_perform() failed: %s\n", __func__, curl_easy_strerror(res)); + return {0, std::move(buffer)}; + } + + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + return {http_code, std::move(buffer)}; +} + +bool common_curl::perform_with_retry(const std::string& url, int max_attempts, int retry_delay_seconds, const char* method_name) { + int remaining_attempts = max_attempts; + + while (remaining_attempts > 0) { + LOG_INF("%s: %s %s (attempt %d of %d)...\n", __func__, method_name, url.c_str(), + max_attempts - remaining_attempts + 1, max_attempts); + + CURLcode res = perform_internal(); + if (res == CURLE_OK) { + return true; + } + + int exponential_backoff_delay = std::pow(retry_delay_seconds, max_attempts - remaining_attempts) * 1000; + LOG_WRN("%s: curl_easy_perform() failed: %s, retrying after %d milliseconds...\n", + __func__, curl_easy_strerror(res), exponential_backoff_delay); + + remaining_attempts--; + if (remaining_attempts == 0) break; + + std::this_thread::sleep_for(std::chrono::milliseconds(exponential_backoff_delay)); + } + + LOG_ERR("%s: curl_easy_perform() failed after %d attempts\n", __func__, max_attempts); + return false; +} + +#else // !LLAMA_USE_CURL + +bool common_curl_available() { + return false; +} + +common_curl::common_curl() {} +common_curl::~common_curl() {} +common_curl::common_curl(common_curl&& other) noexcept {} +common_curl& common_curl::operator=(common_curl&& other) noexcept { return *this; } + +common_curl_response common_curl::head_request(const std::string& url, const common_curl_params& params) { + LOG_ERR("error: built without CURL, cannot make HTTP requests\n"); + return {}; +} + +bool common_curl::download_file(const std::string& url, const std::string& path, const common_curl_params& params) { + LOG_ERR("error: built without CURL, cannot download files\n"); + return false; +} + +std::pair> common_curl::get_content(const std::string& url, const common_curl_params& params) { + LOG_ERR("error: built without CURL, cannot get content\n"); + return {0, {}}; +} + +bool common_curl::perform_with_retry(const std::string& url, int max_attempts, int retry_delay_seconds, const char* method_name) { + LOG_ERR("error: built without CURL, cannot perform HTTP requests\n"); + return false; +} + +#endif // LLAMA_USE_CURL \ No newline at end of file diff --git a/common/curl.h b/common/curl.h new file mode 100644 index 0000000000000..5f2249e98c0ec --- /dev/null +++ b/common/curl.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include +#include + +#ifdef LLAMA_USE_CURL +#include +#endif + +// +// CURL wrapper class to encapsulate all curl operations +// + +struct common_curl_response { + long http_code = 0; + std::string etag; + std::string last_modified; + std::string accept_ranges; +}; + +struct common_curl_params { + std::vector headers; + std::string bearer_token; + long timeout = 0; // in seconds, 0 means no timeout + long max_size = 0; // max response size, 0 means unlimited + bool follow_redirects = true; + bool show_progress = false; + bool ssl_verify = true; +}; + +class common_curl { +public: + common_curl(); + ~common_curl(); + + // Disable copy constructor and assignment + common_curl(const common_curl&) = delete; + common_curl& operator=(const common_curl&) = delete; + + // Move constructor and assignment + common_curl(common_curl&& other) noexcept; + common_curl& operator=(common_curl&& other) noexcept; + + // Perform HEAD request to get metadata + common_curl_response head_request(const std::string& url, const common_curl_params& params = {}); + + // Download file to path with resume support + bool download_file(const std::string& url, const std::string& path, const common_curl_params& params = {}); + + // Get content as vector of chars + std::pair> get_content(const std::string& url, const common_curl_params& params = {}); + + // Retry wrapper for any curl operation + bool perform_with_retry(const std::string& url, int max_attempts = 3, int retry_delay_seconds = 2, const char* method_name = "REQUEST"); + +private: +#ifdef LLAMA_USE_CURL + CURL* curl; + struct curl_slist* headers_list; + + void reset(); + void setup_common_options(const common_curl_params& params); + void setup_headers(const common_curl_params& params); + void cleanup_headers(); + CURLcode perform_internal(); + + // Static callback functions + static size_t header_callback(char* buffer, size_t size, size_t nitems, void* userdata); + static size_t write_callback_file(void* data, size_t size, size_t nmemb, void* userdata); + static size_t write_callback_memory(void* data, size_t size, size_t nmemb, void* userdata); +#endif +}; + +// Check if curl is available +bool common_curl_available(); \ No newline at end of file From 581db33ae017ac4c487a1f64e1d92b3367b6b646 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 12:52:27 +0000 Subject: [PATCH 4/4] Fix missing future include --- common/arg.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/common/arg.cpp b/common/arg.cpp index acdc96febbf19..61e3c4cc426e7 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include