From 0e898248ea9a1d01f238e1422e80280f10fa79a8 Mon Sep 17 00:00:00 2001 From: lawrencewuskyboxlabs Date: Wed, 22 Jul 2026 13:40:40 -0700 Subject: [PATCH] Shared NSURLSessions --- Source/HTTP/Apple/http_apple.h | 8 + Source/HTTP/Apple/http_apple.mm | 235 ++++++++++++++++++++------ Source/HTTP/Apple/session_delegate.h | 6 +- Source/HTTP/Apple/session_delegate.mm | 122 +++++++++++-- 4 files changed, 293 insertions(+), 78 deletions(-) diff --git a/Source/HTTP/Apple/http_apple.h b/Source/HTTP/Apple/http_apple.h index fd002229e..eee868091 100644 --- a/Source/HTTP/Apple/http_apple.h +++ b/Source/HTTP/Apple/http_apple.h @@ -7,13 +7,21 @@ NAMESPACE_XBOX_HTTP_CLIENT_BEGIN +class AppleHttpSessionManager; + class AppleHttpProvider : public IHttpProvider { public: + AppleHttpProvider(); + ~AppleHttpProvider(); + HRESULT PerformAsync( HCCallHandle callHandle, XAsyncBlock *async ) noexcept override; + +private: + std::shared_ptr m_httpSessionManager; }; NAMESPACE_XBOX_HTTP_CLIENT_END diff --git a/Source/HTTP/Apple/http_apple.mm b/Source/HTTP/Apple/http_apple.mm index 71df5a8f5..d18ae7398 100644 --- a/Source/HTTP/Apple/http_apple.mm +++ b/Source/HTTP/Apple/http_apple.mm @@ -10,60 +10,181 @@ NAMESPACE_XBOX_HTTP_CLIENT_BEGIN -class http_task_apple +struct AppleHttpTaskContext { -public: - http_task_apple(_Inout_ XAsyncBlock* asyncBlock, _In_ HCCallHandle call); - bool initiate_request(); - -private: - void completion_handler(NSURLResponse* response, NSError* error); - HCCallHandle m_call; // non owning XAsyncBlock* m_asyncBlock; // non owning +}; + +struct AppleHttpSession +{ + AppleHttpSession(NSURLSession* session, SessionDelegate* delegate); + ~AppleHttpSession(); + AppleHttpSession(AppleHttpSession&& other) noexcept; + AppleHttpSession(const AppleHttpSession& other) = delete; + AppleHttpSession& operator=(const AppleHttpSession& other) = delete; NSURLSession* m_session; - NSURLSessionTask* m_sessionTask; + SessionDelegate* m_delegate; + std::unordered_map m_httpTaskContexts; }; -http_task_apple::http_task_apple(_Inout_ XAsyncBlock* asyncBlock, _In_ HCCallHandle call) : - m_call(call), - m_asyncBlock(asyncBlock), - m_sessionTask(nullptr) +AppleHttpSession::AppleHttpSession(NSURLSession* session, SessionDelegate* delegate) + : m_session(session), + m_delegate(delegate) { - NSURLSessionConfiguration* configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration; +} +AppleHttpSession::~AppleHttpSession() +{ + if (m_session != nil) + { + [m_session finishTasksAndInvalidate]; + m_session = nil; + } + m_delegate = nil; +} + +AppleHttpSession::AppleHttpSession(AppleHttpSession&& other) noexcept + : m_session(other.m_session), + m_delegate(other.m_delegate), + m_httpTaskContexts(std::move(other.m_httpTaskContexts)) +{ + // Prevent session invalidation from other's destructor + other.m_session = nil; + other.m_delegate = nil; +} + +class AppleHttpSessionManager : public std::enable_shared_from_this +{ +public: + HRESULT InitiateRequest( + HCCallHandle call, + XAsyncBlock *async + ) noexcept; + +private: + std::mutex m_httpSessionsMutex; + std::unordered_map m_httpSessions; + + void StartTaskOnSession(HCCallHandle call, XAsyncBlock* asyncBlock, NSURLRequest* request); + void CompletionHandler(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error); +}; + +void AppleHttpSessionManager::StartTaskOnSession(HCCallHandle call, XAsyncBlock* asyncBlock, NSURLRequest* request) +{ uint32_t timeoutInSeconds = 0; - if (FAILED(HCHttpCallRequestGetTimeout(m_call, &timeoutInSeconds))) + if (FAILED(HCHttpCallRequestGetTimeout(call, &timeoutInSeconds))) { // default to 60 to match other default ios behaviour timeoutInSeconds = 60; } - - [configuration setTimeoutIntervalForRequest:(NSTimeInterval)timeoutInSeconds]; - [configuration setTimeoutIntervalForResource:(NSTimeInterval)timeoutInSeconds]; - - SessionDelegate* delegate = [SessionDelegate sessionDelegateWithHCCallHandle:m_call andCompletionHandler:^(NSURLResponse *response, NSError *error) { - std::unique_ptr me{this}; - me->completion_handler(response, error); - }]; - m_session = [NSURLSession sessionWithConfiguration:configuration delegate:delegate delegateQueue:nil]; + + NSURLSessionTask* sessionTask = nil; + bool canStartTask = false; + + { + std::unique_lock uniqueLock(m_httpSessionsMutex); + + auto httpSessionIter = m_httpSessions.find(timeoutInSeconds); + if (httpSessionIter == m_httpSessions.end()) + { + NSURLSessionConfiguration* configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration; + [configuration setTimeoutIntervalForRequest:(NSTimeInterval)timeoutInSeconds]; + [configuration setTimeoutIntervalForResource:(NSTimeInterval)timeoutInSeconds]; + + std::weak_ptr weak_this = shared_from_this(); + + SessionDelegate* delegate = [SessionDelegate sessionDelegateWithCompletionHandler:^(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse *response, NSError *error) { + if (auto me = weak_this.lock()) + { + me->CompletionHandler(sessionTimeout, taskIdentifier, response, error); + } + }]; + NSURLSession* session = [NSURLSession sessionWithConfiguration:configuration delegate:delegate delegateQueue:nil]; + + httpSessionIter = m_httpSessions.emplace(timeoutInSeconds, AppleHttpSession{ session, delegate }).first; + } + + sessionTask = [httpSessionIter->second.m_session dataTaskWithRequest:request]; + NSUInteger taskIdentifier = [sessionTask taskIdentifier]; + + if (httpSessionIter->second.m_httpTaskContexts.count(taskIdentifier) > 0) + { + HC_TRACE_ERROR(HTTPCLIENT, "Shared session with timeout %u already has task with identifier %lu", timeoutInSeconds, taskIdentifier); + } + else + { + bool delegateRegistered = [httpSessionIter->second.m_delegate registerContextForTask:taskIdentifier withCall:call]; + if (!delegateRegistered) + { + HC_TRACE_ERROR(HTTPCLIENT, "Shared session with timeout %u failed to register task with identifier %lu", timeoutInSeconds, taskIdentifier); + } + else + { + httpSessionIter->second.m_httpTaskContexts.emplace(taskIdentifier, AppleHttpTaskContext{ .m_call = call, .m_asyncBlock = asyncBlock }); + canStartTask = true; + } + } + } + + if (sessionTask != nil) + { + if (canStartTask) + { + [sessionTask resume]; + } + else + { + [sessionTask cancel]; + } + } } -void http_task_apple::completion_handler(NSURLResponse* response, NSError* error) +void AppleHttpSessionManager::CompletionHandler(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error) { + AppleHttpTaskContext taskContext; + { + std::unique_lock uniqueLock(m_httpSessionsMutex); + + auto httpSessionIter = m_httpSessions.find(sessionTimeout); + if (httpSessionIter == m_httpSessions.end()) + { + HC_TRACE_ERROR(HTTPCLIENT, "No existing session with timeout %u", sessionTimeout); + return; + } + + auto taskContextIter = httpSessionIter->second.m_httpTaskContexts.find(taskIdentifier); + if (taskContextIter == httpSessionIter->second.m_httpTaskContexts.end()) + { + HC_TRACE_ERROR(HTTPCLIENT, "No existing task context with identifier %lu", taskIdentifier); + return; + } + + taskContext = taskContextIter->second; + + if (httpSessionIter->second.m_httpTaskContexts.size() > 1) + { + httpSessionIter->second.m_httpTaskContexts.erase(taskContextIter); + } + else + { + m_httpSessions.erase(httpSessionIter); + } + } + if (error) { uint32_t errorCode = static_cast([error code]); - HC_TRACE_ERROR(HTTPCLIENT, "HCHttpCallPerform [ID %u] error from NSURLRequest code: %u", HCHttpCallGetId(m_call), errorCode); + HC_TRACE_ERROR(HTTPCLIENT, "HCHttpCallPerform [ID %u] error from NSURLRequest code: %u", HCHttpCallGetId(taskContext.m_call), errorCode); HRESULT errorResult = E_FAIL; if ([[error domain] isEqualToString:NSURLErrorDomain] && [error code] == NSURLErrorNotConnectedToInternet) { errorResult = E_HC_NO_NETWORK; } - HCHttpCallResponseSetNetworkErrorCode(m_call, errorResult, errorCode); - XAsyncComplete(m_asyncBlock, errorResult, 0); + HCHttpCallResponseSetNetworkErrorCode(taskContext.m_call, errorResult, errorCode); + XAsyncComplete(taskContext.m_asyncBlock, errorResult, 0); return; } @@ -72,7 +193,7 @@ uint32_t statusCode = static_cast([httpResponse statusCode]); - HCHttpCallResponseSetStatusCode(m_call, statusCode); + HCHttpCallResponseSetStatusCode(taskContext.m_call, statusCode); NSDictionary* headers = [httpResponse allHeaderFields]; for (NSString* key in headers) @@ -81,21 +202,24 @@ char const* keyCString = [key cStringUsingEncoding:NSUTF8StringEncoding]; char const* valueCString = [value cStringUsingEncoding:NSUTF8StringEncoding]; - HCHttpCallResponseSetHeader(m_call, keyCString, valueCString); + HCHttpCallResponseSetHeader(taskContext.m_call, keyCString, valueCString); } - XAsyncComplete(m_asyncBlock, S_OK, 0); + XAsyncComplete(taskContext.m_asyncBlock, S_OK, 0); } -bool http_task_apple::initiate_request() +HRESULT AppleHttpSessionManager::InitiateRequest( + _In_ HCCallHandle call, + _Inout_ XAsyncBlock* asyncBlock +) noexcept { char const* urlCString = nullptr; char const* methodCString = nullptr; - if (FAILED(HCHttpCallRequestGetUrl(m_call, &methodCString, &urlCString))) + if (FAILED(HCHttpCallRequestGetUrl(call, &methodCString, &urlCString))) { - HCHttpCallResponseSetNetworkErrorCode(m_call, E_FAIL, 0); - XAsyncComplete(m_asyncBlock, E_FAIL, 0); - return false; + HCHttpCallResponseSetNetworkErrorCode(call, E_FAIL, 0); + XAsyncComplete(asyncBlock, E_FAIL, 0); + return S_OK; } NSString* urlString = [[NSString alloc] initWithUTF8String:urlCString]; @@ -107,18 +231,18 @@ [request setHTTPMethod:methodString]; uint32_t numHeaders = 0; - if (FAILED(HCHttpCallRequestGetNumHeaders(m_call, &numHeaders))) + if (FAILED(HCHttpCallRequestGetNumHeaders(call, &numHeaders))) { - HCHttpCallResponseSetNetworkErrorCode(m_call, E_FAIL, 0); - XAsyncComplete(m_asyncBlock, E_FAIL, 0); - return false; + HCHttpCallResponseSetNetworkErrorCode(call, E_FAIL, 0); + XAsyncComplete(asyncBlock, E_FAIL, 0); + return S_OK; } for (uint32_t i = 0; i 0) { - [request setHTTPBodyStream:[RequestBodyStream requestBodyStreamWithHCCallHandle:m_call]]; + [request setHTTPBodyStream:[RequestBodyStream requestBodyStreamWithHCCallHandle:call]]; [request addValue:[NSString stringWithFormat:@"%zu", requestBodySize] forHTTPHeaderField:@"Content-Length"]; } + + StartTaskOnSession(call, asyncBlock, request); + return S_OK; +} - m_sessionTask = [m_session dataTaskWithRequest:request]; - [m_sessionTask resume]; - return true; +AppleHttpProvider::AppleHttpProvider() : m_httpSessionManager(std::make_shared()) +{ } +AppleHttpProvider::~AppleHttpProvider() = default; + HRESULT AppleHttpProvider::PerformAsync( _In_ HCCallHandle call, _Inout_ XAsyncBlock* asyncBlock ) noexcept { - std::unique_ptr httpTask(new xbox::httpclient::http_task_apple(asyncBlock, call)); - HCHttpCallSetContext(call, &httpTask); - if (httpTask->initiate_request()) - { - httpTask.release(); - } - return S_OK; + return m_httpSessionManager->InitiateRequest(call, asyncBlock); } NAMESPACE_XBOX_HTTP_CLIENT_END diff --git a/Source/HTTP/Apple/session_delegate.h b/Source/HTTP/Apple/session_delegate.h index d02a7394c..0ade844ff 100644 --- a/Source/HTTP/Apple/session_delegate.h +++ b/Source/HTTP/Apple/session_delegate.h @@ -6,9 +6,7 @@ @interface SessionDelegate : NSObject -@property (nonatomic, retain) NSMutableData *dataToDownload; -@property (nonatomic) float downloadSize; - -+ (SessionDelegate*) sessionDelegateWithHCCallHandle:(HCCallHandle) call andCompletionHandler:(void(^)(NSURLResponse* response, NSError* error)) completion; ++ (SessionDelegate*) sessionDelegateWithCompletionHandler:(void(^)(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error)) completion; + (void) reportProgress:(HCCallHandle)call progressReportFunction:(HCHttpCallProgressReportFunction)progressReportFunction minimumInterval:(size_t)minimumInterval current:(size_t)current total:(size_t)total progressReportCallbackContext:(void*)progressReportCallbackContext lastProgressReport:(std::chrono::steady_clock::time_point*)lastProgressReport; +- (bool) registerContextForTask:(NSUInteger)taskIdentifier withCall:(HCCallHandle)call; @end diff --git a/Source/HTTP/Apple/session_delegate.mm b/Source/HTTP/Apple/session_delegate.mm index bc1653159..a12d6071e 100644 --- a/Source/HTTP/Apple/session_delegate.mm +++ b/Source/HTTP/Apple/session_delegate.mm @@ -5,15 +5,23 @@ #include #import "session_delegate.h" +struct TaskContext +{ + HCCallHandle _call; // non owning + long long _downloadSize; +}; + @implementation SessionDelegate { - HCCallHandle _call; - void(^_completionHandler)(NSURLResponse* response, NSError* error); + void(^_completionHandler)(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error); + + std::mutex _taskContextsMutex; + std::unordered_map _taskContexts; } -+ (SessionDelegate*) sessionDelegateWithHCCallHandle:(HCCallHandle) call andCompletionHandler:(void(^)(NSURLResponse* response, NSError* error)) completionHandler ++ (SessionDelegate*) sessionDelegateWithCompletionHandler:(void(^)(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error)) completionHandler { - return [[SessionDelegate alloc] initWithHCCallHandle: call andCompletionHandler:completionHandler]; + return [[SessionDelegate alloc] initWithCompletionHandler:completionHandler]; } + (void) reportProgress:(HCCallHandle)call progressReportFunction:(HCHttpCallProgressReportFunction)progressReportFunction minimumInterval:(size_t)minimumInterval current:(size_t)current total:(size_t)total progressReportCallbackContext:(void*)progressReportCallbackContext lastProgressReport:(std::chrono::steady_clock::time_point*)lastProgressReport @@ -38,11 +46,29 @@ + (void) reportProgress:(HCCallHandle)call progressReportFunction:(HCHttpCallPro } } -- (instancetype) initWithHCCallHandle:(HCCallHandle)call andCompletionHandler:(void(^)(NSURLResponse*, NSError*)) completionHandler +- (bool) registerContextForTask:(NSUInteger)taskIdentifier withCall:(HCCallHandle)call +{ + bool registered = false; + { + std::unique_lock uniqueLock(_taskContextsMutex); + + if (_taskContexts.find(taskIdentifier) == _taskContexts.end()) + { + _taskContexts.emplace(taskIdentifier, TaskContext{ ._call = call }); + registered = true; + } + else + { + HC_TRACE_ERROR_HR(HTTPCLIENT, "Task context already exists, cannot register for identifier %lu", taskIdentifier); + } + } + return registered; +} + +- (instancetype) initWithCompletionHandler:(void(^)(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error)) completionHandler { if (self = [super init]) { - _call = call; _completionHandler = completionHandler; return self; } @@ -51,14 +77,40 @@ - (instancetype) initWithHCCallHandle:(HCCallHandle)call andCompletionHandler:(v - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { - _completionHandler([task response], error); + { + std::unique_lock uniqueLock(_taskContextsMutex); + _taskContexts.erase([task taskIdentifier]); + } + + _completionHandler([[session configuration] timeoutIntervalForRequest], [task taskIdentifier], [task response], error); } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)task didReceiveData:(NSData *)data { HCHttpCallResponseBodyWriteFunction writeFunction = nullptr; void* context = nullptr; - if (FAILED(HCHttpCallResponseGetResponseBodyWriteFunction(_call, &writeFunction, &context)) || + + TaskContext taskContext; + bool hasContext = false; + { + std::unique_lock uniqueLock(_taskContextsMutex); + + auto existingTaskContext = _taskContexts.find([task taskIdentifier]); + if (existingTaskContext != _taskContexts.end()) + { + hasContext = true; + taskContext = existingTaskContext->second; + } + } + + if (!hasContext) + { + HC_TRACE_ERROR(HTTPCLIENT, "Task context missing for data of identifier %lu", [task taskIdentifier]); + [task cancel]; + return; + } + + if (FAILED(HCHttpCallResponseGetResponseBodyWriteFunction(taskContext._call, &writeFunction, &context)) || writeFunction == nullptr) { [task cancel]; @@ -69,7 +121,7 @@ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)task { __block HRESULT hr = S_OK; [data enumerateByteRangesUsingBlock:^(const void* bytes, NSRange byteRange, BOOL* stop) { - hr = writeFunction(_call, static_cast(bytes), static_cast(byteRange.length), context); + hr = writeFunction(taskContext._call, static_cast(bytes), static_cast(byteRange.length), context); if (FAILED(hr)) { *stop = YES; @@ -88,18 +140,16 @@ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)task return; } - [_dataToDownload appendData:data]; - size_t downloadMinimumProgressInterval; void* downloadProgressReportCallbackContext{}; HCHttpCallProgressReportFunction downloadProgressReportFunction = nullptr; - HRESULT hr = HCHttpCallRequestGetProgressReportFunction(_call, false, &downloadProgressReportFunction, &downloadMinimumProgressInterval, &downloadProgressReportCallbackContext); + HRESULT hr = HCHttpCallRequestGetProgressReportFunction(taskContext._call, false, &downloadProgressReportFunction, &downloadMinimumProgressInterval, &downloadProgressReportCallbackContext); if (FAILED(hr)) { HC_TRACE_ERROR_HR(HTTPCLIENT, hr, "CurlEasyRequest::ProgressReportCallback: failed getting Progress Report upload function"); } - [SessionDelegate reportProgress:_call progressReportFunction:downloadProgressReportFunction minimumInterval:_call->downloadMinimumProgressReportInterval current:[ _dataToDownload length ] total:_downloadSize progressReportCallbackContext: downloadProgressReportCallbackContext lastProgressReport:&_call->downloadLastProgressReport]; + [SessionDelegate reportProgress:taskContext._call progressReportFunction:downloadProgressReportFunction minimumInterval:taskContext._call->downloadMinimumProgressReportInterval current:taskContext._call->responseBodyBytes.size() total:taskContext._downloadSize progressReportCallbackContext: downloadProgressReportCallbackContext lastProgressReport:&taskContext._call->downloadLastProgressReport]; } - (void)URLSession:(NSURLSession *)session @@ -111,21 +161,57 @@ - (void)URLSession:(NSURLSession *)session size_t uploadMinimumProgressInterval; void* uploadProgressReportCallbackContext{}; HCHttpCallProgressReportFunction uploadProgressReportFunction = nullptr; - HRESULT hr = HCHttpCallRequestGetProgressReportFunction(_call, true, &uploadProgressReportFunction, &uploadMinimumProgressInterval, &uploadProgressReportCallbackContext); + + HCCallHandle call = nullptr; + bool hasContext = false; + { + std::unique_lock uniqueLock(_taskContextsMutex); + + auto existingTaskContext = _taskContexts.find([task taskIdentifier]); + if (existingTaskContext != _taskContexts.end()) + { + hasContext = true; + call = existingTaskContext->second._call; + } + } + + if (!hasContext) + { + HC_TRACE_ERROR(HTTPCLIENT, "Task context missing for send report of identifier %lu", [task taskIdentifier]); + [task cancel]; + return; + } + + HRESULT hr = HCHttpCallRequestGetProgressReportFunction(call, true, &uploadProgressReportFunction, &uploadMinimumProgressInterval, &uploadProgressReportCallbackContext); if (FAILED(hr)) { HC_TRACE_ERROR_HR(HTTPCLIENT, hr, "CurlEasyRequest::ProgressReportCallback: failed getting Progress Report upload function"); } - [SessionDelegate reportProgress:_call progressReportFunction:uploadProgressReportFunction minimumInterval:_call->uploadMinimumProgressReportInterval current:totalBytesSent total:totalBytesExpectedToSend progressReportCallbackContext:uploadProgressReportCallbackContext lastProgressReport:&_call->uploadLastProgressReport]; + [SessionDelegate reportProgress:call progressReportFunction:uploadProgressReportFunction minimumInterval:call->uploadMinimumProgressReportInterval current:totalBytesSent total:totalBytesExpectedToSend progressReportCallbackContext:uploadProgressReportCallbackContext lastProgressReport:&call->uploadLastProgressReport]; } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler { completionHandler(NSURLSessionResponseAllow); - - _downloadSize=[response expectedContentLength]; - _dataToDownload=[[NSMutableData alloc]init]; + + bool hasContext = false; + { + std::unique_lock uniqueLock(_taskContextsMutex); + + auto existingTaskContext = _taskContexts.find([dataTask taskIdentifier]); + if (existingTaskContext != _taskContexts.end()) + { + hasContext = true; + existingTaskContext->second._downloadSize = [response expectedContentLength]; + } + } + + if (!hasContext) + { + HC_TRACE_ERROR(HTTPCLIENT, "Task context missing for response of identifier %lu", [dataTask taskIdentifier]); + [dataTask cancel]; + } } @end