diff options
author | Konstantin Käfer <mail@kkaefer.com> | 2015-01-16 14:04:41 +0100 |
---|---|---|
committer | Konstantin Käfer <mail@kkaefer.com> | 2015-02-04 10:46:37 +0100 |
commit | b9bf66e67ed1d0d1b1d3163255cab099a6ba4a95 (patch) | |
tree | 93ad6df882442e18d9a9771d4b4f06a0a764a0a9 /platform | |
parent | 3bfea8bf30c978173f1ec2fab6f89d6b33afea86 (diff) | |
download | qtlocation-mapboxgl-b9bf66e67ed1d0d1b1d3163255cab099a6ba4a95.tar.gz |
rewrite storage layer to be independent of the Map's event loop
Diffstat (limited to 'platform')
-rw-r--r-- | platform/darwin/http_request_baton_cocoa.mm | 155 | ||||
-rw-r--r-- | platform/darwin/http_request_cocoa.mm | 388 | ||||
-rw-r--r-- | platform/default/asset_request_libuv.cpp | 236 | ||||
-rw-r--r-- | platform/default/cache_database_tmp.cpp | 12 | ||||
-rw-r--r-- | platform/default/http_request_baton_curl.cpp | 629 | ||||
-rw-r--r-- | platform/default/http_request_curl.cpp | 640 |
6 files changed, 1149 insertions, 911 deletions
diff --git a/platform/darwin/http_request_baton_cocoa.mm b/platform/darwin/http_request_baton_cocoa.mm deleted file mode 100644 index 4a59837e32..0000000000 --- a/platform/darwin/http_request_baton_cocoa.mm +++ /dev/null @@ -1,155 +0,0 @@ -#include <mbgl/storage/http_request_baton.hpp> -#include <mbgl/util/std.hpp> -#include <mbgl/util/parsedate.h> -#include <mbgl/util/time.hpp> -#include <mbgl/util/version.hpp> - -#include <uv.h> - -#include <mbgl/util/uv.hpp> - -#import <Foundation/Foundation.h> - -namespace mbgl { - -dispatch_once_t request_initialize = 0; -NSURLSession *session = nullptr; - -NSString *userAgent = nil; - -void HTTPRequestBaton::start(const util::ptr<HTTPRequestBaton> &ptr) { - assert(std::this_thread::get_id() == ptr->threadId); - - // Starts the request. - util::ptr<HTTPRequestBaton> baton = ptr; - - dispatch_once(&request_initialize, ^{ - NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; - sessionConfig.timeoutIntervalForResource = 30; - sessionConfig.HTTPMaximumConnectionsPerHost = 8; - sessionConfig.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; - sessionConfig.URLCache = nil; - - session = [NSURLSession sessionWithConfiguration:sessionConfig]; - - // Write user agent string - NSDictionary *systemVersion = [NSDictionary dictionaryWithContentsOfFile:@"/System/Library/CoreServices/SystemVersion.plist"]; - userAgent = [NSString stringWithFormat:@"MapboxGL/%d.%d.%d (+https://mapbox.com/mapbox-gl/; %s; %@ %@)", - version::major, version::minor, version::patch, version::revision, - [systemVersion objectForKey:@"ProductName"], - [systemVersion objectForKey:@"ProductVersion"] - ]; - }); - - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@(baton->path.c_str())]]; - if (baton->response) { - if (!baton->response->etag.empty()) { - [request addValue:@(baton->response->etag.c_str()) forHTTPHeaderField:@"If-None-Match"]; - } else if (baton->response->modified) { - const std::string time = util::rfc1123(baton->response->modified); - [request addValue:@(time.c_str()) forHTTPHeaderField:@"If-Modified-Since"]; - } - } - - [request addValue:userAgent forHTTPHeaderField:@"User-Agent"]; - - NSURLSessionDataTask *task = [session dataTaskWithRequest:request - completionHandler:^(NSData *data, NSURLResponse *res, NSError *error) { - if (error) { - if ([error code] == NSURLErrorCancelled) { - // The response code remains at 0 to indicate cancelation. - // In addition, we don't need any response object. - baton->response.reset(); - baton->type = HTTPResponseType::Canceled; - } else { - // TODO: Use different codes for host not found, timeout, invalid URL etc. - // These can be categorized in temporary and permanent errors. - baton->response = util::make_unique<Response>(); - baton->response->code = [(NSHTTPURLResponse *)res statusCode]; - baton->response->message = [[error localizedDescription] UTF8String]; - - switch ([error code]) { - case NSURLErrorBadServerResponse: // 5xx errors - baton->type = HTTPResponseType::TemporaryError; - break; - - case NSURLErrorTimedOut: - case NSURLErrorUserCancelledAuthentication: - baton->type = HTTPResponseType::SingularError; // retry immediately - break; - - case NSURLErrorNetworkConnectionLost: - case NSURLErrorCannotFindHost: - case NSURLErrorCannotConnectToHost: - case NSURLErrorDNSLookupFailed: - case NSURLErrorNotConnectedToInternet: - case NSURLErrorInternationalRoamingOff: - case NSURLErrorCallIsActive: - case NSURLErrorDataNotAllowed: - baton->type = HTTPResponseType::ConnectionError; - break; - - default: - baton->type = HTTPResponseType::PermanentError; - } - } - } else if ([res isKindOfClass:[NSHTTPURLResponse class]]) { - const long code = [(NSHTTPURLResponse *)res statusCode]; - if (code == 304) { - // Assume a Response object already exists. - assert(baton->response); - } else { - baton->response = util::make_unique<Response>(); - baton->response->code = code; - baton->response->data = {(const char *)[data bytes], [data length]}; - } - - if (code == 304) { - baton->type = HTTPResponseType::NotModified; - } else if (code == 200) { - baton->type = HTTPResponseType::Successful; - } else { - baton->type = HTTPResponseType::PermanentError; - } - - NSDictionary *headers = [(NSHTTPURLResponse *)res allHeaderFields]; - NSString *cache_control = [headers objectForKey:@"Cache-Control"]; - if (cache_control) { - baton->response->expires = Response::parseCacheControl([cache_control UTF8String]); - } - - NSString *last_modified = [headers objectForKey:@"Last-Modified"]; - if (last_modified) { - baton->response->modified = parse_date([last_modified UTF8String]); - } - - NSString *etag = [headers objectForKey:@"ETag"]; - if (etag) { - baton->response->etag = [etag UTF8String]; - } - } else { - // This should never happen. - baton->type = HTTPResponseType::PermanentError; - baton->response = util::make_unique<Response>(); - baton->response->code = -1; - baton->response->message = "response class is not NSHTTPURLResponse"; - } - - uv_async_send(baton->async); - }]; - - [task resume]; - - baton->ptr = const_cast<void *>(CFBridgingRetain(task)); -} - -void HTTPRequestBaton::stop(const util::ptr<HTTPRequestBaton> &ptr) { - assert(std::this_thread::get_id() == ptr->threadId); - assert(ptr->ptr); - - NSURLSessionDataTask *task = CFBridgingRelease(ptr->ptr); - ptr->ptr = nullptr; - [task cancel]; -} - -} diff --git a/platform/darwin/http_request_cocoa.mm b/platform/darwin/http_request_cocoa.mm new file mode 100644 index 0000000000..163d4cf2db --- /dev/null +++ b/platform/darwin/http_request_cocoa.mm @@ -0,0 +1,388 @@ +#include <mbgl/storage/default/http_request.hpp> +#include <mbgl/storage/default/http_context.hpp> +#include <mbgl/storage/response.hpp> +#include <mbgl/util/uv.hpp> + +#include <mbgl/util/time.hpp> +#include <mbgl/util/parsedate.h> + +#import <Foundation/Foundation.h> + +#include <map> +#include <cassert> + +dispatch_once_t request_initialize = 0; +NSURLSession *session = nullptr; +NSString *userAgent = nil; + +namespace mbgl { + +enum class ResponseStatus : uint8_t { + // This error probably won't be resolved by retrying anytime soon. We are giving up. + PermanentError, + + // This error might be resolved by waiting some time (e.g. server issues). + // We are going to do an exponential back-off and will try again in a few seconds. + TemporaryError, + + // This error was caused by a temporary error and it is likely that it will be resolved + // immediately. We are going to try again right away. This is like the TemporaryError, except + // that we will not perform exponential back-off. + SingularError, + + // This error might be resolved once the network reachability status changes. + // We are going to watch the network status for changes and will retry as soon as the + // operating system notifies us of a network status change. + ConnectionError, + + // The request was canceled mid-way. + Canceled, + + // The request returned data successfully. We retrieved and decoded the data successfully. + Successful, + + // The request confirmed that the data wasn't changed. We already have the data. + NotModified, +}; + +// ------------------------------------------------------------------------------------------------- + +class HTTPCocoaContext; + +class HTTPRequestImpl { +public: + HTTPRequestImpl(HTTPRequest *request, uv_loop_t *loop, std::unique_ptr<Response> response); + ~HTTPRequestImpl(); + + void cancel(); + + void start(); + void handleResult(NSData *data, NSURLResponse *res, NSError *error); + void handleResponse(); + + void retry(uint64_t timeout); + void retryImmediately(); + static void restart(uv_timer_t *timer, int); + +private: + HTTPCocoaContext *context = nullptr; + HTTPRequest *request = nullptr; + NSURLSessionDataTask *task = nullptr; + std::unique_ptr<Response> response; + std::unique_ptr<Response> existingResponse; + ResponseStatus status = ResponseStatus::PermanentError; + uv_async_t *async = nullptr; + int attempts = 0; + uv_timer_t *timer = nullptr; + enum : bool { PreemptImmediately, ExponentialBackoff } strategy = PreemptImmediately; + + static const int maxAttempts = 4; +}; + +// ------------------------------------------------------------------------------------------------- + +class HTTPCocoaContext : public HTTPContext<HTTPCocoaContext> { +public: + HTTPCocoaContext(uv_loop_t *loop); +}; + +template<> pthread_key_t HTTPContext<HTTPCocoaContext>::key{}; +template<> pthread_once_t HTTPContext<HTTPCocoaContext>::once = PTHREAD_ONCE_INIT; + +HTTPCocoaContext::HTTPCocoaContext(uv_loop_t *loop_) : HTTPContext(loop_) {} + +// ------------------------------------------------------------------------------------------------- + +HTTPRequestImpl::HTTPRequestImpl(HTTPRequest *request_, uv_loop_t *loop, + std::unique_ptr<Response> existingResponse_) + : context(HTTPCocoaContext::Get(loop)), + request(request_), + existingResponse(std::move(existingResponse_)), + async(new uv_async_t) { + assert(request); + context->addRequest(request); + + async->data = this; + uv_async_init(loop, async, [](uv_async_t *as, int) { + auto impl = reinterpret_cast<HTTPRequestImpl *>(as->data); + impl->handleResponse(); + }); + + start(); +} + +void HTTPRequestImpl::start() { + assert(!task); + + attempts++; + + @autoreleasepool { + NSMutableURLRequest *req = [[NSMutableURLRequest alloc] + initWithURL:[NSURL URLWithString:@(request->resource.url.c_str())]]; + if (existingResponse) { + if (!existingResponse->etag.empty()) { + [req addValue:@(existingResponse->etag.c_str()) forHTTPHeaderField:@"If-None-Match"]; + } else if (existingResponse->modified) { + const std::string time = util::rfc1123(existingResponse->modified); + [req addValue:@(time.c_str()) forHTTPHeaderField:@"If-Modified-Since"]; + } + } + + [req addValue:userAgent forHTTPHeaderField:@"User-Agent"]; + + task = [session dataTaskWithRequest:req + completionHandler:^(NSData *data, NSURLResponse *res, + NSError *error) { handleResult(data, res, error); }]; + [req release]; + [task resume]; + } +} + +void HTTPRequestImpl::handleResponse() { + task = nullptr; + + if (request) { + if (status == ResponseStatus::TemporaryError && attempts < maxAttempts) { + strategy = ExponentialBackoff; + return retry((1 << (attempts - 1)) * 1000); + } else if (status == ResponseStatus::ConnectionError && attempts < maxAttempts) { + // By default, we will retry every 30 seconds (network change notification will + // preempt the timeout). + strategy = PreemptImmediately; + return retry(30000); + } + + // Actually return the response. + if (status == ResponseStatus::NotModified) { + request->notify(std::move(response), FileCache::Hint::Refresh); + } else { + request->notify(std::move(response), FileCache::Hint::Full); + } + + context->removeRequest(request); + delete request; + request = nullptr; + } + + delete this; +} + +void HTTPRequestImpl::cancel() { + context->removeRequest(request); + request = nullptr; + + [task cancel]; + task = nullptr; +} + +HTTPRequestImpl::~HTTPRequestImpl() { + assert(!task); + assert(async); + + uv::close(async); + + if (request) { + context->removeRequest(request); + request->ptr = nullptr; + } +} + +int64_t parseCacheControl(const char *value) { + if (value) { + unsigned long long seconds = 0; + // TODO: cache-control may contain other information as well: + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 + if (std::sscanf(value, "max-age=%llu", &seconds) == 1) { + return std::chrono::duration_cast<std::chrono::seconds>( + std::chrono::system_clock::now().time_since_epoch()).count() + + seconds; + } + } + + return 0; +} + +void HTTPRequestImpl::handleResult(NSData *data, NSURLResponse *res, NSError *error) { + if (error) { + if ([error code] == NSURLErrorCancelled) { + status = ResponseStatus::Canceled; + } else { + // TODO: Use different codes for host not found, timeout, invalid URL etc. + // These can be categorized in temporary and permanent errors. + response = util::make_unique<Response>(); + response->status = Response::Error; + response->message = [[error localizedDescription] UTF8String]; + + switch ([error code]) { + case NSURLErrorBadServerResponse: // 5xx errors + status = ResponseStatus::TemporaryError; + break; + + case NSURLErrorTimedOut: + case NSURLErrorUserCancelledAuthentication: + status = ResponseStatus::SingularError; // retry immediately + break; + + case NSURLErrorNetworkConnectionLost: + case NSURLErrorCannotFindHost: + case NSURLErrorCannotConnectToHost: + case NSURLErrorDNSLookupFailed: + case NSURLErrorNotConnectedToInternet: + case NSURLErrorInternationalRoamingOff: + case NSURLErrorCallIsActive: + case NSURLErrorDataNotAllowed: + status = ResponseStatus::ConnectionError; + break; + + default: + status = ResponseStatus::PermanentError; + } + } + } else if ([res isKindOfClass:[NSHTTPURLResponse class]]) { + const long responseCode = [(NSHTTPURLResponse *)res statusCode]; + + response = util::make_unique<Response>(); + response->data = {(const char *)[data bytes], [data length]}; + + NSDictionary *headers = [(NSHTTPURLResponse *)res allHeaderFields]; + NSString *cache_control = [headers objectForKey:@"Cache-Control"]; + if (cache_control) { + response->expires = parseCacheControl([cache_control UTF8String]); + } + + NSString *expires = [headers objectForKey:@"Expires"]; + if (expires) { + response->expires = parse_date([expires UTF8String]); + } + + NSString *last_modified = [headers objectForKey:@"Last-Modified"]; + if (last_modified) { + response->modified = parse_date([last_modified UTF8String]); + } + + NSString *etag = [headers objectForKey:@"ETag"]; + if (etag) { + response->etag = [etag UTF8String]; + } + + if (responseCode == 304) { + if (existingResponse) { + // We're going to reuse the old response object, but need to copy over the new + // expires value (if possible). + std::swap(response, existingResponse); + if (existingResponse->expires) { + response->expires = existingResponse->expires; + } + status = ResponseStatus::NotModified; + } else { + // This is an unsolicited 304 response and should only happen on malfunctioning + // HTTP servers. It likely doesn't include any data, but we don't have much options. + response->status = Response::Successful; + status = ResponseStatus::Successful; + } + } else if (responseCode == 200) { + response->status = Response::Successful; + status = ResponseStatus::Successful; + } else if (responseCode >= 500 && responseCode < 600) { + // Server errors may be temporary, so back off exponentially. + response->status = Response::Error; + response->message = "HTTP status code " + std::to_string(responseCode); + status = ResponseStatus::TemporaryError; + } else { + // We don't know how to handle any other errors, so declare them as permanently failing. + response->status = Response::Error; + response->message = "HTTP status code " + std::to_string(responseCode); + status = ResponseStatus::PermanentError; + } + } else { + // This should never happen. + status = ResponseStatus::PermanentError; + response = util::make_unique<Response>(); + response->status = Response::Error; + response->message = "response class is not NSHTTPURLResponse"; + } + + uv_async_send(async); +} + +void HTTPRequestImpl::retry(uint64_t timeout) { + response.reset(); + + assert(!timer); + timer = new uv_timer_t; + timer->data = this; + uv_timer_init(async->loop, timer); + uv_timer_start(timer, restart, timeout, 0); +} + +void HTTPRequestImpl::retryImmediately() { + // All batons get notified when the network status changed, but some of them + // might not actually wait for the network to become available again. + if (timer && strategy == PreemptImmediately) { + // Triggers the timer upon the next event loop iteration. + uv_timer_stop(timer); + uv_timer_start(timer, restart, 0, 0); + } +} + +void HTTPRequestImpl::restart(uv_timer_t *timer, int) { + // Restart the request. + auto impl = reinterpret_cast<HTTPRequestImpl *>(timer->data); + + // Get rid of the timer. + impl->timer = nullptr; + uv::close(timer); + + impl->start(); +} + +// ------------------------------------------------------------------------------------------------- + +HTTPRequest::HTTPRequest(DefaultFileSource *source, const Resource &resource) + : SharedRequestBase(source, resource) { + // Global initialization. + dispatch_once(&request_initialize, ^{ + NSURLSessionConfiguration *sessionConfig = + [NSURLSessionConfiguration defaultSessionConfiguration]; + sessionConfig.timeoutIntervalForResource = 30; + sessionConfig.HTTPMaximumConnectionsPerHost = 8; + sessionConfig.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; + sessionConfig.URLCache = nil; + + session = [NSURLSession sessionWithConfiguration:sessionConfig]; + + // Write user agent string + userAgent = @"MapboxGL"; + }); +} + +HTTPRequest::~HTTPRequest() { + MBGL_VERIFY_THREAD(tid); + + if (ptr) { + reinterpret_cast<HTTPRequestImpl *>(ptr)->cancel(); + } +} + +void HTTPRequest::start(uv_loop_t *loop, std::unique_ptr<Response> response) { + MBGL_VERIFY_THREAD(tid); + + assert(!ptr); + ptr = new HTTPRequestImpl(this, loop, std::move(response)); +} + +void HTTPRequest::retryImmediately() { + MBGL_VERIFY_THREAD(tid); + + if (ptr) { + reinterpret_cast<HTTPRequestImpl *>(ptr)->retryImmediately(); + } +} + +void HTTPRequest::cancel() { + MBGL_VERIFY_THREAD(tid); + + delete this; +} + +} diff --git a/platform/default/asset_request_libuv.cpp b/platform/default/asset_request_libuv.cpp index 0e0b7280a7..36342bc876 100644 --- a/platform/default/asset_request_libuv.cpp +++ b/platform/default/asset_request_libuv.cpp @@ -1,19 +1,26 @@ -#include <mbgl/storage/asset_request.hpp> +#include <mbgl/storage/default/asset_request.hpp> #include <mbgl/storage/response.hpp> -#include <mbgl/platform/platform.hpp> #include <mbgl/util/std.hpp> +#include <mbgl/util/util.hpp> +#include <mbgl/util/uv.hpp> #include <uv.h> +#include <boost/algorithm/string.hpp> -#include <limits> +#include <cassert> + + +namespace algo = boost::algorithm; namespace mbgl { -struct AssetRequestBaton { - AssetRequestBaton(AssetRequest *request_, const std::string &path, uv_loop_t *loop); - ~AssetRequestBaton(); +class AssetRequestImpl { + MBGL_STORE_THREAD(tid) + +public: + AssetRequestImpl(AssetRequest *request, uv_loop_t *loop); + ~AssetRequestImpl(); - void cancel(); static void fileOpened(uv_fs_t *req); static void fileStated(uv_fs_t *req); static void fileRead(uv_fs_t *req); @@ -21,52 +28,32 @@ struct AssetRequestBaton { static void notifyError(uv_fs_t *req); static void cleanup(uv_fs_t *req); - const std::thread::id threadId; + AssetRequest *request = nullptr; + bool canceled = false; uv_fs_t req; uv_file fd = -1; - bool canceled = false; - std::string body; uv_buf_t buffer; + std::unique_ptr<Response> response; }; -AssetRequestBaton::AssetRequestBaton(AssetRequest *request_, const std::string &path, uv_loop_t *loop) - : threadId(std::this_thread::get_id()), request(request_) { - req.data = this; - uv_fs_open(loop, &req, path.c_str(), O_RDONLY, S_IRUSR, fileOpened); -} - -AssetRequestBaton::~AssetRequestBaton() { -} - -void AssetRequestBaton::cancel() { - canceled = true; +AssetRequestImpl::~AssetRequestImpl() { + MBGL_VERIFY_THREAD(tid); - // uv_cancel fails frequently when the request has already been started. - // In that case, we have to let it complete and check the canceled bool - // instead. - uv_cancel((uv_req_t *)&req); + if (request) { + request->ptr = nullptr; + } } -void AssetRequestBaton::notifyError(uv_fs_t *req) { - AssetRequestBaton *ptr = reinterpret_cast<AssetRequestBaton *>(req->data); - assert(std::this_thread::get_id() == ptr->threadId); - - if (ptr->request && req->result < 0 && !ptr->canceled && req->result != UV_ECANCELED) { - ptr->request->response = util::make_unique<Response>(); - ptr->request->response->code = req->result == UV_ENOENT ? 404 : 500; -#if UV_VERSION_MAJOR == 0 && UV_VERSION_MINOR <= 10 - ptr->request->response->message = uv_strerror(uv_last_error(req->loop)); -#else - ptr->request->response->message = uv_strerror(int(req->result)); -#endif - ptr->request->notify(); - } +AssetRequestImpl::AssetRequestImpl(AssetRequest *request_, uv_loop_t *loop) : request(request_) { + req.data = this; + uv_fs_open(loop, &req, (request->resource.url.substr(8)).c_str(), O_RDONLY, S_IRUSR, fileOpened); } -void AssetRequestBaton::fileOpened(uv_fs_t *req) { - AssetRequestBaton *ptr = reinterpret_cast<AssetRequestBaton *>(req->data); - assert(std::this_thread::get_id() == ptr->threadId); +void AssetRequestImpl::fileOpened(uv_fs_t *req) { + assert(req->data); + auto self = reinterpret_cast<AssetRequestImpl *>(req->data); + MBGL_VERIFY_THREAD(self->tid); if (req->result < 0) { // Opening failed or was canceled. There isn't much left we can do. @@ -78,88 +65,93 @@ void AssetRequestBaton::fileOpened(uv_fs_t *req) { // We're going to reuse this handle, so we need to cleanup first. uv_fs_req_cleanup(req); - if (ptr->canceled || !ptr->request) { - // Either the AssetRequest object has been destructed, or the - // request was canceled. + if (self->canceled) { + // The request was canceled. uv_fs_close(req->loop, req, fd, fileClosed); } else { - ptr->fd = fd; + self->fd = fd; uv_fs_fstat(req->loop, req, fd, fileStated); } } } -void AssetRequestBaton::fileStated(uv_fs_t *req) { - AssetRequestBaton *ptr = reinterpret_cast<AssetRequestBaton *>(req->data); - assert(std::this_thread::get_id() == ptr->threadId); +void AssetRequestImpl::fileStated(uv_fs_t *req) { + assert(req->data); + auto self = reinterpret_cast<AssetRequestImpl *>(req->data); + MBGL_VERIFY_THREAD(self->tid); - if (req->result != 0 || ptr->canceled || !ptr->request) { + if (req->result != 0 || self->canceled) { // Stating failed or was canceled. We already have an open file handle // though, which we'll have to close. notifyError(req); uv_fs_req_cleanup(req); - uv_fs_close(req->loop, req, ptr->fd, fileClosed); + uv_fs_close(req->loop, req, self->fd, fileClosed); } else { #if UV_VERSION_MAJOR == 0 && UV_VERSION_MINOR <= 10 - const uv_statbuf_t *stat = static_cast<const uv_statbuf_t *>(req->ptr); + auto stat = static_cast<const uv_statbuf_t *>(req->ptr); #else - const uv_stat_t *stat = static_cast<const uv_stat_t *>(req->ptr); + auto stat = static_cast<const uv_stat_t *>(req->ptr); #endif if (stat->st_size > std::numeric_limits<int>::max()) { // File is too large for us to open this way because uv_buf's only support unsigned // ints as maximum size. - if (ptr->request) { - ptr->request->response = util::make_unique<Response>(); - ptr->request->response->code = UV_EFBIG; + auto response = util::make_unique<Response>(); + response->status = Response::Error; #if UV_VERSION_MAJOR == 0 && UV_VERSION_MINOR <= 10 - ptr->request->response->message = uv_strerror(uv_err_t {UV_EFBIG, 0}); + response->message = uv_strerror(uv_err_t {UV_EFBIG, 0}); #else - ptr->request->response->message = uv_strerror(UV_EFBIG); + response->message = uv_strerror(UV_EFBIG); #endif - ptr->request->notify(); - } + assert(self->request); + self->request->notify(std::move(response), FileCache::Hint::No); + delete self->request; uv_fs_req_cleanup(req); - uv_fs_close(req->loop, req, ptr->fd, fileClosed); + uv_fs_close(req->loop, req, self->fd, fileClosed); } else { - const unsigned int size = (unsigned int)(stat->st_size); - ptr->body.resize(size); - ptr->buffer = uv_buf_init(const_cast<char *>(ptr->body.data()), size); + self->response = util::make_unique<Response>(); + self->response->modified = stat->st_mtimespec.tv_sec; + self->response->etag = std::to_string(stat->st_ino); + const auto size = (unsigned int)(stat->st_size); + self->response->data.resize(size); + self->buffer = uv_buf_init(const_cast<char *>(self->response->data.data()), size); uv_fs_req_cleanup(req); #if UV_VERSION_MAJOR == 0 && UV_VERSION_MINOR <= 10 - uv_fs_read(req->loop, req, ptr->fd, ptr->buffer.base, ptr->buffer.len, -1, fileRead); + uv_fs_read(req->loop, req, self->fd, self->buffer.base, self->buffer.len, -1, fileRead); #else - uv_fs_read(req->loop, req, ptr->fd, &ptr->buffer, 1, 0, fileRead); + uv_fs_read(req->loop, req, self->fd, &self->buffer, 1, 0, fileRead); #endif } } } -void AssetRequestBaton::fileRead(uv_fs_t *req) { - AssetRequestBaton *ptr = reinterpret_cast<AssetRequestBaton *>(req->data); - assert(std::this_thread::get_id() == ptr->threadId); +void AssetRequestImpl::fileRead(uv_fs_t *req) { + assert(req->data); + auto self = reinterpret_cast<AssetRequestImpl *>(req->data); + MBGL_VERIFY_THREAD(self->tid); - if (req->result < 0 || ptr->canceled || !ptr->request) { - // Reading failed or was canceled. We already have an open file handle + if (req->result < 0 || self->canceled) { + // Stating failed or was canceled. We already have an open file handle // though, which we'll have to close. notifyError(req); } else { // File was successfully read. - if (ptr->request) { - ptr->request->response = util::make_unique<Response>(); - ptr->request->response->code = 200; - ptr->request->response->data = std::move(ptr->body); - ptr->request->notify(); - } + self->response->status = Response::Successful; + assert(self->request); + self->request->notify(std::move(self->response), FileCache::Hint::No); + delete self->request; } uv_fs_req_cleanup(req); - uv_fs_close(req->loop, req, ptr->fd, fileClosed); + uv_fs_close(req->loop, req, self->fd, fileClosed); } -void AssetRequestBaton::fileClosed(uv_fs_t *req) { - assert(std::this_thread::get_id() == (reinterpret_cast<AssetRequestBaton *>(req->data))->threadId); +void AssetRequestImpl::fileClosed(uv_fs_t *req) { + assert(req->data); + auto self = reinterpret_cast<AssetRequestImpl *>(req->data); + MBGL_VERIFY_THREAD(self->tid); + (void(self)); // Silence unused variable error in Release mode if (req->result < 0) { // Closing the file failed. But there isn't anything we can do. @@ -168,56 +160,70 @@ void AssetRequestBaton::fileClosed(uv_fs_t *req) { cleanup(req); } -void AssetRequestBaton::cleanup(uv_fs_t *req) { - AssetRequestBaton *ptr = reinterpret_cast<AssetRequestBaton *>(req->data); - assert(std::this_thread::get_id() == ptr->threadId); - - if (ptr->request) { - ptr->request->ptr = nullptr; +void AssetRequestImpl::notifyError(uv_fs_t *req) { + assert(req->data); + auto self = reinterpret_cast<AssetRequestImpl *>(req->data); + MBGL_VERIFY_THREAD(self->tid); + + if (req->result < 0 && !self->canceled && req->result != UV_ECANCELED) { + auto response = util::make_unique<Response>(); + response->status = Response::Error; + response->message = uv::getFileRequestError(req); + assert(self->request); + self->request->notify(std::move(response), FileCache::Hint::No); + delete self->request; } +} +void AssetRequestImpl::cleanup(uv_fs_t *req) { + assert(req->data); + auto self = reinterpret_cast<AssetRequestImpl *>(req->data); + MBGL_VERIFY_THREAD(self->tid); uv_fs_req_cleanup(req); - delete ptr; - ptr = nullptr; + delete self; } +// ------------------------------------------------------------------------------------------------- -AssetRequest::AssetRequest(const std::string &path_, uv_loop_t *loop) - : BaseRequest(path_) { - if (!path.empty() && path[0] == '/') { - // This is an absolute path. We don't allow this. Note that this is not a way to absolutely - // prevent access to resources outside the application bundle; e.g. there could be symlinks - // in the application bundle that link to outside. We don't care about these. - response = util::make_unique<Response>(); - response->code = 403; - response->message = "Path is outside the application bundle"; - notify(); - } else { - // Note: The AssetRequestBaton object is deleted in AssetRequestBaton::cleanup(). - ptr = new AssetRequestBaton(this, platform::applicationRoot() + "/" + path, loop); - } +AssetRequest::AssetRequest(DefaultFileSource *source, const Resource &resource) + : SharedRequestBase(source, resource) { + assert(algo::starts_with(resource.url, "asset://")); } -void AssetRequest::cancel() { - assert(std::this_thread::get_id() == threadId); +AssetRequest::~AssetRequest() { + MBGL_VERIFY_THREAD(tid); if (ptr) { - ptr->cancel(); - - // When deleting a AssetRequest object with a uv_fs_* call is in progress, we are making sure - // that the callback doesn't accidentally reference this object again. - ptr->request = nullptr; - ptr = nullptr; + reinterpret_cast<AssetRequestImpl *>(ptr)->request = nullptr; } +} + +void AssetRequest::start(uv_loop_t *loop, std::unique_ptr<Response> response) { + MBGL_VERIFY_THREAD(tid); + + // We're ignoring the existing response if any. + (void(response)); - notify(); + assert(!ptr); + ptr = new AssetRequestImpl(this, loop); + // Note: the AssetRequestImpl deletes itself. } -AssetRequest::~AssetRequest() { - assert(std::this_thread::get_id() == threadId); - cancel(); +void AssetRequest::cancel() { + MBGL_VERIFY_THREAD(tid); - // Note: The AssetRequestBaton object is deleted in AssetRequestBaton::cleanup(). + if (ptr) { + reinterpret_cast<AssetRequestImpl *>(ptr)->canceled = true; + + // uv_cancel fails frequently when the request has already been started. + // In that case, we have to let it complete and check the canceled bool + // instead. The cancelation callback will delete the AssetRequest object. + uv_cancel((uv_req_t *)&reinterpret_cast<AssetRequestImpl *>(ptr)->req); + } else { + // This request is canceled before we called start. We're safe to delete + // ourselves now. + delete this; + } } } diff --git a/platform/default/cache_database_tmp.cpp b/platform/default/cache_database_tmp.cpp deleted file mode 100644 index 6132ace692..0000000000 --- a/platform/default/cache_database_tmp.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include <mbgl/platform/platform.hpp> - -namespace mbgl { -namespace platform { - -// Returns the path to the default cache database on this system. -std::string defaultCacheDatabase() { - return "/tmp/mbgl-cache.db"; -} - -} -} diff --git a/platform/default/http_request_baton_curl.cpp b/platform/default/http_request_baton_curl.cpp deleted file mode 100644 index 9e8cf64716..0000000000 --- a/platform/default/http_request_baton_curl.cpp +++ /dev/null @@ -1,629 +0,0 @@ -#include <mbgl/mbgl.hpp> -#include <mbgl/storage/http_request_baton.hpp> -#include <mbgl/util/uv-messenger.h> -#include <mbgl/util/time.hpp> -#include <mbgl/util/string.hpp> -#include <mbgl/util/std.hpp> -#include <mbgl/util/version.hpp> - -#ifdef __ANDROID__ - #include <mbgl/android/jni.hpp> - #include <zip.h> - #include <openssl/ssl.h> -#endif - -#include <uv.h> -#include <curl/curl.h> - -#include <sys/utsname.h> - -#include <queue> -#include <cassert> -#include <cstring> -#include <thread> - - -// Check curl library version. -const static bool curl_version_check = []() { - const auto version = curl_version_info(CURLVERSION_NOW); - if (version->version_num != LIBCURL_VERSION_NUM) { - throw std::runtime_error(mbgl::util::sprintf<96>( - "libcurl version mismatch: headers report %d.%d.%d, but library reports %d.%d.%d", - (LIBCURL_VERSION_NUM >> 16) & 0xFF, (LIBCURL_VERSION_NUM >> 8) & 0xFF, LIBCURL_VERSION_NUM & 0xFF, - (version->version_num >> 16) & 0xFF, (version->version_num >> 8) & 0xFF, version->version_num & 0xFF)); - } - return true; -}(); - - -// This file contains code from http://curl.haxx.se/libcurl/c/multi-uv.html: - -/*************************************************************************** - * _ _ ____ _ - * Project ___| | | | _ \| | - * / __| | | | |_) | | - * | (__| |_| | _ <| |___ - * \___|\___/|_| \_\_____| - * - * Copyright (C) 1998 - 2014, Daniel Stenberg, <daniel@haxx.se>, et al. - * - * This software is licensed as described in the file COPYING, which - * you should have received as part of this distribution. The terms - * are also available at http://curl.haxx.se/docs/copyright.html. - * - * You may opt to use, copy, modify, merge, publish, distribute and/or sell - * copies of the Software, and permit persons to whom the Software is - * furnished to do so, under the terms of the COPYING file. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - ***************************************************************************/ - -/* Example application code using the multi socket interface to download - multiple files at once, but instead of using curl_multi_perform and - curl_multi_wait, which uses select(), we use libuv. - It supports epoll, kqueue, etc. on unixes and fast IO completion ports on - Windows, which means, it should be very fast on all platforms.. - - Written by Clemens Gruber, based on an outdated example from uvbook and - some tests from libuv. - - Requires libuv and (of course) libcurl. - - See http://nikhilm.github.com/uvbook/ for more information on libuv. -*/ - -// Handles the request thread + messaging to the thread. -static uv_once_t once; -static uv_loop_t *loop = nullptr; -static uv_messenger_t start_messenger; -static uv_messenger_t stop_messenger; -static uv_thread_t thread; -static std::thread::id thread_id; - -// Used as the CURL timer function to periodically check for socket updates. -static uv_timer_t timeout; - -// CURL multi handle that we use to request multiple URLs at the same time, without having to block -// and spawn threads. -static CURLM *multi = nullptr; - -// CURL share handles are used for sharing session state (e.g.) -static uv_mutex_t share_mutex; -static CURLSH *share = nullptr; - -// A queue that we use for storing resuable CURL easy handles to avoid creating and destroying them -// all the time. -static std::queue<CURL *> handles; - -namespace mbgl { - -struct Context { - const util::ptr<HTTPRequestBaton> baton; - CURL *handle = nullptr; - curl_slist *headers = nullptr; - - Context(const util::ptr<HTTPRequestBaton> &baton_) : baton(baton_) { - assert(baton); - baton->ptr = this; - - if (!handles.empty()) { - handle = handles.front(); - handles.pop(); - } else { - handle = curl_easy_init(); - } - } - - ~Context() { - baton->ptr = nullptr; - - if (headers) { - curl_slist_free_all(headers); - headers = nullptr; - } - - CURLMcode error = curl_multi_remove_handle(multi, handle); - if (error != CURLM_OK) { - baton->response = util::make_unique<Response>(); - baton->response->code = -1; - baton->response->message = curl_multi_strerror(error); - } - - curl_easy_setopt(handle, CURLOPT_PRIVATE, nullptr); - curl_easy_reset(handle); - handles.push(handle); - handle = nullptr; - - if (baton->async) { - uv_async_send(baton->async); - baton->async = nullptr; - } - } -}; - -struct Socket { -private: - uv_poll_t poll_handle; - -public: - const curl_socket_t sockfd = 0; - -public: - Socket(curl_socket_t sockfd_) : sockfd(sockfd_) { - uv_poll_init_socket(loop, &poll_handle, sockfd); - poll_handle.data = this; - } - - void start(int events, uv_poll_cb cb) { - uv_poll_start(&poll_handle, events, cb); - } - - void stop() { - assert(poll_handle.data); - poll_handle.data = nullptr; - uv_poll_stop(&poll_handle); - uv_close((uv_handle_t *)&poll_handle, [](uv_handle_t *handle) { - delete (Socket *)handle->data; - }); - } - -private: - // Make the destructor private to ensure that we can only close the Socket - // with stop(), and disallow manual deletion. - ~Socket() { - assert(!poll_handle.data); - } -}; - -// Locks the CURL share handle -void curl_share_lock(CURL *, curl_lock_data, curl_lock_access, void *) { - uv_mutex_lock(&share_mutex); -} - -// Unlocks the CURL share handle -void curl_share_unlock(CURL *, curl_lock_data, void *) { - uv_mutex_unlock(&share_mutex); -} - -void check_multi_info() { - CURLMsg *message = nullptr; - int pending = 0; - - while ((message = curl_multi_info_read(multi, &pending))) { - switch (message->msg) { - case CURLMSG_DONE: { - Context *context = nullptr; - curl_easy_getinfo(message->easy_handle, CURLINFO_PRIVATE, (char *)&context); - assert(context); - - auto baton = context->baton; - - // This request is complete. We are removing the pointer to the CURL easy handle again - // to prevent this request from getting canceled. - context->baton->ptr = nullptr; - - // Add human-readable error code - if (message->data.result != CURLE_OK) { - baton->response->message = curl_easy_strerror(message->data.result); - baton->response->code = -1; - - switch (message->data.result) { - case CURLE_COULDNT_RESOLVE_PROXY: - case CURLE_COULDNT_RESOLVE_HOST: - case CURLE_COULDNT_CONNECT: - baton->type = HTTPResponseType::ConnectionError; - break; - - case CURLE_OPERATION_TIMEDOUT: - baton->type = HTTPResponseType::TemporaryError; - break; - - default: - baton->type = HTTPResponseType::PermanentError; - } - } else { - long code = 0; - curl_easy_getinfo(message->easy_handle, CURLINFO_RESPONSE_CODE, &code); - - if (code != 304) { - baton->response->code = code; - } - - if (code == 304) { - baton->type = HTTPResponseType::NotModified; - } else if (code == 200) { - baton->type = HTTPResponseType::Successful; - } else if (code >= 500 && code < 600) { - baton->type = HTTPResponseType::TemporaryError; - } else if (code >= 400 && code < 500) { - baton->type = HTTPResponseType::PermanentError; - } else { - assert(!"code must be either 200 or 304"); - } - } - - delete context; - } break; - - default: - // This should never happen, because there are no other message types. - throw std::runtime_error("CURLMSG returned unknown message type"); - } - } -} - -void curl_perform(uv_poll_t *req, int /* status */, int events) { - int flags = 0; - - uv_timer_stop(&timeout); - - if (events & UV_READABLE) { - flags |= CURL_CSELECT_IN; - } - if (events & UV_WRITABLE) { - flags |= CURL_CSELECT_OUT; - } - - Socket *context = (Socket *)req->data; - int running_handles = 0; - curl_multi_socket_action(multi, context->sockfd, flags, &running_handles); - - check_multi_info(); -} - -int handle_socket(CURL * /* handle */, curl_socket_t s, int action, void * /* userp */, void *socketp) { - Socket *socket = (Socket *)socketp; - if (!socket && action != CURL_POLL_REMOVE) { - socket = new Socket(s); - curl_multi_assign(multi, s, (void *)socket); - } - - switch (action) { - case CURL_POLL_IN: - socket->start(UV_READABLE, curl_perform); - break; - case CURL_POLL_OUT: - socket->start(UV_WRITABLE, curl_perform); - break; - case CURL_POLL_REMOVE: - if (socket) { - socket->stop(); - curl_multi_assign(multi, s, NULL); - } - break; - default: - abort(); - } - - return 0; -} - -#if UV_VERSION_MAJOR == 0 && UV_VERSION_MINOR <= 10 -void on_timeout(uv_timer_t * /* req */, int /* status */) { -#else -void on_timeout(uv_timer_t * /* req */) { -#endif - int running_handles; - CURLMcode error = curl_multi_socket_action(multi, CURL_SOCKET_TIMEOUT, 0, &running_handles); - if (error != CURLM_OK) { - throw std::runtime_error(std::string("CURL multi error: ") + curl_multi_strerror(error)); - } - - check_multi_info(); -} - -void start_timeout(CURLM * /* multi */, long timeout_ms, void * /* userp */) { - if (timeout_ms <= 0) { - timeout_ms = 1; /* 0 means directly call socket_action, but we'll do it in a bit */ - } - uv_timer_start(&timeout, on_timeout, timeout_ms, 0); -} - -void thread_init(void *) { -#ifdef __APPLE__ - pthread_setname_np("CURL"); -#endif - thread_id = std::this_thread::get_id(); - - if (curl_global_init(CURL_GLOBAL_ALL)) { - throw std::runtime_error("Could not init cURL"); - } - - uv_timer_init(loop, &timeout); - - CURLSHcode share_error; - share = curl_share_init(); - - share_error = curl_share_setopt(share, CURLSHOPT_LOCKFUNC, curl_share_lock); - if (share_error != CURLSHE_OK) { - throw std::runtime_error(std::string("CURL share error: ") + curl_share_strerror(share_error)); - } - - share_error = curl_share_setopt(share, CURLSHOPT_UNLOCKFUNC, curl_share_unlock); - if (share_error != CURLSHE_OK) { - throw std::runtime_error(std::string("CURL share error: ") + curl_share_strerror(share_error)); - } - - CURLMcode multi_error; - multi = curl_multi_init(); - - multi_error = curl_multi_setopt(multi, CURLMOPT_SOCKETFUNCTION, handle_socket); - if (multi_error != CURLM_OK) { - throw std::runtime_error(std::string("CURL multi error: ") + curl_multi_strerror(multi_error)); - } - multi_error = curl_multi_setopt(multi, CURLMOPT_TIMERFUNCTION, start_timeout); - if (multi_error != CURLM_OK) { - throw std::runtime_error(std::string("CURL multi error: ") + curl_multi_strerror(multi_error)); - } - - // Main event loop. This will not return until the request loop is terminated. - uv_run(loop, UV_RUN_DEFAULT); - - curl_multi_cleanup(multi); - multi = nullptr; - - curl_share_cleanup(share); - share = nullptr; - - thread_id = std::thread::id(); -} - -// This function is called when we have new data for a request. We just append it to the string -// containing the previous data. -size_t curl_write_cb(void *const contents, const size_t size, const size_t nmemb, void *const userp) { - auto &response = *(std::unique_ptr<Response> *)userp; - assert(response); - response->data.append((char *)contents, size * nmemb); - return size * nmemb; -} - -// Compares the beginning of the (non-zero-terminated!) data buffer with the (zero-terminated!) -// header string. If the data buffer contains the header string at the beginning, it returns -// the length of the header string == begin of the value, otherwise it returns npos. -// The comparison of the header is ASCII-case-insensitive. -size_t header_matches(const char *const header, const char *const buffer, const size_t length) { - const size_t header_length = strlen(header); - if (length < header_length) return std::string::npos; - size_t i = 0; - while (i < length && i < header_length && std::tolower(buffer[i]) == header[i]) { - i++; - } - return i == header_length ? i : std::string::npos; -} - -size_t curl_header_cb(char * const buffer, const size_t size, const size_t nmemb, void *const userp) { - const size_t length = size * nmemb; - - auto &response = *(std::unique_ptr<Response> *)userp; - assert(response); - - size_t begin = std::string::npos; - if ((begin = header_matches("last-modified: ", buffer, length)) != std::string::npos) { - // Always overwrite the modification date; We might already have a value here from the - // Date header, but this one is more accurate. - const std::string value { buffer + begin, length - begin - 2 }; // remove \r\n - response->modified = curl_getdate(value.c_str(), nullptr); - } else if ((begin = header_matches("etag: ", buffer, length)) != std::string::npos) { - response->etag = { buffer + begin, length - begin - 2 }; // remove \r\n - } else if ((begin = header_matches("cache-control: ", buffer, length)) != std::string::npos) { - const std::string value { buffer + begin, length - begin - 2 }; // remove \r\n - response->expires = Response::parseCacheControl(value.c_str()); - } - - return length; -} - -// This function is called to load the CA bundle -// from http://curl.haxx.se/libcurl/c/cacertinmem.html -#ifdef __ANDROID__ -static CURLcode sslctx_function(CURL */*curl*/, void *sslctx, void */*parm*/) { - - int error = 0; - struct zip *apk = zip_open(mbgl::android::apkPath.c_str(), 0, &error); - if (apk == nullptr) { - return CURLE_SSL_CACERT_BADFILE; - } - - struct zip_file *apkFile = zip_fopen(apk, "assets/ca-bundle.crt", ZIP_FL_NOCASE); - if (apkFile == nullptr) { - zip_close(apk); - apk = nullptr; - return CURLE_SSL_CACERT_BADFILE; - } - - struct zip_stat stat; - if (zip_stat(apk, "assets/ca-bundle.crt", ZIP_FL_NOCASE, &stat) != 0) { - zip_fclose(apkFile); - apkFile = nullptr; - zip_close(apk); - apk = nullptr; - return CURLE_SSL_CACERT_BADFILE; - } - - if (stat.size > std::numeric_limits<int>::max()) { - zip_fclose(apkFile); - apkFile = nullptr; - zip_close(apk); - apk = nullptr; - return CURLE_SSL_CACERT_BADFILE; - } - - const std::unique_ptr<char[]> pem = util::make_unique<char[]>(stat.size); - - if (static_cast<zip_uint64_t>(zip_fread(apkFile, reinterpret_cast<void *>(pem.get()), stat.size)) != stat.size) { - zip_fclose(apkFile); - apkFile = nullptr; - zip_close(apk); - apk = nullptr; - return CURLE_SSL_CACERT_BADFILE; - } - - // get a pointer to the X509 certificate store (which may be empty!) - X509_STORE *store = SSL_CTX_get_cert_store((SSL_CTX *)sslctx); - if (store == nullptr) { - return CURLE_SSL_CACERT_BADFILE; - } - - // get a BIO - BIO *bio = BIO_new_mem_buf(pem.get(), static_cast<int>(stat.size)); - if (bio == nullptr) { - store = nullptr; - return CURLE_SSL_CACERT_BADFILE; - } - - // use it to read the PEM formatted certificate from memory into an X509 - // structure that SSL can use - X509 *cert = nullptr; - while (PEM_read_bio_X509(bio, &cert, 0, nullptr) != nullptr) { - if (cert == nullptr) { - BIO_free(bio); - bio = nullptr; - store = nullptr; - return CURLE_SSL_CACERT_BADFILE; - } - - // add our certificate to this store - if (X509_STORE_add_cert(store, cert) == 0) { - X509_free(cert); - cert = nullptr; - BIO_free(bio); - bio = nullptr; - store = nullptr; - return CURLE_SSL_CACERT_BADFILE; - } - - X509_free(cert); - cert = nullptr; - } - - // decrease reference counts - BIO_free(bio); - bio = nullptr; - - zip_fclose(apkFile); - apkFile = nullptr; - zip_close(apk); - apk = nullptr; - - // all set to go - return CURLE_OK; -} -#endif - -std::string buildUserAgentString() { -#ifdef __ANDROID__ - return util::sprintf<128>("MapboxGL/%d.%d.%d (+https://mapbox.com/mapbox-gl/; %s; %s %s)", - version::major, version::minor, version::patch, version::revision, "Android", mbgl::android::androidRelease.c_str()); -#else - utsname name; - uname(&name); - return util::sprintf<128>("MapboxGL/%d.%d.%d (+https://mapbox.com/mapbox-gl/; %s; %s %s)", - version::major, version::minor, version::patch, version::revision, name.sysname, name.release); -#endif -} - -// This function must run in the CURL thread. -void start_request(void *const ptr) { - assert(std::this_thread::get_id() == thread_id); - static const std::string userAgent = buildUserAgentString(); - - // The Context object stores information that we need to retain throughout the request, such - // as the actual CURL easy handle, the baton, and the list of headers. The Context itself is - // stored in both the CURL easy handle's PRIVATE field, and the baton's `ptr` field. - auto context = new Context(*(util::ptr<HTTPRequestBaton> *)ptr); - delete (util::ptr<HTTPRequestBaton> *)ptr; - - if (context->baton->response) { - if (!context->baton->response->etag.empty()) { - const std::string header = std::string("If-None-Match: ") + context->baton->response->etag; - context->headers = curl_slist_append(context->headers, header.c_str()); - } else if (context->baton->response->modified) { - const std::string time = - std::string("If-Modified-Since: ") + util::rfc1123(context->baton->response->modified); - context->headers = curl_slist_append(context->headers, time.c_str()); - } - } - - if (context->headers) { - curl_easy_setopt(context->handle, CURLOPT_HTTPHEADER, context->headers); - } - - if (!context->baton->response) { - context->baton->response = util::make_unique<Response>(); - } - - // Carry on the shared pointer in the private information of the CURL handle. - curl_easy_setopt(context->handle, CURLOPT_PRIVATE, context); -#ifndef __ANDROID__ - curl_easy_setopt(context->handle, CURLOPT_CAINFO, "ca-bundle.crt"); -#else - curl_easy_setopt(context->handle, CURLOPT_SSLCERTTYPE, "PEM"); - curl_easy_setopt(context->handle, CURLOPT_SSL_CTX_FUNCTION, sslctx_function); -#endif - curl_easy_setopt(context->handle, CURLOPT_FOLLOWLOCATION, 1); - curl_easy_setopt(context->handle, CURLOPT_URL, context->baton->path.c_str()); - curl_easy_setopt(context->handle, CURLOPT_WRITEFUNCTION, curl_write_cb); - curl_easy_setopt(context->handle, CURLOPT_WRITEDATA, &context->baton->response); - curl_easy_setopt(context->handle, CURLOPT_HEADERFUNCTION, curl_header_cb); - curl_easy_setopt(context->handle, CURLOPT_HEADERDATA, &context->baton->response); - curl_easy_setopt(context->handle, CURLOPT_ACCEPT_ENCODING, "gzip, deflate"); - curl_easy_setopt(context->handle, CURLOPT_USERAGENT, userAgent.c_str()); - curl_easy_setopt(context->handle, CURLOPT_SHARE, share); - - // Start requesting the information. - curl_multi_add_handle(multi, context->handle); -} - -// This function must run in the CURL thread. -void stop_request(void *const ptr) { - assert(std::this_thread::get_id() == thread_id); - auto baton = *(util::ptr<HTTPRequestBaton> *)ptr; - delete (util::ptr<HTTPRequestBaton> *)ptr; - assert(baton); - - if (baton->async) { - baton->type = HTTPResponseType::Canceled; - - assert(baton->ptr); - - // We can still stop the request because it is still in progress. - delete (Context *)baton->ptr; - assert(!baton->ptr); - } else { - // If the async handle is gone, it means that the actual request has been completed before - // we got a chance to cancel it. In this case, this is a no-op. It is likely that - // the pointer below is the last lifeline of the HTTPRequestBaton. This means we're going - // to delete the HTTPRequestBaton in the current (CURL) thread. - } -} - -void create_thread() { - uv_mutex_init(&share_mutex); -#if UV_VERSION_MAJOR == 0 && UV_VERSION_MINOR <= 10 - loop = uv_loop_new(); -#else - loop = new uv_loop_t; - uv_loop_init(loop); -#endif - uv_messenger_init(loop, &start_messenger, start_request); - uv_messenger_init(loop, &stop_messenger, stop_request); - uv_thread_create(&thread, thread_init, nullptr); -} - -// This function must be run from the main thread (== where the HTTPRequestBaton was created) -void HTTPRequestBaton::start(const util::ptr<HTTPRequestBaton> &ptr) { - assert(std::this_thread::get_id() == ptr->threadId); - uv_once(&once, create_thread); - uv_messenger_send(&start_messenger, new util::ptr<HTTPRequestBaton>(ptr)); -} - -// This function must be run from the main thread (== where the HTTPRequestBaton was created) -void HTTPRequestBaton::stop(const util::ptr<HTTPRequestBaton> &ptr) { - assert(std::this_thread::get_id() == ptr->threadId); - uv_once(&once, create_thread); - uv_messenger_send(&stop_messenger, new util::ptr<HTTPRequestBaton>(ptr)); -} - -} diff --git a/platform/default/http_request_curl.cpp b/platform/default/http_request_curl.cpp new file mode 100644 index 0000000000..42764e4927 --- /dev/null +++ b/platform/default/http_request_curl.cpp @@ -0,0 +1,640 @@ +#include <mbgl/storage/default/http_request.hpp> +#include <mbgl/storage/default/http_context.hpp> +#include <mbgl/storage/response.hpp> + +#include <mbgl/util/time.hpp> + +#include <curl/curl.h> + +#include <queue> +#include <map> +#include <cassert> + + +void handleError(CURLMcode code) { + if (code != CURLM_OK) { + throw std::runtime_error(std::string("CURL multi error: ") + curl_multi_strerror(code)); + } +} + +void handleError(CURLcode code) { + if (code != CURLE_OK) { + throw std::runtime_error(std::string("CURL easy error: ") + curl_easy_strerror(code)); + } +} + +namespace mbgl { + +enum class ResponseStatus : int8_t { + // This error probably won't be resolved by retrying anytime soon. We are giving up. + PermanentError, + + // This error might be resolved by waiting some time (e.g. server issues). + // We are going to do an exponential back-off and will try again in a few seconds. + TemporaryError, + + // This error might be resolved once the network reachability status changes. + // We are going to watch the network status for changes and will retry as soon as the + // operating system notifies us of a network status change. + ConnectionError, + + // The request returned data successfully. We retrieved and decoded the data successfully. + Successful, + + // The request confirmed that the data wasn't changed. We already have the data. + NotModified, +}; + +class HTTPRequestImpl; + +class HTTPCURLContext : public HTTPContext<HTTPCURLContext> { + +public: + HTTPCURLContext(uv_loop_t *loop); + ~HTTPCURLContext(); + + static int handleSocket(CURL *handle, curl_socket_t s, int action, void *userp, void *socketp); + static void perform(uv_poll_t *req, int status, int events); + static int startTimeout(CURLM *multi, long timeout_ms, void *userp); +#if UV_VERSION_MAJOR == 0 && UV_VERSION_MINOR <= 10 + static void onTimeout(uv_timer_t *req, int status); +#else + static void onTimeout(uv_timer_t *req); +#endif + + CURL *getHandle(); + void returnHandle(CURL *handle); + void checkMultiInfo(); + +public: + // Used as the CURL timer function to periodically check for socket updates. + uv_timer_t *timeout = nullptr; + + // CURL multi handle that we use to request multiple URLs at the same time, without having to + // block and spawn threads. + CURLM *multi = nullptr; + + // CURL share handles are used for sharing session state (e.g.) + CURLSH *share = nullptr; + + // A queue that we use for storing resuable CURL easy handles to avoid creating and destroying + // them all the time. + std::queue<CURL *> handles; +}; + + +class HTTPRequestImpl { + MBGL_STORE_THREAD(tid) + +public: + HTTPRequestImpl(HTTPRequest *request, uv_loop_t *loop, std::unique_ptr<Response> response); + ~HTTPRequestImpl(); + + void handleResult(CURLcode code); + void abandon(); + void retryImmediately(); + +private: + static size_t headerCallback(char *const buffer, const size_t size, const size_t nmemb, void *userp); + static size_t writeCallback(void *const contents, const size_t size, const size_t nmemb, void *userp); + + void retry(uint64_t timeout); + static void restart(uv_timer_t *timer, int); + void finish(ResponseStatus status); + void start(); + +private: + HTTPCURLContext *context = nullptr; + HTTPRequest *request = nullptr; + + // Will store the current response. + std::unique_ptr<Response> response; + + // In case of revalidation requests, this will store the old response. + std::unique_ptr<Response> existingResponse; + + CURL *handle = nullptr; + curl_slist *headers = nullptr; + + uv_timer_t *timer = nullptr; + enum : bool { PreemptImmediately, ExponentialBackoff } strategy = PreemptImmediately; + int attempts = 0; + + static const int maxAttempts = 4; +}; + + + +struct Socket { +private: + uv_poll_t poll; + +public: + HTTPCURLContext *context = nullptr; + const curl_socket_t sockfd = 0; + +public: + Socket(HTTPCURLContext *context_, curl_socket_t sockfd_) : context(context_), sockfd(sockfd_) { + assert(context); + uv_poll_init_socket(context->loop, &poll, sockfd); + poll.data = this; + } + + void start(int events, uv_poll_cb cb) { + uv_poll_start(&poll, events, cb); + } + + void stop() { + assert(poll.data); + uv_poll_stop(&poll); + uv_close((uv_handle_t *)&poll, [](uv_handle_t *handle) { + assert(handle->data); + delete reinterpret_cast<Socket *>(handle->data); + }); + } + +private: + // Make the destructor private to ensure that we can only close the Socket + // with stop(), and disallow manual deletion. + ~Socket() = default; +}; + +// ------------------------------------------------------------------------------------------------- + +template<> pthread_key_t HTTPContext<HTTPCURLContext>::key{}; +template<> pthread_once_t HTTPContext<HTTPCURLContext>::once = PTHREAD_ONCE_INIT; + +HTTPCURLContext::HTTPCURLContext(uv_loop_t *loop_) : HTTPContext(loop_) { + if (curl_global_init(CURL_GLOBAL_ALL)) { + throw std::runtime_error("Could not init cURL"); + } + + timeout = new uv_timer_t; + timeout->data = this; + uv_timer_init(loop, timeout); + + share = curl_share_init(); + + multi = curl_multi_init(); + handleError(curl_multi_setopt(multi, CURLMOPT_SOCKETFUNCTION, handleSocket)); + handleError(curl_multi_setopt(multi, CURLMOPT_SOCKETDATA, this)); + handleError(curl_multi_setopt(multi, CURLMOPT_TIMERFUNCTION, startTimeout)); + handleError(curl_multi_setopt(multi, CURLMOPT_TIMERDATA, this)); +} + +HTTPCURLContext::~HTTPCURLContext() { + curl_multi_cleanup(multi); + multi = nullptr; + + curl_share_cleanup(share); + share = nullptr; + + uv_timer_stop(timeout); + uv::close(timeout); +} + +CURL *HTTPCURLContext::getHandle() { + if (!handles.empty()) { + auto handle = handles.front(); + handles.pop(); + return handle; + } else { + return curl_easy_init(); + } +} + +void HTTPCURLContext::returnHandle(CURL *handle) { + curl_easy_reset(handle); + handles.push(handle); +} + +void HTTPCURLContext::checkMultiInfo() { + MBGL_VERIFY_THREAD(tid); + CURLMsg *message = nullptr; + int pending = 0; + + while ((message = curl_multi_info_read(multi, &pending))) { + switch (message->msg) { + case CURLMSG_DONE: { + HTTPRequestImpl *baton = nullptr; + curl_easy_getinfo(message->easy_handle, CURLINFO_PRIVATE, (char *)&baton); + assert(baton); + baton->handleResult(message->data.result); + } break; + + default: + // This should never happen, because there are no other message types. + throw std::runtime_error("CURLMsg returned unknown message type"); + } + } +} + +void HTTPCURLContext::perform(uv_poll_t *req, int /* status */, int events) { + assert(req->data); + auto socket = reinterpret_cast<Socket *>(req->data); + auto context = socket->context; + MBGL_VERIFY_THREAD(context->tid); + + int flags = 0; + + if (events & UV_READABLE) { + flags |= CURL_CSELECT_IN; + } + if (events & UV_WRITABLE) { + flags |= CURL_CSELECT_OUT; + } + + + int running_handles = 0; + curl_multi_socket_action(context->multi, socket->sockfd, flags, &running_handles); + context->checkMultiInfo(); +} + +int HTTPCURLContext::handleSocket(CURL * /* handle */, curl_socket_t s, int action, void *userp, + void *socketp) { + auto socket = reinterpret_cast<Socket *>(socketp); + assert(userp); + auto context = reinterpret_cast<HTTPCURLContext *>(userp); + MBGL_VERIFY_THREAD(context->tid); + + if (!socket && action != CURL_POLL_REMOVE) { + socket = new Socket(context, s); + curl_multi_assign(context->multi, s, (void *)socket); + } + + switch (action) { + case CURL_POLL_IN: + socket->start(UV_READABLE, perform); + break; + case CURL_POLL_OUT: + socket->start(UV_WRITABLE, perform); + break; + case CURL_POLL_REMOVE: + if (socket) { + socket->stop(); + curl_multi_assign(context->multi, s, nullptr); + } + break; + default: + throw std::runtime_error("Unhandled CURL socket action"); + } + + return 0; +} + +#if UV_VERSION_MAJOR == 0 && UV_VERSION_MINOR <= 10 +void HTTPCURLContext::onTimeout(uv_timer_t *req, int /* status */) { +#else +void HTTPCURLContext::onTimeout(uv_timer_t *req) { +#endif + assert(req->data); + auto context = reinterpret_cast<HTTPCURLContext *>(req->data); + MBGL_VERIFY_THREAD(context->tid); + int running_handles; + CURLMcode error = curl_multi_socket_action(context->multi, CURL_SOCKET_TIMEOUT, 0, &running_handles); + if (error != CURLM_OK) { + throw std::runtime_error(std::string("CURL multi error: ") + curl_multi_strerror(error)); + } + context->checkMultiInfo(); +} + +int HTTPCURLContext::startTimeout(CURLM * /* multi */, long timeout_ms, void *userp) { + assert(userp); + auto context = reinterpret_cast<HTTPCURLContext *>(userp); + MBGL_VERIFY_THREAD(context->tid); + if (timeout_ms < 0) { + // A timeout of 0 ms means that the timer will invoked in the next loop iteration. + timeout_ms = 0; + } + uv_timer_stop(context->timeout); + uv_timer_start(context->timeout, onTimeout, timeout_ms, 0); + return 0; +} + +// ------------------------------------------------------------------------------------------------- + +HTTPRequestImpl::HTTPRequestImpl(HTTPRequest *request_, uv_loop_t *loop, std::unique_ptr<Response> response_) + : context(HTTPCURLContext::Get(loop)), + request(request_), + existingResponse(std::move(response_)), + handle(context->getHandle()) { + assert(request); + context->addRequest(request); + + // If there's already a response, set the correct etags/modified headers to make sure we are + // getting a 304 response if possible. This avoids redownloading unchanged data. + if (existingResponse) { + if (!existingResponse->etag.empty()) { + const std::string header = std::string("If-None-Match: ") + existingResponse->etag; + headers = curl_slist_append(headers, header.c_str()); + } else if (existingResponse->modified) { + const std::string time = + std::string("If-Modified-Since: ") + util::rfc1123(existingResponse->modified); + headers = curl_slist_append(headers, time.c_str()); + } + } + + if (headers) { + curl_easy_setopt(handle, CURLOPT_HTTPHEADER, headers); + } + + handleError(curl_easy_setopt(handle, CURLOPT_PRIVATE, this)); + handleError(curl_easy_setopt(handle, CURLOPT_CAINFO, "ca-bundle.crt")); + handleError(curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1)); + handleError(curl_easy_setopt(handle, CURLOPT_URL, request->resource.url.c_str())); + handleError(curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, writeCallback)); + handleError(curl_easy_setopt(handle, CURLOPT_WRITEDATA, this)); + handleError(curl_easy_setopt(handle, CURLOPT_HEADERFUNCTION, headerCallback)); + handleError(curl_easy_setopt(handle, CURLOPT_HEADERDATA, this)); + handleError(curl_easy_setopt(handle, CURLOPT_ACCEPT_ENCODING, "gzip, deflate")); + handleError(curl_easy_setopt(handle, CURLOPT_USERAGENT, "MapboxGL/1.0")); + handleError(curl_easy_setopt(handle, CURLOPT_SHARE, context->share)); + + start(); +} + +void HTTPRequestImpl::abandon() { + if (request) { + context->removeRequest(request); + request = nullptr; + } +} + +void HTTPRequestImpl::start() { + // Count up the attempts. + attempts++; + + // Start requesting the information. + handleError(curl_multi_add_handle(context->multi, handle)); +} + +HTTPRequestImpl::~HTTPRequestImpl() { + MBGL_VERIFY_THREAD(tid); + + if (request) { + context->removeRequest(request); + request->ptr = nullptr; + } + + handleError(curl_multi_remove_handle(context->multi, handle)); + context->returnHandle(handle); + handle = nullptr; + + if (timer) { + // Stop the backoff timer to avoid re-triggering this request. + uv_timer_stop(timer); + uv::close(timer); + timer = nullptr; + } + + if (headers) { + curl_slist_free_all(headers); + headers = nullptr; + } +} + +// This function is called when we have new data for a request. We just append it to the string +// containing the previous data. +size_t HTTPRequestImpl::writeCallback(void *const contents, const size_t size, const size_t nmemb, void *userp) { + assert(userp); + auto impl = reinterpret_cast<HTTPRequestImpl *>(userp); + MBGL_VERIFY_THREAD(impl->tid); + + if (!impl->response) { + impl->response = util::make_unique<Response>(); + } + + impl->response->data.append((char *)contents, size * nmemb); + return size * nmemb; +} + +// Compares the beginning of the (non-zero-terminated!) data buffer with the (zero-terminated!) +// header string. If the data buffer contains the header string at the beginning, it returns +// the length of the header string == begin of the value, otherwise it returns npos. +// The comparison of the header is ASCII-case-insensitive. +size_t headerMatches(const char *const header, const char *const buffer, const size_t length) { + const size_t headerLength = strlen(header); + if (length < headerLength) { + return std::string::npos; + } + size_t i = 0; + while (i < length && i < headerLength && std::tolower(buffer[i]) == std::tolower(header[i])) { + i++; + } + return i == headerLength ? i : std::string::npos; +} + +int64_t parseCacheControl(const char *value) { + if (value) { + unsigned long long seconds = 0; + // TODO: cache-control may contain other information as well: + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 + if (std::sscanf(value, "max-age=%llu", &seconds) == 1) { + return std::chrono::duration_cast<std::chrono::seconds>( + std::chrono::system_clock::now().time_since_epoch()).count() + + seconds; + } + } + + return 0; +} + +size_t HTTPRequestImpl::headerCallback(char *const buffer, const size_t size, const size_t nmemb, void *userp) { + assert(userp); + auto baton = reinterpret_cast<HTTPRequestImpl *>(userp); + MBGL_VERIFY_THREAD(baton->tid); + + if (!baton->response) { + baton->response = util::make_unique<Response>(); + } + + const size_t length = size * nmemb; + size_t begin = std::string::npos; + if ((begin = headerMatches("last-modified: ", buffer, length)) != std::string::npos) { + // Always overwrite the modification date; We might already have a value here from the + // Date header, but this one is more accurate. + const std::string value { buffer + begin, length - begin - 2 }; // remove \r\n + baton->response->modified = curl_getdate(value.c_str(), nullptr); + } else if ((begin = headerMatches("etag: ", buffer, length)) != std::string::npos) { + baton->response->etag = { buffer + begin, length - begin - 2 }; // remove \r\n + } else if ((begin = headerMatches("cache-control: ", buffer, length)) != std::string::npos) { + const std::string value { buffer + begin, length - begin - 2 }; // remove \r\n + baton->response->expires = parseCacheControl(value.c_str()); + } else if ((begin = headerMatches("expires: ", buffer, length)) != std::string::npos) { + const std::string value { buffer + begin, length - begin - 2 }; // remove \r\n + baton->response->expires = curl_getdate(value.c_str(), nullptr); + } + + return length; +} + + +void HTTPRequestImpl::retry(uint64_t timeout) { + handleError(curl_multi_remove_handle(context->multi, handle)); + + response.reset(); + + assert(!timer); + timer = new uv_timer_t; + timer->data = this; + uv_timer_init(context->loop, timer); + uv_timer_start(timer, restart, timeout, 0); +} + +void HTTPRequestImpl::retryImmediately() { + // All batons get notified when the network status changed, but some of them + // might not actually wait for the network to become available again. + if (timer && strategy == PreemptImmediately) { + // Triggers the timer upon the next event loop iteration. + uv_timer_stop(timer); + uv_timer_start(timer, restart, 0, 0); + } +} + +void HTTPRequestImpl::restart(uv_timer_t *timer, int) { + // Restart the request. + auto baton = reinterpret_cast<HTTPRequestImpl *>(timer->data); + + // Get rid of the timer. + baton->timer = nullptr; + uv::close(timer); + + baton->start(); +} + +void HTTPRequestImpl::finish(ResponseStatus status) { + if (status == ResponseStatus::TemporaryError && attempts < maxAttempts) { + strategy = ExponentialBackoff; + return retry((1 << (attempts - 1)) * 1000); + } else if (status == ResponseStatus::ConnectionError && attempts < maxAttempts) { + // By default, we will retry every 30 seconds (network change notification will + // preempt the timeout). + strategy = PreemptImmediately; + return retry(30000); + } + + // Actually return the response. + if (status == ResponseStatus::NotModified) { + request->notify(std::move(response), FileCache::Hint::Refresh); + } else { + request->notify(std::move(response), FileCache::Hint::Full); + } + + delete request; + delete this; +} + +void HTTPRequestImpl::handleResult(CURLcode code) { + MBGL_VERIFY_THREAD(tid); + + if (!request) { + // In this case, it doesn't make sense to even process the response even further since + // the request was canceled anyway. + delete this; + return; + } + + // Make sure a response object exists in case we haven't got any headers + // or content. + if (!response) { + response = util::make_unique<Response>(); + } + + // Add human-readable error code + if (code != CURLE_OK) { + response->message = curl_easy_strerror(code); + + switch (code) { + case CURLE_COULDNT_RESOLVE_PROXY: + case CURLE_COULDNT_RESOLVE_HOST: + case CURLE_COULDNT_CONNECT: + response->status = Response::Error; + return finish(ResponseStatus::ConnectionError); + + case CURLE_OPERATION_TIMEDOUT: + response->status = Response::Error; + return finish(ResponseStatus::TemporaryError); + + default: + response->status = Response::Error; + return finish(ResponseStatus::PermanentError); + } + } else { + long responseCode = 0; + curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &responseCode); + + if (responseCode == 304) { + if (existingResponse) { + // We're going to reuse the old response object, but need to copy over the new + // expires value (if possible). + std::swap(response, existingResponse); + if (existingResponse->expires) { + response->expires = existingResponse->expires; + } + return finish(ResponseStatus::NotModified); + } else { + // This is an unsolicited 304 response and should only happen on malfunctioning + // HTTP servers. It likely doesn't include any data, but we don't have much options. + response->status = Response::Successful; + return finish(ResponseStatus::Successful); + } + } else if (responseCode == 200) { + response->status = Response::Successful; + return finish(ResponseStatus::Successful); + } else if (responseCode >= 500 && responseCode < 600) { + // Server errors may be temporary, so back off exponentially. + response->status = Response::Error; + response->message = "HTTP status code " + std::to_string(responseCode); + return finish(ResponseStatus::TemporaryError); + } else { + // We don't know how to handle any other errors, so declare them as permanently failing. + response->status = Response::Error; + response->message = "HTTP status code " + std::to_string(responseCode); + return finish(ResponseStatus::PermanentError); + } + } + + throw std::runtime_error("Response hasn't been handled"); +} + +// ------------------------------------------------------------------------------------------------- + +HTTPRequest::HTTPRequest(DefaultFileSource *source, const Resource &resource) + : SharedRequestBase(source, resource) { +} + +HTTPRequest::~HTTPRequest() { + MBGL_VERIFY_THREAD(tid); + + if (ptr) { + reinterpret_cast<HTTPRequestImpl *>(ptr)->abandon(); + } +} + +void HTTPRequest::start(uv_loop_t *loop, std::unique_ptr<Response> response) { + MBGL_VERIFY_THREAD(tid); + + assert(!ptr); + ptr = new HTTPRequestImpl(this, loop, std::move(response)); +} + +void HTTPRequest::retryImmediately() { + MBGL_VERIFY_THREAD(tid); + + if (ptr) { + reinterpret_cast<HTTPRequestImpl *>(ptr)->retryImmediately(); + } +} + +void HTTPRequest::cancel() { + MBGL_VERIFY_THREAD(tid); + + if (ptr) { + delete reinterpret_cast<HTTPRequestImpl *>(ptr); + ptr = nullptr; + } + + delete this; +} + +} |