GBA002/Pods/GTMSessionFetcher/Source/GTMSessionFetcherService.m
Riley Testut 6cca0f244f Replaces frameworks with static libraries
As of iOS 13.3.1, apps installed with free developer accounts that contain embedded frameworks fail to launch. To work around this, we now link all dependencies via Cocoapods as static libraries.
2020-02-03 19:28:23 -08:00

1370 lines
46 KiB
Objective-C

/* Copyright 2014 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
#import "GTMSessionFetcherService.h"
NSString *const kGTMSessionFetcherServiceSessionBecameInvalidNotification
= @"kGTMSessionFetcherServiceSessionBecameInvalidNotification";
NSString *const kGTMSessionFetcherServiceSessionKey
= @"kGTMSessionFetcherServiceSessionKey";
#if !GTMSESSION_BUILD_COMBINED_SOURCES
@interface GTMSessionFetcher (ServiceMethods)
- (BOOL)beginFetchMayDelay:(BOOL)mayDelay
mayAuthorize:(BOOL)mayAuthorize;
@end
#endif // !GTMSESSION_BUILD_COMBINED_SOURCES
@interface GTMSessionFetcherService ()
@property(atomic, strong, readwrite) NSDictionary *delayedFetchersByHost;
@property(atomic, strong, readwrite) NSDictionary *runningFetchersByHost;
@end
// Since NSURLSession doesn't support a separate delegate per task (!), instances of this
// class serve as a session delegate trampoline.
//
// This class maps a session's tasks to fetchers, and resends delegate messages to the task's
// fetcher.
@interface GTMSessionFetcherSessionDelegateDispatcher : NSObject<NSURLSessionDelegate>
// The session for the tasks in this dispatcher's task-to-fetcher map.
@property(atomic) NSURLSession *session;
// The timer interval for invalidating a session that has no active tasks.
@property(atomic) NSTimeInterval discardInterval;
// The current discard timer.
@property(atomic, readonly) NSTimer *discardTimer;
- (instancetype)initWithParentService:(GTMSessionFetcherService *)parentService
sessionDiscardInterval:(NSTimeInterval)discardInterval;
- (void)setFetcher:(GTMSessionFetcher *)fetcher
forTask:(NSURLSessionTask *)task;
- (void)removeFetcher:(GTMSessionFetcher *)fetcher;
// Before using a session, tells the delegate dispatcher to stop the discard timer.
- (void)startSessionUsage;
// When abandoning a delegate dispatcher, we want to avoid the session retaining
// the delegate after tasks complete.
- (void)abandon;
@end
@implementation GTMSessionFetcherService {
NSMutableDictionary *_delayedFetchersByHost;
NSMutableDictionary *_runningFetchersByHost;
NSUInteger _maxRunningFetchersPerHost;
// When this ivar is nil, the service will not reuse sessions.
GTMSessionFetcherSessionDelegateDispatcher *_delegateDispatcher;
// Fetchers will wait on this if another fetcher is creating the shared NSURLSession.
dispatch_semaphore_t _sessionCreationSemaphore;
dispatch_queue_t _callbackQueue;
NSOperationQueue *_delegateQueue;
NSHTTPCookieStorage *_cookieStorage;
NSString *_userAgent;
NSTimeInterval _timeout;
NSURLCredential *_credential; // Username & password.
NSURLCredential *_proxyCredential; // Credential supplied to proxy servers.
NSInteger _cookieStorageMethod;
id<GTMFetcherAuthorizationProtocol> _authorizer;
// For waitForCompletionOfAllFetchersWithTimeout: we need to wait on stopped fetchers since
// they've not yet finished invoking their queued callbacks. This array is nil except when
// waiting on fetchers.
NSMutableArray *_stoppedFetchersToWaitFor;
// For fetchers that enqueued their callbacks before stopAllFetchers was called on the service,
// set a barrier so the callbacks know to bail out.
NSDate *_stoppedAllFetchersDate;
}
@synthesize maxRunningFetchersPerHost = _maxRunningFetchersPerHost,
configuration = _configuration,
configurationBlock = _configurationBlock,
cookieStorage = _cookieStorage,
userAgent = _userAgent,
challengeBlock = _challengeBlock,
credential = _credential,
proxyCredential = _proxyCredential,
allowedInsecureSchemes = _allowedInsecureSchemes,
allowLocalhostRequest = _allowLocalhostRequest,
allowInvalidServerCertificates = _allowInvalidServerCertificates,
retryEnabled = _retryEnabled,
retryBlock = _retryBlock,
maxRetryInterval = _maxRetryInterval,
minRetryInterval = _minRetryInterval,
properties = _properties,
unusedSessionTimeout = _unusedSessionTimeout,
testBlock = _testBlock;
#if GTM_BACKGROUND_TASK_FETCHING
@synthesize skipBackgroundTask = _skipBackgroundTask;
#endif
- (instancetype)init {
self = [super init];
if (self) {
_delayedFetchersByHost = [[NSMutableDictionary alloc] init];
_runningFetchersByHost = [[NSMutableDictionary alloc] init];
_maxRunningFetchersPerHost = 10;
_cookieStorageMethod = -1;
_unusedSessionTimeout = 60.0;
_delegateDispatcher =
[[GTMSessionFetcherSessionDelegateDispatcher alloc] initWithParentService:self
sessionDiscardInterval:_unusedSessionTimeout];
_callbackQueue = dispatch_get_main_queue();
_delegateQueue = [[NSOperationQueue alloc] init];
_delegateQueue.maxConcurrentOperationCount = 1;
_delegateQueue.name = @"com.google.GTMSessionFetcher.NSURLSessionDelegateQueue";
_sessionCreationSemaphore = dispatch_semaphore_create(1);
// Starting with the SDKs for OS X 10.11/iOS 9, the service has a default useragent.
// Apps can remove this and get the default system "CFNetwork" useragent by setting the
// fetcher service's userAgent property to nil.
#if (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_11) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_11) \
|| (TARGET_OS_IPHONE && defined(__IPHONE_9_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0)
_userAgent = GTMFetcherStandardUserAgentString(nil);
#endif
}
return self;
}
- (void)dealloc {
[self detachAuthorizer];
[_delegateDispatcher abandon];
}
#pragma mark Generate a new fetcher
// Clients may override this method. Clients should not override any other library methods.
- (id)fetcherWithRequest:(NSURLRequest *)request
fetcherClass:(Class)fetcherClass {
GTMSessionFetcher *fetcher = [[fetcherClass alloc] initWithRequest:request
configuration:self.configuration];
fetcher.callbackQueue = self.callbackQueue;
fetcher.sessionDelegateQueue = self.sessionDelegateQueue;
fetcher.challengeBlock = self.challengeBlock;
fetcher.credential = self.credential;
fetcher.proxyCredential = self.proxyCredential;
fetcher.authorizer = self.authorizer;
fetcher.cookieStorage = self.cookieStorage;
fetcher.allowedInsecureSchemes = self.allowedInsecureSchemes;
fetcher.allowLocalhostRequest = self.allowLocalhostRequest;
fetcher.allowInvalidServerCertificates = self.allowInvalidServerCertificates;
fetcher.configurationBlock = self.configurationBlock;
fetcher.retryEnabled = self.retryEnabled;
fetcher.retryBlock = self.retryBlock;
fetcher.maxRetryInterval = self.maxRetryInterval;
fetcher.minRetryInterval = self.minRetryInterval;
fetcher.properties = self.properties;
fetcher.service = self;
if (self.cookieStorageMethod >= 0) {
[fetcher setCookieStorageMethod:self.cookieStorageMethod];
}
#if GTM_BACKGROUND_TASK_FETCHING
fetcher.skipBackgroundTask = self.skipBackgroundTask;
#endif
NSString *userAgent = self.userAgent;
if (userAgent.length > 0
&& [request valueForHTTPHeaderField:@"User-Agent"] == nil) {
[fetcher setRequestValue:userAgent
forHTTPHeaderField:@"User-Agent"];
}
fetcher.testBlock = self.testBlock;
return fetcher;
}
- (GTMSessionFetcher *)fetcherWithRequest:(NSURLRequest *)request {
return [self fetcherWithRequest:request
fetcherClass:[GTMSessionFetcher class]];
}
- (GTMSessionFetcher *)fetcherWithURL:(NSURL *)requestURL {
return [self fetcherWithRequest:[NSURLRequest requestWithURL:requestURL]];
}
- (GTMSessionFetcher *)fetcherWithURLString:(NSString *)requestURLString {
NSURL *url = [NSURL URLWithString:requestURLString];
return [self fetcherWithURL:url];
}
// Returns a session for the fetcher's host, or nil.
- (NSURLSession *)session {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
NSURLSession *session = _delegateDispatcher.session;
return session;
}
}
// Returns a session for the fetcher's host, or nil. For shared sessions, this
// waits on a semaphore, blocking other fetchers while the caller creates the
// session if needed.
- (NSURLSession *)sessionForFetcherCreation {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (!_delegateDispatcher) {
// This fetcher is creating a non-shared session, so skip the semaphore usage.
return nil;
}
}
// Wait if another fetcher is currently creating a session; avoid waiting
// inside the @synchronized block, as that can deadlock.
dispatch_semaphore_wait(_sessionCreationSemaphore, DISPATCH_TIME_FOREVER);
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
// Before getting the NSURLSession for task creation, it is
// important to invalidate and nil out the session discard timer; otherwise
// the session can be invalidated between when it is returned to the
// fetcher, and when the fetcher attempts to create its NSURLSessionTask.
[_delegateDispatcher startSessionUsage];
NSURLSession *session = _delegateDispatcher.session;
if (session) {
// The calling fetcher will receive a preexisting session, so
// we can allow other fetchers to create a session.
dispatch_semaphore_signal(_sessionCreationSemaphore);
} else {
// No existing session was obtained, so the calling fetcher will create the session;
// it *must* invoke fetcherDidCreateSession: to signal the dispatcher's semaphore after
// the session has been created (or fails to be created) to avoid a hang.
}
return session;
}
}
- (id<NSURLSessionDelegate>)sessionDelegate {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _delegateDispatcher;
}
}
#pragma mark Queue Management
- (void)addRunningFetcher:(GTMSessionFetcher *)fetcher
forHost:(NSString *)host {
// Add to the array of running fetchers for this host, creating the array if needed.
NSMutableArray *runningForHost = [_runningFetchersByHost objectForKey:host];
if (runningForHost == nil) {
runningForHost = [NSMutableArray arrayWithObject:fetcher];
[_runningFetchersByHost setObject:runningForHost forKey:host];
} else {
[runningForHost addObject:fetcher];
}
}
- (void)addDelayedFetcher:(GTMSessionFetcher *)fetcher
forHost:(NSString *)host {
// Add to the array of delayed fetchers for this host, creating the array if needed.
NSMutableArray *delayedForHost = [_delayedFetchersByHost objectForKey:host];
if (delayedForHost == nil) {
delayedForHost = [NSMutableArray arrayWithObject:fetcher];
[_delayedFetchersByHost setObject:delayedForHost forKey:host];
} else {
[delayedForHost addObject:fetcher];
}
}
- (BOOL)isDelayingFetcher:(GTMSessionFetcher *)fetcher {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
NSString *host = fetcher.request.URL.host;
if (host == nil) {
return NO;
}
NSArray *delayedForHost = [_delayedFetchersByHost objectForKey:host];
NSUInteger idx = [delayedForHost indexOfObjectIdenticalTo:fetcher];
BOOL isDelayed = (delayedForHost != nil) && (idx != NSNotFound);
return isDelayed;
}
}
- (BOOL)fetcherShouldBeginFetching:(GTMSessionFetcher *)fetcher {
// Entry point from the fetcher
NSURL *requestURL = fetcher.request.URL;
NSString *host = requestURL.host;
// Addresses "file:///path" case where localhost is the implicit host.
if (host.length == 0 && [requestURL isFileURL]) {
host = @"localhost";
}
if (host.length == 0) {
// Data URIs legitimately have no host, reject other hostless URLs.
GTMSESSION_ASSERT_DEBUG([[requestURL scheme] isEqual:@"data"], @"%@ lacks host", fetcher);
return YES;
}
BOOL shouldBeginResult;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
NSMutableArray *runningForHost = [_runningFetchersByHost objectForKey:host];
if (runningForHost != nil
&& [runningForHost indexOfObjectIdenticalTo:fetcher] != NSNotFound) {
GTMSESSION_ASSERT_DEBUG(NO, @"%@ was already running", fetcher);
return YES;
}
BOOL shouldRunNow = (fetcher.usingBackgroundSession
|| _maxRunningFetchersPerHost == 0
|| _maxRunningFetchersPerHost >
[[self class] numberOfNonBackgroundSessionFetchers:runningForHost]);
if (shouldRunNow) {
[self addRunningFetcher:fetcher forHost:host];
shouldBeginResult = YES;
} else {
[self addDelayedFetcher:fetcher forHost:host];
shouldBeginResult = NO;
}
} // @synchronized(self)
// We'll save the host that serves as the key for this fetcher's array
// to avoid any chance of the underlying request changing, stranding
// the fetcher in the wrong array
fetcher.serviceHost = host;
return shouldBeginResult;
}
- (void)startFetcher:(GTMSessionFetcher *)fetcher {
[fetcher beginFetchMayDelay:NO
mayAuthorize:YES];
}
// Internal utility. Returns a fetcher's delegate if it's a dispatcher, or nil if the fetcher
// is its own delegate (possibly via proxy) and has no dispatcher.
- (GTMSessionFetcherSessionDelegateDispatcher *)delegateDispatcherForFetcher:(GTMSessionFetcher *)fetcher {
GTMSessionCheckNotSynchronized(self);
NSURLSession *fetcherSession = fetcher.session;
if (fetcherSession) {
id<NSURLSessionDelegate> fetcherDelegate = fetcherSession.delegate;
// If the delegate is non-nil and claims to be a GTMSessionFetcher, there is no dispatcher;
// assume the fetcher is the delegate or has been proxied (some third-party frameworks
// are known to swizzle NSURLSession to proxy its delegate).
BOOL hasDispatcher = (fetcherDelegate != nil &&
![fetcherDelegate isKindOfClass:[GTMSessionFetcher class]]);
if (hasDispatcher) {
GTMSESSION_ASSERT_DEBUG([fetcherDelegate isKindOfClass:[GTMSessionFetcherSessionDelegateDispatcher class]],
@"Fetcher delegate class: %@", [fetcherDelegate class]);
return (GTMSessionFetcherSessionDelegateDispatcher *)fetcherDelegate;
}
}
return nil;
}
- (void)fetcherDidCreateSession:(GTMSessionFetcher *)fetcher {
if (fetcher.canShareSession) {
NSURLSession *fetcherSession = fetcher.session;
GTMSESSION_ASSERT_DEBUG(fetcherSession != nil, @"Fetcher missing its session: %@", fetcher);
GTMSessionFetcherSessionDelegateDispatcher *delegateDispatcher =
[self delegateDispatcherForFetcher:fetcher];
if (delegateDispatcher) {
GTMSESSION_ASSERT_DEBUG(delegateDispatcher.session == nil,
@"Fetcher made an extra session: %@", fetcher);
// Save this fetcher's session.
delegateDispatcher.session = fetcherSession;
// Allow other fetchers to request this session now.
dispatch_semaphore_signal(_sessionCreationSemaphore);
}
}
}
- (void)fetcherDidBeginFetching:(GTMSessionFetcher *)fetcher {
// If this fetcher has a separate delegate with a shared session, then
// this fetcher should be added to the delegate's map of tasks to fetchers.
GTMSessionFetcherSessionDelegateDispatcher *delegateDispatcher =
[self delegateDispatcherForFetcher:fetcher];
if (delegateDispatcher) {
GTMSESSION_ASSERT_DEBUG(fetcher.canShareSession,
@"Inappropriate shared session: %@", fetcher);
// There should already be a session, from this or a previous fetcher.
//
// Sanity check that the fetcher's session is the delegate's shared session.
NSURLSession *sharedSession = delegateDispatcher.session;
NSURLSession *fetcherSession = fetcher.session;
GTMSESSION_ASSERT_DEBUG(sharedSession != nil, @"Missing delegate session: %@", fetcher);
GTMSESSION_ASSERT_DEBUG(fetcherSession == sharedSession,
@"Inconsistent session: %@ %@ (shared: %@)",
fetcher, fetcherSession, sharedSession);
if (sharedSession != nil && fetcherSession == sharedSession) {
NSURLSessionTask *task = fetcher.sessionTask;
GTMSESSION_ASSERT_DEBUG(task != nil, @"Missing session task: %@", fetcher);
if (task) {
[delegateDispatcher setFetcher:fetcher
forTask:task];
}
}
}
}
- (void)stopFetcher:(GTMSessionFetcher *)fetcher {
[fetcher stopFetching];
}
- (void)fetcherDidStop:(GTMSessionFetcher *)fetcher {
// Entry point from the fetcher
NSString *host = fetcher.serviceHost;
if (!host) {
// fetcher has been stopped previously
return;
}
// This removeFetcher: invocation is a fallback; typically, fetchers are removed from the task
// map when the task completes.
GTMSessionFetcherSessionDelegateDispatcher *delegateDispatcher =
[self delegateDispatcherForFetcher:fetcher];
[delegateDispatcher removeFetcher:fetcher];
NSMutableArray *fetchersToStart;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
// If a test is waiting for all fetchers to stop, it needs to wait for this one
// to invoke its callbacks on the callback queue.
[_stoppedFetchersToWaitFor addObject:fetcher];
NSMutableArray *runningForHost = [_runningFetchersByHost objectForKey:host];
[runningForHost removeObject:fetcher];
NSMutableArray *delayedForHost = [_delayedFetchersByHost objectForKey:host];
[delayedForHost removeObject:fetcher];
while (delayedForHost.count > 0
&& [[self class] numberOfNonBackgroundSessionFetchers:runningForHost]
< _maxRunningFetchersPerHost) {
// Start another delayed fetcher running, scanning for the minimum
// priority value, defaulting to FIFO for equal priorities
GTMSessionFetcher *nextFetcher = nil;
for (GTMSessionFetcher *delayedFetcher in delayedForHost) {
if (nextFetcher == nil
|| delayedFetcher.servicePriority < nextFetcher.servicePriority) {
nextFetcher = delayedFetcher;
}
}
if (nextFetcher) {
[self addRunningFetcher:nextFetcher forHost:host];
runningForHost = [_runningFetchersByHost objectForKey:host];
[delayedForHost removeObjectIdenticalTo:nextFetcher];
if (!fetchersToStart) {
fetchersToStart = [NSMutableArray array];
}
[fetchersToStart addObject:nextFetcher];
}
}
if (runningForHost.count == 0) {
// None left; remove the empty array
[_runningFetchersByHost removeObjectForKey:host];
}
if (delayedForHost.count == 0) {
[_delayedFetchersByHost removeObjectForKey:host];
}
} // @synchronized(self)
// Start fetchers outside of the synchronized block to avoid a deadlock.
for (GTMSessionFetcher *nextFetcher in fetchersToStart) {
[self startFetcher:nextFetcher];
}
// The fetcher is no longer in the running or the delayed array,
// so remove its host and thread properties
fetcher.serviceHost = nil;
}
- (NSUInteger)numberOfFetchers {
NSUInteger running = [self numberOfRunningFetchers];
NSUInteger delayed = [self numberOfDelayedFetchers];
return running + delayed;
}
- (NSUInteger)numberOfRunningFetchers {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
NSUInteger sum = 0;
for (NSString *host in _runningFetchersByHost) {
NSArray *fetchers = [_runningFetchersByHost objectForKey:host];
sum += fetchers.count;
}
return sum;
}
}
- (NSUInteger)numberOfDelayedFetchers {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
NSUInteger sum = 0;
for (NSString *host in _delayedFetchersByHost) {
NSArray *fetchers = [_delayedFetchersByHost objectForKey:host];
sum += fetchers.count;
}
return sum;
}
}
- (NSArray *)issuedFetchers {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
NSMutableArray *allFetchers = [NSMutableArray array];
void (^accumulateFetchers)(id, id, BOOL *) = ^(NSString *host,
NSArray *fetchersForHost,
BOOL *stop) {
[allFetchers addObjectsFromArray:fetchersForHost];
};
[_runningFetchersByHost enumerateKeysAndObjectsUsingBlock:accumulateFetchers];
[_delayedFetchersByHost enumerateKeysAndObjectsUsingBlock:accumulateFetchers];
GTMSESSION_ASSERT_DEBUG(allFetchers.count == [NSSet setWithArray:allFetchers].count,
@"Fetcher appears multiple times\n running: %@\n delayed: %@",
_runningFetchersByHost, _delayedFetchersByHost);
return allFetchers.count > 0 ? allFetchers : nil;
}
}
- (NSArray *)issuedFetchersWithRequestURL:(NSURL *)requestURL {
NSString *host = requestURL.host;
if (host.length == 0) return nil;
NSURL *targetURL = [requestURL absoluteURL];
NSArray *allFetchers = [self issuedFetchers];
NSIndexSet *indexes = [allFetchers indexesOfObjectsPassingTest:^BOOL(GTMSessionFetcher *fetcher,
NSUInteger idx,
BOOL *stop) {
NSURL *fetcherURL = [fetcher.request.URL absoluteURL];
return [fetcherURL isEqual:targetURL];
}];
NSArray *result = nil;
if (indexes.count > 0) {
result = [allFetchers objectsAtIndexes:indexes];
}
return result;
}
- (void)stopAllFetchers {
NSArray *delayedFetchersByHost;
NSArray *runningFetchersByHost;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
// Set the time barrier so fetchers know not to call back even if
// the stop calls below occur after the fetchers naturally
// stopped and so were removed from _runningFetchersByHost,
// but while the callbacks were already enqueued before stopAllFetchers
// was invoked.
_stoppedAllFetchersDate = [[NSDate alloc] init];
// Remove fetchers from the delayed list to avoid fetcherDidStop: from
// starting more fetchers running as a side effect of stopping one
delayedFetchersByHost = _delayedFetchersByHost.allValues;
[_delayedFetchersByHost removeAllObjects];
runningFetchersByHost = _runningFetchersByHost.allValues;
[_runningFetchersByHost removeAllObjects];
}
for (NSArray *delayedForHost in delayedFetchersByHost) {
for (GTMSessionFetcher *fetcher in delayedForHost) {
[self stopFetcher:fetcher];
}
}
for (NSArray *runningForHost in runningFetchersByHost) {
for (GTMSessionFetcher *fetcher in runningForHost) {
[self stopFetcher:fetcher];
}
}
}
- (NSDate *)stoppedAllFetchersDate {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _stoppedAllFetchersDate;
}
}
#pragma mark Accessors
- (BOOL)reuseSession {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _delegateDispatcher != nil;
}
}
- (void)setReuseSession:(BOOL)shouldReuse {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
BOOL wasReusing = (_delegateDispatcher != nil);
if (shouldReuse != wasReusing) {
[self abandonDispatcher];
if (shouldReuse) {
_delegateDispatcher =
[[GTMSessionFetcherSessionDelegateDispatcher alloc] initWithParentService:self
sessionDiscardInterval:_unusedSessionTimeout];
} else {
_delegateDispatcher = nil;
}
}
}
}
- (void)resetSession {
GTMSessionCheckNotSynchronized(self);
dispatch_semaphore_wait(_sessionCreationSemaphore, DISPATCH_TIME_FOREVER);
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
[self resetSessionInternal];
}
dispatch_semaphore_signal(_sessionCreationSemaphore);
}
- (void)resetSessionInternal {
GTMSessionCheckSynchronized(self);
// The old dispatchers may be retained as delegates of any ongoing sessions by those sessions.
if (_delegateDispatcher) {
[self abandonDispatcher];
_delegateDispatcher =
[[GTMSessionFetcherSessionDelegateDispatcher alloc] initWithParentService:self
sessionDiscardInterval:_unusedSessionTimeout];
}
}
- (void)resetSessionForDispatcherDiscardTimer:(NSTimer *)timer {
GTMSessionCheckNotSynchronized(self);
dispatch_semaphore_wait(_sessionCreationSemaphore, DISPATCH_TIME_FOREVER);
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_delegateDispatcher.discardTimer == timer) {
// If the delegate dispatcher's current discardTimer is the same object as the timer
// that fired, no fetcher has recently attempted to start using the session by calling
// startSessionUsage, which invalidates and nils out the timer.
[self resetSessionInternal];
} else {
// A fetcher has invalidated the timer between its triggering and now, potentially
// meaning a fetcher has requested access to the NSURLSession, and may be in the process
// of starting a new task. The dispatcher should not be abandoned, as this can lead
// to a race condition between calling -finishTasksAndInvalidate on the NSURLSession
// and the fetcher attempting to create a new task.
}
}
dispatch_semaphore_signal(_sessionCreationSemaphore);
}
- (NSTimeInterval)unusedSessionTimeout {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _unusedSessionTimeout;
}
}
- (void)setUnusedSessionTimeout:(NSTimeInterval)timeout {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_unusedSessionTimeout = timeout;
_delegateDispatcher.discardInterval = timeout;
}
}
// This method should be called inside of @synchronized(self)
- (void)abandonDispatcher {
GTMSessionCheckSynchronized(self);
[_delegateDispatcher abandon];
}
- (NSDictionary *)runningFetchersByHost {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return [_runningFetchersByHost copy];
}
}
- (void)setRunningFetchersByHost:(NSDictionary *)dict {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_runningFetchersByHost = [dict mutableCopy];
}
}
- (NSDictionary *)delayedFetchersByHost {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return [_delayedFetchersByHost copy];
}
}
- (void)setDelayedFetchersByHost:(NSDictionary *)dict {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_delayedFetchersByHost = [dict mutableCopy];
}
}
- (id<GTMFetcherAuthorizationProtocol>)authorizer {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _authorizer;
}
}
- (void)setAuthorizer:(id<GTMFetcherAuthorizationProtocol>)obj {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (obj != _authorizer) {
[self detachAuthorizer];
}
_authorizer = obj;
}
// Use the fetcher service for the authorization fetches if the auth
// object supports fetcher services
if ([obj respondsToSelector:@selector(setFetcherService:)]) {
#if GTM_USE_SESSION_FETCHER
[obj setFetcherService:self];
#else
[obj setFetcherService:(id)self];
#endif
}
}
// This should be called inside a @synchronized(self) block except during dealloc.
- (void)detachAuthorizer {
// This method is called by the fetcher service's dealloc and setAuthorizer:
// methods; do not override.
//
// The fetcher service retains the authorizer, and the authorizer has a
// weak pointer to the fetcher service (a non-zeroing pointer for
// compatibility with iOS 4 and Mac OS X 10.5/10.6.)
//
// When this fetcher service no longer uses the authorizer, we want to remove
// the authorizer's dependence on the fetcher service. Authorizers can still
// function without a fetcher service.
if ([_authorizer respondsToSelector:@selector(fetcherService)]) {
id authFetcherService = [_authorizer fetcherService];
if (authFetcherService == self) {
[_authorizer setFetcherService:nil];
}
}
}
- (dispatch_queue_t GTM_NONNULL_TYPE)callbackQueue {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _callbackQueue;
} // @synchronized(self)
}
- (void)setCallbackQueue:(dispatch_queue_t GTM_NULLABLE_TYPE)queue {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_callbackQueue = queue ?: dispatch_get_main_queue();
} // @synchronized(self)
}
- (NSOperationQueue * GTM_NONNULL_TYPE)sessionDelegateQueue {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _delegateQueue;
} // @synchronized(self)
}
- (void)setSessionDelegateQueue:(NSOperationQueue * GTM_NULLABLE_TYPE)queue {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_delegateQueue = queue ?: [NSOperationQueue mainQueue];
} // @synchronized(self)
}
- (NSOperationQueue *)delegateQueue {
// Provided for compatibility with the old fetcher service. The gtm-oauth2 code respects
// any custom delegate queue for calling the app.
return nil;
}
+ (NSUInteger)numberOfNonBackgroundSessionFetchers:(NSArray *)fetchers {
NSUInteger sum = 0;
for (GTMSessionFetcher *fetcher in fetchers) {
if (!fetcher.usingBackgroundSession) {
++sum;
}
}
return sum;
}
@end
@implementation GTMSessionFetcherService (TestingSupport)
+ (instancetype)mockFetcherServiceWithFakedData:(NSData *)fakedDataOrNil
fakedError:(NSError *)fakedErrorOrNil {
#if !GTM_DISABLE_FETCHER_TEST_BLOCK
NSURL *url = [NSURL URLWithString:@"http://example.invalid"];
NSHTTPURLResponse *fakedResponse =
[[NSHTTPURLResponse alloc] initWithURL:url
statusCode:(fakedErrorOrNil ? 500 : 200)
HTTPVersion:@"HTTP/1.1"
headerFields:nil];
return [self mockFetcherServiceWithFakedData:fakedDataOrNil
fakedResponse:fakedResponse
fakedError:fakedErrorOrNil];
#else
GTMSESSION_ASSERT_DEBUG(0, @"Test blocks disabled");
return nil;
#endif // GTM_DISABLE_FETCHER_TEST_BLOCK
}
+ (instancetype)mockFetcherServiceWithFakedData:(NSData *)fakedDataOrNil
fakedResponse:(NSHTTPURLResponse *)fakedResponse
fakedError:(NSError *)fakedErrorOrNil {
#if !GTM_DISABLE_FETCHER_TEST_BLOCK
GTMSessionFetcherService *service = [[self alloc] init];
service.allowedInsecureSchemes = @[ @"http" ];
service.testBlock = ^(GTMSessionFetcher *fetcherToTest,
GTMSessionFetcherTestResponse testResponse) {
testResponse(fakedResponse, fakedDataOrNil, fakedErrorOrNil);
};
return service;
#else
GTMSESSION_ASSERT_DEBUG(0, @"Test blocks disabled");
return nil;
#endif // GTM_DISABLE_FETCHER_TEST_BLOCK
}
#pragma mark Synchronous Wait for Unit Testing
- (BOOL)waitForCompletionOfAllFetchersWithTimeout:(NSTimeInterval)timeoutInSeconds {
NSDate *giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds];
_stoppedFetchersToWaitFor = [NSMutableArray array];
BOOL shouldSpinRunLoop = [NSThread isMainThread];
const NSTimeInterval kSpinInterval = 0.001;
BOOL didTimeOut = NO;
while (([self numberOfFetchers] > 0 || _stoppedFetchersToWaitFor.count > 0)) {
didTimeOut = [giveUpDate timeIntervalSinceNow] < 0;
if (didTimeOut) break;
GTMSessionFetcher *stoppedFetcher = _stoppedFetchersToWaitFor.firstObject;
if (stoppedFetcher) {
[_stoppedFetchersToWaitFor removeObject:stoppedFetcher];
[stoppedFetcher waitForCompletionWithTimeout:10.0 * kSpinInterval];
}
if (shouldSpinRunLoop) {
NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:kSpinInterval];
[[NSRunLoop currentRunLoop] runUntilDate:stopDate];
} else {
[NSThread sleepForTimeInterval:kSpinInterval];
}
}
_stoppedFetchersToWaitFor = nil;
return !didTimeOut;
}
@end
@implementation GTMSessionFetcherService (BackwardsCompatibilityOnly)
- (NSInteger)cookieStorageMethod {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _cookieStorageMethod;
}
}
- (void)setCookieStorageMethod:(NSInteger)cookieStorageMethod {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_cookieStorageMethod = cookieStorageMethod;
}
}
@end
@implementation GTMSessionFetcherSessionDelegateDispatcher {
__weak GTMSessionFetcherService *_parentService;
NSURLSession *_session;
// The task map maps NSURLSessionTasks to GTMSessionFetchers
NSMutableDictionary *_taskToFetcherMap;
// The discard timer will invalidate sessions after the session's last task completes.
NSTimer *_discardTimer;
NSTimeInterval _discardInterval;
}
@synthesize discardInterval = _discardInterval,
session = _session;
- (instancetype)init {
[self doesNotRecognizeSelector:_cmd];
return nil;
}
- (instancetype)initWithParentService:(GTMSessionFetcherService *)parentService
sessionDiscardInterval:(NSTimeInterval)discardInterval {
self = [super init];
if (self) {
_discardInterval = discardInterval;
_parentService = parentService;
}
return self;
}
- (NSString *)description {
return [NSString stringWithFormat:@"%@ %p %@ %@",
[self class], self,
_session ?: @"<no session>",
_taskToFetcherMap.count > 0 ? _taskToFetcherMap : @"<no tasks>"];
}
- (NSTimer *)discardTimer {
GTMSessionCheckNotSynchronized(self);
@synchronized(self) {
return _discardTimer;
}
}
// This method should be called inside of a @synchronized(self) block.
- (void)startDiscardTimer {
GTMSessionCheckSynchronized(self);
[_discardTimer invalidate];
_discardTimer = nil;
if (_discardInterval > 0) {
_discardTimer = [NSTimer timerWithTimeInterval:_discardInterval
target:self
selector:@selector(discardTimerFired:)
userInfo:nil
repeats:NO];
[_discardTimer setTolerance:(_discardInterval / 10)];
[[NSRunLoop mainRunLoop] addTimer:_discardTimer forMode:NSRunLoopCommonModes];
}
}
// This method should be called inside of a @synchronized(self) block.
- (void)destroyDiscardTimer {
GTMSessionCheckSynchronized(self);
[_discardTimer invalidate];
_discardTimer = nil;
}
- (void)discardTimerFired:(NSTimer *)timer {
GTMSessionFetcherService *service;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
NSUInteger numberOfTasks = _taskToFetcherMap.count;
if (numberOfTasks == 0) {
service = _parentService;
}
}
// Inform the service that the discard timer has fired, and should check whether the
// service can abandon us. -resetSession cannot be called directly, as there is a
// race condition that must be guarded against with the NSURLSession being returned
// from sessionForFetcherCreation outside other locks. The service can take steps
// to prevent resetting the session if that has occurred.
//
// The service must be called from outside the @synchronized block.
[service resetSessionForDispatcherDiscardTimer:timer];
}
- (void)abandon {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
[self destroySessionAndTimer];
}
}
- (void)startSessionUsage {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
[self destroyDiscardTimer];
}
}
// This method should be called inside of a @synchronized(self) block.
- (void)destroySessionAndTimer {
GTMSessionCheckSynchronized(self);
[self destroyDiscardTimer];
// Break any retain cycle from the session holding the delegate.
[_session finishTasksAndInvalidate];
// Immediately clear the session so no new task may be issued with it.
//
// The _taskToFetcherMap needs to stay valid until the outstanding tasks finish.
_session = nil;
}
- (void)setFetcher:(GTMSessionFetcher *)fetcher forTask:(NSURLSessionTask *)task {
GTMSESSION_ASSERT_DEBUG(fetcher != nil, @"missing fetcher");
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_taskToFetcherMap == nil) {
_taskToFetcherMap = [[NSMutableDictionary alloc] init];
}
if (fetcher) {
[_taskToFetcherMap setObject:fetcher forKey:task];
[self destroyDiscardTimer];
}
}
}
- (void)removeFetcher:(GTMSessionFetcher *)fetcher {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
// Typically, a fetcher should be removed when its task invokes
// URLSession:task:didCompleteWithError:.
//
// When fetching with a testBlock, though, the task completed delegate
// method may not be invoked, requiring cleanup here.
NSArray *tasks = [_taskToFetcherMap allKeysForObject:fetcher];
GTMSESSION_ASSERT_DEBUG(tasks.count <= 1, @"fetcher task not unmapped: %@", tasks);
[_taskToFetcherMap removeObjectsForKeys:tasks];
if (_taskToFetcherMap.count == 0) {
[self startDiscardTimer];
}
}
}
// This helper method provides synchronized access to the task map for the delegate
// methods below.
- (id)fetcherForTask:(NSURLSessionTask *)task {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return [_taskToFetcherMap objectForKey:task];
}
}
- (void)removeTaskFromMap:(NSURLSessionTask *)task {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
[_taskToFetcherMap removeObjectForKey:task];
}
}
- (void)setSession:(NSURLSession *)session {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_session = session;
}
}
- (NSURLSession *)session {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _session;
}
}
- (NSTimeInterval)discardInterval {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _discardInterval;
}
}
- (void)setDiscardInterval:(NSTimeInterval)interval {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_discardInterval = interval;
}
}
// NSURLSessionDelegate protocol methods.
// - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session;
//
// TODO(seh): How do we route this to an appropriate fetcher?
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error {
GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ didBecomeInvalidWithError:%@",
[self class], self, session, error);
NSDictionary *localTaskToFetcherMap;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_session = nil;
localTaskToFetcherMap = [_taskToFetcherMap copy];
}
// Any "suspended" tasks may not have received callbacks from NSURLSession when the session
// completes; we'll call them now.
[localTaskToFetcherMap enumerateKeysAndObjectsUsingBlock:^(NSURLSessionTask *task,
GTMSessionFetcher *fetcher,
BOOL *stop) {
if (fetcher.session == session) {
// Our delegate method URLSession:task:didCompleteWithError: will rely on
// _taskToFetcherMap so that should still contain this fetcher.
NSError *canceledError = [NSError errorWithDomain:NSURLErrorDomain
code:NSURLErrorCancelled
userInfo:nil];
[self URLSession:session task:task didCompleteWithError:canceledError];
} else {
GTMSESSION_ASSERT_DEBUG(0, @"Unexpected session in fetcher: %@ has %@ (expected %@)",
fetcher, fetcher.session, session);
}
}];
// Our tests rely on this notification to know the session discard timer fired.
NSDictionary *userInfo = @{ kGTMSessionFetcherServiceSessionKey : session };
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:kGTMSessionFetcherServiceSessionBecameInvalidNotification
object:_parentService
userInfo:userInfo];
}
#pragma mark - NSURLSessionTaskDelegate
// NSURLSessionTaskDelegate protocol methods.
//
// We won't test here if the fetcher responds to these since we only want this
// class to implement the same delegate methods the fetcher does (so NSURLSession's
// tests for respondsToSelector: will have the same result whether the session
// delegate is the fetcher or this dispatcher.)
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
newRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLRequest *))completionHandler {
id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task];
[fetcher URLSession:session
task:task
willPerformHTTPRedirection:response
newRequest:request
completionHandler:completionHandler];
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))handler {
id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task];
[fetcher URLSession:session
task:task
didReceiveChallenge:challenge
completionHandler:handler];
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
needNewBodyStream:(void (^)(NSInputStream *bodyStream))handler {
id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task];
[fetcher URLSession:session
task:task
needNewBodyStream:handler];
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task];
[fetcher URLSession:session
task:task
didSendBodyData:bytesSent
totalBytesSent:totalBytesSent
totalBytesExpectedToSend:totalBytesExpectedToSend];
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task];
// This is the usual way tasks are removed from the task map.
[self removeTaskFromMap:task];
[fetcher URLSession:session
task:task
didCompleteWithError:error];
}
// NSURLSessionDataDelegate protocol methods.
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition))handler {
id<NSURLSessionDataDelegate> fetcher = [self fetcherForTask:dataTask];
[fetcher URLSession:session
dataTask:dataTask
didReceiveResponse:response
completionHandler:handler];
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask {
id<NSURLSessionDataDelegate> fetcher = [self fetcherForTask:dataTask];
GTMSESSION_ASSERT_DEBUG(fetcher != nil, @"Missing fetcher for %@", dataTask);
[self removeTaskFromMap:dataTask];
if (fetcher) {
GTMSESSION_ASSERT_DEBUG([fetcher isKindOfClass:[GTMSessionFetcher class]],
@"Expecting GTMSessionFetcher");
[self setFetcher:(GTMSessionFetcher *)fetcher forTask:downloadTask];
}
[fetcher URLSession:session
dataTask:dataTask
didBecomeDownloadTask:downloadTask];
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
id<NSURLSessionDataDelegate> fetcher = [self fetcherForTask:dataTask];
[fetcher URLSession:session
dataTask:dataTask
didReceiveData:data];
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse *))handler {
id<NSURLSessionDataDelegate> fetcher = [self fetcherForTask:dataTask];
[fetcher URLSession:session
dataTask:dataTask
willCacheResponse:proposedResponse
completionHandler:handler];
}
// NSURLSessionDownloadDelegate protocol methods.
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location {
id<NSURLSessionDownloadDelegate> fetcher = [self fetcherForTask:downloadTask];
[fetcher URLSession:session
downloadTask:downloadTask
didFinishDownloadingToURL:location];
}
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalWritten
totalBytesExpectedToWrite:(int64_t)totalExpected {
id<NSURLSessionDownloadDelegate> fetcher = [self fetcherForTask:downloadTask];
[fetcher URLSession:session
downloadTask:downloadTask
didWriteData:bytesWritten
totalBytesWritten:totalWritten
totalBytesExpectedToWrite:totalExpected];
}
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes {
id<NSURLSessionDownloadDelegate> fetcher = [self fetcherForTask:downloadTask];
[fetcher URLSession:session
downloadTask:downloadTask
didResumeAtOffset:fileOffset
expectedTotalBytes:expectedTotalBytes];
}
@end