create
This commit is contained in:
1419
Pods/GTMSessionFetcher/Sources/Core/GTMSessionFetcherService.m
generated
Normal file
1419
Pods/GTMSessionFetcher/Sources/Core/GTMSessionFetcherService.m
generated
Normal file
@@ -0,0 +1,1419 @@
|
||||
/* 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 "GTMSessionFetcher/GTMSessionFetcherService.h"
|
||||
#import "GTMSessionFetcherService+Internal.h"
|
||||
|
||||
#include <os/lock.h>
|
||||
|
||||
NSString *const kGTMSessionFetcherServiceSessionBecameInvalidNotification =
|
||||
@"kGTMSessionFetcherServiceSessionBecameInvalidNotification";
|
||||
NSString *const kGTMSessionFetcherServiceSessionKey = @"kGTMSessionFetcherServiceSessionKey";
|
||||
|
||||
static id<GTMUserAgentProvider> SharedStandardUserAgentProvider(void) {
|
||||
static dispatch_once_t onceToken;
|
||||
static id<GTMUserAgentProvider> standardUserAgentProvider;
|
||||
dispatch_once(&onceToken, ^{
|
||||
standardUserAgentProvider = [[GTMStandardUserAgentProvider alloc] initWithBundle:nil];
|
||||
});
|
||||
return standardUserAgentProvider;
|
||||
}
|
||||
|
||||
#if !GTMSESSION_BUILD_COMBINED_SOURCES
|
||||
@interface GTMSessionFetcher (ServiceMethods)
|
||||
- (BOOL)beginFetchMayDelay:(BOOL)mayDelay
|
||||
mayAuthorize:(BOOL)mayAuthorize
|
||||
mayDecorate:(BOOL)mayDecorate;
|
||||
@end
|
||||
#endif // !GTMSESSION_BUILD_COMBINED_SOURCES
|
||||
|
||||
@interface GTMSessionFetcherService ()
|
||||
|
||||
@property(atomic, strong, readwrite) NSDictionary *delayedFetchersByHost;
|
||||
@property(atomic, strong, readwrite) NSDictionary *runningFetchersByHost;
|
||||
|
||||
// Ordered collection of id<GTMFetcherDecoratorProtocol>, held weakly.
|
||||
@property(atomic, strong, readonly) NSPointerArray *decoratorsPointerArray;
|
||||
|
||||
@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.
|
||||
os_unfair_lock _sessionCreationLock;
|
||||
|
||||
BOOL _callbackQueueIsConcurrent;
|
||||
dispatch_queue_t _callbackQueue;
|
||||
NSOperationQueue *_delegateQueue;
|
||||
NSHTTPCookieStorage *_cookieStorage;
|
||||
id<GTMUserAgentProvider> _userAgentProvider;
|
||||
NSTimeInterval _timeout;
|
||||
|
||||
NSURLCredential *_credential; // Username & password.
|
||||
NSURLCredential *_proxyCredential; // Credential supplied to proxy servers.
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated"
|
||||
id<GTMFetcherAuthorizationProtocol> _authorizer;
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Clang-format likes to cram all @synthesize items onto the fewest lines, rather than one-per.
|
||||
// clang-format off
|
||||
@synthesize maxRunningFetchersPerHost = _maxRunningFetchersPerHost,
|
||||
configuration = _configuration,
|
||||
configurationBlock = _configurationBlock,
|
||||
cookieStorage = _cookieStorage,
|
||||
challengeBlock = _challengeBlock,
|
||||
credential = _credential,
|
||||
proxyCredential = _proxyCredential,
|
||||
allowedInsecureSchemes = _allowedInsecureSchemes,
|
||||
allowLocalhostRequest = _allowLocalhostRequest,
|
||||
allowInvalidServerCertificates = _allowInvalidServerCertificates,
|
||||
retryEnabled = _retryEnabled,
|
||||
retryBlock = _retryBlock,
|
||||
maxRetryInterval = _maxRetryInterval,
|
||||
minRetryInterval = _minRetryInterval,
|
||||
metricsCollectionBlock = _metricsCollectionBlock,
|
||||
properties = _properties,
|
||||
unusedSessionTimeout = _unusedSessionTimeout,
|
||||
userAgentProvider = _userAgentProvider,
|
||||
decoratorsPointerArray = _decoratorsPointerArray,
|
||||
testBlock = _testBlock;
|
||||
// clang-format on
|
||||
|
||||
#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;
|
||||
_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";
|
||||
|
||||
_sessionCreationLock = OS_UNFAIR_LOCK_INIT;
|
||||
|
||||
// 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 or userAgentProvider properties to nil.
|
||||
//
|
||||
// Formatting the User-Agent string can be expensive, so create a shared cache
|
||||
// which asynchronously calculates and caches the standard User-Agent.
|
||||
_userAgentProvider = SharedStandardUserAgentProvider();
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self detachAuthorizer];
|
||||
[_delegateDispatcher abandon];
|
||||
}
|
||||
|
||||
#pragma mark Generate a new fetcher
|
||||
|
||||
// Creates a serial queue targetting the service's callback, meant to be provided to a new
|
||||
// GTMSessionFetcher instance.
|
||||
//
|
||||
// This method is not intended to be overrideable by clients.
|
||||
- (nonnull dispatch_queue_t)serialQueueForNewFetcher:(GTMSessionFetcher *)fetcher {
|
||||
@synchronized(self) {
|
||||
GTMSessionMonitorSynchronized(self);
|
||||
|
||||
if (!_callbackQueueIsConcurrent) return _callbackQueue;
|
||||
|
||||
static const char *kQueueLabel = "com.google.GTMSessionFetcher.serialCallbackQueue";
|
||||
return dispatch_queue_create_with_target(kQueueLabel, DISPATCH_QUEUE_SERIAL, _callbackQueue);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 serialQueueForNewFetcher:fetcher];
|
||||
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;
|
||||
if (@available(iOS 10.0, *)) {
|
||||
fetcher.metricsCollectionBlock = self.metricsCollectionBlock;
|
||||
}
|
||||
fetcher.properties = self.properties;
|
||||
fetcher.service = self;
|
||||
|
||||
#if GTM_BACKGROUND_TASK_FETCHING
|
||||
fetcher.skipBackgroundTask = self.skipBackgroundTask;
|
||||
#endif
|
||||
|
||||
fetcher.userAgentProvider = self.userAgentProvider;
|
||||
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];
|
||||
}
|
||||
|
||||
- (void)addDecorator:(id<GTMFetcherDecoratorProtocol>)decorator {
|
||||
@synchronized(self) {
|
||||
if (!_decoratorsPointerArray) {
|
||||
_decoratorsPointerArray = [NSPointerArray weakObjectsPointerArray];
|
||||
}
|
||||
[_decoratorsPointerArray addPointer:(__bridge void *)decorator];
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable NSArray<id<GTMFetcherDecoratorProtocol>> *)decorators {
|
||||
@synchronized(self) {
|
||||
return _decoratorsPointerArray.allObjects;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeDecorator:(id<GTMFetcherDecoratorProtocol>)decorator {
|
||||
@synchronized(self) {
|
||||
NSUInteger i = 0;
|
||||
for (id<GTMFetcherDecoratorProtocol> decoratorCandidate in _decoratorsPointerArray) {
|
||||
if (decoratorCandidate == decorator) {
|
||||
break;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
GTMSESSION_ASSERT_DEBUG(i < _decoratorsPointerArray.count,
|
||||
@"decorator %@ must be passed to -addDecorator: before removing",
|
||||
decorator);
|
||||
if (i < _decoratorsPointerArray.count) {
|
||||
[_decoratorsPointerArray removePointerAtIndex:i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a session for the fetcher's host, or nil.
|
||||
- (NSURLSession *)session {
|
||||
@synchronized(self) {
|
||||
GTMSessionMonitorSynchronized(self);
|
||||
|
||||
NSURLSession *session = _delegateDispatcher.session;
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSURLSession *)sessionWithCreationBlock:
|
||||
(NS_NOESCAPE GTMSessionFetcherSessionCreationBlock)creationBlock {
|
||||
@synchronized(self) {
|
||||
GTMSessionMonitorSynchronized(self);
|
||||
if (!_delegateDispatcher) {
|
||||
// This fetcher is creating a non-shared session, so skip locking.
|
||||
return creationBlock(nil);
|
||||
}
|
||||
}
|
||||
|
||||
@try {
|
||||
NSURLSession *session;
|
||||
// Wait if another fetcher is currently creating a session; avoid waiting inside the
|
||||
// @synchronized block as that could deadlock.
|
||||
os_unfair_lock_lock(&_sessionCreationLock);
|
||||
@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];
|
||||
|
||||
session = _delegateDispatcher.session;
|
||||
if (!session) {
|
||||
session = creationBlock(_delegateDispatcher);
|
||||
_delegateDispatcher.session = session;
|
||||
}
|
||||
}
|
||||
return session;
|
||||
} @finally {
|
||||
// Ensure the lock is always released, even if creationBlock throws.
|
||||
os_unfair_lock_unlock(&_sessionCreationLock);
|
||||
}
|
||||
}
|
||||
|
||||
- (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 mayDecorate: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)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 {
|
||||
[self fetcherDidStop:fetcher callbacksPending:false];
|
||||
}
|
||||
|
||||
- (void)fetcherDidStop:(GTMSessionFetcher *)fetcher callbacksPending:(BOOL) callbacksPending {
|
||||
// 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.
|
||||
if (!callbacksPending) {
|
||||
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);
|
||||
os_unfair_lock_lock(&_sessionCreationLock);
|
||||
|
||||
@synchronized(self) {
|
||||
GTMSessionMonitorSynchronized(self);
|
||||
[self resetSessionInternal];
|
||||
}
|
||||
|
||||
os_unfair_lock_unlock(&_sessionCreationLock);
|
||||
}
|
||||
|
||||
- (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);
|
||||
|
||||
os_unfair_lock_lock(&_sessionCreationLock);
|
||||
@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.
|
||||
}
|
||||
}
|
||||
|
||||
os_unfair_lock_unlock(&_sessionCreationLock);
|
||||
}
|
||||
|
||||
- (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];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated"
|
||||
- (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:)]) {
|
||||
[obj setFetcherService:self];
|
||||
}
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (nonnull dispatch_queue_t)callbackQueue {
|
||||
@synchronized(self) {
|
||||
GTMSessionMonitorSynchronized(self);
|
||||
|
||||
return _callbackQueue;
|
||||
} // @synchronized(self)
|
||||
}
|
||||
|
||||
- (void)setCallbackQueue:(dispatch_queue_t)queue {
|
||||
[self setCallbackQueue:queue isConcurrent:NO];
|
||||
}
|
||||
|
||||
- (void)setConcurrentCallbackQueue:(dispatch_queue_t)queue {
|
||||
[self setCallbackQueue:queue isConcurrent:YES];
|
||||
}
|
||||
|
||||
- (void)setCallbackQueue:(dispatch_queue_t)queue isConcurrent:(BOOL)isConcurrent {
|
||||
@synchronized(self) {
|
||||
GTMSessionMonitorSynchronized(self);
|
||||
|
||||
#if DEBUG
|
||||
// Warn when changing from a concurrent queue to a serial queue.
|
||||
if (_callbackQueueIsConcurrent && (!isConcurrent || !queue)) {
|
||||
GTMSESSION_LOG_DEBUG(
|
||||
@"WARNING: Resetting the service callback queue from concurrent to serial");
|
||||
}
|
||||
#endif // DEBUG
|
||||
|
||||
_callbackQueue = queue ?: dispatch_get_main_queue();
|
||||
_callbackQueueIsConcurrent = queue ? isConcurrent : NO;
|
||||
} // @synchronized(self)
|
||||
}
|
||||
|
||||
- (NSOperationQueue *)sessionDelegateQueue {
|
||||
@synchronized(self) {
|
||||
GTMSessionMonitorSynchronized(self);
|
||||
|
||||
return _delegateQueue;
|
||||
} // @synchronized(self)
|
||||
}
|
||||
|
||||
- (void)setSessionDelegateQueue:(NSOperationQueue *)queue {
|
||||
@synchronized(self) {
|
||||
GTMSessionMonitorSynchronized(self);
|
||||
|
||||
_delegateQueue = queue ?: [NSOperationQueue mainQueue];
|
||||
} // @synchronized(self)
|
||||
}
|
||||
|
||||
- (nullable NSString *)userAgent {
|
||||
@synchronized(self) {
|
||||
return _userAgentProvider.userAgent;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setUserAgent:(nullable NSString *)userAgent {
|
||||
@synchronized(self) {
|
||||
if (userAgent) {
|
||||
_userAgentProvider = [[GTMUserAgentStringProvider alloc]
|
||||
initWithUserAgentString:(NSString *_Nonnull)userAgent];
|
||||
} else {
|
||||
// Support setUserAgent:nil to disable `GTMStandardUserAgentProvider`.
|
||||
_userAgentProvider = nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable id<GTMUserAgentProvider>)userAgentProvider {
|
||||
@synchronized(self) {
|
||||
return _userAgentProvider;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setUserAgentProvider:(nullable id<GTMUserAgentProvider>)userAgentProvider {
|
||||
@synchronized(self) {
|
||||
_userAgentProvider = userAgentProvider;
|
||||
}
|
||||
}
|
||||
|
||||
- (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 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 {
|
||||
GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %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];
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
task:(NSURLSessionTask *)task
|
||||
didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics
|
||||
API_AVAILABLE(ios(10.0), macosx(10.12), tvos(10.0), watchos(6.0)) {
|
||||
id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task];
|
||||
[fetcher URLSession:session task:task didFinishCollectingMetrics:metrics];
|
||||
}
|
||||
|
||||
// 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
|
||||
Reference in New Issue
Block a user