/* 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"; #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; NSString *_userAgent; 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, userAgent = _userAgent, 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, 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 property to nil. _userAgent = GTMFetcherStandardUserAgentString(nil); } 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 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]; } - (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 { // 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); 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) } - (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