Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Source/HTTP/Apple/http_apple.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppleHttpSessionManager> m_httpSessionManager;
};

NAMESPACE_XBOX_HTTP_CLIENT_END
235 changes: 179 additions & 56 deletions Source/HTTP/Apple/http_apple.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSUInteger, AppleHttpTaskContext> 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<AppleHttpSessionManager>
{
public:
HRESULT InitiateRequest(
HCCallHandle call,
XAsyncBlock *async
) noexcept;

private:
std::mutex m_httpSessionsMutex;
std::unordered_map<uint32_t, AppleHttpSession> 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<http_task_apple> 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<std::mutex> 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<AppleHttpSessionManager> 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<std::mutex> 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<uint32_t>([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;
}

Expand All @@ -72,7 +193,7 @@

uint32_t statusCode = static_cast<uint32_t>([httpResponse statusCode]);

HCHttpCallResponseSetStatusCode(m_call, statusCode);
HCHttpCallResponseSetStatusCode(taskContext.m_call, statusCode);

NSDictionary* headers = [httpResponse allHeaderFields];
for (NSString* key in headers)
Expand All @@ -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];
Expand All @@ -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<numHeaders; ++i)
{
char const* headerName;
char const* headerValue;
if (SUCCEEDED(HCHttpCallRequestGetHeaderAtIndex(m_call, i, &headerName, &headerValue)))
if (SUCCEEDED(HCHttpCallRequestGetHeaderAtIndex(call, i, &headerName, &headerValue)))
{
NSString* headerNameString = [[NSString alloc] initWithUTF8String:headerName];
NSString* headerValueString = [[NSString alloc] initWithUTF8String:headerValue];
Expand All @@ -130,37 +254,36 @@
HCHttpCallRequestBodyReadFunction readFunction = nullptr;
size_t requestBodySize = 0;
void* context = nullptr;
if (FAILED(HCHttpCallRequestGetRequestBodyReadFunction(m_call, &readFunction, &requestBodySize, &context))
if (FAILED(HCHttpCallRequestGetRequestBodyReadFunction(call, &readFunction, &requestBodySize, &context))
|| readFunction == nullptr)
{
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;
}

if (requestBodySize > 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<AppleHttpSessionManager>())
{
}

AppleHttpProvider::~AppleHttpProvider() = default;

HRESULT AppleHttpProvider::PerformAsync(
_In_ HCCallHandle call,
_Inout_ XAsyncBlock* asyncBlock
) noexcept
{
std::unique_ptr<xbox::httpclient::http_task_apple> 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
6 changes: 2 additions & 4 deletions Source/HTTP/Apple/session_delegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@

@interface SessionDelegate : NSObject<NSURLSessionTaskDelegate, NSURLSessionDelegate, NSURLSessionDataDelegate>

@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
Loading