From 24f448579cbb4774e6da2f63348bc0efdf13c94c Mon Sep 17 00:00:00 2001 From: lawrencewuskyboxlabs <138919137+lawrencewuskyboxlabs@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:46:37 -0700 Subject: [PATCH 1/4] First implementation of reusing Apple sessions --- Source/HTTP/Apple/http_apple.h | 8 + Source/HTTP/Apple/http_apple.mm | 216 +++++++++++++++++++------- Source/HTTP/Apple/session_delegate.h | 5 +- Source/HTTP/Apple/session_delegate.mm | 81 +++++++--- 4 files changed, 232 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..9b23be3cd 100644 --- a/Source/HTTP/Apple/http_apple.mm +++ b/Source/HTTP/Apple/http_apple.mm @@ -7,63 +7,165 @@ #include "http_apple.h" #include "request_body_stream.h" #include "session_delegate.h" +#include 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 +{ NSURLSession* m_session; - NSURLSessionTask* m_sessionTask; + 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) +class AppleHttpSessionManager : public std::enable_shared_from_this { - NSURLSessionConfiguration* configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration; +public: + HRESULT InitiateRequest( + HCCallHandle call, + XAsyncBlock *async + ) noexcept; + +private: + std::shared_mutex m_httpSessionsMutex; + std::unordered_map m_httpSessions; + + void StartTaskOnSession(HCCallHandle call, XAsyncBlock* asyncBlock, NSURLRequest* request); + HCCallHandle GetCallHandle(uint32_t sessionTimeout, NSUInteger taskIdentifier); + 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; } + + { + std::unique_lock uniqueLock(m_httpSessionsMutex); + + NSURLSession* session = nil; + auto httpSessionIter = m_httpSessions.find(timeoutInSeconds); + if (httpSessionIter != m_httpSessions.end()) + { + session = httpSessionIter->second.m_session; + } + else { + NSURLSessionConfiguration* configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration; + [configuration setTimeoutIntervalForRequest:(NSTimeInterval)timeoutInSeconds]; + [configuration setTimeoutIntervalForResource:(NSTimeInterval)timeoutInSeconds]; + + std::weak_ptr weak_this = shared_from_this(); + + SessionDelegate* delegate = [SessionDelegate sessionDelegateWithCallHandleRetriever:^HCCallHandle(uint32_t sessionTimeout, NSUInteger taskIdentifier) { + if (auto me = weak_this.lock()) + { + return me->GetCallHandle(sessionTimeout, taskIdentifier); + } + else { + return nullptr; + } + } andCompletionHandler:^(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse *response, NSError *error) { + if (auto me = weak_this.lock()) + { + me->CompletionHandler(sessionTimeout, taskIdentifier, response, error); + } + }]; + + session = [NSURLSession sessionWithConfiguration:configuration delegate:delegate delegateQueue:nil]; + httpSessionIter = m_httpSessions.emplace(timeoutInSeconds, AppleHttpSession{ .m_session = session }).first; + } + + NSURLSessionTask* sessionTask = [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 %u", timeoutInSeconds, taskIdentifier); + [sessionTask cancel]; + } + else + { + httpSessionIter->second.m_httpTaskContexts.emplace(taskIdentifier, AppleHttpTaskContext{ .m_call = call, .m_asyncBlock = asyncBlock }); + [sessionTask resume]; + } + } +} - [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]; +HCCallHandle AppleHttpSessionManager::GetCallHandle(uint32_t sessionTimeout, NSUInteger taskIdentifier) +{ + std::shared_lock sharedLock(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 nullptr; + } + + 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 %u", taskIdentifier); + return nullptr; + } + + return taskContextIter->second.m_call; } -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 %u", taskIdentifier); + return; + } + + taskContext = taskContextIter->second; + + if (httpSessionIter->second.m_httpTaskContexts.size() > 1) + { + httpSessionIter->second.m_httpTaskContexts.erase(taskContextIter); + } + else + { + [httpSessionIter->second.m_session finishTasksAndInvalidate]; + 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 +174,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 +183,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 +212,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..531b0dc72 100644 --- a/Source/HTTP/Apple/session_delegate.h +++ b/Source/HTTP/Apple/session_delegate.h @@ -6,9 +6,6 @@ @interface SessionDelegate : NSObject -@property (nonatomic, retain) NSMutableData *dataToDownload; -@property (nonatomic) float downloadSize; - -+ (SessionDelegate*) sessionDelegateWithHCCallHandle:(HCCallHandle) call andCompletionHandler:(void(^)(NSURLResponse* response, NSError* error)) completion; ++ (SessionDelegate*) sessionDelegateWithCallHandleRetriever:(HCCallHandle(^)(uint32_t sessionTimeout, NSUInteger taskIdentifier)) callRetriever andCompletionHandler:(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; @end diff --git a/Source/HTTP/Apple/session_delegate.mm b/Source/HTTP/Apple/session_delegate.mm index bc1653159..eb6138184 100644 --- a/Source/HTTP/Apple/session_delegate.mm +++ b/Source/HTTP/Apple/session_delegate.mm @@ -4,16 +4,26 @@ #include "pch.h" #include #import "session_delegate.h" +#include + +struct TaskContext +{ + HCCallHandle _call; // non owning + long long _downloadSize; +}; @implementation SessionDelegate { - HCCallHandle _call; - void(^_completionHandler)(NSURLResponse* response, NSError* error); + HCCallHandle(^_callHandleRetriever)(uint32_t sessionTimeout, NSUInteger taskIdentifier); + void(^_completionHandler)(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error); + + std::shared_mutex _taskContextsMutex; + std::unordered_map _taskContexts; } -+ (SessionDelegate*) sessionDelegateWithHCCallHandle:(HCCallHandle) call andCompletionHandler:(void(^)(NSURLResponse* response, NSError* error)) completionHandler ++ (SessionDelegate*) sessionDelegateWithCallHandleRetriever:(HCCallHandle(^)(uint32_t sessionTimeout, NSUInteger taskIdentifier)) callRetriever andCompletionHandler:(void(^)(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error)) completionHandler { - return [[SessionDelegate alloc] initWithHCCallHandle: call andCompletionHandler:completionHandler]; + return [[SessionDelegate alloc] initWithCallHandleRetriever: callRetriever andCompletionHandler: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 +48,11 @@ + (void) reportProgress:(HCCallHandle)call progressReportFunction:(HCHttpCallPro } } -- (instancetype) initWithHCCallHandle:(HCCallHandle)call andCompletionHandler:(void(^)(NSURLResponse*, NSError*)) completionHandler +- (instancetype) initWithCallHandleRetriever:(HCCallHandle(^)(uint32_t sessionTimeout, NSUInteger taskIdentifier)) callRetriever andCompletionHandler:(void(^)(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error)) completionHandler { if (self = [super init]) { - _call = call; + _callHandleRetriever = callRetriever; _completionHandler = completionHandler; return self; } @@ -51,14 +61,33 @@ - (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; + { + std::shared_lock sharedLock(_taskContextsMutex); + auto existingTaskContext = _taskContexts.find([task taskIdentifier]); + if (existingTaskContext == _taskContexts.end()) + { + HC_TRACE_ERROR(HTTPCLIENT, "Task context missing for identifier %u", [task taskIdentifier]); + [task cancel]; + return; + } + taskContext = existingTaskContext->second; + } + + if (FAILED(HCHttpCallResponseGetResponseBodyWriteFunction(taskContext._call, &writeFunction, &context)) || writeFunction == nullptr) { [task cancel]; @@ -69,7 +98,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 +117,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 +138,39 @@ - (void)URLSession:(NSURLSession *)session size_t uploadMinimumProgressInterval; void* uploadProgressReportCallbackContext{}; HCHttpCallProgressReportFunction uploadProgressReportFunction = nullptr; - HRESULT hr = HCHttpCallRequestGetProgressReportFunction(_call, true, &uploadProgressReportFunction, &uploadMinimumProgressInterval, &uploadProgressReportCallbackContext); + + HCCallHandle call; + { + std::shared_lock sharedLock(_taskContextsMutex); + auto existingTaskContext = _taskContexts.find([task taskIdentifier]); + if (existingTaskContext == _taskContexts.end()) + { + HC_TRACE_ERROR(HTTPCLIENT, "Task context missing for identifier %u", [task taskIdentifier]); + [task cancel]; + return; + } + call = existingTaskContext->second._call; + } + + 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]; + + HCCallHandle call = _callHandleRetriever([[session configuration] timeoutIntervalForRequest], [dataTask taskIdentifier]); + + { + std::unique_lock uniqueLock(_taskContextsMutex); + _taskContexts.emplace([dataTask taskIdentifier], TaskContext{ ._call = call, ._downloadSize = [response expectedContentLength]}); + } } @end From 93f4a4b25167dfce5c16f1959727f8f11e6e425f Mon Sep 17 00:00:00 2001 From: lawrencewuskyboxlabs <138919137+lawrencewuskyboxlabs@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:52:44 -0700 Subject: [PATCH 2/4] Remove need for call retriever. Use NSLock in delegate. --- Source/HTTP/Apple/http_apple.mm | 64 ++++++------------ Source/HTTP/Apple/session_delegate.h | 3 +- Source/HTTP/Apple/session_delegate.mm | 94 ++++++++++++++++++++------- 3 files changed, 90 insertions(+), 71 deletions(-) diff --git a/Source/HTTP/Apple/http_apple.mm b/Source/HTTP/Apple/http_apple.mm index 9b23be3cd..0ed16a95f 100644 --- a/Source/HTTP/Apple/http_apple.mm +++ b/Source/HTTP/Apple/http_apple.mm @@ -20,6 +20,7 @@ struct AppleHttpSession { NSURLSession* m_session; + SessionDelegate* m_delegate; std::unordered_map m_httpTaskContexts; }; @@ -32,11 +33,10 @@ HRESULT InitiateRequest( ) noexcept; private: - std::shared_mutex m_httpSessionsMutex; + std::mutex m_httpSessionsMutex; std::unordered_map m_httpSessions; void StartTaskOnSession(HCCallHandle call, XAsyncBlock* asyncBlock, NSURLRequest* request); - HCCallHandle GetCallHandle(uint32_t sessionTimeout, NSUInteger taskIdentifier); void CompletionHandler(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error); }; @@ -50,82 +50,56 @@ HRESULT InitiateRequest( } { - std::unique_lock uniqueLock(m_httpSessionsMutex); + std::unique_lock uniqueLock(m_httpSessionsMutex); - NSURLSession* session = nil; auto httpSessionIter = m_httpSessions.find(timeoutInSeconds); - if (httpSessionIter != m_httpSessions.end()) + if (httpSessionIter == m_httpSessions.end()) { - session = httpSessionIter->second.m_session; - } - else { NSURLSessionConfiguration* configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration; [configuration setTimeoutIntervalForRequest:(NSTimeInterval)timeoutInSeconds]; [configuration setTimeoutIntervalForResource:(NSTimeInterval)timeoutInSeconds]; std::weak_ptr weak_this = shared_from_this(); - SessionDelegate* delegate = [SessionDelegate sessionDelegateWithCallHandleRetriever:^HCCallHandle(uint32_t sessionTimeout, NSUInteger taskIdentifier) { - if (auto me = weak_this.lock()) - { - return me->GetCallHandle(sessionTimeout, taskIdentifier); - } - else { - return nullptr; - } - } andCompletionHandler:^(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse *response, NSError *error) { + 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]; - session = [NSURLSession sessionWithConfiguration:configuration delegate:delegate delegateQueue:nil]; - httpSessionIter = m_httpSessions.emplace(timeoutInSeconds, AppleHttpSession{ .m_session = session }).first; + httpSessionIter = m_httpSessions.emplace(timeoutInSeconds, AppleHttpSession{ .m_session = session, .m_delegate = delegate }).first; } - NSURLSessionTask* sessionTask = [session dataTaskWithRequest:request]; + NSURLSessionTask* 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 %u", timeoutInSeconds, taskIdentifier); [sessionTask cancel]; + return; } - else + + bool delegateRegistered = [httpSessionIter->second.m_delegate registerContextForTask:taskIdentifier withCall:call]; + if (!delegateRegistered) { - httpSessionIter->second.m_httpTaskContexts.emplace(taskIdentifier, AppleHttpTaskContext{ .m_call = call, .m_asyncBlock = asyncBlock }); - [sessionTask resume]; + HC_TRACE_ERROR(HTTPCLIENT, "Shared session with timeout %u failed to register task with identifier %u", timeoutInSeconds, taskIdentifier); + [sessionTask cancel]; + return; } + + httpSessionIter->second.m_httpTaskContexts.emplace(taskIdentifier, AppleHttpTaskContext{ .m_call = call, .m_asyncBlock = asyncBlock }); + [sessionTask resume]; } } -HCCallHandle AppleHttpSessionManager::GetCallHandle(uint32_t sessionTimeout, NSUInteger taskIdentifier) -{ - std::shared_lock sharedLock(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 nullptr; - } - - 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 %u", taskIdentifier); - return nullptr; - } - - return taskContextIter->second.m_call; -} - void AppleHttpSessionManager::CompletionHandler(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error) { AppleHttpTaskContext taskContext; { - std::unique_lock uniqueLock(m_httpSessionsMutex); + std::unique_lock uniqueLock(m_httpSessionsMutex); auto httpSessionIter = m_httpSessions.find(sessionTimeout); if (httpSessionIter == m_httpSessions.end()) diff --git a/Source/HTTP/Apple/session_delegate.h b/Source/HTTP/Apple/session_delegate.h index 531b0dc72..0ade844ff 100644 --- a/Source/HTTP/Apple/session_delegate.h +++ b/Source/HTTP/Apple/session_delegate.h @@ -6,6 +6,7 @@ @interface SessionDelegate : NSObject -+ (SessionDelegate*) sessionDelegateWithCallHandleRetriever:(HCCallHandle(^)(uint32_t sessionTimeout, NSUInteger taskIdentifier)) callRetriever andCompletionHandler:(void(^)(uint32_t sessionTimeout, NSUInteger taskIdentifier, 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 eb6138184..f011a44ff 100644 --- a/Source/HTTP/Apple/session_delegate.mm +++ b/Source/HTTP/Apple/session_delegate.mm @@ -4,7 +4,6 @@ #include "pch.h" #include #import "session_delegate.h" -#include struct TaskContext { @@ -14,16 +13,15 @@ @implementation SessionDelegate { - HCCallHandle(^_callHandleRetriever)(uint32_t sessionTimeout, NSUInteger taskIdentifier); void(^_completionHandler)(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error); - std::shared_mutex _taskContextsMutex; + NSLock* _taskContextsLock; std::unordered_map _taskContexts; } -+ (SessionDelegate*) sessionDelegateWithCallHandleRetriever:(HCCallHandle(^)(uint32_t sessionTimeout, NSUInteger taskIdentifier)) callRetriever andCompletionHandler:(void(^)(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error)) completionHandler ++ (SessionDelegate*) sessionDelegateWithCompletionHandler:(void(^)(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error)) completionHandler { - return [[SessionDelegate alloc] initWithCallHandleRetriever: callRetriever 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 @@ -48,12 +46,31 @@ + (void) reportProgress:(HCCallHandle)call progressReportFunction:(HCHttpCallPro } } -- (instancetype) initWithCallHandleRetriever:(HCCallHandle(^)(uint32_t sessionTimeout, NSUInteger taskIdentifier)) callRetriever andCompletionHandler:(void(^)(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error)) completionHandler +- (bool) registerContextForTask:(NSUInteger)taskIdentifier withCall:(HCCallHandle)call +{ + bool registered = false; + { + [_taskContextsLock lock]; + 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 %u", taskIdentifier); + } + [_taskContextsLock unlock]; + } + return registered; +} + +- (instancetype) initWithCompletionHandler:(void(^)(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error)) completionHandler { if (self = [super init]) { - _callHandleRetriever = callRetriever; _completionHandler = completionHandler; + _taskContextsLock = [[NSLock alloc] init]; return self; } return nil; @@ -62,8 +79,9 @@ - (instancetype) initWithCallHandleRetriever:(HCCallHandle(^)(uint32_t sessionTi - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { { - std::unique_lock uniqueLock(_taskContextsMutex); + [_taskContextsLock lock]; _taskContexts.erase([task taskIdentifier]); + [_taskContextsLock unlock]; } _completionHandler([[session configuration] timeoutIntervalForRequest], [task taskIdentifier], [task response], error); @@ -75,16 +93,23 @@ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)task void* context = nullptr; TaskContext taskContext; + bool hasContext = false; { - std::shared_lock sharedLock(_taskContextsMutex); + [_taskContextsLock lock]; auto existingTaskContext = _taskContexts.find([task taskIdentifier]); - if (existingTaskContext == _taskContexts.end()) + if (existingTaskContext != _taskContexts.end()) { - HC_TRACE_ERROR(HTTPCLIENT, "Task context missing for identifier %u", [task taskIdentifier]); - [task cancel]; - return; + hasContext = true; + taskContext = existingTaskContext->second; } - taskContext = existingTaskContext->second; + [_taskContextsLock unlock]; + } + + if (!hasContext) + { + HC_TRACE_ERROR(HTTPCLIENT, "Task context missing for data of identifier %u", [task taskIdentifier]); + [task cancel]; + return; } if (FAILED(HCHttpCallResponseGetResponseBodyWriteFunction(taskContext._call, &writeFunction, &context)) || @@ -139,17 +164,24 @@ - (void)URLSession:(NSURLSession *)session void* uploadProgressReportCallbackContext{}; HCHttpCallProgressReportFunction uploadProgressReportFunction = nullptr; - HCCallHandle call; + HCCallHandle call = nullptr; + bool hasContext = false; { - std::shared_lock sharedLock(_taskContextsMutex); + [_taskContextsLock lock]; auto existingTaskContext = _taskContexts.find([task taskIdentifier]); - if (existingTaskContext == _taskContexts.end()) + if (existingTaskContext != _taskContexts.end()) { - HC_TRACE_ERROR(HTTPCLIENT, "Task context missing for identifier %u", [task taskIdentifier]); - [task cancel]; - return; + hasContext = true; + call = existingTaskContext->second._call; } - call = existingTaskContext->second._call; + [_taskContextsLock unlock]; + } + + if (!hasContext) + { + HC_TRACE_ERROR(HTTPCLIENT, "Task context missing for send report of identifier %u", [task taskIdentifier]); + [task cancel]; + return; } HRESULT hr = HCHttpCallRequestGetProgressReportFunction(call, true, &uploadProgressReportFunction, &uploadMinimumProgressInterval, &uploadProgressReportCallbackContext); @@ -162,14 +194,26 @@ - (void)URLSession:(NSURLSession *)session } -- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler { +- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler +{ completionHandler(NSURLSessionResponseAllow); - HCCallHandle call = _callHandleRetriever([[session configuration] timeoutIntervalForRequest], [dataTask taskIdentifier]); + bool hasContext = false; + { + [_taskContextsLock lock]; + auto existingTaskContext = _taskContexts.find([dataTask taskIdentifier]); + if (existingTaskContext != _taskContexts.end()) + { + hasContext = true; + existingTaskContext->second._downloadSize = [response expectedContentLength]; + } + [_taskContextsLock unlock]; + } + if (!hasContext) { - std::unique_lock uniqueLock(_taskContextsMutex); - _taskContexts.emplace([dataTask taskIdentifier], TaskContext{ ._call = call, ._downloadSize = [response expectedContentLength]}); + HC_TRACE_ERROR(HTTPCLIENT, "Task context missing for response of identifier %u", [dataTask taskIdentifier]); + [dataTask cancel]; } } From 1803db9e63660ddd31bf7c9ac178099b3ab1a9d0 Mon Sep 17 00:00:00 2001 From: lawrencewuskyboxlabs <138919137+lawrencewuskyboxlabs@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:01:17 -0700 Subject: [PATCH 3/4] Small cleanup --- Source/HTTP/Apple/http_apple.mm | 1 - Source/HTTP/Apple/session_delegate.mm | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Source/HTTP/Apple/http_apple.mm b/Source/HTTP/Apple/http_apple.mm index 0ed16a95f..1623cc926 100644 --- a/Source/HTTP/Apple/http_apple.mm +++ b/Source/HTTP/Apple/http_apple.mm @@ -7,7 +7,6 @@ #include "http_apple.h" #include "request_body_stream.h" #include "session_delegate.h" -#include NAMESPACE_XBOX_HTTP_CLIENT_BEGIN diff --git a/Source/HTTP/Apple/session_delegate.mm b/Source/HTTP/Apple/session_delegate.mm index f011a44ff..802901606 100644 --- a/Source/HTTP/Apple/session_delegate.mm +++ b/Source/HTTP/Apple/session_delegate.mm @@ -194,8 +194,7 @@ - (void)URLSession:(NSURLSession *)session } -- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler -{ +- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler { completionHandler(NSURLSessionResponseAllow); bool hasContext = false; From ed27f2c277f936cc0502897d6e24cdf5dd0773b2 Mon Sep 17 00:00:00 2001 From: lawrencewuskyboxlabs Date: Thu, 23 Apr 2026 16:56:55 -0700 Subject: [PATCH 4/4] Mutex instead of NSLock. %ul instead of %u. --- Source/HTTP/Apple/http_apple.mm | 19 ++++++++++++++--- Source/HTTP/Apple/session_delegate.mm | 30 +++++++++++++-------------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/Source/HTTP/Apple/http_apple.mm b/Source/HTTP/Apple/http_apple.mm index 1623cc926..0f3d3e76a 100644 --- a/Source/HTTP/Apple/http_apple.mm +++ b/Source/HTTP/Apple/http_apple.mm @@ -26,6 +26,9 @@ class AppleHttpSessionManager : public std::enable_shared_from_this { public: + AppleHttpSessionManager() = default; + ~AppleHttpSessionManager(); + HRESULT InitiateRequest( HCCallHandle call, XAsyncBlock *async @@ -39,6 +42,16 @@ HRESULT InitiateRequest( void CompletionHandler(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error); }; +AppleHttpSessionManager::~AppleHttpSessionManager() +{ + std::unique_lock uniqueLock(m_httpSessionsMutex); + + for (auto it = m_httpSessions.begin(); it != m_httpSessions.end(); ++it) + { + [it->second.m_session finishTasksAndInvalidate]; + } +} + void AppleHttpSessionManager::StartTaskOnSession(HCCallHandle call, XAsyncBlock* asyncBlock, NSURLRequest* request) { uint32_t timeoutInSeconds = 0; @@ -76,7 +89,7 @@ HRESULT InitiateRequest( if (httpSessionIter->second.m_httpTaskContexts.count(taskIdentifier) > 0) { - HC_TRACE_ERROR(HTTPCLIENT, "Shared session with timeout %u already has task with identifier %u", timeoutInSeconds, taskIdentifier); + HC_TRACE_ERROR(HTTPCLIENT, "Shared session with timeout %u already has task with identifier %lu", timeoutInSeconds, taskIdentifier); [sessionTask cancel]; return; } @@ -84,7 +97,7 @@ HRESULT InitiateRequest( 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 %u", timeoutInSeconds, taskIdentifier); + HC_TRACE_ERROR(HTTPCLIENT, "Shared session with timeout %u failed to register task with identifier %lu", timeoutInSeconds, taskIdentifier); [sessionTask cancel]; return; } @@ -110,7 +123,7 @@ HRESULT InitiateRequest( 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 %u", taskIdentifier); + HC_TRACE_ERROR(HTTPCLIENT, "No existing task context with identifier %lu", taskIdentifier); return; } diff --git a/Source/HTTP/Apple/session_delegate.mm b/Source/HTTP/Apple/session_delegate.mm index 802901606..a12d6071e 100644 --- a/Source/HTTP/Apple/session_delegate.mm +++ b/Source/HTTP/Apple/session_delegate.mm @@ -15,7 +15,7 @@ @implementation SessionDelegate { void(^_completionHandler)(uint32_t sessionTimeout, NSUInteger taskIdentifier, NSURLResponse* response, NSError* error); - NSLock* _taskContextsLock; + std::mutex _taskContextsMutex; std::unordered_map _taskContexts; } @@ -50,7 +50,8 @@ - (bool) registerContextForTask:(NSUInteger)taskIdentifier withCall:(HCCallHandl { bool registered = false; { - [_taskContextsLock lock]; + std::unique_lock uniqueLock(_taskContextsMutex); + if (_taskContexts.find(taskIdentifier) == _taskContexts.end()) { _taskContexts.emplace(taskIdentifier, TaskContext{ ._call = call }); @@ -58,9 +59,8 @@ - (bool) registerContextForTask:(NSUInteger)taskIdentifier withCall:(HCCallHandl } else { - HC_TRACE_ERROR_HR(HTTPCLIENT, "Task context already exists, cannot register for identifier %u", taskIdentifier); + HC_TRACE_ERROR_HR(HTTPCLIENT, "Task context already exists, cannot register for identifier %lu", taskIdentifier); } - [_taskContextsLock unlock]; } return registered; } @@ -70,7 +70,6 @@ - (instancetype) initWithCompletionHandler:(void(^)(uint32_t sessionTimeout, NSU if (self = [super init]) { _completionHandler = completionHandler; - _taskContextsLock = [[NSLock alloc] init]; return self; } return nil; @@ -79,9 +78,8 @@ - (instancetype) initWithCompletionHandler:(void(^)(uint32_t sessionTimeout, NSU - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { { - [_taskContextsLock lock]; + std::unique_lock uniqueLock(_taskContextsMutex); _taskContexts.erase([task taskIdentifier]); - [_taskContextsLock unlock]; } _completionHandler([[session configuration] timeoutIntervalForRequest], [task taskIdentifier], [task response], error); @@ -95,19 +93,19 @@ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)task TaskContext taskContext; bool hasContext = false; { - [_taskContextsLock lock]; + std::unique_lock uniqueLock(_taskContextsMutex); + auto existingTaskContext = _taskContexts.find([task taskIdentifier]); if (existingTaskContext != _taskContexts.end()) { hasContext = true; taskContext = existingTaskContext->second; } - [_taskContextsLock unlock]; } if (!hasContext) { - HC_TRACE_ERROR(HTTPCLIENT, "Task context missing for data of identifier %u", [task taskIdentifier]); + HC_TRACE_ERROR(HTTPCLIENT, "Task context missing for data of identifier %lu", [task taskIdentifier]); [task cancel]; return; } @@ -167,19 +165,19 @@ - (void)URLSession:(NSURLSession *)session HCCallHandle call = nullptr; bool hasContext = false; { - [_taskContextsLock lock]; + std::unique_lock uniqueLock(_taskContextsMutex); + auto existingTaskContext = _taskContexts.find([task taskIdentifier]); if (existingTaskContext != _taskContexts.end()) { hasContext = true; call = existingTaskContext->second._call; } - [_taskContextsLock unlock]; } if (!hasContext) { - HC_TRACE_ERROR(HTTPCLIENT, "Task context missing for send report of identifier %u", [task taskIdentifier]); + HC_TRACE_ERROR(HTTPCLIENT, "Task context missing for send report of identifier %lu", [task taskIdentifier]); [task cancel]; return; } @@ -199,19 +197,19 @@ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)data bool hasContext = false; { - [_taskContextsLock lock]; + std::unique_lock uniqueLock(_taskContextsMutex); + auto existingTaskContext = _taskContexts.find([dataTask taskIdentifier]); if (existingTaskContext != _taskContexts.end()) { hasContext = true; existingTaskContext->second._downloadSize = [response expectedContentLength]; } - [_taskContextsLock unlock]; } if (!hasContext) { - HC_TRACE_ERROR(HTTPCLIENT, "Task context missing for response of identifier %u", [dataTask taskIdentifier]); + HC_TRACE_ERROR(HTTPCLIENT, "Task context missing for response of identifier %lu", [dataTask taskIdentifier]); [dataTask cancel]; } }