diff --git a/CHANGELOG.md b/CHANGELOG.md index 564c7d9..cc9b66f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,66 @@ ## Info -**Document version:** 2.8.2 +**Document version:** 2.14.0 -**Last updated:** 08/30/2019 +**Last updated:** 04/30/2020 **Author:** Nolan O'Brien ## History +### 2.14.0 + +- Update `TNLCommunicationAgent` to handle reachability behavior changes + - The `Network.framwork` can yield an "other" interface when VPN is enabled (on Mac binaries) + - Coerce these into _WiFi_ since we don't have a good way to determine the actual interface used at the moment + - The `Network.framework` used to yield _WiFi_ for any network connection on a simulator, but now yields _Wired_ + - Rename `TNLNetworkReachabilityReachableViaWiFi` to `TNLNetworkReachabilityReachableViaEthernet` and handle both cases as _Ethernet_ + +### 2.13.0 + +- Refactor _Service Unavailable Backoff_ system to be more abstract and support *any* trigger for backoff + - All `*serviceUnavailableBackoff*` APIs are refactored into `*backoff*` APIs + - Introduce `[TNLGlobalConfiguration backoffSignaler]` + - Implement your own `TNLBackoffSignaler` to customize behavior ... or ... + - Default will use `TNLSimpleBackoffSignaler` which will signal on *HTTP 503* + +### 2.12.0 + +- Abstract out _Service Unavailable Backoff Behavior_ for customization to be applied + - See `[TNLGlobalConfiguration serviceUnavailableBackoffBehaviorProvider]` for providing a custom backoff behavior + - Implement your own `TNLServiceUnavailableBackoffBehaviorProvider` to customize behavior + - Due to _Service Unavailable_ signaling being opaque, only the HTTP Headers and the URL can be provided + - Default will use `TNLSimpleServiceUnavailableBackoffBehaviorProvider` + - Exact same behavior as before (introduced in **TNL** prior to v2.0 open sourcing) + +### 2.11.0 + +- Change the `TNLURLSessionAuthChallengeCompletionBlock` arguments + - Leave the _disposition_ parameter + - Change the _credentials_ parameter of `NSURLCredentials` to be _credentialsOrCancelContext_ of `id` + - This will permit `TNLAuthenticationChallengeHandler` instance to be able to cancel the challenge and provide extra context in the resulting error code + - Twitter will use this to cancel _401_ login auth challenges when we don't want a redundant request to be made (since it just yields the same response) + - This is to facilitate working around the behavior in `NSURLSession` where an _HTTP 401_ response with `WWW-Authenticate` header will always be transparently be retried (even when unmodified yielding the identical _401_ response). + - An additionaly problem is that canceling the _401_ response will discard the response's body. If there is information needed in the response body, it will be lost. + - Twitter has updated its `WWW-Authenticate` header responses to include additional metadata since the response body cannot be used. + - See https://tools.ietf.org/html/rfc7235#section-4.1 + - Apple Feedback: `FB7697492` + +### 2.10.0 + +- Add retriable dependencies to `TNLRequestOperation` + - Whenever a `TNLRetryPolicyProvider` would yield a retry, that retry will be delayed by the longer of 2 things: + 1. The delay provided by the retry policy provider (minimum of 100 milliseconds) + 2. Waiting for all `dependencies` on the `TNLRequestOperation` to complete + - Normally, all `dependencies` of a retrying `TNLRequestOperation` will be complete before it has started but it is now possible to add dependencies after the request operation has already started to increase the dependencies on a retry starting. + +### 2.9.0 + +- Introduce `tnlcli`, a command-line-interface for __TNL__ + - Like _cURL_ but with __TNL__ features + - Verbose mode provides all __TNL__ logging to stdout / stderr along with lots of other details + ### 2.8.2 - Add _expensive_ and _constrained_ conditions to the reachability flags on iOS 13+ diff --git a/README.md b/README.md index b8ed6b8..f97bb88 100644 --- a/README.md +++ b/README.md @@ -508,9 +508,44 @@ APPRetrieveBlobRequest *request = [[APPRetrieveBlobRequest alloc] initWithBlobId // APISendMessageResponse's implementation for messageId is just: // return self.result[@"newMessageId"]; ``` + +## Using the Command-Line-Interface + +__Twitter Network Layer__ includes a target for building a _macOS_ tool called `tnlcli`. You can build this tool +run it from _Terminal_ from you _Mac_, similar to _cURL_ or other networking command line utilities. + +### Usage + +``` +Usage: tnlcli [options] url + + Example: tnlcli --request-method HEAD --response-header-mode file,print --response-header-file response_headers.json https://google.com + +Argument Options: +----------------- + + --request-config-file TNLRequestConfiguration as a json file + --request-headers-file json file of key-value-pairs for using as headers + --request-body-file file for the HTTP body + + --request-header "Field: Value" A header to provide with the request (will override the header if also in the request header file). Can provide multiple headers. + --request-config "config: value" A config setting for the TNLRequestConfiguration of the request (will override the config if also in the request config file). Can provide multiple configs. + --request-method HTTP Method from Section 9 in HTTP/1.1 spec (RFC 2616), such as GET, POST, HEAD, etc + + --response-body-mode "file" or "print" or a combo using commas + --response-body-file file for the response body to save to (requires "file" for --response-body-mode + --response-headers-mode "file" or "print" or a combo using commas + --response-headers-file file for the response headers to save to (as json) + + --dump-cert-chain-directory directory for the certification chain to be dumped to (as DER files) + + --verbose Will print verbose information and force the --response-body-mode and --responde-headers-mode to have "print". + --version Will print ther version information. +``` + # License -Copyright 2014-2018 Twitter, Inc. +Copyright 2014-2020 Twitter, Inc. Licensed under the Apache License, Version 2.0: https://www.apache.org/licenses/LICENSE-2.0 diff --git a/Source/NSCachedURLResponse+TNLAdditions.h b/Source/NSCachedURLResponse+TNLAdditions.h index 35a6038..c2d1131 100644 --- a/Source/NSCachedURLResponse+TNLAdditions.h +++ b/Source/NSCachedURLResponse+TNLAdditions.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 11/22/15. -// Copyright © 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/NSCachedURLResponse+TNLAdditions.m b/Source/NSCachedURLResponse+TNLAdditions.m index 687f39f..4f7000f 100644 --- a/Source/NSCachedURLResponse+TNLAdditions.m +++ b/Source/NSCachedURLResponse+TNLAdditions.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 11/22/15. -// Copyright © 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSCachedURLResponse+TNLAdditions.h" diff --git a/Source/NSCoder+TNLAdditions.h b/Source/NSCoder+TNLAdditions.h index 1ca6a44..7fa3693 100644 --- a/Source/NSCoder+TNLAdditions.h +++ b/Source/NSCoder+TNLAdditions.h @@ -2,8 +2,8 @@ // NSCoder+TNLAdditions.h // TwitterNetworkLayer // -// Created by Nolan on 6/5/19. -// Copyright © 2019 Twitter. All rights reserved. +// Created on 6/5/19. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/NSCoder+TNLAdditions.m b/Source/NSCoder+TNLAdditions.m index fc941a8..00a5bfb 100644 --- a/Source/NSCoder+TNLAdditions.m +++ b/Source/NSCoder+TNLAdditions.m @@ -2,8 +2,8 @@ // NSCoder+TNLAdditions.m // TwitterNetworkLayer // -// Created by Nolan on 6/5/19. -// Copyright © 2019 Twitter. All rights reserved. +// Created on 6/5/19. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSCoder+TNLAdditions.h" diff --git a/Source/NSData+TNLAdditions.h b/Source/NSData+TNLAdditions.h index e595592..a2af556 100644 --- a/Source/NSData+TNLAdditions.h +++ b/Source/NSData+TNLAdditions.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 9/9/16. -// Copyright © 2016 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/NSData+TNLAdditions.m b/Source/NSData+TNLAdditions.m index cbb88e8..e820c5a 100644 --- a/Source/NSData+TNLAdditions.m +++ b/Source/NSData+TNLAdditions.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 9/9/16. -// Copyright © 2016 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/NSDictionary+TNLAdditions.h b/Source/NSDictionary+TNLAdditions.h index 00c3494..ebff53f 100644 --- a/Source/NSDictionary+TNLAdditions.h +++ b/Source/NSDictionary+TNLAdditions.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/24/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/NSDictionary+TNLAdditions.m b/Source/NSDictionary+TNLAdditions.m index a517225..2a7c441 100644 --- a/Source/NSDictionary+TNLAdditions.m +++ b/Source/NSDictionary+TNLAdditions.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/24/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSDictionary+TNLAdditions.h" diff --git a/Source/NSHTTPCookieStorage+TNLAdditions.h b/Source/NSHTTPCookieStorage+TNLAdditions.h index 41a800b..691d286 100644 --- a/Source/NSHTTPCookieStorage+TNLAdditions.h +++ b/Source/NSHTTPCookieStorage+TNLAdditions.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 2/9/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/NSHTTPCookieStorage+TNLAdditions.m b/Source/NSHTTPCookieStorage+TNLAdditions.m index 2ea6737..8116ace 100644 --- a/Source/NSHTTPCookieStorage+TNLAdditions.m +++ b/Source/NSHTTPCookieStorage+TNLAdditions.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 2/9/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSHTTPCookieStorage+TNLAdditions.h" diff --git a/Source/NSNumber+TNLURLCoding.h b/Source/NSNumber+TNLURLCoding.h index 2ca7eac..ab0c136 100644 --- a/Source/NSNumber+TNLURLCoding.h +++ b/Source/NSNumber+TNLURLCoding.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 9/17/15. -// Copyright © 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import @@ -26,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN For convenience, `TNLBoolean` is provided so that the object can be encoded as `@"true"` or `@"false"` based on the `boolValue` of the object always, regardless of encoding format or options. */ -@interface NSNumber (TNLURLCoding) +@interface NSNumber (TNLBooleanCoding) /** Returns a `TNLBoolean` object that will encode as `@"true"` or `@"false"` based on the receiver's @@ -41,6 +41,27 @@ NS_ASSUME_NONNULL_BEGIN @end +/** + Category for converting numbers to strings + */ +@interface NSNumber (TNLStringCoding) + +/** + For most `NSNumber` instances, this is a faster way of getting the string value than `stringValue` + or `descriptionWithLocale:`. + Interally falls back to `descriptionWithLocale:` if it cannot convert (never encountered a case yet). + + Random sampling on 124,000 NSNumbers on 1 thread (via Xcode Simulator on 10-core 3GHz Xeon W) + -[NSNumber stringValue] = 1.149726s + -[NSNumber tnl_quickStringValue] = 0.775426s + Pretty consistently achieves 33% speedup on average. + Using `TNLURLEncodeDictionary` for converting a `TNLRequestConfiguration` into an identifier string + (very regular within TNL) has more than 60% speedup. + */ +- (NSString *)tnl_quickStringValue; + +@end + /** `TNLBoolean` is a convenience object in case it is desirable to have an object for URL encoding that is boolean and will yield either `@"true"` or `@"false"` always instead of conditionally based diff --git a/Source/NSNumber+TNLURLCoding.m b/Source/NSNumber+TNLURLCoding.m index 3204369..a79ed6a 100644 --- a/Source/NSNumber+TNLURLCoding.m +++ b/Source/NSNumber+TNLURLCoding.m @@ -3,14 +3,14 @@ // TwitterNetworkLayer // // Created on 9/17/15. -// Copyright © 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSNumber+TNLURLCoding.h" NS_ASSUME_NONNULL_BEGIN -@implementation NSNumber (TNLURLCoding) +@implementation NSNumber (TNLBooleanCoding) - (BOOL)tnl_isBoolean { diff --git a/Source/NSOperationQueue+TNLSafety.h b/Source/NSOperationQueue+TNLSafety.h index 03e4cb9..1a5142f 100644 --- a/Source/NSOperationQueue+TNLSafety.h +++ b/Source/NSOperationQueue+TNLSafety.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/14/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/NSOperationQueue+TNLSafety.m b/Source/NSOperationQueue+TNLSafety.m index 032c9a8..aefcf1d 100644 --- a/Source/NSOperationQueue+TNLSafety.m +++ b/Source/NSOperationQueue+TNLSafety.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/14/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSOperationQueue+TNLSafety.h" @@ -84,16 +84,18 @@ - (void)addOperation:(NSOperation *)op return; } - dispatch_async(_queue, ^{ + tnl_dispatch_async_autoreleasing(_queue, ^{ [self->_operations addObject:op]; [op addObserver:self forKeyPath:@"isFinished" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:NULL]; // There are race conditions where the isFinished KVO may never be observed. // Use this async check to weed out any early finishing operations that we didn't observe finishing. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(TNLOperationSafetyGuardCheckForAlreadyFinishedOperationDelay * NSEC_PER_SEC)), self->_queue, ^{ - if (op.isFinished) { - // Call our KVO observer to unify the code path for removing the observer - [self observeValueForKeyPath:@"isFinished" ofObject:op change:@{ NSKeyValueChangeNewKey : @YES } context:NULL]; + @autoreleasepool { + if (op.isFinished) { + // Call our KVO observer to unify the code path for removing the observer + [self observeValueForKeyPath:@"isFinished" ofObject:op change:@{ NSKeyValueChangeNewKey : @YES } context:NULL]; + } } }); }); @@ -119,7 +121,9 @@ - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable i if ([keyPath isEqualToString:@"isFinished"] && [change[NSKeyValueChangeNewKey] boolValue]) { NSOperation *op = object; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(TNLOperationSafetyGuardRemoveOperationAfterFinishedDelay * NSEC_PER_SEC)), _queue, ^{ - [self _tnl_background_removeOperation:op]; + @autoreleasepool { + [self _tnl_background_removeOperation:op]; + } }); } } diff --git a/Source/NSURL+TNLAdditions.h b/Source/NSURL+TNLAdditions.h index a7eca7f..25804e3 100644 --- a/Source/NSURL+TNLAdditions.h +++ b/Source/NSURL+TNLAdditions.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 12/20/16. -// Copyright © 2016 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/NSURL+TNLAdditions.m b/Source/NSURL+TNLAdditions.m index e102a16..574312c 100644 --- a/Source/NSURL+TNLAdditions.m +++ b/Source/NSURL+TNLAdditions.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 12/20/16. -// Copyright © 2016 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSURL+TNLAdditions.h" diff --git a/Source/NSURLAuthenticationChallenge+TNLAdditions.h b/Source/NSURLAuthenticationChallenge+TNLAdditions.h new file mode 100644 index 0000000..7db486a --- /dev/null +++ b/Source/NSURLAuthenticationChallenge+TNLAdditions.h @@ -0,0 +1,34 @@ +// +// NSURLAuthenticationChallenge+TNLAdditions.h +// TwitterNetworkLayer +// +// Created on 3/17/20. +// Copyright © 2020 Twitter. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +//! `NSURLAuthenticationMethodOAuth` which Apple uses but doesn't expose +FOUNDATION_EXTERN NSString * const TNLNSURLAuthenticationMethodOAuth; +//! `NSURLAuthenticationMethodOAuth2` which Apple uses but doesn't expose +FOUNDATION_EXTERN NSString * const TNLNSURLAuthenticationMethodOAuth2; + +//! Is the given challenge method a password challenge? +FOUNDATION_EXTERN BOOL TNLIsPasswordChallengeAuthenticationChallengeMethod(NSString * __nullable method); + +/** + __TNL__ additions for `NSURLAuthenticationChallenge` + */ +@interface NSURLAuthenticationChallenge (TNLAdditions) + +/** + @return `YES` if this challenge is an HTTP WWW Authenticate based challenge from an HTTP 401. + @note By default, __TNL__ will reject this kind of challenge's protection space if there is no `proposedCredential`, use `TNLAuthenticationChallengeHandler` to specify a different behavior if desired. This differs from `NSURLSession` default behavior! + */ +- (BOOL)tnl_isHTTPWWWAuthenticationChallenge; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/NSURLAuthenticationChallenge+TNLAdditions.m b/Source/NSURLAuthenticationChallenge+TNLAdditions.m new file mode 100644 index 0000000..d238471 --- /dev/null +++ b/Source/NSURLAuthenticationChallenge+TNLAdditions.m @@ -0,0 +1,64 @@ +// +// NSURLAuthenticationChallenge+TNLAdditions.m +// TwitterNetworkLayer +// +// Created on 3/17/20. +// Copyright © 2020 Twitter. All rights reserved. +// + +#import "NSURLAuthenticationChallenge+TNLAdditions.h" + +NSString * const TNLNSURLAuthenticationMethodOAuth = @"NSURLAuthenticationMethodOAuth"; +NSString * const TNLNSURLAuthenticationMethodOAuth2 = @"NSURLAuthenticationMethodOAuth2"; + +BOOL TNLIsPasswordChallengeAuthenticationChallengeMethod(NSString * __nullable method) +{ + return [method isEqualToString:NSURLAuthenticationMethodHTTPBasic] || + [method isEqualToString:TNLNSURLAuthenticationMethodOAuth] || + [method isEqualToString:TNLNSURLAuthenticationMethodOAuth2]; +} + +@implementation NSURLAuthenticationChallenge (TNLAdditions) + +- (BOOL)tnl_isHTTPWWWAuthenticationChallenge +{ + // Must be an HTTP response + NSHTTPURLResponse *failureResponse = (id)self.failureResponse; + if (![failureResponse isKindOfClass:[NSHTTPURLResponse class]]) { + return NO; + } + + // Must be an HTTP 401 + if (401 != failureResponse.statusCode) { + return NO; + } + + NSURLProtectionSpace *protectionSpace = self.protectionSpace; + + // Must be fore the `http` or `https` protocols + if (![protectionSpace.protocol isEqualToString:NSURLProtectionSpaceHTTPS] && ![protectionSpace.proxyType isEqualToString:NSURLProtectionSpaceHTTP]) { + return NO; + } + + // Uncomment to log WWW-Authenticate header for debugging +//#if DEBUG +// NSLog(@"WWW-Authenticate: %@", [failureResponse valueForHTTPHeaderField:@"WWW-Authenticate"]); +//#endif + + // Must have an auth `realm` + if (!protectionSpace.realm) { + return NO; + } + + NSString * method = protectionSpace.authenticationMethod; + + // Password auth challenge + if (TNLIsPasswordChallengeAuthenticationChallengeMethod(method)) { + return YES; + } + + // other HTTP challenge, maybe digest? + return NO; +} + +@end diff --git a/Source/NSURLCache+TNLAdditions.h b/Source/NSURLCache+TNLAdditions.h index 7bcb030..eaad43b 100644 --- a/Source/NSURLCache+TNLAdditions.h +++ b/Source/NSURLCache+TNLAdditions.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/12/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import @@ -31,7 +31,7 @@ NS_ASSUME_NONNULL_BEGIN This is useful for setting on a `TNLRequestConfiguration` so that if the configuration is reused for multiple `TNLRequestOperation` instances, the `NSURLCache` that will be used will be the `[NSURLCache sharedURLCache]` at the time the `TNLRequestOperation` run. This is in contrast to - having the `[TNLRequestConfiguration URLCached]` being set to the `[NSURLCache sharedURLCache]` + having the `[TNLRequestConfiguration URLCache]` being set to the `[NSURLCache sharedURLCache]` since that will not updated as the shared `NSURLCache` is updated. @return the shared `NSURLCache` proxy diff --git a/Source/NSURLCache+TNLAdditions.m b/Source/NSURLCache+TNLAdditions.m index 24f737d..f04e56a 100644 --- a/Source/NSURLCache+TNLAdditions.m +++ b/Source/NSURLCache+TNLAdditions.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/12/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #include diff --git a/Source/NSURLCredentialStorage+TNLAdditions.h b/Source/NSURLCredentialStorage+TNLAdditions.h index ad32718..3492183 100644 --- a/Source/NSURLCredentialStorage+TNLAdditions.h +++ b/Source/NSURLCredentialStorage+TNLAdditions.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 12/5/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/NSURLCredentialStorage+TNLAdditions.m b/Source/NSURLCredentialStorage+TNLAdditions.m index 91eaec8..34800b1 100644 --- a/Source/NSURLCredentialStorage+TNLAdditions.m +++ b/Source/NSURLCredentialStorage+TNLAdditions.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 12/5/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSURLCredentialStorage+TNLAdditions.h" diff --git a/Source/NSURLRequest+TNLAdditions.h b/Source/NSURLRequest+TNLAdditions.h index 5c46653..b1becd5 100644 --- a/Source/NSURLRequest+TNLAdditions.h +++ b/Source/NSURLRequest+TNLAdditions.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 11/9/16. -// Copyright © 2016 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/NSURLRequest+TNLAdditions.m b/Source/NSURLRequest+TNLAdditions.m index f641ce9..659d4c3 100644 --- a/Source/NSURLRequest+TNLAdditions.m +++ b/Source/NSURLRequest+TNLAdditions.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 11/9/16. -// Copyright © 2016 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSURL+TNLAdditions.h" diff --git a/Source/NSURLResponse+TNLAdditions.h b/Source/NSURLResponse+TNLAdditions.h index 8d12933..179f75f 100644 --- a/Source/NSURLResponse+TNLAdditions.h +++ b/Source/NSURLResponse+TNLAdditions.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 11/13/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import @@ -29,13 +29,20 @@ NS_ASSUME_NONNULL_BEGIN @interface NSHTTPURLResponse (TNLAdditions) /** - Convenience method for converting the `"Retry-After"` value into a delay. + Convenience method for converting the `"Retry-After"` value into a value. Returns `NSDate` if the string was for a date. Returns `NSNumber` wrapped `NSTimeInterval` (aka `double`) if the string was for a delay. Returns `nil` if the string could not be parsed. */ + (nullable id)tnl_parseRetryAfterValueFromString:(nullable NSString *)retryAfterValueString; +/** + Convenience method for converting the value of `"Retry-After"` into a delay. + Provide the `id` result from `tnl_parseRetryAfterValueFromString:`. + Returns `0` on `nil` or unexpected _retryAfterValue_. + */ ++ (NSTimeInterval)tnl_delayFromRetryAfterValue:(nullable id)retryAfterValue; + /** Calls `tnl_parseRetryAfterValueFromString:` with the `"Retry-After"` response header's value as the provided string. diff --git a/Source/NSURLResponse+TNLAdditions.m b/Source/NSURLResponse+TNLAdditions.m index 9b851c2..6ba4d88 100644 --- a/Source/NSURLResponse+TNLAdditions.m +++ b/Source/NSURLResponse+TNLAdditions.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 11/13/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #include @@ -80,6 +80,16 @@ + (nullable id)tnl_parseRetryAfterValueFromString:(nullable NSString *)retryAfte } } ++ (NSTimeInterval)tnl_delayFromRetryAfterValue:(nullable id)retryAfterValue +{ + if ([retryAfterValue isKindOfClass:[NSNumber class]]) { + return [(NSNumber *)retryAfterValue doubleValue]; + } else if ([retryAfterValue isKindOfClass:[NSDate class]]) { + return [(NSDate *)retryAfterValue timeIntervalSinceNow]; + } + return 0; +} + - (nullable id)tnl_parsedRetryAfterValue { NSString *retryAfter = self.allHeaderFields[@"Retry-After"]; diff --git a/Source/NSURLSessionConfiguration+TNLAdditions.h b/Source/NSURLSessionConfiguration+TNLAdditions.h index 806c67d..885a394 100644 --- a/Source/NSURLSessionConfiguration+TNLAdditions.h +++ b/Source/NSURLSessionConfiguration+TNLAdditions.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/12/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/NSURLSessionConfiguration+TNLAdditions.m b/Source/NSURLSessionConfiguration+TNLAdditions.m index 3bf6a2d..be29764 100644 --- a/Source/NSURLSessionConfiguration+TNLAdditions.m +++ b/Source/NSURLSessionConfiguration+TNLAdditions.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/12/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSURLSessionConfiguration+TNLAdditions.h" diff --git a/Source/NSURLSessionTaskMetrics+TNLAdditions.h b/Source/NSURLSessionTaskMetrics+TNLAdditions.h index 7f3a0fa..2fe76d9 100644 --- a/Source/NSURLSessionTaskMetrics+TNLAdditions.h +++ b/Source/NSURLSessionTaskMetrics+TNLAdditions.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 7/25/16. -// Copyright © 2016 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import @@ -37,6 +37,11 @@ NS_ASSUME_NONNULL_BEGIN */ - (NSDictionary *)tnl_dictionaryValue; +/** + A dictionary description of the transaction metrics that is serializable + */ +- (NSDictionary *)tnl_dictionaryDescription; + /** returns the `resourceFetchType` as a readable debug string */ diff --git a/Source/NSURLSessionTaskMetrics+TNLAdditions.m b/Source/NSURLSessionTaskMetrics+TNLAdditions.m index 5aa001b..e1ce1d0 100644 --- a/Source/NSURLSessionTaskMetrics+TNLAdditions.m +++ b/Source/NSURLSessionTaskMetrics+TNLAdditions.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 7/25/16. -// Copyright © 2016 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSURLSessionTaskMetrics+TNLAdditions.h" @@ -52,7 +52,17 @@ @implementation NSURLSessionTaskTransactionMetrics (TNLAdditions) - (NSDictionary *)tnl_dictionaryValue { - NSMutableDictionary *d = [[self tnl_medadata] mutableCopy]; + return [self _tnl_dictionaryValue:NO]; +} + +- (NSDictionary *)tnl_dictionaryDescription +{ + return [self _tnl_dictionaryValue:YES]; +} + +- (NSMutableDictionary *)_tnl_dictionaryValue:(BOOL)sanitizeForSerialization +{ + NSMutableDictionary *d = [self _tnl_metadata:sanitizeForSerialization]; #define APPLY_VALUE(key, value) \ do { \ @@ -72,7 +82,11 @@ @implementation NSURLSessionTaskTransactionMetrics (TNLAdditions) APPLY_VALUE(@"total", [self tnl_totalDuration]); d[@"statusCode"] = @([(NSHTTPURLResponse *)self.response statusCode]); - APPLY_VALUE(@"URL", self.request.URL); + if (sanitizeForSerialization) { + APPLY_VALUE(@"URL", self.request.URL.absoluteString); + } else { + APPLY_VALUE(@"URL", self.request.URL); + } #undef APPLY_VALUE @@ -80,6 +94,11 @@ @implementation NSURLSessionTaskTransactionMetrics (TNLAdditions) } - (NSDictionary *)tnl_medadata +{ + return [self _tnl_metadata:NO]; +} + +- (NSMutableDictionary *)_tnl_metadata:(BOOL)sanitizeForSerialization { NSMutableDictionary *d = [[NSMutableDictionary alloc] init]; @@ -97,7 +116,13 @@ @implementation NSURLSessionTaskTransactionMetrics (TNLAdditions) d[@"newConnection"] = @(!self.reusedConnection); d[@"proxy"] = @(self.proxyConnection); -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 +#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 +#define TNL_CAN_LOG_NEW_METRICS 1 +#elif TARGET_OS_OSX && __MAC_OS_X_VERSION_MAX_ALLOWED >= 101500 +#define TNL_CAN_LOG_NEW_METRICS 1 +#endif + +#if TNL_CAN_LOG_NEW_METRICS if (tnl_available_ios_13) { d[@"header_tx"] = @(self.countOfRequestHeaderBytesSent); d[@"body_tx"] = @(self.countOfRequestBodyBytesSent); diff --git a/Source/TNLAttemptMetaData.h b/Source/TNLAttemptMetaData.h index 387dfc8..45ae8d6 100644 --- a/Source/TNLAttemptMetaData.h +++ b/Source/TNLAttemptMetaData.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 1/16/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import @@ -19,6 +19,8 @@ NS_ASSUME_NONNULL_BEGIN @interface TNLAttemptMetaData : NSObject /** The meta data as a dictionary */ - (NSDictionary *)metaDataDictionary; +/** Meta data in a serializable format (can be lossy and drop info if not serializable!) */ +- (NSDictionary *)dictionaryDescription; @end /** diff --git a/Source/TNLAttemptMetaData.m b/Source/TNLAttemptMetaData.m index 6c62c59..9cc142b 100644 --- a/Source/TNLAttemptMetaData.m +++ b/Source/TNLAttemptMetaData.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 1/16/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLAttemptMetaData_Project.h" @@ -15,7 +15,7 @@ @interface TNLAttemptMetaData () { - NSDictionary *_metaDataDictionary; + NSDictionary *_metaDataDictionary; BOOL _final; } @end @@ -96,6 +96,28 @@ - (void)finalizeMetaData _metaDataDictionary = [_metaDataDictionary copy]; } +- (NSDictionary *)dictionaryDescription +{ + NSMutableDictionary *d = [[NSMutableDictionary alloc] initWithCapacity:_metaDataDictionary.count]; + [_metaDataDictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { + if ([obj isKindOfClass:[NSString class]] || [obj isKindOfClass:[NSNumber class]]) { + d[key] = obj; + return; + } + if ([obj isKindOfClass:[NSData class]]) { + const NSUInteger length = [(NSData *)obj length]; + if (length <= 256) { + d[key] = [(NSData *)obj base64EncodedStringWithOptions:0]; + } else { + d[key] = [NSString stringWithFormat:@"NSData: %lu bytes", (unsigned long)length]; + } + return; + } + // other types, just skip + }]; + return d; +} + @end // Helper macros for coding & printing object; saves us from key & value name typos. diff --git a/Source/TNLAttemptMetaData_Project.h b/Source/TNLAttemptMetaData_Project.h index 3136474..3310893 100644 --- a/Source/TNLAttemptMetaData_Project.h +++ b/Source/TNLAttemptMetaData_Project.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 1/15/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLAttemptMetaData.h" diff --git a/Source/TNLAttemptMetrics.h b/Source/TNLAttemptMetrics.h index 72f8c28..f2c067f 100644 --- a/Source/TNLAttemptMetrics.h +++ b/Source/TNLAttemptMetrics.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 1/15/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import @@ -141,6 +141,9 @@ static const NSInteger TNLAttemptCompleteDispositionCount = 3; /** calculate the duration of the attempt */ - (NSTimeInterval)duration; +/** description of the attempt metrics in a serializable dictionary */ +- (NSDictionary *)dictionaryDescription:(BOOL)verbose; + @end /** diff --git a/Source/TNLAttemptMetrics.m b/Source/TNLAttemptMetrics.m index 11e1188..1a160d5 100644 --- a/Source/TNLAttemptMetrics.m +++ b/Source/TNLAttemptMetrics.m @@ -3,10 +3,11 @@ // TwitterNetworkLayer // // Created on 1/15/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSCoder+TNLAdditions.h" +#import "NSURLSessionTaskMetrics+TNLAdditions.h" #import "TNL_Project.h" #import "TNLAttemptMetaData_Project.h" #import "TNLAttemptMetrics_Project.h" @@ -200,23 +201,6 @@ - (void)setCommunicationMetricsWithAgent:(nullable TNLCommunicationAgent *)agent } #endif -- (NSString *)description -{ - NSMutableString *string = [NSMutableString string]; - [string appendFormat:@"<%@ %p: type=%@, duration=%.2fs", NSStringFromClass([self class]), self, TNLAttemptTypeToString(self.attemptType), self.duration]; - if (self.URLResponse) { - [string appendFormat:@", HTTP=%ld", (long)self.URLResponse.statusCode]; - } - if (self.operationError) { - [string appendFormat:@", error=%@.%ld", self.operationError.domain, (long)self.operationError.code]; - } - if (self.metaData) { - [string appendFormat:@", metaData.class=%@", NSStringFromClass([self.metaData class])]; - } - [string appendString:@">"]; - return string; -} - - (NSTimeInterval)duration { const NSTimeInterval duration = [(_endDate ?: [NSDate date]) timeIntervalSinceDate:_startDate]; @@ -437,6 +421,90 @@ - (id)copyWithZone:(nullable NSZone *)zone return dupeSubmetric; } +#pragma mark Description + +- (NSDictionary *)dictionaryDescription:(BOOL)verbose +{ + NSMutableDictionary *attemptDict = [NSMutableDictionary dictionary]; + attemptDict[@"attemptInfo"] = @{ + @"id" : @(_attemptId), + @"type" : TNLAttemptTypeToString(_attemptType), + @"duration" : @(self.duration) + }; + +#define UP_D(d, key, val) \ +({ \ + id value__ = (val); \ + if (value__) { \ + (d)[(key)] = value__; \ + } \ +}) + + NSMutableDictionary *other = [[NSMutableDictionary alloc] init]; + attemptDict[@"other"] = other; + + UP_D(other, @"error", self.operationError.description); + if (_URLResponse) { + other[@"statusCode"] = @(_URLResponse.statusCode); + } + + if (verbose) { + UP_D(other, @"timeStart", TNLHTTPDateToString(self.startDate, TNLHTTPDateFormatAuto)); + UP_D(other, @"timeEnd", TNLHTTPDateToString(self.endDate, TNLHTTPDateFormatAuto)); + UP_D(other, @"apiErrors", self.APIErrors.firstObject.description); + UP_D(other, @"parseErrors", self.responseBodyParseError.description); +#if !TARGET_OS_WATCH + UP_D(other, @"reachStatus", TNLNetworkReachabilityStatusToString(_reachabilityStatus)); + UP_D(other, @"reachFlags", TNLDebugStringFromNetworkReachabilityFlags(_reachabilityFlags)); + UP_D(other, @"radio", self.WWANRadioAccessTechnology); + UP_D(other, @"captivePortalStatus", TNLCaptivePortalStatusToString(_captivePortalStatus)); + UP_D(other, @"carrierInfo", self.carrierInfo ? TNLCarrierInfoToDictionaryDescription(self.carrierInfo) : nil); +#endif + if (_URLRequest) { + attemptDict[@"zRequest"] = @{ + @"method" : _URLRequest.HTTPMethod ?: @"GET", + @"URL" : _URLRequest.URL.absoluteURL.absoluteString, + @"headers" : _URLRequest.allHTTPHeaderFields ?: @{}, + @"bodyLength" : _URLRequest.HTTPBody ? @(_URLRequest.HTTPBody.length) : [NSNull null], + }; + } + if (_URLResponse) { + attemptDict[@"zResponse"] = @{ + @"statusCode" : @(_URLResponse.statusCode), + @"URL" : _URLResponse.URL.absoluteString ?: [NSNull null], + @"headers" : _URLResponse.allHeaderFields ?: @{} + }; + } + if (_metaData) { + attemptDict[@"metaData"] = _metaData.dictionaryDescription; + } + NSDictionary *taskMetrics = self.taskTransactionMetrics.tnl_dictionaryDescription; + if (taskMetrics) { + attemptDict[@"transactionMetrics"] = taskMetrics; + } + } +#undef UP_D + + return attemptDict; +} + +- (NSString *)description +{ + NSMutableString *string = [NSMutableString string]; + [string appendFormat:@"<%@ %p: type=%@, duration=%.2fs", NSStringFromClass([self class]), self, TNLAttemptTypeToString(self.attemptType), self.duration]; + if (self.URLResponse) { + [string appendFormat:@", HTTP=%ld", (long)self.URLResponse.statusCode]; + } + if (self.operationError) { + [string appendFormat:@", error=%@.%ld", self.operationError.domain, (long)self.operationError.code]; + } + if (self.metaData) { + [string appendFormat:@", metaData.class=%@", NSStringFromClass([self.metaData class])]; + } + [string appendString:@">"]; + return string; +} + @end NS_ASSUME_NONNULL_END diff --git a/Source/TNLAttemptMetrics_Project.h b/Source/TNLAttemptMetrics_Project.h index d7c1828..418b468 100644 --- a/Source/TNLAttemptMetrics_Project.h +++ b/Source/TNLAttemptMetrics_Project.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 1/15/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLAttemptMetrics.h" diff --git a/Source/TNLAuthenticationChallengeHandler.h b/Source/TNLAuthenticationChallengeHandler.h index d899d44..1c70237 100644 --- a/Source/TNLAuthenticationChallengeHandler.h +++ b/Source/TNLAuthenticationChallengeHandler.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 4/10/18. -// Copyright © 2018 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import @@ -12,7 +12,8 @@ NS_ASSUME_NONNULL_BEGIN @class TNLRequestOperation; -typedef void(^TNLURLSessionAuthChallengeCompletionBlock)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential); +typedef void(^TNLURLSessionAuthChallengeCompletionBlock)(NSURLSessionAuthChallengeDisposition disposition, + id __nullable credentialOrCancelContext); /** Protocol for handling authentication challenges @@ -31,12 +32,13 @@ typedef void(^TNLURLSessionAuthChallengeCompletionBlock)(NSURLSessionAuthChallen See `NSURLSessionAuthChallengeDisposition` - typedef void(^TNLURLSessionAuthChallengeCompletionBlock)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential); + typedef void(^TNLURLSessionAuthChallengeCompletionBlock)(NSURLSessionAuthChallengeDisposition disposition, id credentialOrCancelContext); - _disposition_ - the way to handle the _challenge_ (default == `NSURLSessionAuthChallengePerformDefaultHandling`) - - _credential_ - - the credential to use in handling the _challenge_ + - _credentialOrCancelContext_ + - the credential to use in handling the _challenge_ for `NSURLSessionAuthChallengeUseCredential` + - the context to the error that will be yielded from `NSURLSessionAuthChallengeCancelAuthenticationChallenge` (using @param challenge The `NSURLAuthenticationChallenge` to respond to @param op The `TNLRequestOperation` that triggered the challenge, `nil` is a global challenge diff --git a/Source/TNLBackgroundURLSessionTaskOperationManager.h b/Source/TNLBackgroundURLSessionTaskOperationManager.h index 6bc0986..89d2d01 100644 --- a/Source/TNLBackgroundURLSessionTaskOperationManager.h +++ b/Source/TNLBackgroundURLSessionTaskOperationManager.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/6/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLBackgroundURLSessionTaskOperationManager.m b/Source/TNLBackgroundURLSessionTaskOperationManager.m index 9f2c29e..e5e4eef 100644 --- a/Source/TNLBackgroundURLSessionTaskOperationManager.m +++ b/Source/TNLBackgroundURLSessionTaskOperationManager.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/6/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLBackoff.h b/Source/TNLBackoff.h new file mode 100644 index 0000000..f542814 --- /dev/null +++ b/Source/TNLBackoff.h @@ -0,0 +1,150 @@ +// +// TNLBackoff.h +// TwitterNetworkLayer +// +// Created on 3/31/20. +// Copyright © 2020 Twitter. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Constants + +//! Default "Backoff" value for the `TNLSimpleBackoffBehaviorProvider` +FOUNDATION_EXTERN const NSTimeInterval TNLSimpleRetryAfterBackoffValueDefault; +//! Minimum "Backoff" value for the `TNLSimpleBackoffBehaviorProvider` +FOUNDATION_EXTERN const NSTimeInterval TNLSimpleRetryAfterBackoffValueMinimum; +//! Maximum "Backoff" value before reverting to use the `Default` value for the `TNLSimpleBackoffBehaviorProvider` +FOUNDATION_EXTERN const NSTimeInterval TNLSimpleRetryAfterMaximumBackoffValueBeforeTreatedAsGoAway; + +#pragma mark Structs + +//! `TNLBackoffBehavior` struct to encapsulate the settings that control "Backoff" behavior +typedef struct TNLBackoffBehavior_T { + /** + The "Backoff" duration... + how long all future enqueued matching requests will wait before any fire (based on the `TNLGlobalConfigurationBackoffMode`). + + If `0.0` or less, no "backoff" will happen. + */ + NSTimeInterval backoffDuration; + /** + The "Serialize Requests" duration... + how long requests being enqueued will continue to execute serially after the backoff signal was + encountered (based on the `TNLGlobalConfigurationBackoffMode`). + + Once this duration expires (or if `<= 0.0`), + serialization of requests will continue as long as there are requests in the queue that were serialized + (which will continue to enqueue serially while there is an outstanding "backoff" duration running). + Requests being sent after both "Serialize Requests" and "Backoff" durations have expired will + wait for the existing serially enqueued requests to complete before executing concurrently. + + Default == `0.0` + */ + NSTimeInterval serializeDuration; + /** + The minimum amount of time to elapse between the _start_ of each serial request. + This does not indicate the duration between serial requests...that will vary based on the duration of any given request's duration. + + Default == `0.0` + */ + NSTimeInterval serialDelayDuration; +} TNLBackoffBehavior; + +//! Make a `TNLBackoffBehavior` +NS_INLINE TNLBackoffBehavior TNLBackoffBehaviorMake(NSTimeInterval backoff, + NSTimeInterval serialize, + NSTimeInterval delay) +{ + TNLBackoffBehavior behavior; + behavior.backoffDuration = backoff; + behavior.serializeDuration = serialize; + behavior.serialDelayDuration = delay; + return behavior; +} + +//! Make a disabled `TNLBackoffBehavior` +#define TNLBackoffBehaviorDisabled() TNLBackoffBehaviorMake(0, 0, 0) + +#pragma mark Protocols + +/** + The `TNLBackoffBehaviorProvider` protocol provides the callback for selecting the + `TNLBackoffBehavior` for an encountered backoff signal. + + Due to opaque nature of signaling backoffs, only the _URL_ and _headers_ are provided. + */ +@protocol TNLBackoffBehaviorProvider + +/** + The callback to provide the backoff behavior for a `503` _Service Unavailable_ signal. + @param URL the `NSURL` of the request that raised the signal. + @param headers the HTTP response headers of the request that raised the signal. + @return the `TNLBackoffBehavior` for how to backoff (or not). + */ +- (TNLBackoffBehavior)tnl_backoffBehaviorForURL:(NSURL *)URL + responseHeaders:(nullable NSDictionary *)headers; + +@end + +/** + The `TNLBackoffSignaler` protocol provides an abstraction point for deciding if a backoff signal should be raised or not. + */ +@protocol TNLBackoffSignaler + +/** + The callback to check if the given response information should signal a backoff. + @param URL the `NSURL` of the request that might raise the signal. + @param host the (optional) host override for the request that might raise the signal. + @param statusCode the `TNLHTTPStatusCode` of the response that might raise the signal. + @param responseHeaders the HTTP headers of the response that might raise the signal. + @return `YES` if backoff signal should be raised, `NO` otherwise. + */ +- (BOOL)tnl_shouldSignalBackoffForURL:(NSURL *)URL + host:(nullable NSString *)host + statusCode:(TNLHTTPStatusCode)statusCode + responseHeaders:(nullable NSDictionary *)responseHeaders; + +@end + +#pragma mark Simple Concrete Classes (Default Implementations) + +/** + A simple `TNLBackoffBehaviorProvider` implemenation. + + The behavior's `backoffDuration` will be set to the `"Retry-After"` duration + (computed from the value either being a date or a duration in seconds). + If no `"Retry-After"` header is present, `TNLSimpleRetryAfterBackoffValueDefault` will be used. + If the duration would be greater than `TNLSimpleRetryAfterMaximumBackoffValueBeforeTreatedAsGoAway`, + then `TNLSimpleRetryAfterBackoffValueDefault` will be used. + The duration will have a minimum value of `TNLSimpleRetryAfterBackoffValueMinimum`. + + The default **TNL** behavior provider is an instance of this class with `serializeDuration` set to `0.0`. + */ +@interface TNLSimpleBackoffBehaviorProvider : NSObject + +/** + The `serializeDuration` for any `TNLBackoffBehavior` this provider would return. + Default value == `0.0` + */ +@property (atomic) NSTimeInterval serializeDuration; + +/** + The `serialDelayDuration` for any `TNLBackoffBehavior` this provider would return. + Default value == `0.0` + */ +@property (atomic) NSTimeInterval serialDelayDuration; + +@end + +/** + A simple `TNLBackoffSignaler` implementation. + + Simply triggers the backoff signal if the _statusCode_ is `503` _Service Unavailable_. + */ +@interface TNLSimpleBackoffSignaler : NSObject +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/TNLBackoff.m b/Source/TNLBackoff.m new file mode 100644 index 0000000..fbfa38f --- /dev/null +++ b/Source/TNLBackoff.m @@ -0,0 +1,55 @@ +// +// TNLBackoff.m +// TwitterNetworkLayer +// +// Created on 3/31/20. +// Copyright © 2020 Twitter. All rights reserved. +// + +#import "NSDictionary+TNLAdditions.h" +#import "NSURLResponse+TNLAdditions.h" +#import "TNLBackoff.h" + +NS_ASSUME_NONNULL_BEGIN + +const NSTimeInterval TNLSimpleRetryAfterBackoffValueDefault = 1.0; +const NSTimeInterval TNLSimpleRetryAfterBackoffValueMinimum = 0.1; +const NSTimeInterval TNLSimpleRetryAfterMaximumBackoffValueBeforeTreatedAsGoAway = 10.0; + +@implementation TNLSimpleBackoffBehaviorProvider + +- (TNLBackoffBehavior)tnl_backoffBehaviorForURL:(NSURL *)URL + responseHeaders:(nullable NSDictionary *)headers +{ + NSTimeInterval backoff = TNLSimpleRetryAfterBackoffValueDefault; + + NSString *retryAfterString = [headers tnl_objectsForCaseInsensitiveKey:@"retry-after"].firstObject; + id retryAfterValue = [NSHTTPURLResponse tnl_parseRetryAfterValueFromString:retryAfterString]; + if (retryAfterValue) { + backoff = [NSHTTPURLResponse tnl_delayFromRetryAfterValue:retryAfterValue]; + } + + if (backoff < TNLSimpleRetryAfterBackoffValueMinimum) { + backoff = TNLSimpleRetryAfterBackoffValueMinimum; + } else if (backoff > TNLSimpleRetryAfterMaximumBackoffValueBeforeTreatedAsGoAway) { + backoff = TNLSimpleRetryAfterBackoffValueDefault; + } + + return TNLBackoffBehaviorMake(backoff, self.serializeDuration, self.serialDelayDuration); +} + +@end + +@implementation TNLSimpleBackoffSignaler + +- (BOOL)tnl_shouldSignalBackoffForURL:(NSURL *)URL + host:(nullable NSString *)host + statusCode:(TNLHTTPStatusCode)statusCode + responseHeaders:(nullable NSDictionary *)responseHeaders +{ + return TNLHTTPStatusCodeServiceUnavailable == statusCode; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/TNLCommunicationAgent.h b/Source/TNLCommunicationAgent.h index b438911..ca60d61 100644 --- a/Source/TNLCommunicationAgent.h +++ b/Source/TNLCommunicationAgent.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/2/16. -// Copyright © 2016 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import @@ -55,10 +55,13 @@ typedef NS_ENUM(NSInteger, TNLNetworkReachabilityStatus) TNLNetworkReachabilityUndetermined = -1, /** Unreachable */ TNLNetworkReachabilityNotReachable = 0, - /** reachable via 802.11 WiFi */ - TNLNetworkReachabilityReachableViaWiFi = 1, + /** reachable via 802.3 Ethernet or 802.11 WiFi */ + TNLNetworkReachabilityReachableViaEthernet = 1, /** reachabile via WWAN (cellular) */ TNLNetworkReachabilityReachableViaWWAN = 2, + + /** deprecated, use `TNLNetworkReachabilityReachableViaEthernet` instead */ + TNLNetworkReachabilityReachableViaWiFi __attribute__((deprecated)) = TNLNetworkReachabilityReachableViaEthernet, }; //! Convert a `TNLNetworkReachabilityStatus` to an `NSString` @@ -201,6 +204,9 @@ typedef void(^TNLCommunicationAgentIdentifyCaptivePortalStatusCallback)(TNLCapti /** the network host for the agent */ @property (nonatomic, copy, readonly) NSString *host; +/** if there is a cellular interface or not */ +@property (class, nonatomic, readonly) BOOL hasCellularInterface; + /** designated initializer */ - (instancetype)initWithInternetReachabilityHost:(NSString *)host NS_DESIGNATED_INITIALIZER; @@ -353,6 +359,9 @@ typedef void(^TNLCommunicationAgentIdentifyCaptivePortalStatusCallback)(TNLCapti @end +//! Convert a `TNLCarrierInfo` into a serializable dictionary, will contain `[NSNull null]` for `nil` properties +FOUNDATION_EXTERN NSDictionary *TNLCarrierInfoToDictionaryDescription(id carrierInfo); + NS_ASSUME_NONNULL_END #endif // !TARGET_OS_WATCH diff --git a/Source/TNLCommunicationAgent.m b/Source/TNLCommunicationAgent.m index 6d51e30..b18f97a 100644 --- a/Source/TNLCommunicationAgent.m +++ b/Source/TNLCommunicationAgent.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/2/16. -// Copyright © 2016 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #include @@ -32,6 +32,7 @@ static void _ReachabilityCallback(__unused SCNetworkReachabilityRef target, void* info); static TNLNetworkReachabilityStatus _NetworkReachabilityStatusFromFlags(TNLNetworkReachabilityFlags flags) __attribute__((const)); static TNLNetworkReachabilityFlags _NetworkReachabilityFlagsFromPath(nw_path_t path); +static BOOL _HasCellularInterface(void); #define _NWPathStatusToFlag(status) ((status > 0) ? ((uint32_t)1 << (uint32_t)((status) - 1)) : 0) #define _NWInterfaceTypeToFlag(itype) ((uint32_t)1 << (uint32_t)8 << (uint32_t)(itype)) @@ -136,6 +137,16 @@ @implementation TNLCommunicationAgent } _flags; } ++ (BOOL)hasCellularInterface +{ + static BOOL sHasCellular = NO; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sHasCellular = _HasCellularInterface(); + }); + return sHasCellular; +} + - (instancetype)initWithInternetReachabilityHost:(NSString *)host { TNLAssert(host != nil); @@ -157,9 +168,7 @@ - (instancetype)initWithInternetReachabilityHost:(NSString *)host _agentOperationQueue.name = @"TNLCommunicationAgent.queue"; _agentOperationQueue.maxConcurrentOperationCount = 1; _agentOperationQueue.underlyingQueue = _agentQueue; - if ([_agentOperationQueue respondsToSelector:@selector(setQualityOfService:)]) { - _agentOperationQueue.qualityOfService = NSQualityOfServiceUtility; - } + _agentOperationQueue.qualityOfService = NSQualityOfServiceUtility; _agentWrapper = [[TNLCommunicationAgentWeakWrapper alloc] init]; _agentWrapper.communicationAgent = self; @@ -204,7 +213,7 @@ - (void)dealloc dispatch_queue_t agentQueue = _agentQueue; TNLCommunicationAgentWeakWrapper *weakWrapper = _agentWrapper; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), agentQueue, ^{ - dispatch_async(agentQueue, ^{ + tnl_dispatch_async_autoreleasing(agentQueue, ^{ weakWrapper.communicationAgent = nil; }); }); @@ -212,42 +221,42 @@ - (void)dealloc - (void)addObserver:(id)observer { - dispatch_async(_agentQueue, ^{ + tnl_dispatch_async_autoreleasing(_agentQueue, ^{ _agent_addObserver(self, observer); }); } - (void)removeObserver:(id)observer { - dispatch_async(_agentQueue, ^{ + tnl_dispatch_async_autoreleasing(_agentQueue, ^{ _agent_removeObserver(self, observer); }); } - (void)identifyReachability:(TNLCommunicationAgentIdentifyReachabilityCallback)callback { - dispatch_async(_agentQueue, ^{ + tnl_dispatch_async_autoreleasing(_agentQueue, ^{ _agent_identifyReachability(self, callback); }); } - (void)identifyCarrierInfo:(TNLCommunicationAgentIdentifyCarrierInfoCallback)callback { - dispatch_async(_agentQueue, ^{ + tnl_dispatch_async_autoreleasing(_agentQueue, ^{ _agent_identifyCarrierInfo(self, callback); }); } - (void)identifyWWANRadioAccessTechnology:(TNLCommunicationAgentIdentifyWWANRadioAccessTechnologyCallback)callback { - dispatch_async(_agentQueue, ^{ + tnl_dispatch_async_autoreleasing(_agentQueue, ^{ _agent_identifyWWANRadioAccessTechnology(self, callback); }); } - (void)identifyCaptivePortalStatus:(TNLCommunicationAgentIdentifyCaptivePortalStatusCallback)callback { - dispatch_async(_agentQueue, ^{ + tnl_dispatch_async_autoreleasing(_agentQueue, ^{ _agent_identifyCaptivePortalStatus(self, callback); }); } @@ -600,7 +609,9 @@ static void _agent_startCaptivePortalCheckTimer(SELF_ARG, NSTimeInterval delay) __weak typeof(self) weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), self->_agentQueue, ^{ - _agent_triggerCaptivePortalCheckIfNeeded(weakSelf); + @autoreleasepool { + _agent_triggerCaptivePortalCheckIfNeeded(weakSelf); + } }); } @@ -907,13 +918,24 @@ static TNLNetworkReachabilityStatus _NetworkReachabilityStatusFromFlags(TNLNetwo } if ((mask & TNLNetworkReachabilityMaskPathIntefaceTypeWifi) != 0) { - return TNLNetworkReachabilityReachableViaWiFi; + return TNLNetworkReachabilityReachableViaEthernet; + } + + if ((mask & TNLNetworkReachabilityMaskPathIntefaceTypeWired) != 0) { + return TNLNetworkReachabilityReachableViaEthernet; } if ((mask & TNLNetworkReachabilityMaskPathIntefaceTypeCellular) != 0) { return TNLNetworkReachabilityReachableViaWWAN; } + // "Other" happens when using VPN or other tunneling protocol. + // On iOS/tvOS/watchOS devices: WiFi or Wired or Cellular would have been hit above. + // On Mac and iOS Simulator: WiFi or Wired will _NOT_ be provide in the flags so, even though we coerce to WiFi in this case on Mac, presume Ethernet if we get here. + if ((mask & TNLNetworkReachabilityMaskPathIntefaceTypeOther) != 0) { + return TNLNetworkReachabilityReachableViaEthernet; + } + return TNLNetworkReachabilityUndetermined; } @@ -928,15 +950,15 @@ static TNLNetworkReachabilityStatus _NetworkReachabilityStatusFromFlags(TNLNetwo #endif if ((flags & kSCNetworkReachabilityFlagsConnectionRequired) == 0) { - return TNLNetworkReachabilityReachableViaWiFi; + return TNLNetworkReachabilityReachableViaEthernet; } if ((flags & kSCNetworkReachabilityFlagsInterventionRequired) == 0) { if ((flags & kSCNetworkReachabilityFlagsConnectionOnDemand) != 0) { - return TNLNetworkReachabilityReachableViaWiFi; + return TNLNetworkReachabilityReachableViaEthernet; } if ((flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0) { - return TNLNetworkReachabilityReachableViaWiFi; + return TNLNetworkReachabilityReachableViaEthernet; } } @@ -974,6 +996,17 @@ static TNLNetworkReachabilityFlags _NetworkReachabilityFlagsFromPath(nw_path_t p } } +#if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST || TARGET_OS_OSX + // When run on macOS (however the avenue) we will coerce + // to have an ethernet connection when we detect `Other` but no actual interface. + // This is most commonly due to VPN connections "hiding" the physical interface on Macs. + if (flags & TNLNetworkReachabilityMaskPathIntefaceTypeOther) { + if (TNL_BITMASK_EXCLUDES_FLAGS(flags, TNLNetworkReachabilityMaskPathIntefaceTypeWifi | TNLNetworkReachabilityMaskPathIntefaceTypeCellular | TNLNetworkReachabilityMaskPathIntefaceTypeWired)) { + flags |= TNLNetworkReachabilityMaskPathIntefaceTypeWifi; + } + } +#endif + if (tnl_available_ios_13) { if (nw_path_is_expensive(path)) { flags |= TNLNetworkReachabilityMaskPathConditionExpensive; @@ -1092,7 +1125,7 @@ TNLWWANRadioAccessGeneration TNLWWANRadioAccessGenerationForTechnologyValue(TNLW switch (status) { case TNLNetworkReachabilityNotReachable: return @"unreachable"; - case TNLNetworkReachabilityReachableViaWiFi: + case TNLNetworkReachabilityReachableViaEthernet: return @"wifi"; case TNLNetworkReachabilityReachableViaWWAN: return @"wwan"; @@ -1206,4 +1239,40 @@ NS_INLINE const char _DebugCharFromReachabilityFlag(TNLNetworkReachabilityFlags ]; } +NSDictionary *TNLCarrierInfoToDictionaryDescription(id carrierInfo) +{ + return @{ + @"carrierName" : carrierInfo.carrierName ?: [NSNull null], + @"mobileCountryCode" : carrierInfo.mobileCountryCode ?: [NSNull null], + @"mobileNetworkCode" : carrierInfo.mobileNetworkCode ?: [NSNull null], + @"isoCountryCode" : carrierInfo.isoCountryCode ?: [NSNull null], + @"allowsVOIP" : @(carrierInfo.allowsVOIP) + }; +} + +#import + +static BOOL _HasCellularInterface() +{ + struct ifaddrs * addrs; + if (getifaddrs(&addrs) != 0) { + return NO; + } + + tnl_defer(^{ + freeifaddrs(addrs); + }); + + for (const struct ifaddrs * cursor = addrs; cursor != NULL; cursor = cursor->ifa_next) { + NSString *name = @(cursor->ifa_name); + if ([name isEqualToString:@"pdp_ip0"]) { + // All cellular interfaces are `pdp_ip`. + // There can be multiple, but the first one will always be number `0`. + return YES; + } + } + + return NO; +} + #endif // !TARGET_OS_WATCH diff --git a/Source/TNLCommunicationAgent_Project.h b/Source/TNLCommunicationAgent_Project.h index ebb7881..d2b7243 100644 --- a/Source/TNLCommunicationAgent_Project.h +++ b/Source/TNLCommunicationAgent_Project.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 03/29/2018. -// Copyright © 2018 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // diff --git a/Source/TNLContentCoding.h b/Source/TNLContentCoding.h index 4e95523..1eae045 100644 --- a/Source/TNLContentCoding.h +++ b/Source/TNLContentCoding.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 11/19/16. -// Copyright © 2016 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLError.h b/Source/TNLError.h index ca19b20..2c73e2c 100644 --- a/Source/TNLError.h +++ b/Source/TNLError.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 7/17/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import @@ -21,9 +21,12 @@ FOUNDATION_EXTERN NSString * const TNLErrorCancelSourceLocalizedDescriptionKey; FOUNDATION_EXTERN NSString * const TNLErrorCodeStringKey; FOUNDATION_EXTERN NSString * const TNLErrorHostKey; FOUNDATION_EXTERN NSString * const TNLErrorRequestKey; +FOUNDATION_EXTERN NSString * const TNLErrorResponseKey; FOUNDATION_EXTERN NSString * const TNLErrorProtectionSpaceHostKey; FOUNDATION_EXTERN NSString * const TNLErrorCertificateChainDescriptionsKey; FOUNDATION_EXTERN NSString * const TNLErrorAuthenticationChallengeMethodKey; +FOUNDATION_EXTERN NSString * const TNLErrorAuthenticationChallengeRealmKey; +FOUNDATION_EXTERN NSString * const TNLErrorAuthenticationChallengeCancelContextKey; #define TNLErrorCodePageSize (100) diff --git a/Source/TNLError.m b/Source/TNLError.m index 863af20..5b98555 100644 --- a/Source/TNLError.m +++ b/Source/TNLError.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 7/17/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNL_Project.h" @@ -22,9 +22,12 @@ NSString * const TNLErrorCodeStringKey = @"TNLError.string"; NSString * const TNLErrorHostKey = @"host"; NSString * const TNLErrorRequestKey = @"request"; +NSString * const TNLErrorResponseKey = @"response"; NSString * const TNLErrorProtectionSpaceHostKey = @"protectionSpaceHost"; NSString * const TNLErrorCertificateChainDescriptionsKey = @"certificateChainDescriptions"; -NSString * const TNLErrorAuthenticationChallengeMethodKey = @"authenticationChallengeMethod"; +NSString * const TNLErrorAuthenticationChallengeMethodKey = @"authChallengeMethod"; +NSString * const TNLErrorAuthenticationChallengeRealmKey = @"authChallengeRealm"; +NSString * const TNLErrorAuthenticationChallengeCancelContextKey = @"authChallengeCancelContext"; NSString *TNLErrorCodeToString(TNLErrorCode code) { diff --git a/Source/TNLGlobalConfiguration.h b/Source/TNLGlobalConfiguration.h index 963bcd7..262fc74 100644 --- a/Source/TNLGlobalConfiguration.h +++ b/Source/TNLGlobalConfiguration.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 11/21/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import @@ -15,6 +15,8 @@ NS_ASSUME_NONNULL_BEGIN @protocol TNLHostSanitizer; @protocol TNLLogger; @protocol TNLNetworkObserver; +@protocol TNLBackoffBehaviorProvider; +@protocol TNLBackoffSignaler; @class TNLRequestConfiguration; @class TNLRequestOperation; @@ -45,28 +47,27 @@ typedef NS_ENUM(NSInteger, TNLGlobalConfigurationIdleTimeoutMode) }; /** -`TNLGlobalConfigurationServiceUnavailableBackoffMode` enumerates the modes for how to handle - service unavailable (503) responses with backoff behavior. The backoff behavior is to observe the - `Retry-After` of the last matching request receiving a service unavailable response (503) before - executing backed off requests one at a time until either A) all requests are flushed or B) another - 503 is encountered restarting the backoff. +`TNLGlobalConfigurationBackoffMode` enumerates the modes for how to handle + responses that signal for backoff with a backoff behavior. - https://docs.google.com/document/d/1Gs3P0aSuEYMjvCfKkolRseS4Fnm3p%5FVDQwbu4LNCRKA/edit?usp=sharing + The backoff behavior is defined by a `TNLBackoffBehavior` which is provided + per backoff signaling response via a `TNLBackoffBehaviorProvider` set on the + `TNLGlobalConfiguration.backoffBehaviorProvider`. */ -typedef NS_ENUM(NSInteger, TNLGlobalConfigurationServiceUnavailableBackoffMode) +typedef NS_ENUM(NSInteger, TNLGlobalConfigurationBackoffMode) { /** Don't automatically back off when 503s are encountered */ - TNLGlobalConfigurationServiceUnavailableBackoffModeDisabled = 0, + TNLGlobalConfigurationBackoffModeDisabled = 0, /** Automatically back off requests when the `host` matches prior requests that saw a 503. */ - TNLGlobalConfigurationServiceUnavailableBackoffModeKeyOffHost = 1, + TNLGlobalConfigurationBackoffModeKeyOffHost = 1, /** Automatically back off requests when the `host` and `path` match prior requests that saw a 503. */ - TNLGlobalConfigurationServiceUnavailableBackoffModeKeyOffHostAndPath = 2, + TNLGlobalConfigurationBackoffModeKeyOffHostAndPath = 2, }; /** @@ -216,11 +217,32 @@ FOUNDATION_EXTERN NSTimeInterval const TNLGlobalConfigurationURLSessionInactivit @property (nonatomic) TNLPriority operationAutomaticDependencyPriorityThreshold; /** - The backoff mode when a 503 is encountered. + The backoff mode when a backoff signal is encountered. - Default == `TNLGlobalConfigurationServiceUnavailableBackoffModeDisabled` + Default == `TNLGlobalConfigurationBackoffModeDisabled` */ -@property (atomic) TNLGlobalConfigurationServiceUnavailableBackoffMode serviceUnavailableBackoffMode; +@property (atomic) TNLGlobalConfigurationBackoffMode backoffMode; + +/** + The backoff behavior provider when a backoff signal is encountered and `backoffMode` is not `Disabled`. + Setting to `nil` will reset to an instance of `TNLSimpleBackoffBehaviorProvider`. + Default == an instance of `TNLSimpleBackoffBehaviorProvider`. + */ +@property (atomic, null_resettable) id backoffBehaviorProvider; + +/** + The backoff signaler for when an HTTP response should trigger a backoff signal. + Setting to `nil` will reset to an instance of `TNLSimpleBackoffSignaler`. + Default == an instance of `TNLSimpleBackoffSignaler` + */ +@property (atomic, null_resettable) id backoffSignaler; + +/** + Whether the backoff behavior should use the original request's host or the hydrated request's host. + + Default == `NO` (don't use original request's host, use the hydrated request's host) + */ +@property (atomic) BOOL shouldBackoffUseOriginalRequestHost; #pragma mark Pruning inactive NSURLSession instances @@ -302,8 +324,8 @@ FOUNDATION_EXTERN NSTimeInterval const TNLGlobalConfigurationURLSessionInactivit Configure the how "data" timeouts behave within *TNL*. Zero or negative disables the timeout. Default == `0.0` + TODO: have this be on the request configuration! */ -// TODO: have this be on the configuration! @property (nonatomic, readwrite) NSTimeInterval timeoutIntervalBetweenDataTransfer; #pragma mark Runtime Config diff --git a/Source/TNLGlobalConfiguration.m b/Source/TNLGlobalConfiguration.m index 89d8ad1..18fbecf 100644 --- a/Source/TNLGlobalConfiguration.m +++ b/Source/TNLGlobalConfiguration.m @@ -3,10 +3,11 @@ // TwitterNetworkLayer // // Created on 11/21/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNL_Project.h" +#import "TNLBackoff.h" #import "TNLGlobalConfiguration_Project.h" #import "TNLLogger.h" #import "TNLRequestOperationQueue_Project.h" @@ -38,6 +39,7 @@ @implementation TNLGlobalConfiguration NSMutableDictionary *_runningBackgroundTasks; dispatch_queue_t _backgroundTaskQueue; NSArray> *_authHandlers; + id _backoffSignaler; #if TARGET_OS_IOS || TARGET_OS_TV UIBackgroundTaskIdentifier _sharedUIApplicationBackgroundTaskIdentifier; @@ -77,6 +79,7 @@ - (instancetype)initInternal _timeoutIntervalBetweenDataTransfer = 0.0; _operationAutomaticDependencyPriorityThreshold = (TNLPriority)NSIntegerMax; _internalURLSessionInactivityThreshold = TNLGlobalConfigurationURLSessionInactivityThresholdDefault; + _backoffSignaler = [[TNLSimpleBackoffSignaler alloc] init]; #if TARGET_OS_IOS || TARGET_OS_TV _sharedUIApplicationBackgroundTaskIdentifier = 0; @@ -116,7 +119,7 @@ - (instancetype)initInternal _lastApplicationState = sharedUIApplication.applicationState; } else { _lastApplicationState = UIApplicationStateInactive; - dispatch_async(dispatch_get_main_queue(), ^{ + tnl_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{ self.lastApplicationState = sharedUIApplication.applicationState; }); } @@ -214,14 +217,53 @@ - (void)removeHeaderProvider:(id)provider return [TNLRequestOperationQueue allGlobalHeaderProviders]; } -- (TNLGlobalConfigurationServiceUnavailableBackoffMode)serviceUnavailableBackoffMode +- (TNLGlobalConfigurationBackoffMode)backoffMode { - return [TNLURLSessionManager sharedInstance].serviceUnavailableBackoffMode; + return [TNLURLSessionManager sharedInstance].backoffMode; } -- (void)setServiceUnavailableBackoffMode:(TNLGlobalConfigurationServiceUnavailableBackoffMode)mode +- (void)setBackoffMode:(TNLGlobalConfigurationBackoffMode)mode { - [TNLURLSessionManager sharedInstance].serviceUnavailableBackoffMode = mode; + [TNLURLSessionManager sharedInstance].backoffMode = mode; +} + +- (id)backoffBehaviorProvider +{ + return [TNLURLSessionManager sharedInstance].backoffBehaviorProvider; +} + +- (void)setBackoffBehaviorProvider:(nullable id)provider +{ + [TNLURLSessionManager sharedInstance].backoffBehaviorProvider = provider; +} + +- (id)backoffSignaler +{ + if (dispatch_queue_get_label(tnl_network_queue()) == dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)) { + return _backoffSignaler; + } + + __block id signaler = nil; + dispatch_sync(tnl_network_queue(), ^{ + signaler = self->_backoffSignaler; + }); + return signaler; +} + +- (void)setBackoffSignaler:(nullable id)backoffSignaler +{ + if (!backoffSignaler) { + backoffSignaler = [[TNLSimpleBackoffSignaler alloc] init]; + } + + if (dispatch_queue_get_label(tnl_network_queue()) == dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)) { + _backoffSignaler = backoffSignaler; + return; + } + + dispatch_async(tnl_network_queue(), ^{ + self->_backoffSignaler = backoffSignaler; + }); } - (TNLGlobalConfigurationURLSessionPruneOptions)URLSessionPruneOptions @@ -296,10 +338,12 @@ - (void)addAuthenticationChallengeHandler:(id - (void)removeAuthenticationChallengeHandler:(id)handler { dispatch_barrier_async(_configurationQueue, ^{ - if (self->_authHandlers) { - NSMutableArray> *handlers = [self->_authHandlers mutableCopy]; - [handlers removeObject:handler]; - self->_authHandlers = (handlers.count > 0) ? [handlers copy] : nil; + @autoreleasepool { + if (self->_authHandlers) { + NSMutableArray> *handlers = [self->_authHandlers mutableCopy]; + [handlers removeObject:handler]; + self->_authHandlers = (handlers.count > 0) ? [handlers copy] : nil; + } } }); } diff --git a/Source/TNLGlobalConfiguration_Project.h b/Source/TNLGlobalConfiguration_Project.h index 959d2eb..39ce1af 100644 --- a/Source/TNLGlobalConfiguration_Project.h +++ b/Source/TNLGlobalConfiguration_Project.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 12/1/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #include diff --git a/Source/TNLHTTP.h b/Source/TNLHTTP.h index 0628f0f..26745cd 100644 --- a/Source/TNLHTTP.h +++ b/Source/TNLHTTP.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 6/9/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import @@ -218,7 +218,7 @@ NS_INLINE BOOL TNLHTTPStatusCodeIsDefinitiveSuccess(TNLHTTPStatusCode statusCode } } -// HTTP Content-Type constants +#pragma mark - HTTP Content-Type constants FOUNDATION_EXTERN NSString * const TNLHTTPContentTypeJPEGImage; FOUNDATION_EXTERN NSString * const TNLHTTPContentTypeQuicktimeVideo; @@ -231,6 +231,8 @@ FOUNDATION_EXTERN NSString * const TNLHTTPContentTypeURLEncodedString; //! Is the content type a textual format (limited to UTF8 [default] and ASCII currently), helpful for determining if something is printable or compressable FOUNDATION_EXTERN BOOL TNLHTTPContentTypeIsTextual(NSString * __nullable contentType); +#pragma mark - HTTP Dates + /** Enum for the different HTTP formats specified by the HTTP specification. diff --git a/Source/TNLHTTP.m b/Source/TNLHTTP.m index 791318e..a504335 100644 --- a/Source/TNLHTTP.m +++ b/Source/TNLHTTP.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 6/9/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNL_Project.h" diff --git a/Source/TNLHTTPHeaderProvider.h b/Source/TNLHTTPHeaderProvider.h index f98aeec..65f67b5 100644 --- a/Source/TNLHTTPHeaderProvider.h +++ b/Source/TNLHTTPHeaderProvider.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 4/13/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLHTTPRequest.h b/Source/TNLHTTPRequest.h index 228eaa0..8fc545e 100644 --- a/Source/TNLHTTPRequest.h +++ b/Source/TNLHTTPRequest.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 2/28/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLHTTPRequest.m b/Source/TNLHTTPRequest.m index 457b9a9..30ec3d3 100644 --- a/Source/TNLHTTPRequest.m +++ b/Source/TNLHTTPRequest.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 2/28/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSDictionary+TNLAdditions.h" diff --git a/Source/TNLHostSanitizer.h b/Source/TNLHostSanitizer.h index 2d95a8c..8370275 100644 --- a/Source/TNLHostSanitizer.h +++ b/Source/TNLHostSanitizer.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 11/21/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLInternalKeys.h b/Source/TNLInternalKeys.h index 141eb8b..8e78b06 100644 --- a/Source/TNLInternalKeys.h +++ b/Source/TNLInternalKeys.h @@ -2,8 +2,8 @@ // TNLInternalKeys.h // TwitterNetworkLayer // -// Created by Nolan O'Brien on 6/15/18. -// Copyright © 2018 Twitter. All rights reserved. +// Created on 6/15/18. +// Copyright © 2020 Twitter. All rights reserved. // #pragma mark Global Keys diff --git a/Source/TNLLRUCache.h b/Source/TNLLRUCache.h index 66d9c97..3adb6a5 100644 --- a/Source/TNLLRUCache.h +++ b/Source/TNLLRUCache.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/27/15. -// Copyright © 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLLRUCache.m b/Source/TNLLRUCache.m index f9a79f6..efc875c 100644 --- a/Source/TNLLRUCache.m +++ b/Source/TNLLRUCache.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/27/15. -// Copyright © 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNL_Project.h" diff --git a/Source/TNLLogger.h b/Source/TNLLogger.h index c6d0852..e2a4d0c 100644 --- a/Source/TNLLogger.h +++ b/Source/TNLLogger.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 3/23/16. -// Copyright © 2016 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLNetwork.h b/Source/TNLNetwork.h index 6ff97cc..5e4ace7 100644 --- a/Source/TNLNetwork.h +++ b/Source/TNLNetwork.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 9/15/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import @@ -105,27 +105,39 @@ NS_ROOT_CLASS + (BOOL)hasExecutingNetworkConnections; /** - Provide a signal to __TNL__ that a service unavailable HTTP status code was encountered. - This will be used for backing off requests (within __TNL__) to the provided _URL_ `host`. + Provide a signal to __TNL__ that a backoff signaling HTTP response was encountered. + This will be used for backing off requests (within __TNL__) to the provided _URL_ `host` (or provided _host_ if a different host from the _URL_ is preferred). + @param URL the `NSURL` of the backoff signaling response + @param host the optional host to use instead of the _URL_ `host` + @param headers the HTTP headers in the response accompanying the backoff signal */ -+ (void)serviceUnavailableEncounteredForURL:(NSURL *)URL - retryAfterDelay:(NSTimeInterval)delay; ++ (void)backoffSignalEncounteredForURL:(NSURL *)URL + host:(nullable NSString *)host + responseHTTPHeaders:(nullable NSDictionary *)headers; /** Provide a signal to __TNL__ when an HTTP response was encountered. - If the `statusCode` is `TNLHTTPStatusCodeServiceUnavailable`, then - `serviceUnavailableEncounteredForHost:retryAfterDelay:` will be called with the parsed - `"Retry-After"` value. + Checks `[TNLGlobalConfiguration backoffSignaler]` and if backoff is signaled, + then `backoffSignalEncounteredForHost:host:responseHTTPHeaders:` will be called with the response's + `allHeaderFields`. + @param response the `NSHTTPURLResponse` to examine + @param host the optional host to use instead of the _response_ `URL.host` */ -+ (void)HTTPURLResponseEncounteredOutsideOfTNL:(NSHTTPURLResponse *)response; ++ (void)HTTPURLResponseEncounteredOutsideOfTNL:(NSHTTPURLResponse *)response + host:(nullable NSString *)host; /** Apply backoff dependencies to a given `NSOperation` (from outside of __TNL__) that depends on a - service unavailable backoff be resolved before it should start. + backoff be resolved before it should start. + @param op the `NSOperation` to apply dependencies to + @param URL the `NSURL` of the operation + @param host the optional host for the operation (will be used instead of the `host` from the given _URL_ for keying off of) + @param isLongPoll whether or not the operation is a long polling operation */ -+ (void)applyServiceUnavailableBackoffDependenciesToOperation:(NSOperation *)op - withURL:(NSURL *)URL - isLongPollRequest:(BOOL)isLongPoll; ++ (void)applyBackoffDependenciesToOperation:(NSOperation *)op + withURL:(NSURL *)URL + host:(nullable NSString *)host + isLongPollRequest:(BOOL)isLongPoll; @end diff --git a/Source/TNLNetwork.m b/Source/TNLNetwork.m index 6732846..d331ba8 100644 --- a/Source/TNLNetwork.m +++ b/Source/TNLNetwork.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 9/15/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLNetwork.h" diff --git a/Source/TNLNetworkObserver.h b/Source/TNLNetworkObserver.h index c95b2df..c99c8cc 100644 --- a/Source/TNLNetworkObserver.h +++ b/Source/TNLNetworkObserver.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 6/11/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import @@ -36,7 +36,7 @@ NS_ASSUME_NONNULL_BEGIN @param op The source `TNLRequestOperation` @param URLRequest The `NSURLRequest` used in the attempt - @param type The `TNLAttemptType` of the attempt + @param metrics The `TNLAttemptMetrics` of the attempt */ - (void)tnl_requestOperation:(TNLRequestOperation *)op didStartAttemptRequest:(NSURLRequest *)URLRequest diff --git a/Source/TNLParameterCollection.h b/Source/TNLParameterCollection.h index 7e22b57..a684c05 100644 --- a/Source/TNLParameterCollection.h +++ b/Source/TNLParameterCollection.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/24/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLParameterCollection.m b/Source/TNLParameterCollection.m index b279dce..c9ffc8b 100644 --- a/Source/TNLParameterCollection.m +++ b/Source/TNLParameterCollection.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/24/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNL_Project.h" diff --git a/Source/TNLPriority.h b/Source/TNLPriority.h index 68b4366..fb98b65 100644 --- a/Source/TNLPriority.h +++ b/Source/TNLPriority.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 7/17/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLPriority.m b/Source/TNLPriority.m index 3f536bf..c08260d 100644 --- a/Source/TNLPriority.m +++ b/Source/TNLPriority.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 7/17/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLPseudoURLProtocol.h b/Source/TNLPseudoURLProtocol.h index 769837f..dfcb9ee 100644 --- a/Source/TNLPseudoURLProtocol.h +++ b/Source/TNLPseudoURLProtocol.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/29/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLPseudoURLProtocol.m b/Source/TNLPseudoURLProtocol.m index e7205f8..ddb1050 100644 --- a/Source/TNLPseudoURLProtocol.m +++ b/Source/TNLPseudoURLProtocol.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/29/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSData+TNLAdditions.h" @@ -70,21 +70,21 @@ + (void)registerURLResponse:(NSHTTPURLResponse *)response userInfo:(config) ? @{ @"config" : [config copy] } : nil storagePolicy:NSURLCacheStorageAllowed]; - dispatch_barrier_async(sOriginQueue, ^{ + tnl_dispatch_barrier_async_autoreleasing(sOriginQueue, ^{ sOriginToResponseDictionary[_UnderlyingURLString(endpoint)] = cachedResponse; }); } + (void)unregisterEndpoint:(NSURL *)endpoint { - dispatch_barrier_async(sOriginQueue, ^{ + tnl_dispatch_barrier_async_autoreleasing(sOriginQueue, ^{ [sOriginToResponseDictionary removeObjectForKey:_UnderlyingURLString(endpoint)]; }); } + (void)unregisterAllEndpoints { - dispatch_barrier_async(sOriginQueue, ^{ + tnl_dispatch_barrier_async_autoreleasing(sOriginQueue, ^{ [sOriginToResponseDictionary removeAllObjects]; }); } @@ -143,7 +143,7 @@ + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b - (void)startLoading { self.protocolRunLoop = CFRunLoopGetCurrent(); - dispatch_async(_protocolQueue, ^{ + tnl_dispatch_async_autoreleasing(_protocolQueue, ^{ ABORT_IF_NECESSARY(); NSURLRequest *request = self.request; @@ -156,12 +156,12 @@ - (void)startLoading if (response) { if (self.cachedResponse) { - dispatch_async(self->_protocolQueue, ^{ + tnl_dispatch_async_autoreleasing(self->_protocolQueue, ^{ ABORT_IF_NECESSARY(); _executeClientBlock(self, ^(id client){ [client URLProtocol:self cachedResponseIsValid:self.cachedResponse]; }); - dispatch_async(self->_protocolQueue, ^{ + tnl_dispatch_async_autoreleasing(self->_protocolQueue, ^{ ABORT_IF_NECESSARY(); self.stopped = YES; _executeClientBlock(self, ^(id client){ @@ -186,70 +186,74 @@ - (void)startLoading latency = ((double)config.latency) / 1000.0; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((delay + latency) * NSEC_PER_SEC)), self->_protocolQueue, ^{ - ABORT_IF_NECESSARY(); - - if (config.failureError) { - self.stopped = YES; - _executeClientBlock(self, ^(id client){ - [client URLProtocol:self didFailWithError:config.failureError]; - }); - return; - } + @autoreleasepool { + ABORT_IF_NECESSARY(); - NSHTTPURLResponse *httpResponse = (id)response.response; - NSData *data = response.data; - NSInteger statusCode = (config.statusCode > 0) ? config.statusCode : httpResponse.statusCode; + if (config.failureError) { + self.stopped = YES; + _executeClientBlock(self, ^(id client){ + [client URLProtocol:self didFailWithError:config.failureError]; + }); + return; + } - // See if we need to change to a 206 - if (200 == statusCode && config.canProvideRange) { + NSHTTPURLResponse *httpResponse = (id)response.response; + NSData *data = response.data; + NSInteger statusCode = (config.statusCode > 0) ? config.statusCode : httpResponse.statusCode; - const NSRange range = _RangeForRequest(request, data.length, config.stringForIfRange); - if (range.location != NSNotFound) { - // subrange requested, provide it - statusCode = 206; - data = [data subdataWithRange:range]; - } + // See if we need to change to a 206 + if (200 == statusCode && config.canProvideRange) { - } + const NSRange range = _RangeForRequest(request, data.length, config.stringForIfRange); + if (range.location != NSNotFound) { + // subrange requested, provide it + statusCode = 206; + data = [data subdataWithRange:range]; + } - // On 3xx (when `Location` is provided), execute redirection based on config behavior. - if (statusCode >= 300 && statusCode < 400) { - const TNLPseudoURLProtocolRedirectBehavior behavior = (config) ? config.redirectBehavior : TNLPseudoURLProtocolRedirectBehaviorFollowLocation; - if (_handleRedirect(self, request, httpResponse, behavior)) { - // was handled, return - return; } - } - httpResponse = _UpdateResponse(httpResponse, data.length, statusCode); + // On 3xx (when `Location` is provided), execute redirection based on config behavior. + if (statusCode >= 300 && statusCode < 400) { + const TNLPseudoURLProtocolRedirectBehavior behavior = (config) ? config.redirectBehavior : TNLPseudoURLProtocolRedirectBehaviorFollowLocation; + if (_handleRedirect(self, request, httpResponse, behavior)) { + // was handled, return + return; + } + } - dispatch_async(self->_protocolQueue, ^{ - ABORT_IF_NECESSARY(); - _executeClientBlock(self, ^(id client){ - [client URLProtocol:self - didReceiveResponse:httpResponse - cacheStoragePolicy:response.storagePolicy]; - }); + httpResponse = _UpdateResponse(httpResponse, data.length, statusCode); - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(latency * NSEC_PER_SEC)), self->_protocolQueue, ^{ + tnl_dispatch_async_autoreleasing(self->_protocolQueue, ^{ ABORT_IF_NECESSARY(); - - NSUInteger bps = NSUIntegerMax; - if (config.bps > 0) { - bps = (NSUInteger)MIN(config.bps / 8ULL, (uint64_t)NSUIntegerMax); - } - - _chunkData(self, - data, - bps, - 0 /*bytesSent*/, - MAX(latency, 0.25) /*latency*/); + _executeClientBlock(self, ^(id client){ + [client URLProtocol:self + didReceiveResponse:httpResponse + cacheStoragePolicy:response.storagePolicy]; + }); + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(latency * NSEC_PER_SEC)), self->_protocolQueue, ^{ + @autoreleasepool { + ABORT_IF_NECESSARY(); + + NSUInteger bps = NSUIntegerMax; + if (config.bps > 0) { + bps = (NSUInteger)MIN(config.bps / 8ULL, (uint64_t)NSUIntegerMax); + } + + _chunkData(self, + data, + bps, + 0 /*bytesSent*/, + MAX(latency, 0.25) /*latency*/); + } + }); }); - }); + } }); } } else { - dispatch_async(self->_protocolQueue, ^{ + tnl_dispatch_async_autoreleasing(self->_protocolQueue, ^{ ABORT_IF_NECESSARY(); self.stopped = YES; _executeClientBlock(self, ^(id client){ @@ -356,7 +360,7 @@ static void _chunkData(SELF_ARG, } if (bytesSent >= data.length) { - dispatch_async(self->_protocolQueue, ^{ + tnl_dispatch_async_autoreleasing(self->_protocolQueue, ^{ ABORT_IF_NECESSARY(); self.stopped = YES; _executeClientBlock(self, ^(id client){ @@ -365,8 +369,10 @@ static void _chunkData(SELF_ARG, }); } else { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(latency * NSEC_PER_SEC)), self->_protocolQueue, ^{ - ABORT_IF_NECESSARY(); - _chunkData(self, data, bps, bytesSent, latency); + @autoreleasepool { + ABORT_IF_NECESSARY(); + _chunkData(self, data, bps, bytesSent, latency); + } }); } } @@ -397,7 +403,9 @@ static void _executeClientBlock(SELF_ARG, } CFRunLoopPerformBlock(self->_protocolRunLoop, kCFRunLoopDefaultMode, ^{ - block(self.client); + @autoreleasepool { + block(self.client); + } }); CFRunLoopWakeUp(self->_protocolRunLoop); } diff --git a/Source/TNLRequest+Utilities.h b/Source/TNLRequest+Utilities.h index c0a611b..5def1eb 100644 --- a/Source/TNLRequest+Utilities.h +++ b/Source/TNLRequest+Utilities.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 2/28/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #if !APPLEDOC diff --git a/Source/TNLRequest.h b/Source/TNLRequest.h index 40772fc..f0fdbe6 100644 --- a/Source/TNLRequest.h +++ b/Source/TNLRequest.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/23/14. -// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Copyright © 2020 Twitter, Inc. All rights reserved. // #import @@ -412,7 +412,7 @@ NS_ASSUME_NONNULL_BEGIN @end -#pragma twitter stopignorestylecheck +#pragma twitter endignorestylecheck /** `NSURLRequest` implicitly conforms to `TNLRequest`. This category makes that conformance explicit. diff --git a/Source/TNLRequest.m b/Source/TNLRequest.m index 9c4c893..513fd00 100644 --- a/Source/TNLRequest.m +++ b/Source/TNLRequest.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/23/14. -// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Copyright © 2020 Twitter, Inc. All rights reserved. // #import "NSDictionary+TNLAdditions.h" diff --git a/Source/TNLRequestAuthorizer.h b/Source/TNLRequestAuthorizer.h index 34bcaa2..b6b9552 100644 --- a/Source/TNLRequestAuthorizer.h +++ b/Source/TNLRequestAuthorizer.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/14/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLRequestConfiguration.h b/Source/TNLRequestConfiguration.h index 3a6fb86..70e0327 100644 --- a/Source/TNLRequestConfiguration.h +++ b/Source/TNLRequestConfiguration.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 7/15/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import @@ -159,12 +159,12 @@ typedef NS_OPTIONS(NSInteger, TNLRequestConnectivityOptions) { */ typedef NS_ENUM(NSInteger, TNLResponseHashComputeAlgorithm) { TNLResponseHashComputeAlgorithmNone = 0, - TNLResponseHashComputeAlgorithmMD2 __attribute__((deprecated)) = 'md_2', - TNLResponseHashComputeAlgorithmMD4 __attribute__((deprecated)) = 'md_4', - TNLResponseHashComputeAlgorithmMD5 __attribute__((deprecated)) = 'md_5', - TNLResponseHashComputeAlgorithmSHA1 = 'sha1', - TNLResponseHashComputeAlgorithmSHA256 = 's256', - TNLResponseHashComputeAlgorithmSHA512 = 's512', + TNLResponseHashComputeAlgorithmMD2 __attribute__((deprecated)) = 'md_2', // 1835294514 + TNLResponseHashComputeAlgorithmMD4 __attribute__((deprecated)) = 'md_4', // 1835294516 + TNLResponseHashComputeAlgorithmMD5 __attribute__((deprecated)) = 'md_5', // 1835294517 + TNLResponseHashComputeAlgorithmSHA1 = 'sha1', // 1936220465 + TNLResponseHashComputeAlgorithmSHA256 = 's256', // 1932670262 + TNLResponseHashComputeAlgorithmSHA512 = 's512', // 1932865842 }; /** diff --git a/Source/TNLRequestConfiguration.m b/Source/TNLRequestConfiguration.m index 8aa61f6..cc01565 100644 --- a/Source/TNLRequestConfiguration.m +++ b/Source/TNLRequestConfiguration.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 7/15/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #include @@ -60,6 +60,17 @@ #define kConfigurationOperationTimeoutDefault (kAnatomyTimeouts[TNLRequestAnatomyDefault].operationTimeout) static const NSTimeInterval kConfigurationDeferrableIntervalDefault = 0.0; +TNLStaticAssert(TNLResponseHashComputeAlgorithmNone == 0, ALGORITHM_NONE_WRONG_VALUE); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +TNLStaticAssert(TNLResponseHashComputeAlgorithmMD2 == 1835294514, ALGORITHM_MD2_WRONG_VALUE); +TNLStaticAssert(TNLResponseHashComputeAlgorithmMD4 == 1835294516, ALGORITHM_MD4_WRONG_VALUE); +TNLStaticAssert(TNLResponseHashComputeAlgorithmMD5 == 1835294517, ALGORITHM_MD5_WRONG_VALUE); +#pragma clang diagnostic pop +TNLStaticAssert(TNLResponseHashComputeAlgorithmSHA1 == 1936220465, ALGORITHM_SHA1_WRONG_VALUE); +TNLStaticAssert(TNLResponseHashComputeAlgorithmSHA256 == 1932670262, ALGORITHM_SHA256_WRONG_VALUE); +TNLStaticAssert(TNLResponseHashComputeAlgorithmSHA512 == 1932865842, ALGORITHM_SHA512_WRONG_VALUE); + @interface TNLRequestConfiguration () - (instancetype)initWithConfiguration:(nullable TNLRequestConfiguration *)config; diff --git a/Source/TNLRequestConfiguration_Project.h b/Source/TNLRequestConfiguration_Project.h index 5614e39..4d95a79 100644 --- a/Source/TNLRequestConfiguration_Project.h +++ b/Source/TNLRequestConfiguration_Project.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/13/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLParameterCollection.h" diff --git a/Source/TNLRequestDelegate.h b/Source/TNLRequestDelegate.h index bbe788d..eff1fc5 100644 --- a/Source/TNLRequestDelegate.h +++ b/Source/TNLRequestDelegate.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 11/20/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLRequestEventHandler.h b/Source/TNLRequestEventHandler.h index 194dc9a..ec0ed3a 100644 --- a/Source/TNLRequestEventHandler.h +++ b/Source/TNLRequestEventHandler.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/14/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLRequestHydrater.h b/Source/TNLRequestHydrater.h index d96de0f..e534601 100644 --- a/Source/TNLRequestHydrater.h +++ b/Source/TNLRequestHydrater.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/14/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLRequestOperation.h b/Source/TNLRequestOperation.h index e933fee..54d50b2 100644 --- a/Source/TNLRequestOperation.h +++ b/Source/TNLRequestOperation.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/23/14. -// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Copyright © 2020 Twitter, Inc. All rights reserved. // #import @@ -75,6 +75,8 @@ typedef void(^TNLRequestDidCompleteBlock)(TNLRequestOperation *op, TNLResponse * */ @interface TNLRequestOperation : TNLSafeOperation +#pragma mark Operation Properties + /** Randomly generated operationId that will be the same for all attempts of this request operation */ @property (nonatomic, readonly) int64_t operationId; @@ -85,6 +87,8 @@ typedef void(^TNLRequestDidCompleteBlock)(TNLRequestOperation *op, TNLResponse * /** The `TNLRequestDelegate` for this request operation */ @property (nonatomic, readonly, weak, nullable) id requestDelegate; +#pragma mark Request State + /** The original `TNLRequest` for this operation */ @property (nonatomic, readonly, nullable) id originalRequest; /** The hydrated `TNLRequest` for this operation, see `TNLRequestHydrater` */ @@ -121,6 +125,8 @@ typedef void(^TNLRequestDidCompleteBlock)(TNLRequestOperation *op, TNLResponse * */ @property (nonatomic, readonly) float uploadProgress; +#pragma mark Mutable Properties + /** Any additional context desired to be set with the operation. */ @@ -137,6 +143,7 @@ typedef void(^TNLRequestDidCompleteBlock)(TNLRequestOperation *op, TNLResponse * */ @property (atomic) TNLPriority priority; +#pragma mark Designated Constructor /** Create a new operation for later enqueuing to a `TNLRequestOperationQueue` @@ -152,6 +159,7 @@ typedef void(^TNLRequestDidCompleteBlock)(TNLRequestOperation *op, TNLResponse * configuration:(nullable TNLRequestConfiguration *)config delegate:(nullable id)delegate; +#pragma mark Cancelation /** Cancel the operation @@ -190,6 +198,8 @@ typedef void(^TNLRequestDidCompleteBlock)(TNLRequestOperation *op, TNLResponse * */ - (void)cancel __attribute__((deprecated("do not use 'cancel' directly. Call 'cancelWithSource:' or cancelWithSource:underlyingError:' instead"))); +#pragma mark Wait until finished + /** Wait for the operation to finish. See `[NSOperation waitUntilFinished]`. @warning This blocks on a semaphore so the thread will be completely blocked. @@ -204,6 +214,22 @@ typedef void(^TNLRequestDidCompleteBlock)(TNLRequestOperation *op, TNLResponse * */ - (void)waitUntilFinishedWithoutBlockingRunLoop; +#pragma mark Dependencies + +/** + Same as `[NSOperation addDependency:]` with one exception. + + Normally, all dependencies are effective until the dependent operation starts executing. + With `TNLRequestOperation`, however, dependencies are also used when retrying the operation. + If the `TNLRetryPolicyProvider` triggers a retry of the `TNLRequestOperation`, it will wait + for all `dependencies` to complete before the retry starts. + This effectively means that if a dependency is added after the target operation had already + started, that dependency would take effect on any subsequent retry. + Likewise, any dependency removed before a retry is triggered will not be used as a dependency + on the retry. + */ +- (void)addDependency:(NSOperation *)op; + @end /** diff --git a/Source/TNLRequestOperation.m b/Source/TNLRequestOperation.m index ea4809a..6dc19c3 100644 --- a/Source/TNLRequestOperation.m +++ b/Source/TNLRequestOperation.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/23/14. -// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Copyright © 2020 Twitter, Inc. All rights reserved. // #include @@ -43,6 +43,8 @@ static NSString * const kRedactedKeyValue = @""; +static volatile atomic_uint_fast64_t sNextRetryId = ATOMIC_VAR_INIT(1); + static dispatch_queue_t _RequestOperationDefaultCallbackQueue(void); static dispatch_queue_t _RequestOperationDefaultCallbackQueue() { @@ -78,6 +80,10 @@ static dispatch_queue_t _URLSessionTaskOperationPropertyQueue() return sQueue; } +@interface TNLTimerOperation : TNLSafeOperation +- (instancetype)initWithDelay:(NSTimeInterval)delay; +@end + @interface TNLRequestOperation () // Private Properties @@ -178,16 +184,17 @@ static void _network_retryDuringStateTransition(SELF_ARG, TNLResponse *attemptResponse, id retryPolicyProvider); -#pragma mark Retry Timer +#pragma mark Retry -static void _network_startRetryTimer(SELF_ARG, - NSTimeInterval retryInterval, - TNLResponse *oldResponse, - id __nullable retryPolicyProvider); -static void _network_invalidateRetryTimer(SELF_ARG); -static void _network_retryTimerDidFire(SELF_ARG, - TNLResponse *oldResponse, - id __nullable retryPolicyProvider); +static void _network_startRetry(SELF_ARG, + NSTimeInterval retryDelay, + TNLResponse *oldResponse, + id __nullable retryPolicyProvider); +static void _network_invalidateRetry(SELF_ARG); +static void _network_tryRetry(SELF_ARG, + uint64_t retryId, + TNLResponse *oldResponse, + id __nullable retryPolicyProvider); #pragma mark Operation Timeout Timer @@ -311,13 +318,15 @@ @implementation TNLRequestOperation TNLResponseMetrics *_metrics; // Timers - dispatch_source_t _retryDelayTimerSource; dispatch_source_t _operationTimeoutTimerSource; dispatch_source_t _attemptTimeoutTimerSource; dispatch_source_t _callbackTimeoutTimerSource; uint64_t _callbackTimeoutTimerStartMachTime; uint64_t _callbackTimeoutTimerPausedMachTime; + // Retry + uint64_t _activeRetryId; + // Flags that can only be written on the background queue struct { BOOL didEnqueue:1; @@ -336,6 +345,18 @@ @implementation TNLRequestOperation volatile atomic_bool _didCompleteFinishedCallback; } +#pragma mark overrides with no behavior change + +- (void)addDependency:(NSOperation *)op +{ + [super addDependency:op]; +} + +- (void)waitUntilFinished +{ + [super waitUntilFinished]; +} + #pragma mark init/dealloc #pragma clang diagnostic push @@ -401,10 +422,10 @@ - (BOOL)dealloc_isObservingApplicationStates TNL_THREAD_SANITIZER_DISABLED - (void)dealloc { - tnl_dispatch_timer_invalidate(_retryDelayTimerSource); tnl_dispatch_timer_invalidate(_operationTimeoutTimerSource); tnl_dispatch_timer_invalidate(_attemptTimeoutTimerSource); tnl_dispatch_timer_invalidate(_callbackTimeoutTimerSource); + _activeRetryId = 0; // invalidate any pending retry TNLBackgroundTaskIdentifier backgroundTaskIdentifier = self.dealloc_backgroundTaskIdentifier; if (TNLBackgroundTaskInvalid != backgroundTaskIdentifier) { @@ -628,13 +649,11 @@ - (void)network_URLSessionTaskOperationIsWaitingForConnectivity:(TNLURLSessionTa id eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperartionIsWaitingForConnectivity:); if ([eventHandler respondsToSelector:callback]) { - dispatch_barrier_async(_callbackQueue, ^{ - @autoreleasepool { - NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); - _updateTag(self, tag); - [eventHandler tnl_requestOperartionIsWaitingForConnectivity:self]; - _clearTag(self, tag); - } + tnl_dispatch_barrier_async_autoreleasing(_callbackQueue, ^{ + NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); + _updateTag(self, tag); + [eventHandler tnl_requestOperartionIsWaitingForConnectivity:self]; + _clearTag(self, tag); }); } } @@ -648,14 +667,12 @@ - (void)network_URLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp id eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:didReceiveURLResponse:); if ([eventHandler respondsToSelector:callback]) { - dispatch_barrier_async(_callbackQueue, ^{ - @autoreleasepool { - NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); - _updateTag(self, tag); - [eventHandler tnl_requestOperation:self - didReceiveURLResponse:URLResponse]; - _clearTag(self, tag); - } + tnl_dispatch_barrier_async_autoreleasing(_callbackQueue, ^{ + NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); + _updateTag(self, tag); + [eventHandler tnl_requestOperation:self + didReceiveURLResponse:URLResponse]; + _clearTag(self, tag); }); } } @@ -710,22 +727,20 @@ static void _network_willPerformRedirect(SELF_ARG, id redirecter = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:willRedirectFromRequest:withResponse:toRequest:completion:); if ([redirecter respondsToSelector:callback]) { - dispatch_barrier_async(self->_callbackQueue, ^{ - @autoreleasepool { - NSString *tag = TAG_FROM_METHOD(redirecter, @protocol(TNLRequestRedirecter), callback); - _updateTag(self, tag); - [redirecter tnl_requestOperation:self - willRedirectFromRequest:fromRequest - withResponse:response - toRequest:toRequest - completion:^(id finalToRequest) { - _clearTag(self, tag); - // all `TNLURLSessionTaskOperationDelegate` completion blocks must be called from tnl_network_queue - tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ - completion(finalToRequest); - }); - }]; - } + tnl_dispatch_barrier_async_autoreleasing(self->_callbackQueue, ^{ + NSString *tag = TAG_FROM_METHOD(redirecter, @protocol(TNLRequestRedirecter), callback); + _updateTag(self, tag); + [redirecter tnl_requestOperation:self + willRedirectFromRequest:fromRequest + withResponse:response + toRequest:toRequest + completion:^(id finalToRequest) { + _clearTag(self, tag); + // all `TNLURLSessionTaskOperationDelegate` completion blocks must be called from tnl_network_queue + tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ + completion(finalToRequest); + }); + }]; }); } else { // No callback to call, revert to Default behavior @@ -792,15 +807,13 @@ - (void)network_URLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp id eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:didRedirectFromURLRequest:toURLRequest:); if ([eventHandler respondsToSelector:callback]) { - dispatch_barrier_async(_callbackQueue, ^{ - @autoreleasepool { - NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); - _updateTag(self, tag); - [eventHandler tnl_requestOperation:self - didRedirectFromURLRequest:fromRequest - toURLRequest:toRequest]; - _clearTag(self, tag); - } + tnl_dispatch_barrier_async_autoreleasing(_callbackQueue, ^{ + NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); + _updateTag(self, tag); + [eventHandler tnl_requestOperation:self + didRedirectFromURLRequest:fromRequest + toURLRequest:toRequest]; + _clearTag(self, tag); }); } } @@ -818,15 +831,13 @@ static void _network_notifyHostSanitized(SELF_ARG, id eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:didSanitizeFromHost:toHost:); if ([eventHandler respondsToSelector:callback]) { - dispatch_barrier_async(self->_callbackQueue, ^{ - @autoreleasepool { - NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); - _updateTag(self, tag); - [eventHandler tnl_requestOperation:self - didSanitizeFromHost:oldHost - toHost:newHost]; - _clearTag(self, tag); - } + tnl_dispatch_barrier_async_autoreleasing(self->_callbackQueue, ^{ + NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); + _updateTag(self, tag); + [eventHandler tnl_requestOperation:self + didSanitizeFromHost:oldHost + toHost:newHost]; + _clearTag(self, tag); }); } } @@ -887,14 +898,12 @@ - (void)network_URLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp id eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:didUpdateUploadProgress:); if ([eventHandler respondsToSelector:callback]) { - dispatch_barrier_async(_callbackQueue, ^{ - @autoreleasepool { - NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); - _updateTag(self, tag); - [eventHandler tnl_requestOperation:self - didUpdateUploadProgress:progress]; - _clearTag(self, tag); - } + tnl_dispatch_barrier_async_autoreleasing(_callbackQueue, ^{ + NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); + _updateTag(self, tag); + [eventHandler tnl_requestOperation:self + didUpdateUploadProgress:progress]; + _clearTag(self, tag); }); } } @@ -918,14 +927,12 @@ - (void)network_URLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp id eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:didUpdateDownloadProgress:); if ([eventHandler respondsToSelector:callback]) { - dispatch_barrier_async(_callbackQueue, ^{ - @autoreleasepool { - NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); - _updateTag(self, tag); - [eventHandler tnl_requestOperation:self - didUpdateDownloadProgress:progress]; - _clearTag(self, tag); - } + tnl_dispatch_barrier_async_autoreleasing(_callbackQueue, ^{ + NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); + _updateTag(self, tag); + [eventHandler tnl_requestOperation:self + didUpdateDownloadProgress:progress]; + _clearTag(self, tag); }); } } @@ -941,14 +948,12 @@ - (void)network_URLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp id eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:didReceiveData:); if ([eventHandler respondsToSelector:callback]) { - dispatch_barrier_async(self->_callbackQueue, ^{ - @autoreleasepool { - NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); - _updateTag(self, tag); - [eventHandler tnl_requestOperation:self - didReceiveData:data]; - _clearTag(self, tag); - } + tnl_dispatch_barrier_async_autoreleasing(self->_callbackQueue, ^{ + NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); + _updateTag(self, tag); + [eventHandler tnl_requestOperation:self + didReceiveData:data]; + _clearTag(self, tag); }); } break; @@ -974,17 +979,15 @@ - (void)network_URLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp id eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:didStartRequestWithURLSessionTaskIdentifier:URLSessionConfigurationIdentifier:URLSessionSharedContainerIdentifier:isBackgroundRequest:); if ([eventHandler respondsToSelector:callback]) { - dispatch_barrier_async(_callbackQueue, ^{ - @autoreleasepool { - NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); - _updateTag(self, tag); - [eventHandler tnl_requestOperation:self - didStartRequestWithURLSessionTaskIdentifier:taskId - URLSessionConfigurationIdentifier:configIdentifier - URLSessionSharedContainerIdentifier:sharedContainerIdentifier - isBackgroundRequest:isBackgroundRequest]; - _clearTag(self, tag); - } + tnl_dispatch_barrier_async_autoreleasing(_callbackQueue, ^{ + NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); + _updateTag(self, tag); + [eventHandler tnl_requestOperation:self + didStartRequestWithURLSessionTaskIdentifier:taskId + URLSessionConfigurationIdentifier:configIdentifier + URLSessionSharedContainerIdentifier:sharedContainerIdentifier + isBackgroundRequest:isBackgroundRequest]; + _clearTag(self, tag); }); } } @@ -1040,11 +1043,6 @@ - (void)network_URLSessionTaskOperation:(TNLURLSessionTaskOperation *)taskOp #pragma mark Wait -- (void)waitUntilFinished -{ - [super waitUntilFinished]; -} - - (void)waitUntilFinishedWithoutBlockingRunLoop { // Default implementation is to block the thread until the execution completes. @@ -1278,35 +1276,33 @@ static void _network_hydrateRequest(SELF_ARG, tnl_request_preparation_block_t ne id hydrater = self.internalDelegate; id originalRequest = self.originalRequest; SEL callback = @selector(tnl_requestOperation:hydrateRequest:completion:); - dispatch_barrier_async(self->_callbackQueue, ^{ - @autoreleasepool { - if ([hydrater respondsToSelector:callback]) { - NSString *tag = TAG_FROM_METHOD(hydrater, @protocol(TNLRequestHydrater), callback); - _updateTag(self, tag); - [hydrater tnl_requestOperation:self - hydrateRequest:originalRequest - completion:^(id hydratedRequest, NSError *error) { - _clearTag(self, tag); - - tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ - if (!_network_getIsPreparing(self)) { - return; - } + tnl_dispatch_barrier_async_autoreleasing(self->_callbackQueue, ^{ + if ([hydrater respondsToSelector:callback]) { + NSString *tag = TAG_FROM_METHOD(hydrater, @protocol(TNLRequestHydrater), callback); + _updateTag(self, tag); + [hydrater tnl_requestOperation:self + hydrateRequest:originalRequest + completion:^(id hydratedRequest, NSError *error) { + _clearTag(self, tag); - if (error) { - _network_fail(self, TNLErrorCreateWithCodeAndUnderlyingError(TNLErrorCodeRequestOperationFailedToHydrateRequest, error)); - } else { - self.hydratedRequest = hydratedRequest ?: originalRequest; - nextBlock(); - } - }); - }]; - } else { tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ - self.hydratedRequest = originalRequest; - nextBlock(); + if (!_network_getIsPreparing(self)) { + return; + } + + if (error) { + _network_fail(self, TNLErrorCreateWithCodeAndUnderlyingError(TNLErrorCodeRequestOperationFailedToHydrateRequest, error)); + } else { + self.hydratedRequest = hydratedRequest ?: originalRequest; + nextBlock(); + } }); - } + }]; + } else { + tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ + self.hydratedRequest = originalRequest; + nextBlock(); + }); } }); } @@ -1372,11 +1368,15 @@ static void _network_validateConfiguration(SELF_ARG, tnl_request_preparation_blo TNLRequestConfiguration *config = self->_requestConfiguration; - if (config.attemptTimeout - config.idleTimeout < -0.05) { + const BOOL hasAttemptTimeout = config.attemptTimeout >= MIN_TIMER_INTERVAL; + const BOOL hasIdleTimeout = config.idleTimeout >= MIN_TIMER_INTERVAL; + const BOOL hasOperationTimeout = config.operationTimeout >= MIN_TIMER_INTERVAL; + + if (hasAttemptTimeout && hasIdleTimeout && (config.attemptTimeout - config.idleTimeout < -0.05)) { TNLLogWarning(@"Attempt Timeout (%.2f) should not be shorter than the Idle Timeout (%.2f)!", config.attemptTimeout, config.idleTimeout); } - if (config.operationTimeout - config.attemptTimeout < -0.05) { + if (hasOperationTimeout && hasAttemptTimeout && (config.operationTimeout - config.attemptTimeout < -0.05)) { TNLLogWarning(@"Operation Timeout (%.2f) should not be shorter than the Attempt Timeout (%.2f)!", config.operationTimeout, config.attemptTimeout); } @@ -1690,33 +1690,31 @@ static void _network_authorizeScratchURLRequest(SELF_ARG, tnl_request_preparatio return; } - dispatch_barrier_async(self->_callbackQueue, ^{ - @autoreleasepool { - NSString *tag = TAG_FROM_METHOD(authorizer, @protocol(TNLRequestAuthorizer), callback); - _updateTag(self, tag); - [authorizer tnl_requestOperation:self - authorizeURLRequest:[self->_scratchURLRequest copy] - completion:^(NSString *authHeader, NSError *error) { - _clearTag(self, tag); + tnl_dispatch_barrier_async_autoreleasing(self->_callbackQueue, ^{ + NSString *tag = TAG_FROM_METHOD(authorizer, @protocol(TNLRequestAuthorizer), callback); + _updateTag(self, tag); + [authorizer tnl_requestOperation:self + authorizeURLRequest:[self->_scratchURLRequest copy] + completion:^(NSString *authHeader, NSError *error) { + _clearTag(self, tag); - tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ - if (!_network_getIsPreparing(self)) { - return; - } + tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ + if (!_network_getIsPreparing(self)) { + return; + } - if (error) { - _network_fail(self, TNLErrorCreateWithCodeAndUnderlyingError(TNLErrorCodeRequestOperationFailedToAuthorizeRequest, error)); - return; - } + if (error) { + _network_fail(self, TNLErrorCreateWithCodeAndUnderlyingError(TNLErrorCodeRequestOperationFailedToAuthorizeRequest, error)); + return; + } - if (authHeader) { - [self->_scratchURLRequest setValue:(authHeader.length > 0) ? authHeader : nil - forHTTPHeaderField:@"Authorization"]; - } - nextBlock(); - }); - }]; - } + if (authHeader) { + [self->_scratchURLRequest setValue:(authHeader.length > 0) ? authHeader : nil + forHTTPHeaderField:@"Authorization"]; + } + nextBlock(); + }); + }]; }); } @@ -1757,9 +1755,10 @@ static void _network_connect(SELF_ARG, TNLAssert(_network_getIsPreparing(self)); TNLAssertMessage(self.URLSessionTaskOperation == nil, @"Already have a TNLURLSessionTaskOperation? state = %@", TNLRequestOperationStateToString(self.state)); - _network_transitionState(self, - TNLRequestOperationStateStarting, - nil /*attemptResponse*/); + + // Do not update the `.state` here. + // The `.URLSessionTaskOperation` will update to `TNLRequestOperationStateStarting` once it starts + // (which may be delayed by 503 backoffs). [self.requestOperationQueue findURLSessionTaskOperationForRequestOperation:self complete:^(TNLURLSessionTaskOperation *taskOp) { @@ -1790,27 +1789,25 @@ static void _network_startURLSessionTaskOperation(SELF_ARG, return; } - dispatch_barrier_async(self->_callbackQueue, ^{ - @autoreleasepool { - NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); - _updateTag(self, tag); - [eventHandler tnl_requestOperation:self - readyToEnqueueUnderlyingNetworkingOperation:isRetry - enqueueBlock:^(NSArray *dependencies) { - _clearTag(self, tag); - // add dependencies synchronously with callback - if (dependencies) { - TNLLogDebug(@"Added dependencies to %@: %@", taskOp, dependencies); - for (NSOperation *op in dependencies) { - [taskOp addDependency:op]; - } + tnl_dispatch_barrier_async_autoreleasing(self->_callbackQueue, ^{ + NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); + _updateTag(self, tag); + [eventHandler tnl_requestOperation:self + readyToEnqueueUnderlyingNetworkingOperation:isRetry + enqueueBlock:^(NSArray *dependencies) { + _clearTag(self, tag); + // add dependencies synchronously with callback + if (dependencies) { + TNLLogDebug(@"Added dependencies to %@: %@", taskOp, dependencies); + for (NSOperation *op in dependencies) { + [taskOp addDependency:op]; } - // dispatch to network queue to start the op - tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ - [taskOp enqueueToOperationQueueIfNeeded:self.requestOperationQueue]; - }); - }]; - } + } + // dispatch to network queue to start the op + tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ + [taskOp enqueueToOperationQueueIfNeeded:self.requestOperationQueue]; + }); + }]; }); } @@ -1902,28 +1899,24 @@ static void _network_retry(SELF_ARG, if (!self->_backgroundFlags.silentStart) { SEL callback = @selector(tnl_requestOperation:didStartRetryFromResponse:); if ([retryPolicyProvider respondsToSelector:callback]) { - dispatch_barrier_async(_RetryPolicyProviderQueue(retryPolicyProvider), ^{ - @autoreleasepool { - NSString *tag = TAG_FROM_METHOD(retryPolicyProvider, @protocol(TNLRequestRetryPolicyProvider), callback); - _updateTag(self, tag); - [retryPolicyProvider tnl_requestOperation:self - didStartRetryFromResponse:oldResponse]; - _clearTag(self, tag); - } + tnl_dispatch_barrier_async_autoreleasing(_RetryPolicyProviderQueue(retryPolicyProvider), ^{ + NSString *tag = TAG_FROM_METHOD(retryPolicyProvider, @protocol(TNLRequestRetryPolicyProvider), callback); + _updateTag(self, tag); + [retryPolicyProvider tnl_requestOperation:self + didStartRetryFromResponse:oldResponse]; + _clearTag(self, tag); }); } id eventHandler = self.internalDelegate; callback = @selector(tnl_requestOperation:didStartRetryFromResponse:policyProvider:); if ([eventHandler respondsToSelector:callback]) { - dispatch_barrier_async(self->_callbackQueue, ^{ - @autoreleasepool { - NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); - _updateTag(self, tag); - [eventHandler tnl_requestOperation:self - didStartRetryFromResponse:oldResponse - policyProvider:retryPolicyProvider]; - _clearTag(self, tag); - } + tnl_dispatch_barrier_async_autoreleasing(self->_callbackQueue, ^{ + NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); + _updateTag(self, tag); + [eventHandler tnl_requestOperation:self + didStartRetryFromResponse:oldResponse + policyProvider:retryPolicyProvider]; + _clearTag(self, tag); }); } } @@ -1992,7 +1985,7 @@ static void _network_cleanupAfterComplete(SELF_ARG) return; } - _network_invalidateRetryTimer(self); + _network_invalidateRetry(self); _network_invalidateOperationTimeoutTimer(self); } @@ -2141,15 +2134,13 @@ static void _network_completeStateTransition(SELF_ARG, id eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:didTransitionFromState:toState:); if ([eventHandler respondsToSelector:callback]) { - dispatch_barrier_async(self->_callbackQueue, ^{ - @autoreleasepool { - NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); - _updateTag(self, tag); - [eventHandler tnl_requestOperation:self - didTransitionFromState:oldState - toState:state]; - _clearTag(self, tag); - } + tnl_dispatch_barrier_async_autoreleasing(self->_callbackQueue, ^{ + NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); + _updateTag(self, tag); + [eventHandler tnl_requestOperation:self + didTransitionFromState:oldState + toState:state]; + _clearTag(self, tag); }); } @@ -2157,7 +2148,9 @@ static void _network_completeStateTransition(SELF_ARG, if (finishedDidChange) { // have aggressive assert here, whether TNL asserts are enabled or not - __TNLAssert(attemptResponse != nil); + if (nil == attemptResponse) { + __TNLAssertTriggering(); + } #if NS_BLOCK_ASSERTIONS assert(attemptResponse != nil); #else @@ -2271,15 +2264,13 @@ static void _network_didCompleteAttempt(SELF_ARG, id eventHandler = self.internalDelegate; SEL callback = @selector(tnl_requestOperation:didCompleteAttemptWithResponse:disposition:); if ([eventHandler respondsToSelector:callback]) { - dispatch_barrier_async(self->_callbackQueue, ^{ - @autoreleasepool { - NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); - _updateTag(self, tag); - [eventHandler tnl_requestOperation:self - didCompleteAttemptWithResponse:response - disposition:disposition]; - _clearTag(self, tag); - } + tnl_dispatch_barrier_async_autoreleasing(self->_callbackQueue, ^{ + NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); + _updateTag(self, tag); + [eventHandler tnl_requestOperation:self + didCompleteAttemptWithResponse:response + disposition:disposition]; + _clearTag(self, tag); }); } } @@ -2305,28 +2296,26 @@ static void _network_complete(SELF_ARG, SEL callback = @selector(tnl_requestOperation:didCompleteWithResponse:); const BOOL hasCompletionCallback = [eventHandler respondsToSelector:callback]; dispatch_block_t block = ^{ - @autoreleasepool { - if (hasCompletionCallback) { - NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); - _updateTag(self, tag); - [eventHandler tnl_requestOperation:self - didCompleteWithResponse:response]; - _clearTag(self, tag); - } - _finalizeCompletion(self); // finalize from the completion queue - tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ - _network_endBackgroundTask(self); - }); + if (hasCompletionCallback) { + NSString *tag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), callback); + _updateTag(self, tag); + [eventHandler tnl_requestOperation:self + didCompleteWithResponse:response]; + _clearTag(self, tag); } + _finalizeCompletion(self); // finalize from the completion queue + tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ + _network_endBackgroundTask(self); + }); }; if (self->_callbackQueue == self->_completionQueue) { - dispatch_barrier_async(self->_completionQueue, block); + tnl_dispatch_barrier_async_autoreleasing(self->_completionQueue, block); } else { // dispatch to callback queue to flush the callback queue dispatch_barrier_async(self->_callbackQueue, ^{ // dispatch to completion queue for completion - dispatch_barrier_async(self->_completionQueue, block); + tnl_dispatch_barrier_async_autoreleasing(self->_completionQueue, block); }); } } @@ -2693,10 +2682,10 @@ static void _network_forciblyRetryInvalidatedURLSessionRequest(SELF_ARG, _network_transitionState(self, TNLRequestOperationStateWaitingToRetry, nil /*attemptResponse*/); - _network_startRetryTimer(self, - MIN_TIMER_INTERVAL, - attemptResponse, - nil /*retryPolicyProvider*/); + _network_startRetry(self, + MIN_TIMER_INTERVAL, + attemptResponse, + nil /*retryPolicyProvider*/); // don't need to end the background task here since we are triggering // the retry in order to circumvent a race condition that causes a failure, // not actually retrying after a legitimate failure that could be of any @@ -2771,78 +2760,77 @@ static void _network_retryDuringStateTransition(SELF_ARG, TNLRequestConfiguration *requestConfig = self->_requestConfiguration; // Dispatch to the retry queue to get retry policy info - dispatch_barrier_async(_RetryPolicyProviderQueue(retryPolicyProvider), ^{ + tnl_dispatch_barrier_async_autoreleasing(_RetryPolicyProviderQueue(retryPolicyProvider), ^{ - @autoreleasepool { - NSString *tag = TAG_FROM_METHOD(retryPolicyProvider, @protocol(TNLRequestRetryPolicyProvider), @selector(tnl_shouldRetryRequestOperation:withResponse:)); - _updateTag(self, tag); - const BOOL retry = [retryPolicyProvider tnl_shouldRetryRequestOperation:self - withResponse:attemptResponse]; - _clearTag(self, tag); + NSString *tag = TAG_FROM_METHOD(retryPolicyProvider, @protocol(TNLRequestRetryPolicyProvider), @selector(tnl_shouldRetryRequestOperation:withResponse:)); + _updateTag(self, tag); + const BOOL retry = [retryPolicyProvider tnl_shouldRetryRequestOperation:self + withResponse:attemptResponse]; + _clearTag(self, tag); - if (retry) { - - NSTimeInterval operationTimeout = requestConfig.operationTimeout; - BOOL didUpdateOperationTimeout = NO; - TNLRequestConfiguration *newConfig = _retryQueue_pullNewRequestConfiguration(self, - retryPolicyProvider, - attemptResponse, - requestConfig); - if (newConfig) { - TNLLogDebug(@"Retry policy updated config: %@", @{ - @"operation" : self, - @"attemptResponse" : attemptResponse, - @"oldConfig" : requestConfig, - @"newConfig" : newConfig - }); - const NSTimeInterval newTimeout = newConfig.operationTimeout; - if (newTimeout != operationTimeout) { - operationTimeout = newTimeout; - didUpdateOperationTimeout = YES; - } + if (retry) { + + NSTimeInterval operationTimeout = requestConfig.operationTimeout; + BOOL didUpdateOperationTimeout = NO; + TNLRequestConfiguration *newConfig = _retryQueue_pullNewRequestConfiguration(self, + retryPolicyProvider, + attemptResponse, + requestConfig); + if (newConfig) { + TNLLogDebug(@"Retry policy updated config: %@", @{ + @"operation" : self, + @"attemptResponse" : attemptResponse, + @"oldConfig" : requestConfig, + @"newConfig" : newConfig + }); + const NSTimeInterval newTimeout = newConfig.operationTimeout; + if (newTimeout != operationTimeout) { + operationTimeout = newTimeout; + didUpdateOperationTimeout = YES; } + } - const NSTimeInterval retryDelay = _retryQueue_pullRetryDelay(self, retryPolicyProvider, attemptResponse); - const NSTimeInterval elapsedTime = TNLComputeDuration(enqueueMachTime, mach_absolute_time()); + const BOOL hasOperationTimeout = operationTimeout >= MIN_TIMER_INTERVAL; + const NSTimeInterval retryDelay = _retryQueue_pullRetryDelay(self, retryPolicyProvider, attemptResponse); + const NSTimeInterval elapsedTime = TNLComputeDuration(enqueueMachTime, mach_absolute_time()); - // Only retry if the attempt won't be too far into the future - if ((elapsedTime + retryDelay) < operationTimeout) { - TNLLogDebug(@"Retry will start in %.3f seconds", retryDelay); + // Only retry if the attempt won't be too far into the future + if (!hasOperationTimeout || ((elapsedTime + retryDelay) < operationTimeout)) { + TNLLogDebug(@"Retry will start in %.3f seconds", retryDelay); - NSTimeInterval newOperationTimeout = -1.0; // negative won't update timeout - if (didUpdateOperationTimeout) { - newOperationTimeout = operationTimeout - elapsedTime; - TNLAssert(newOperationTimeout >= 0.0); - } - _retryQueue_doRetry(self, - oldState, - retryPolicyProvider, - attemptResponse, - retryDelay, - eventHandler, - hasCachedCancel, - newConfig, - newOperationTimeout); - return; + NSTimeInterval newOperationTimeout = -1.0; // negative won't update timeout + if (didUpdateOperationTimeout && hasOperationTimeout) { + newOperationTimeout = operationTimeout - elapsedTime; + TNLAssert(newOperationTimeout >= 0.0); } - - TNLLogDebug(@"Retry is past timeout, not retrying"); + _retryQueue_doRetry(self, + oldState, + retryPolicyProvider, + attemptResponse, + retryDelay, + eventHandler, + hasCachedCancel, + newConfig, + newOperationTimeout); + return; } - // won't retry - tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ - self->_backgroundFlags.inRetryCheck = NO; - if (_network_getIsStateFinished(self)) { - return; - } - - TNLAssert(attemptResponse != nil); - _network_completeStateTransition(self, - oldState, - state, - attemptResponse); - }); + TNLLogDebug(@"Retry is past timeout, not retrying"); } + + // won't retry + tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ + self->_backgroundFlags.inRetryCheck = NO; + if (_network_getIsStateFinished(self)) { + return; + } + + TNLAssert(attemptResponse != nil); + _network_completeStateTransition(self, + oldState, + state, + attemptResponse); + }); }); } @@ -2929,8 +2917,20 @@ static void _retryQueue_doRetry(SELF_ARG, } } - if (newConfig) { - tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ + tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ + self->_backgroundFlags.inRetryCheck = NO; + if (_network_getHasFailedOrFinished(self)) { + return; + } + + // don't use stale hasCachedCancel var here, + // check the fresh _cachedCancel ref directly + if (self->_cachedCancelError != nil) { + _network_fail(self, self->_cachedCancelError); + return; + } + + if (newConfig) { // update the config self->_requestConfiguration = newConfig; // update the operation timeout if it had changed @@ -2938,117 +2938,131 @@ static void _retryQueue_doRetry(SELF_ARG, _network_invalidateOperationTimeoutTimer(self); _network_startOperationTimeoutTimer(self, newOperationTimeout); } - }); - } + } - // Dispatch to the callback queue in case we need to event to the event handler - willStartRetryCallback = @selector(tnl_requestOperation:willStartRetryFromResponse:policyProvider:afterDelay:); - dispatch_barrier_async(self->_callbackQueue, ^{ - @autoreleasepool { - if (!hasCachedCancel) { - if ([eventHandler respondsToSelector:willStartRetryCallback] && ((__bridge void *)eventHandler != (__bridge void *)retryPolicyProvider)) { - NSString *eventTag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), willStartRetryCallback); - _updateTag(self, eventTag); - [eventHandler tnl_requestOperation:self - willStartRetryFromResponse:attemptResponse - policyProvider:retryPolicyProvider - afterDelay:retryDelay]; - _clearTag(self, eventTag); - } + self.URLSessionTaskOperation = nil; + + // Transition to "Waiting to Retry", forcibly updating to "Starting" first, if necessary + TNLRequestOperationState updatedOldState = oldState; + if (TNLRequestOperationStatePreparingRequest == oldState) { + _network_completeStateTransition(self, + oldState, + TNLRequestOperationStateStarting, + nil /*attemptResponse*/); + updatedOldState = TNLRequestOperationStateStarting; + } + _network_completeStateTransition(self, + updatedOldState, + TNLRequestOperationStateWaitingToRetry, + attemptResponse); + + // Dispatch to the callback queue in case we need to event to the event handler + SEL eventHandlerWillStartRetryCallback = @selector(tnl_requestOperation:willStartRetryFromResponse:policyProvider:afterDelay:); + tnl_dispatch_barrier_async_autoreleasing(self->_callbackQueue, ^{ + if ([eventHandler respondsToSelector:eventHandlerWillStartRetryCallback] && ((__bridge void *)eventHandler != (__bridge void *)retryPolicyProvider)) { + NSString *eventTag = TAG_FROM_METHOD(eventHandler, @protocol(TNLRequestEventHandler), eventHandlerWillStartRetryCallback); + _updateTag(self, eventTag); + [eventHandler tnl_requestOperation:self + willStartRetryFromResponse:attemptResponse + policyProvider:retryPolicyProvider + afterDelay:retryDelay]; + _clearTag(self, eventTag); } - // Finish with dispatch to background queue to update state and start retry timer (if necessary) + // Finish with dispatch to background queue to start retry timer tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ - self->_backgroundFlags.inRetryCheck = NO; - if (_network_getHasFailedOrFinished(self)) { - return; - } - - // don't use stale hasCachedCancel var here, - // check the fresh _cachedCancel ref directly - if (self->_cachedCancelError != nil) { - _network_fail(self, self->_cachedCancelError); - } else { - self.URLSessionTaskOperation = nil; - TNLRequestOperationState updatedOldState = oldState; - if (TNLRequestOperationStatePreparingRequest == oldState) { - // forcibly update to "Starting" before updating to "Waiting to Retry" - _network_completeStateTransition(self, - oldState, - TNLRequestOperationStateStarting, - nil /*attemptResponse*/); - updatedOldState = TNLRequestOperationStateStarting; - } - _network_completeStateTransition(self, - updatedOldState, - TNLRequestOperationStateWaitingToRetry, - attemptResponse); - _network_startRetryTimer(self, - retryDelay, - attemptResponse, - retryPolicyProvider); - // end the background task while waiting to retry, - // we only want the active request to be guarded with a bg task - _network_endBackgroundTask(self); - } + _network_startRetry(self, + retryDelay, + attemptResponse, + retryPolicyProvider); + // end the background task while waiting to retry, + // we only want the active request to be guarded with a bg task + _network_endBackgroundTask(self); }); - } + }); }); } -#pragma mark Retry Timer +#pragma mark Retry -static void _network_startRetryTimer(SELF_ARG, - NSTimeInterval retryInterval, - TNLResponse *oldResponse, - id __nullable retryPolicyProvider) +static void _network_startRetry(SELF_ARG, + NSTimeInterval retryDelay, + TNLResponse *oldResponse, + id __nullable retryPolicyProvider) { if (!self) { return; } - _network_invalidateRetryTimer(self); + // Update the active retry number (effectively invalidating prior retries) + const uint64_t currentRetryId = atomic_fetch_add(&sNextRetryId, 1); + self->_activeRetryId = currentRetryId; - if (retryInterval < MIN_TIMER_INTERVAL) { - _network_retryTimerDidFire(self, - oldResponse, - retryPolicyProvider); - } else { - __weak typeof(self) weakSelf = self; - self->_retryDelayTimerSource = tnl_dispatch_timer_create_and_start(tnl_network_queue(), - retryInterval, - TIMER_LEEWAY_WITH_FIRE_INTERVAL(retryInterval), - NO /*repeats*/, - ^{ - _network_retryTimerDidFire(weakSelf, - oldResponse, - retryPolicyProvider); - }); + // The block try the actual retry + __weak typeof(self) weakSelf = self; + dispatch_block_t tryRetryBlock = ^{ + _network_tryRetry(weakSelf, + currentRetryId, + oldResponse, + retryPolicyProvider); + }; + + // Can we retry without any delay? + NSArray *dependencies = self.dependencies; + if (dependencies.count == 0 && retryDelay < MIN_TIMER_INTERVAL) { + // retry without a delay + tryRetryBlock(); + return; } + + // Set up operation to gate the retry on + NSOperation *retryDependencyOperation = [[TNLSafeOperation alloc] init]; + retryDependencyOperation.completionBlock = ^{ + if (dispatch_queue_get_label(tnl_network_queue()) == dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)) { + tryRetryBlock(); + } else { + dispatch_async(tnl_network_queue(), tryRetryBlock); + } + }; + + // Set up dependencies + for (NSOperation *op in dependencies) { + if (!op.isFinished && !op.isCancelled) { + [retryDependencyOperation addDependency:op]; + } + } + if (retryDelay >= MIN_TIMER_INTERVAL) { + // the retry delay is concurrent with the other dependencies, so it doesn't need the other + // dependencies itself and simply be added to our dependency operation + NSOperation *delayOp = [[TNLTimerOperation alloc] initWithDelay:retryDelay]; + [retryDependencyOperation addDependency:delayOp]; + [TNLNetworkOperationQueue() addOperation:delayOp]; + } + + // Add our dependency operation to wait for our retry to trigger + [TNLNetworkOperationQueue() addOperation:retryDependencyOperation]; } -static void _network_invalidateRetryTimer(SELF_ARG) +static void _network_invalidateRetry(SELF_ARG) { if (!self) { return; } - tnl_dispatch_timer_invalidate(self->_retryDelayTimerSource); - self->_retryDelayTimerSource = NULL; + self->_activeRetryId = 0; } -static void _network_retryTimerDidFire(SELF_ARG, - TNLResponse *oldResponse, - id __nullable retryPolicyProvider) +static void _network_tryRetry(SELF_ARG, + uint64_t retryId, + TNLResponse *oldResponse, + id __nullable retryPolicyProvider) { if (!self) { return; } - if (self->_retryDelayTimerSource) { - TNLLogInformation(@"%@::_network_retryTimerDidFire()", self); - - _network_invalidateRetryTimer(self); + if (retryId == self->_activeRetryId) { + TNLLogInformation(@"%@::_network_tryRetry()", self); _network_retry(self, oldResponse, retryPolicyProvider); } } @@ -3095,7 +3109,7 @@ static void _network_operationTimeoutTimerDidFire(SELF_ARG) _network_invalidateOperationTimeoutTimer(self); _network_invalidateAttemptTimeoutTimer(self); - _network_invalidateRetryTimer(self); + _network_invalidateRetry(self); if (!_network_getHasFailedOrFinished(self)) { _network_fail(self, TNLErrorCreateWithCode(TNLErrorCodeRequestOperationOperationTimedOut)); @@ -3249,7 +3263,7 @@ static void _network_attemptTimeoutTimerDidFire(SELF_ARG) TNLLogInformation(@"%@::_network_attemptTimeoutTimerDidFire()", self); _network_invalidateAttemptTimeoutTimer(self); - _network_invalidateRetryTimer(self); + _network_invalidateRetry(self); // Don't invalidate the operation timeout if (!_network_getHasFailedOrFinished(self)) { @@ -3500,6 +3514,86 @@ static void _clearTag(SELF_ARG, @end +#pragma mark - TNLTimerOperation + +@implementation TNLTimerOperation +{ + NSTimeInterval _delay; + volatile atomic_bool _finished; + volatile atomic_bool _executing; +} + +- (instancetype)initWithDelay:(NSTimeInterval)delay +{ + if (self = [self init]) { + _delay = delay; + } + return self; +} + +- (instancetype)init +{ + if (self = [super init]) { + atomic_init(&_finished, false); + atomic_init(&_executing, false); + } + return self; +} + +- (void)start +{ + if ([self isCancelled]) { + [self willChangeValueForKey:@"isFinished"]; + atomic_store(&_finished, true); + [self didChangeValueForKey:@"isFinished"]; + return; + } + + [self willChangeValueForKey:@"isExecuting"]; + atomic_store(&_executing, true); + [self didChangeValueForKey:@"isExecuting"]; + [self run]; +} + +- (void)run +{ + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_delay * NSEC_PER_SEC)), tnl_network_queue(), ^{ + [self completeOperation]; + }); +} + +- (BOOL)isExecuting +{ + return atomic_load(&_executing); +} + +- (BOOL)isFinished +{ + return atomic_load(&_finished); +} + +- (BOOL)isConcurrent +{ + return YES; +} + +- (BOOL)isAsynchronous +{ + return YES; +} + +- (void)completeOperation +{ + [self willChangeValueForKey:@"isFinished"]; + [self willChangeValueForKey:@"isExecuting"]; + atomic_store(&_executing, false); + atomic_store(&_finished, true); + [self didChangeValueForKey:@"isExecuting"]; + [self didChangeValueForKey:@"isFinished"]; +} + +@end + #pragma mark - Functions NSString *TNLRequestOperationStateToString(TNLRequestOperationState state) diff --git a/Source/TNLRequestOperationCancelSource.h b/Source/TNLRequestOperationCancelSource.h index 2bfda5b..0bd0342 100644 --- a/Source/TNLRequestOperationCancelSource.h +++ b/Source/TNLRequestOperationCancelSource.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/21/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLRequestOperationCancelSource.m b/Source/TNLRequestOperationCancelSource.m index 782bc57..12e4834 100644 --- a/Source/TNLRequestOperationCancelSource.m +++ b/Source/TNLRequestOperationCancelSource.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/21/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLRequestOperationCancelSource.h" diff --git a/Source/TNLRequestOperationQueue.h b/Source/TNLRequestOperationQueue.h index d3eeb4f..55b1e99 100644 --- a/Source/TNLRequestOperationQueue.h +++ b/Source/TNLRequestOperationQueue.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/23/14. -// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Copyright © 2020 Twitter, Inc. All rights reserved. // #import diff --git a/Source/TNLRequestOperationQueue.m b/Source/TNLRequestOperationQueue.m index df34cfa..c642ec9 100644 --- a/Source/TNLRequestOperationQueue.m +++ b/Source/TNLRequestOperationQueue.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/23/14. -// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Copyright © 2020 Twitter, Inc. All rights reserved. // #include @@ -14,6 +14,7 @@ #import "NSURLSessionConfiguration+TNLAdditions.h" #import "TNL_Project.h" #import "TNLBackgroundURLSessionTaskOperationManager.h" +#import "TNLBackoff.h" #import "TNLGlobalConfiguration.h" #import "TNLNetwork.h" #import "TNLNetworkObserver.h" @@ -38,7 +39,7 @@ static void _GlobalRequestOperationQueueAddOperation(TNLRequestOperation *op); -static volatile atomic_int_fast64_t __attribute__((aligned(8))) sGlobalExecutingConnectionCount = 0; +static volatile atomic_int_fast64_t __attribute__((aligned(8))) sGlobalExecutingConnectionCount = ATOMIC_VAR_INIT(0); static NSMapTable *sGlobalRequestOperationQueueMapTable = nil; static NSMutableSet> *sGlobalNetworkObservers = nil; static NSOperationQueue *sGlobalRequestOperationQueue = nil; @@ -239,7 +240,7 @@ - (instancetype)initWithIdentifier:(NSString *)identifier _stagedRequestOperations = [[NSMutableArray alloc] init]; __block BOOL didRegister = NO; - tnl_dispatch_sync_autoreleasing(_GlobalOperationQueueQueue(), ^{ + dispatch_sync(_GlobalOperationQueueQueue(), ^{ didRegister = [sGlobalRequestOperationQueueMapTable objectForKey:self.identifier] == nil; if (didRegister) { [sGlobalRequestOperationQueueMapTable setObject:self forKey:self.identifier]; @@ -284,7 +285,7 @@ + (BOOL)handleBackgroundURLSessionEvents:(nullable NSString *)identifier - (void)setNetworkObserver:(nullable id)networkObserver { - dispatch_async(_sessionStateQueue, ^{ + tnl_dispatch_async_autoreleasing(_sessionStateQueue, ^{ self->_networkObserver = networkObserver; }); } @@ -309,7 +310,7 @@ - (void)suspend METHOD_LOG(); - dispatch_async(_sessionStateQueue, ^{ + tnl_dispatch_async_autoreleasing(_sessionStateQueue, ^{ self->_suspendCount++; }); } @@ -378,7 +379,7 @@ - (TNLRequestOperation *)enqueueRequest:(nullable id)request - (void)syncAddRequestOperation:(TNLRequestOperation *)op { - tnl_dispatch_sync_autoreleasing(_sessionStateQueue, ^{ + dispatch_sync(_sessionStateQueue, ^{ if (self->_suspendCount > 0) { [self->_stagedRequestOperations addObject:op]; } else { @@ -570,37 +571,47 @@ + (void)decrementExecutingNetworkConnections } } -+ (void)serviceUnavailableEncounteredForURL:(NSURL *)URL - retryAfterDelay:(NSTimeInterval)delay ++ (void)backoffSignalEncounteredForURL:(NSURL *)URL + host:(nullable NSString *)host + responseHTTPHeaders:(nullable NSDictionary *)headers { - [[TNLURLSessionManager sharedInstance] serviceUnavailableEncounteredForURL:URL - retryAfterDelay:delay]; + [[TNLURLSessionManager sharedInstance] backoffSignalEncounteredForURL:URL + host:host + responseHTTPHeaders:headers]; } + (void)HTTPURLResponseEncounteredOutsideOfTNL:(NSHTTPURLResponse *)response + host:(nullable NSString *)host { - if (response.statusCode == TNLHTTPStatusCodeServiceUnavailable) { - NSURL *URL = response.URL; - if (URL) { - NSTimeInterval delay = TNLGlobalServiceUnavailableRetryAfterBackoffValueDefault; - id retryAfterValue = response.tnl_parsedRetryAfterValue; - if ([retryAfterValue isKindOfClass:[NSNumber class]]) { - delay = [(NSNumber *)retryAfterValue doubleValue]; - } else if ([retryAfterValue isKindOfClass:[NSDate class]]) { - delay = [(NSDate *)retryAfterValue timeIntervalSinceNow]; - } - [self serviceUnavailableEncounteredForURL:URL retryAfterDelay:delay]; - } + NSURL *URL = response.URL; + if (!URL) { + return; } + + const TNLHTTPStatusCode statusCode = response.statusCode; + NSDictionary *headers = response.allHeaderFields; + const BOOL shouldSignal = [[TNLGlobalConfiguration sharedInstance].backoffSignaler tnl_shouldSignalBackoffForURL:URL + host:host + statusCode:statusCode + responseHeaders:headers]; + if (!shouldSignal) { + return; + } + + [self backoffSignalEncounteredForURL:URL + host:host + responseHTTPHeaders:headers]; } -+ (void)applyServiceUnavailableBackoffDependenciesToOperation:(NSOperation *)op - withURL:(NSURL *)URL - isLongPollRequest:(BOOL)isLongPoll ++ (void)applyBackoffDependenciesToOperation:(NSOperation *)op + withURL:(NSURL *)URL + host:(nullable NSString *)host + isLongPollRequest:(BOOL)isLongPoll { - [[TNLURLSessionManager sharedInstance] applyServiceUnavailableBackoffDependenciesToOperation:op - withURL:URL - isLongPollRequest:isLongPoll]; + [[TNLURLSessionManager sharedInstance] applyBackoffDependenciesToOperation:op + withURL:URL + host:host + isLongPollRequest:isLongPoll]; } @end diff --git a/Source/TNLRequestOperationQueue_Project.h b/Source/TNLRequestOperationQueue_Project.h index b309175..505d9bf 100644 --- a/Source/TNLRequestOperationQueue_Project.h +++ b/Source/TNLRequestOperationQueue_Project.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/23/14. -// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Copyright © 2020 Twitter, Inc. All rights reserved. // #import "TNLAttemptMetrics.h" diff --git a/Source/TNLRequestOperationState.h b/Source/TNLRequestOperationState.h index 5dca524..052e035 100644 --- a/Source/TNLRequestOperationState.h +++ b/Source/TNLRequestOperationState.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/23/14. -// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Copyright © 2020 Twitter, Inc. All rights reserved. // #import diff --git a/Source/TNLRequestOperation_Project.h b/Source/TNLRequestOperation_Project.h index a771b5f..2ccb930 100644 --- a/Source/TNLRequestOperation_Project.h +++ b/Source/TNLRequestOperation_Project.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/23/14. -// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Copyright © 2020 Twitter, Inc. All rights reserved. // #import "TNL_Project.h" diff --git a/Source/TNLRequestRedirecter.h b/Source/TNLRequestRedirecter.h index ad58234..1f37f39 100644 --- a/Source/TNLRequestRedirecter.h +++ b/Source/TNLRequestRedirecter.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 2/9/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLRequestRetryPolicyConfiguration.h b/Source/TNLRequestRetryPolicyConfiguration.h index f26ed70..62083cd 100644 --- a/Source/TNLRequestRetryPolicyConfiguration.h +++ b/Source/TNLRequestRetryPolicyConfiguration.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/26/14. -// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Copyright © 2020 Twitter, Inc. All rights reserved. // #import diff --git a/Source/TNLRequestRetryPolicyConfiguration.m b/Source/TNLRequestRetryPolicyConfiguration.m index e330b07..c696c2c 100644 --- a/Source/TNLRequestRetryPolicyConfiguration.m +++ b/Source/TNLRequestRetryPolicyConfiguration.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/26/14. -// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Copyright © 2020 Twitter, Inc. All rights reserved. // #import "TNL_Project.h" diff --git a/Source/TNLRequestRetryPolicyProvider.h b/Source/TNLRequestRetryPolicyProvider.h index 7fa7cfc..3b8f8c3 100644 --- a/Source/TNLRequestRetryPolicyProvider.h +++ b/Source/TNLRequestRetryPolicyProvider.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/26/14. -// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Copyright © 2020 Twitter, Inc. All rights reserved. // #import diff --git a/Source/TNLResponse.h b/Source/TNLResponse.h index 6b2c82c..79ddf4c 100644 --- a/Source/TNLResponse.h +++ b/Source/TNLResponse.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/23/14. -// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Copyright © 2020 Twitter, Inc. All rights reserved. // #import @@ -299,6 +299,9 @@ NS_SWIFT_NAME(response(request:operationError:info:metrics:)); /** The underlying attempt metrics as `TNLAttemptMetrics` objects */ @property (nonatomic, readonly, nullable) NSArray *attemptMetrics; +/** A description of the response metrics as a serializable dictionary object */ +- (NSDictionary *)dictionaryDescription:(BOOL)verbose; + /** Helper init for custom `TNLResponseMetrics` */ @@ -354,7 +357,7 @@ NS_SWIFT_NAME(response(request:operationError:info:metrics:)); mechanism encapsulated by `TNLRequestOperation`. @param duration The `totalDuration` of the metrics. `firstAttemptStartMachTime` will be `0`. - @param URLRequest The `NSURLRequest` for the encapsulated attempt + @param request The `NSURLRequest` for the encapsulated attempt @param URLResponse The `NSHTTPURLResponse` for the encapsulated attempt @param error The `NSError` for the encapsulated attempt diff --git a/Source/TNLResponse.m b/Source/TNLResponse.m index 754f94d..8b9674c 100644 --- a/Source/TNLResponse.m +++ b/Source/TNLResponse.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/23/14. -// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Copyright © 2020 Twitter, Inc. All rights reserved. // #import @@ -752,41 +752,31 @@ - (void)addMetaData:(TNLAttemptMetaData *)metaData taskMetrics:(NSURLSessionTask } } -- (NSString *)description +- (NSDictionary *)dictionaryDescription:(BOOL)verbose { NSMutableDictionary *topDictionary = [NSMutableDictionary dictionary]; topDictionary[@"complete"] = (_completeMachTime != 0) ? @"true" : @"false"; topDictionary[@"duration"] = @(self.totalDuration); - topDictionary[@"attemptTime"] = @(self.currentAttemptDuration); - topDictionary[@"queueTime"] = @(self.queuedDuration); - topDictionary[@"allAttemptsTime"] = @(self.allAttemptsDuration); + if (verbose) { + topDictionary[@"attemptTime"] = @(self.currentAttemptDuration); + topDictionary[@"queueTime"] = @(self.queuedDuration); + topDictionary[@"allAttemptsTime"] = @(self.allAttemptsDuration); + } NSMutableArray *attempts = [NSMutableArray arrayWithCapacity:self.attemptMetrics.count]; for (TNLAttemptMetrics *metrics in self.attemptMetrics) { - NSMutableDictionary *attemptDict = [NSMutableDictionary dictionary]; - attemptDict[@"duration"] = @(metrics.duration); - attemptDict[@"type"] = TNLAttemptTypeToString(metrics.attemptType); - if (metrics.metaData != nil) { - attemptDict[@"metadata"] = metrics.metaData; - } - attemptDict[@"statusCode"] = @(metrics.URLResponse.statusCode); - if (metrics.URLResponse.URL) { - attemptDict[@"URL"] = metrics.URLResponse.URL; - } - if (metrics.operationError) { - attemptDict[@"error"] = [NSString stringWithFormat:@"%@.%ld", metrics.operationError.domain, (long)metrics.operationError.code]; - } -#if DEBUG - if (metrics.taskTransactionMetrics) { - attemptDict[@"transactionMetrics"] = metrics.taskTransactionMetrics.tnl_dictionaryValue; - } -#endif + NSDictionary *attemptDict = [metrics dictionaryDescription:verbose]; [attempts addObject:attemptDict]; } if (attempts.count > 0) { topDictionary[@"attempts"] = attempts; } - return [NSString stringWithFormat:@"<%@ %p: %@>", NSStringFromClass([self class]), self, topDictionary]; + return topDictionary; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@ %p: %@>", NSStringFromClass([self class]), self, [self dictionaryDescription:NO]]; } - (NSUInteger)hash diff --git a/Source/TNLResponse_Project.h b/Source/TNLResponse_Project.h index 23220f7..3b96848 100644 --- a/Source/TNLResponse_Project.h +++ b/Source/TNLResponse_Project.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 9/17/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLResponse.h" diff --git a/Source/TNLSafeOperation.h b/Source/TNLSafeOperation.h index 96decec..a441b3f 100644 --- a/Source/TNLSafeOperation.h +++ b/Source/TNLSafeOperation.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 6/1/17 -// Copyright © 2017 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import @@ -21,3 +21,4 @@ NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END + diff --git a/Source/TNLSafeOperation.m b/Source/TNLSafeOperation.m index 91cfb64..aec0f9e 100644 --- a/Source/TNLSafeOperation.m +++ b/Source/TNLSafeOperation.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 6/1/17 -// Copyright © 2017 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNL_Project.h" @@ -61,3 +61,4 @@ - (void)tnl_clearCompletionBlock NS_ASSUME_NONNULL_END + diff --git a/Source/TNLSimpleRequestDelegate.h b/Source/TNLSimpleRequestDelegate.h index ef6f8ff..f47c86e 100644 --- a/Source/TNLSimpleRequestDelegate.h +++ b/Source/TNLSimpleRequestDelegate.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 11/25/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLRequestDelegate.h" diff --git a/Source/TNLSimpleRequestDelegate.m b/Source/TNLSimpleRequestDelegate.m index 1183794..0480229 100644 --- a/Source/TNLSimpleRequestDelegate.m +++ b/Source/TNLSimpleRequestDelegate.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 11/25/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNL_Project.h" diff --git a/Source/TNLTemporaryFile.h b/Source/TNLTemporaryFile.h index 2d30e24..912746c 100644 --- a/Source/TNLTemporaryFile.h +++ b/Source/TNLTemporaryFile.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 7/18/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLTemporaryFile.m b/Source/TNLTemporaryFile.m index 465d10b..1ca9483 100644 --- a/Source/TNLTemporaryFile.m +++ b/Source/TNLTemporaryFile.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 7/18/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNL_Project.h" diff --git a/Source/TNLTemporaryFile_Project.h b/Source/TNLTemporaryFile_Project.h index 4e9eb9a..fdfb95f 100644 --- a/Source/TNLTemporaryFile_Project.h +++ b/Source/TNLTemporaryFile_Project.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 7/18/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLTemporaryFile.h" diff --git a/Source/TNLTimeoutOperation.h b/Source/TNLTimeoutOperation.h index 9712f89..4511d6b 100644 --- a/Source/TNLTimeoutOperation.h +++ b/Source/TNLTimeoutOperation.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 12/7/17. -// Copyright © 2017 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLSafeOperation.h" diff --git a/Source/TNLTimeoutOperation.m b/Source/TNLTimeoutOperation.m index 8c9d5d8..f9094b5 100644 --- a/Source/TNLTimeoutOperation.m +++ b/Source/TNLTimeoutOperation.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 12/7/17. -// Copyright © 2017 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #include @@ -66,7 +66,9 @@ - (void)start if (_timeoutDuration > 0.0) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_timeoutDuration * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ - _complete(self); + @autoreleasepool { + _complete(self); + } }); } else { _complete(self); diff --git a/Source/TNLTiming.h b/Source/TNLTiming.h index 9e709f6..18784df 100644 --- a/Source/TNLTiming.h +++ b/Source/TNLTiming.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/12/16. -// Copyright © 2016 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLTiming.m b/Source/TNLTiming.m index 300d6ec..2670770 100644 --- a/Source/TNLTiming.m +++ b/Source/TNLTiming.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/12/16. -// Copyright © 2016 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLURLCoding.h b/Source/TNLURLCoding.h index e12e5ce..189a9f9 100644 --- a/Source/TNLURLCoding.h +++ b/Source/TNLURLCoding.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 7/28/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/Source/TNLURLCoding.m b/Source/TNLURLCoding.m index cdbb079..a1ca1a0 100644 --- a/Source/TNLURLCoding.m +++ b/Source/TNLURLCoding.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 7/28/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSNumber+TNLURLCoding.h" @@ -15,6 +15,7 @@ static NSString * __nullable TNLStringValue(id object, TNLURLEncodingOptions options, NSString * __nullable contextKey); +static NSString *TNLNumberStringValue(NSNumber *number); static id __nullable TNLURLEncodableValue(id value, TNLURLEncodableDictionaryOptions options, NSString * __nullable contextKey); @@ -283,6 +284,49 @@ static void TNLAppendParameterValue(NSMutableString *parameterString, return TNL_BITMASK_HAS_SUBSET_FLAGS(options, TNLURLEncodableDictionaryOptionOutputMutableDictionary) ? dict : [dict copy]; } +// This function is ~15% more performant than `-[NSNumber stringValue]` -- which matters when encoding values as rapidly and often as TNL does +static NSString *TNLNumberStringValue(NSNumber *number) +{ + static NSString * __nonnull kSmallPositives[] = { @"0", @"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9" }; + const char *objCType = [number objCType]; + if (objCType) { + switch (*objCType) { + case 'c': + case 'i': + case 's': + case 'l': + case 'q': + { + const long long value = [number longLongValue]; + if (value < 10 && value >= 0) { + return kSmallPositives[value]; + } + return [[NSString alloc] initWithFormat:@"%lli", value]; + } + case 'C': + case 'I': + case 'S': + case 'L': + case 'Q': + { + const unsigned long long value = [number unsignedLongLongValue]; + if (value < 10) { + return kSmallPositives[value]; + } + return [[NSString alloc] initWithFormat:@"%llu", value]; + } + case 'f': + return [[NSString alloc] initWithFormat:@"%0.7g", [number floatValue]]; + case 'd': + return [[NSString alloc] initWithFormat:@"%0.16g", [number doubleValue]]; + default: + break; + } + } + + return [number descriptionWithLocale:nil]; +} + static NSString *TNLStringValue(id object, TNLURLEncodingOptions options, NSString * __nullable contextKey) @@ -290,20 +334,15 @@ static void TNLAppendParameterValue(NSMutableString *parameterString, NSString *value = nil; if ([object isKindOfClass:[NSString class]]) { value = object; - } else if ([object respondsToSelector:@selector(tnl_URLEncodableStringValue)]) { - value = [object tnl_URLEncodableStringValue]; } else if ([object isKindOfClass:[NSNumber class]]) { - if ([object tnl_isBoolean]) { - if (TNL_BITMASK_HAS_SUBSET_FLAGS(options, TNLURLEncodingOptionEncodeBooleanNumbersAsTrueOrFalse)) { - // use "true"/"false" - value = [object boolValue] ? @"true" : @"false"; - } else { - // though `stringValue` should emit "1"/"0", we'll be robust in case this underlying implementation detail changes in the future - value = [object boolValue] ? @"1" : @"0"; - } + if (TNL_BITMASK_HAS_SUBSET_FLAGS(options, TNLURLEncodingOptionEncodeBooleanNumbersAsTrueOrFalse) && [object tnl_isBoolean]) { + // use "true"/"false" instead of default "1"/"0" + value = [object boolValue] ? @"true" : @"false"; } else { - value = [object stringValue]; + value = TNLNumberStringValue(object); } + } else if ([object respondsToSelector:@selector(tnl_URLEncodableStringValue)]) { + value = [object tnl_URLEncodableStringValue]; } if (!value && TNL_BITMASK_HAS_SUBSET_FLAGS(options, TNLURLEncodingOptionTreatUnsupportedValuesAsEmpty)) { @@ -342,4 +381,14 @@ static id TNLURLEncodableValue(id value, return returnValue; } +// Implemented in TNLURLCoding.m to utilize the static TNLNumberStringValue function +@implementation NSNumber (TNLStringCoding) + +- (NSString *)tnl_quickStringValue +{ + return TNLNumberStringValue(self); +} + +@end + NS_ASSUME_NONNULL_END diff --git a/Source/TNLURLSessionManager.h b/Source/TNLURLSessionManager.h index 9279678..3427014 100644 --- a/Source/TNLURLSessionManager.h +++ b/Source/TNLURLSessionManager.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/23/15. -// Copyright © 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import @@ -29,11 +29,6 @@ TNLMutableParametersFromURLSessionConfiguration(NSURLSessionConfiguration * __nu FOUNDATION_EXTERN BOOL TNLURLSessionIdentifierIsTaggedForTNL(NSString *identifier); -#pragma mark Constants - -FOUNDATION_EXTERN NSTimeInterval TNLGlobalServiceUnavailableRetryAfterBackoffValueDefault; -FOUNDATION_EXTERN NSTimeInterval TNLGlobalServiceUnavailableRetryAfterMaximumBackoffValueBeforeTreatedAsGoAway; - #pragma mark Declarations typedef void(^TNLRequestOperationQueueFindTaskOperationCompleteBlock)(TNLURLSessionTaskOperation * taskOp); @@ -61,12 +56,15 @@ typedef void(^TNLURLSessionManagerGetAllSessionsCallback)(NSArray *)headers; +@property (atomic) TNLGlobalConfigurationBackoffMode backoffMode; +@property (atomic, null_resettable) id backoffBehaviorProvider; - (void)pruneUnusedURLSessions; - (void)pruneURLSessionMatchingRequestConfiguration:(TNLRequestConfiguration *)config diff --git a/Source/TNLURLSessionManager.m b/Source/TNLURLSessionManager.m index 64b7d2d..7ab4d3e 100644 --- a/Source/TNLURLSessionManager.m +++ b/Source/TNLURLSessionManager.m @@ -3,19 +3,23 @@ // TwitterNetworkLayer // // Created on 10/23/15. -// Copyright © 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #include #include #import "NSCachedURLResponse+TNLAdditions.h" +#import "NSDictionary+TNLAdditions.h" #import "NSOperationQueue+TNLSafety.h" +#import "NSURLAuthenticationChallenge+TNLAdditions.h" +#import "NSURLResponse+TNLAdditions.h" #import "NSURLSessionConfiguration+TNLAdditions.h" #import "NSURLSessionTaskMetrics+TNLAdditions.h" #import "TNL_Project.h" #import "TNLAuthenticationChallengeHandler.h" #import "TNLBackgroundURLSessionTaskOperationManager.h" +#import "TNLBackoff.h" #import "TNLGlobalConfiguration_Project.h" #import "TNLLRUCache.h" #import "TNLNetwork.h" @@ -34,9 +38,6 @@ static const NSUInteger kMaxURLSessionContextCount = 12; -NSTimeInterval TNLGlobalServiceUnavailableRetryAfterBackoffValueDefault = 1.0; -NSTimeInterval TNLGlobalServiceUnavailableRetryAfterMaximumBackoffValueBeforeTreatedAsGoAway = 10.0; - static NSString * const kInAppURLSessionContextIdentifier = @"tnl.op.queue"; static NSString * const kManagerVersionKey = @"smv"; @@ -44,10 +45,12 @@ static NSString *_GenerateReuseIdentifier(NSString * __nullable operationQueueId, NSString *URLSessionConfigurationIdentificationString, TNLRequestExecutionMode executionmode); static void _ConfigureSessionConfigurationWithRequestConfiguration(NSURLSessionConfiguration * __nullable sessionConfig, TNLRequestConfiguration * requestConfig); -static NSString * __nullable _ServiceUnavailableBackoffKeyFromURL(const TNLGlobalConfigurationServiceUnavailableBackoffMode mode, NSURL *URL); +static NSString * __nullable _BackoffKeyFromURL(const TNLGlobalConfigurationBackoffMode mode, NSURL *URL, NSString * __nullable host); static void TNLMutableParametersStripNonURLSessionProperties(TNLMutableParameterCollection *params); static void TNLMutableParametersStripNonBackgroundURLSessionProperties(TNLMutableParameterCollection *params); static void TNLMutableParametersStripOverriddenURLSessionProperties(TNLMutableParameterCollection *params); +typedef BOOL (^_FilterBlock)(id obj); +static NSArray *_FilterArray(NSArray *source, _FilterBlock filterBlock); #pragma mark - Global Session Management @@ -56,14 +59,16 @@ static dispatch_queue_t sSynchronizeQueue; static NSOperationQueue *sSynchronizeOperationQueue; static NSOperationQueue *sURLSessionTaskOperationQueue; -static BOOL sSynchronizeOperationQueueIsBackedBySynchronizeQueue = NO; static TNLURLSessionContextLRUCacheDelegate *sSessionContextsDelegate; static TNLLRUCache *sAppSessionContexts; static TNLLRUCache *sBackgroundSessionContexts; static NSMutableSet *sActiveURLSessionTaskOperations; static NSMutableDictionary *sBackgroundSessionCompletionHandlerDictionary; static NSMutableDictionary *> *sOutstandingBackoffOperations = nil; -static TNLGlobalConfigurationServiceUnavailableBackoffMode sBackoffMode = TNLGlobalConfigurationServiceUnavailableBackoffModeDisabled; +static NSMutableDictionary *> *sOutstandingSerializeOperations = nil; +static NSTimeInterval sSerialDelayDuration = 0.0; +static TNLGlobalConfigurationBackoffMode sBackoffMode = TNLGlobalConfigurationBackoffModeDisabled; +static id sBackoffBehaviorProvider = nil; #pragma mark - Session Context @@ -133,13 +138,15 @@ static void _synchronize_removeContext(PRIVATE_SELF(TNLURLSessionManagerV1), TNLURLSessionContext *context); static void _synchronize_storeContext(PRIVATE_SELF(TNLURLSessionManagerV1), TNLURLSessionContext *context); -static void _synchronize_applyServiceUnavailableBackoffDependencies(PRIVATE_SELF(TNLURLSessionManagerV1), - NSOperation *op, - NSURL *URL, - BOOL isLongPoll); -static void _synchronize_serviceUnavailableEncountered(PRIVATE_SELF(TNLURLSessionManagerV1), - NSURL *URL, - NSTimeInterval retryAfterDelay); +static void _synchronize_applyBackoffDependencies(PRIVATE_SELF(TNLURLSessionManagerV1), + NSOperation *op, + NSURL *URL, + NSString * __nullable host, + BOOL isLongPoll); +static void _synchronize_backoffSignalEncountered(PRIVATE_SELF(TNLURLSessionManagerV1), + NSURL *URL, + NSString * __nullable host, + NSDictionary * __nullable headers); static void _synchronize_pruneLimit(PRIVATE_SELF(TNLURLSessionManagerV1)); static void _synchronize_pruneUnused(PRIVATE_SELF(TNLURLSessionManagerV1)); static void _synchronize_pruneConfig(PRIVATE_SELF(TNLURLSessionManagerV1), @@ -148,6 +155,20 @@ static void _synchronize_pruneConfig(PRIVATE_SELF(TNLURLSessionManagerV1), static void _executeOnSynchronizeGCDQueueFromSynchronizeOperationQueue(dispatch_block_t block); +static void _private_notifyAuthChallengeCanceled(PRIVATE_SELF(TNLURLSessionManagerV1), + TNLURLSessionTaskOperation * __nullable operation, + NSURLAuthenticationChallenge *challenge, + id __nullable handler, + NSURLSession *session, + id __nullable cancelContext); +static void _private_handleAuthChallenge(PRIVATE_SELF(TNLURLSessionManagerV1), + NSURLAuthenticationChallenge *challenge, + NSURLSession *session, + TNLURLSessionTaskOperation * __nullable operation, + NSNumber * __nullable currentDisposition, + NSMutableArray> * remainingAuthChallengeHandlers, + TNLURLSessionAuthChallengeCompletionBlock completion); + @end /** @@ -340,54 +361,80 @@ - (void)syncAddURLSessionTaskOperation:(TNLURLSessionTaskOperation *)op const NSTimeInterval attemptTimeout = op.requestConfiguration.attemptTimeout; const BOOL isLongPollRequest = (attemptTimeout < 1.0) || (attemptTimeout >= NSTimeIntervalSince1970); NSURL *URL = op.hydratedURLRequest.URL; - _synchronize_applyServiceUnavailableBackoffDependencies(self, - op, - URL, - isLongPollRequest); + NSString *host = nil; + if ([TNLGlobalConfiguration sharedInstance].shouldBackoffUseOriginalRequestHost) { + host = op.originalURLRequest.URL.host; + } + _synchronize_applyBackoffDependencies(self, + op, + URL, + host, + isLongPollRequest); [sURLSessionTaskOperationQueue tnl_safeAddOperation:op]; }); } -- (void)applyServiceUnavailableBackoffDependenciesToOperation:(NSOperation *)op - withURL:(NSURL *)URL - isLongPollRequest:(BOOL)isLongPoll +- (void)applyBackoffDependenciesToOperation:(NSOperation *)op + withURL:(NSURL *)URL + host:(nullable NSString *)host + isLongPollRequest:(BOOL)isLongPoll { dispatch_sync(sSynchronizeQueue, ^{ - _synchronize_applyServiceUnavailableBackoffDependencies(self, - op, - URL, - isLongPoll); + _synchronize_applyBackoffDependencies(self, + op, + URL, + host, + isLongPoll); }); } -- (void)serviceUnavailableEncounteredForURL:(NSURL *)URL - retryAfterDelay:(NSTimeInterval)delay +- (void)backoffSignalEncounteredForURL:(NSURL *)URL + host:(nullable NSString *)host + responseHTTPHeaders:(nullable NSDictionary *)headers { tnl_dispatch_async_autoreleasing(sSynchronizeQueue, ^{ - _synchronize_serviceUnavailableEncountered(self, URL, delay); + _synchronize_backoffSignalEncountered(self, URL, host, headers); }); } -- (void)setServiceUnavailableBackoffMode:(TNLGlobalConfigurationServiceUnavailableBackoffMode)mode +- (void)setBackoffMode:(TNLGlobalConfigurationBackoffMode)mode { tnl_dispatch_async_autoreleasing(sSynchronizeQueue, ^{ if (sBackoffMode != mode) { sBackoffMode = mode; // reset our backoffs [sOutstandingBackoffOperations removeAllObjects]; + [sOutstandingSerializeOperations removeAllObjects]; } }); } -- (TNLGlobalConfigurationServiceUnavailableBackoffMode)serviceUnavailableBackoffMode +- (TNLGlobalConfigurationBackoffMode)backoffMode { - __block TNLGlobalConfigurationServiceUnavailableBackoffMode mode; + __block TNLGlobalConfigurationBackoffMode mode; dispatch_sync(sSynchronizeQueue, ^{ mode = sBackoffMode; }); return mode; } +- (void)setBackoffBehaviorProvider:(nullable id)provider +{ + tnl_dispatch_async_autoreleasing(sSynchronizeQueue, ^{ + sBackoffBehaviorProvider = provider ?: [[TNLSimpleBackoffBehaviorProvider alloc] init]; + // does not affect our existing backoffs + }); +} + +- (id)backoffBehaviorProvider +{ + __block id provider; + dispatch_sync(sSynchronizeQueue, ^{ + provider = sBackoffBehaviorProvider; + }); + return provider; +} + - (void)pruneUnusedURLSessions { tnl_dispatch_async_autoreleasing(sSynchronizeQueue, ^{ @@ -410,10 +457,12 @@ @implementation TNLURLSessionManagerV1 (Synchronize) static void _executeOnSynchronizeGCDQueueFromSynchronizeOperationQueue(dispatch_block_t block) { - if (dispatch_queue_get_label(sSynchronizeQueue) == dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)) { - block(); - } else { - tnl_dispatch_sync_autoreleasing(sSynchronizeQueue, block); + @autoreleasepool { + if (dispatch_queue_get_label(sSynchronizeQueue) == dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)) { + block(); + } else { + dispatch_sync(sSynchronizeQueue, block); + } } } @@ -588,7 +637,7 @@ static void _synchronize_dissassociateURLSessionTaskOperation(PRIVATE_SELF(TNLUR delegate:self delegateQueue:sSynchronizeOperationQueue]; - static volatile atomic_int_fast64_t __attribute__((aligned(8))) sSessionId = 0; + static volatile atomic_int_fast64_t __attribute__((aligned(8))) sSessionId = ATOMIC_VAR_INIT(0); const int64_t sessionId = atomic_fetch_add(&sSessionId, 1); context = [[TNLURLSessionContext alloc] initWithURLSession:session @@ -732,82 +781,135 @@ static void _synchronize_pruneConfig(PRIVATE_SELF(TNLURLSessionManagerV1), } } -static void _synchronize_applyServiceUnavailableBackoffDependencies(PRIVATE_SELF(TNLURLSessionManagerV1), - NSOperation *op, - NSURL *URL, - BOOL isLongPoll) +static void _synchronize_applyBackoffDependencies(PRIVATE_SELF(TNLURLSessionManagerV1), + NSOperation *op, + NSURL *URL, + NSString * __nullable host, + BOOL isLongPoll) { if (!self) { return; } // get the key (depends on the mode) - NSString *key = _ServiceUnavailableBackoffKeyFromURL(sBackoffMode, URL); + NSString *key = _BackoffKeyFromURL(sBackoffMode, URL, host); if (!key) { // no key, no dependencies to apply return; } - // do we have backoff ops? - NSHashTable *ops = sOutstandingBackoffOperations[key]; - if (!ops) { - return; + NSHashTable *serialOps = sOutstandingSerializeOperations[key]; + NSArray *serialOpsArray = _FilterArray(serialOps.allObjects, ^BOOL(NSOperation * obj){ + return !obj.isFinished; + }); + if (!serialOpsArray.count) { + // no serial ops left, clear it + [sOutstandingSerializeOperations removeObjectForKey:key]; + serialOps = nil; + serialOpsArray = nil; + } + + NSHashTable *backoffOps = sOutstandingBackoffOperations[key]; + NSArray *backoffOpsArray = _FilterArray(backoffOps.allObjects, ^BOOL(NSOperation * obj){ + return !obj.isFinished; + }); + if (!backoffOpsArray.count) { + if (!serialOps) { + // no backoff ops left, clear it + [sOutstandingBackoffOperations removeObjectForKey:key]; + backoffOps = nil; + backoffOpsArray = nil; + } else if (!backoffOps) { + // serial ops but no backoff ops, establish an empty hash-table to populate + backoffOps = [NSHashTable weakObjectsHashTable]; + sOutstandingBackoffOperations[key] = backoffOps; + } } - // we have an in process backoff! - NSArray *opsArray = ops.allObjects; - if (!opsArray.count) { - // no backoff ops left, clear it out - [sOutstandingBackoffOperations removeObjectForKey:key]; + // No backoff ops or serializing ops to back off with + if (!serialOps && !backoffOps) { return; } + TNLAssert(backoffOps != nil); // make this new operation dependent on prior backoff ops - for (NSOperation *otherOp in opsArray) { + for (NSOperation *otherOp in backoffOpsArray) { [op addDependency:otherOp]; } - // store the op if not a long poll request - if (!isLongPoll) { - [ops addObject:op]; + // add serial delay to slow things down while running serially + if (sSerialDelayDuration > 0 && serialOps.count > 0) { + NSOperation *timeoutOperation = [[TNLTimeoutOperation alloc] initWithTimeoutDuration:sSerialDelayDuration]; + for (NSOperation *dep in op.dependencies) { + [timeoutOperation addDependency:dep]; + } + [backoffOps addObject:timeoutOperation]; + [sURLSessionTaskOperationQueue tnl_safeAddOperation:timeoutOperation]; + } + + // store the op if not a long poll request AND we are still in a serialization mode + if (!isLongPoll && serialOps.count > 0) { + [backoffOps addObject:op]; } } -static void _synchronize_serviceUnavailableEncountered(PRIVATE_SELF(TNLURLSessionManagerV1), - NSURL *URL, - NSTimeInterval backoffDuration) +static void _synchronize_backoffSignalEncountered(PRIVATE_SELF(TNLURLSessionManagerV1), + NSURL *URL, + NSString * __nullable host, + NSDictionary * __nullable headers) { if (!self) { return; } - if (backoffDuration < 0.1) { - backoffDuration = 0.1; - } else if (backoffDuration > TNLGlobalServiceUnavailableRetryAfterMaximumBackoffValueBeforeTreatedAsGoAway) { - // too long to be treated as a backoff, fall back to the reasonable default - backoffDuration = TNLGlobalServiceUnavailableRetryAfterBackoffValueDefault; - } - - NSString *key = _ServiceUnavailableBackoffKeyFromURL(sBackoffMode, URL); + NSString *key = _BackoffKeyFromURL(sBackoffMode, URL, host); if (!key) { // no key, no backoff to apply return; } - NSOperation *timeoutOperation = [[TNLTimeoutOperation alloc] initWithTimeoutDuration:backoffDuration]; - NSHashTable *ops = sOutstandingBackoffOperations[key]; - if (!ops) { - ops = [NSHashTable weakObjectsHashTable]; - sOutstandingBackoffOperations[key] = ops; + const TNLBackoffBehavior backoffBehavior = [sBackoffBehaviorProvider tnl_backoffBehaviorForURL:URL + responseHeaders:headers]; + + if (backoffBehavior.backoffDuration > 0) { + NSOperation *timeoutOperation = [[TNLTimeoutOperation alloc] initWithTimeoutDuration:backoffBehavior.backoffDuration]; + NSHashTable *ops = sOutstandingBackoffOperations[key]; + if (!ops) { + ops = [NSHashTable weakObjectsHashTable]; + sOutstandingBackoffOperations[key] = ops; + } + + // make all outstanding backed off ops depend on this new backoff op + for (NSOperation *op in ops.allObjects) { + [op addDependency:timeoutOperation]; + } + + [ops addObject:timeoutOperation]; + [sURLSessionTaskOperationQueue tnl_safeAddOperation:timeoutOperation]; + + // also add to the outstanding serialization ops + NSHashTable *serialOps = sOutstandingSerializeOperations[key]; + if (!serialOps) { + serialOps = [NSHashTable weakObjectsHashTable]; + sOutstandingSerializeOperations[key] = serialOps; + } + [serialOps addObject:timeoutOperation]; } - // make all outstanding backed off ops depend on this new backoff op - for (NSOperation *op in ops.allObjects) { - [op addDependency:timeoutOperation]; + if (backoffBehavior.serializeDuration > 0) { + NSOperation *timeoutOperation = [[TNLTimeoutOperation alloc] initWithTimeoutDuration:backoffBehavior.serializeDuration]; + NSHashTable *ops = sOutstandingSerializeOperations[key]; + if (!ops) { + ops = [NSHashTable weakObjectsHashTable]; + sOutstandingSerializeOperations[key] = ops; + } + + // track the new serialize timer + [ops addObject:timeoutOperation]; + [sURLSessionTaskOperationQueue tnl_safeAddOperation:timeoutOperation]; } - [ops addObject:timeoutOperation]; - [sURLSessionTaskOperationQueue tnl_safeAddOperation:timeoutOperation]; + sSerialDelayDuration = backoffBehavior.serialDelayDuration; } @end @@ -839,29 +941,70 @@ - (void)URLSession:(NSURLSession *)session METHOD_LOG(); NSMutableArray> *handlers = [[[TNLGlobalConfiguration sharedInstance] internalAuthenticationChallengeHandlers] mutableCopy]; - [self private_handleAuthChallenge:challenge - session:session - operation:nil - currentDisposition:nil - remainingAuthChallengeHandlers:handlers - completion:completionHandler]; -} - -- (void)private_handleAuthChallenge:(NSURLAuthenticationChallenge *)challenge - session:(NSURLSession *)session - operation:(nullable TNLURLSessionTaskOperation *)operation - currentDisposition:(nullable NSNumber *)currentDisposition - remainingAuthChallengeHandlers:(NSMutableArray> *)handlers - completion:(TNLURLSessionAuthChallengeCompletionBlock)completion -{ - void (^challengeBlock)(id handler, NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential); - challengeBlock = ^(id handler, NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential) { + _private_handleAuthChallenge(self, + challenge, + session, + nil /*operation*/, + nil /*currentDisposition*/, + handlers, + completionHandler); +} + +static void _private_notifyAuthChallengeCanceled(PRIVATE_SELF(TNLURLSessionManagerV1), + TNLURLSessionTaskOperation * __nullable operation, + NSURLAuthenticationChallenge *challenge, + id __nullable handler, + NSURLSession *session, + id __nullable cancelContext) +{ + if (!self) { + return; + } + + if (operation) { + // just the provided operation + if ((id)[NSNull null] != operation) { + [operation handler:handler + didCancelAuthenticationChallenge:challenge + forURLSession:session + context:cancelContext]; + } + return; + } + + // all the downstream operations + _executeOnSynchronizeGCDQueueFromSynchronizeOperationQueue(^{ + TNLURLSessionContext *context = _synchronize_getSessionContext(self, session); + for (TNLURLSessionTaskOperation *op in context.URLSessionTaskOperations) { + [op handler:handler + didCancelAuthenticationChallenge:challenge + forURLSession:session + context:cancelContext]; + } + }); +} + +static void _private_handleAuthChallenge(PRIVATE_SELF(TNLURLSessionManagerV1), + NSURLAuthenticationChallenge *challenge, + NSURLSession *session, + TNLURLSessionTaskOperation * __nullable operation, + NSNumber * __nullable currentDisposition, + NSMutableArray> * handlers, + TNLURLSessionAuthChallengeCompletionBlock completion) +{ + if (!self) { + return; + } + + void (^challengeBlock)(id handler, NSURLSessionAuthChallengeDisposition disposition, id credentialOrContext); + challengeBlock = ^(id handler, NSURLSessionAuthChallengeDisposition disposition, id credentialOrContext) { NSNumber *newDisposition = currentDisposition; switch (disposition) { case NSURLSessionAuthChallengeUseCredential: { // There are credentials! Done! - completion(disposition, credential); + TNLAssert(!credentialOrContext || [credentialOrContext isKindOfClass:[NSURLCredential class]]); + completion(disposition, (NSURLCredential *)credentialOrContext); return; } case NSURLSessionAuthChallengeCancelAuthenticationChallenge: @@ -869,24 +1012,12 @@ - (void)private_handleAuthChallenge:(NSURLAuthenticationChallenge *)challenge // The challenge is forced to cancel! // 1) notify downstream request operations - if (operation) { - // just the provided operation - if ((id)[NSNull null] != operation) { - [operation handler:handler - didCancelAuthenticationChallenge:challenge - forURLSession:session]; - } - } else { - // all the downstream operations - _executeOnSynchronizeGCDQueueFromSynchronizeOperationQueue(^{ - TNLURLSessionContext *context = _synchronize_getSessionContext(self, session); - for (TNLURLSessionTaskOperation *op in context.URLSessionTaskOperations) { - [op handler:handler - didCancelAuthenticationChallenge:challenge - forURLSession:session]; - } - }); - } + _private_notifyAuthChallengeCanceled(self, + operation, + challenge, + handler, + session, + credentialOrContext); // 2) complete completion(disposition, nil); @@ -905,12 +1036,13 @@ - (void)private_handleAuthChallenge:(NSURLAuthenticationChallenge *)challenge } // default: keep the disposition unchanged } - [self private_handleAuthChallenge:challenge - session:session - operation:operation - currentDisposition:newDisposition - remainingAuthChallengeHandlers:handlers - completion:completion]; + _private_handleAuthChallenge(self, + challenge, + session, + operation, + newDisposition, + handlers, + completion); }; if (currentDisposition) { @@ -1009,12 +1141,13 @@ - (void)URLSession:(NSURLSession *)session } } - [self private_handleAuthChallenge:challenge - session:session - operation:op ?: (id)[NSNull null] - currentDisposition:nil - remainingAuthChallengeHandlers:handlers - completion:completionHandler]; + _private_handleAuthChallenge(self, + challenge, + session, + op ?: (id)[NSNull null], + nil /*currentDisposition*/, + handlers, + completionHandler); } - (void)URLSession:(NSURLSession *)session @@ -1697,6 +1830,42 @@ static void TNLMutableParametersStripOverriddenURLSessionProperties(TNLMutablePa params[TNLRequestConfigurationPropertyKeyNetworkServiceType] = nil; } +static NSArray *_FilterArray(NSArray *source, _FilterBlock filterBlock) +{ + NSMutableIndexSet *set = [[NSMutableIndexSet alloc] init]; + [source enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if (filterBlock(obj)) { + [set addIndex:idx]; + } + }]; + + if (set.count == 0) { + // no matches + return nil; + } + + if (((set.lastIndex - set.firstIndex) + 1) == set.count) { + // contiguous! + + if (set.firstIndex == 0 && set.lastIndex == (source.count - 1)) { + // same as source + return [source copy]; + } else { + return [source subarrayWithRange:NSMakeRange(set.firstIndex, set.count)]; + } + } + + // non-contiguous + + NSMutableArray *destination = [[NSMutableArray alloc] initWithCapacity:set.count]; + [source enumerateObjectsAtIndexes:set + options:0 + usingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + [destination addObject:obj]; + }]; + return [destination copy]; +} + BOOL TNLURLSessionIdentifierIsTaggedForTNL(NSString *identifier) { return [identifier hasPrefix:[TNLTwitterNetworkLayerURLScheme stringByAppendingString:@"://"]]; @@ -1790,15 +1959,26 @@ static void _ConfigureSessionConfigurationWithRequestConfiguration(NSURLSessionC sessionConfig.timeoutIntervalForResource = (requestConfig.attemptTimeout < MIN_TIMER_INTERVAL) ? NSTimeIntervalSince1970 : requestConfig.attemptTimeout; } -static NSString * __nullable _ServiceUnavailableBackoffKeyFromURL(const TNLGlobalConfigurationServiceUnavailableBackoffMode mode, - NSURL *URL) +static NSString * __nullable _BackoffKeyFromURL(const TNLGlobalConfigurationBackoffMode mode, + NSURL *URL, + NSString * __nullable host) { + if (TNLGlobalConfigurationBackoffModeDisabled == mode) { + // return early to avoid the lowercase string overhead + return nil; + } + + if (host) { + host = host.lowercaseString; + } else { + host = URL.host.lowercaseString; + } + switch (mode) { - case TNLGlobalConfigurationServiceUnavailableBackoffModeKeyOffHost: - return [URL.host lowercaseString]; - case TNLGlobalConfigurationServiceUnavailableBackoffModeKeyOffHostAndPath: + case TNLGlobalConfigurationBackoffModeKeyOffHost: + return host; + case TNLGlobalConfigurationBackoffModeKeyOffHostAndPath: { - NSString *host = [URL.host lowercaseString]; NSString *path = [URL.path lowercaseString]; const BOOL pathPrefixedWithSlash = [path hasPrefix:@"/"]; @@ -1815,7 +1995,7 @@ static void _ConfigureSessionConfigurationWithRequestConfiguration(NSURLSessionC return [NSString stringWithFormat:@"%@%@%@", host, (pathPrefixedWithSlash) ? @"" : @"/", path]; } - case TNLGlobalConfigurationServiceUnavailableBackoffModeDisabled: + case TNLGlobalConfigurationBackoffModeDisabled: return nil; } @@ -1834,28 +2014,23 @@ static void _PrepareSessionManagement() sSynchronizeOperationQueue = [[NSOperationQueue alloc] init]; sSynchronizeOperationQueue.name = @"TNLURLSessionManager.synchronize.operation.queue"; sSynchronizeOperationQueue.maxConcurrentOperationCount = 1; - if ([sSynchronizeOperationQueue respondsToSelector:@selector(setQualityOfService:)]) { - sSynchronizeOperationQueue.qualityOfService = (NSQualityOfServiceUtility + NSQualityOfServiceUserInitiated / 2); - } - if ([sSynchronizeOperationQueue respondsToSelector:@selector(setUnderlyingQueue:)]) { - sSynchronizeOperationQueue.underlyingQueue = sSynchronizeQueue; - sSynchronizeOperationQueueIsBackedBySynchronizeQueue = YES; - } + sSynchronizeOperationQueue.qualityOfService = (NSQualityOfServiceUtility + NSQualityOfServiceUserInitiated / 2); + sSynchronizeOperationQueue.underlyingQueue = sSynchronizeQueue; sURLSessionTaskOperationQueue = [[NSOperationQueue alloc] init]; sURLSessionTaskOperationQueue.name = @"TNLURLSessionManager.task.operation.queue"; sURLSessionTaskOperationQueue.maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount; - if ([sURLSessionTaskOperationQueue respondsToSelector:@selector(setQualityOfService:)]) { - sURLSessionTaskOperationQueue.qualityOfService = (NSQualityOfServiceUtility + NSQualityOfServiceUserInitiated / 2); - } + sURLSessionTaskOperationQueue.qualityOfService = (NSQualityOfServiceUtility + NSQualityOfServiceUserInitiated / 2); // State sOutstandingBackoffOperations = [[NSMutableDictionary alloc] init]; + sOutstandingSerializeOperations = [[NSMutableDictionary alloc] init]; sSessionContextsDelegate = [[TNLURLSessionContextLRUCacheDelegate alloc] init]; sAppSessionContexts = [[TNLLRUCache alloc] initWithEntries:nil delegate:sSessionContextsDelegate]; sBackgroundSessionContexts = [[TNLLRUCache alloc] initWithEntries:nil delegate:sSessionContextsDelegate]; sActiveURLSessionTaskOperations = [[NSMutableSet alloc] init]; sBackgroundSessionCompletionHandlerDictionary = [[NSMutableDictionary alloc] init]; + sBackoffBehaviorProvider = [[TNLSimpleBackoffBehaviorProvider alloc] init]; }); } diff --git a/Source/TNLURLSessionTaskOperation.h b/Source/TNLURLSessionTaskOperation.h index fad30d2..a77a568 100644 --- a/Source/TNLURLSessionTaskOperation.h +++ b/Source/TNLURLSessionTaskOperation.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 6/11/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLAttemptMetaData.h" @@ -70,9 +70,10 @@ NS_ASSUME_NONNULL_BEGIN @end @interface TNLURLSessionTaskOperation (TNLURLSessionManagerMethods) -- (void)handler:(id)handler +- (void)handler:(nullable id)handler didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge - forURLSession:(NSURLSession *)session; + forURLSession:(NSURLSession *)session + context:(nullable id)cancelContext; @end @interface TNLURLSessionTaskOperation (TNLRequestOperationMethods) diff --git a/Source/TNLURLSessionTaskOperation.m b/Source/TNLURLSessionTaskOperation.m index b6b0f90..dd4455e 100644 --- a/Source/TNLURLSessionTaskOperation.m +++ b/Source/TNLURLSessionTaskOperation.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 6/11/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #include @@ -21,6 +21,7 @@ #import "TNL_Project.h" #import "TNLAttemptMetaData_Project.h" #import "TNLAttemptMetrics.h" +#import "TNLBackoff.h" #import "TNLContentCoding.h" #import "TNLError.h" #import "TNLGlobalConfiguration.h" @@ -425,7 +426,7 @@ - (void)setURLSession:(NSURLSession *)URLSession supportsTaskMetrics:(BOOL)taskM TNLAssert(URLSession != nil); _URLSession = URLSession; if (!taskMetrics) { - dispatch_async(tnl_network_queue(), ^{ + tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ // Task metrics are explicity not supported self->_flags.encounteredCompletionBeforeTaskMetrics = 1; }); @@ -682,15 +683,20 @@ - (void)URLSession:(NSURLSession *)session }); } -- (void)handler:(id)handler +- (void)handler:(nullable id)handler didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge forURLSession:(NSURLSession *)session + context:(nullable id)cancelContext { NSURLProtectionSpace *protectionSpace = challenge.protectionSpace; NSString *protectionSpaceHost = protectionSpace.host; NSURLRequest *currentRequest = self.currentURLRequest; NSArray *certDescriptions = TNLSecTrustGetCertificateChainDescriptions(protectionSpace.serverTrust); NSString *authMethod = protectionSpace.authenticationMethod; + NSHTTPURLResponse *failedResponse = (id)challenge.failureResponse; + if (failedResponse && ![failedResponse isKindOfClass:[NSHTTPURLResponse class]]) { + failedResponse = nil; + } tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{ NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init]; @@ -703,9 +709,20 @@ - (void)handler:(id)handler if (authMethod) { userInfo[TNLErrorAuthenticationChallengeMethodKey] = authMethod; } + if (challenge.protectionSpace.realm != nil) { + userInfo[TNLErrorAuthenticationChallengeRealmKey] = challenge.protectionSpace.realm; + } if (certDescriptions) { userInfo[TNLErrorCertificateChainDescriptionsKey] = certDescriptions; } + if (cancelContext) { + userInfo[TNLErrorAuthenticationChallengeCancelContextKey] = cancelContext; + } + if (failedResponse) { + userInfo[TNLErrorResponseKey] = failedResponse; + // ... and, if we have the cancelled response, cache it for completion handling + self->_cancelledRedirectResponse = failedResponse; + } self->_authChallengeCancelledUserInfo = [userInfo copy]; }); } @@ -860,7 +877,7 @@ static void _network_handleRedirect(SELF_ARG, if (sanitiziationError) { _network_fail(self, sanitiziationError); return; - } if (self.isComplete || self.finalizing) { + } else if (self.isComplete || self.finalizing) { return; } else if (_network_shouldCancel(self)) { _network_cancel(self); @@ -982,7 +999,9 @@ - (void)URLSession:(NSURLSession *)session self->_cachedCompletionError = theError; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kTaskMetricsNotSeenOnCompletionDelayCompletionDuration * NSEC_PER_SEC)), tnl_network_queue(), ^{ - _network_completeCachedCompletionIfPossible(self); + @autoreleasepool { + _network_completeCachedCompletionIfPossible(self); + } }); return; @@ -2110,16 +2129,24 @@ static void _network_transitionState(SELF_ARG, // stop handling responses self->_flags.shouldCaptureResponse = 0; - // service unavailable (HTTP 503) signal + // check for backoff signal TNLResponseInfo *info = self->_finalResponse.info; - if (info.statusCode == TNLHTTPStatusCodeServiceUnavailable) { - const NSTimeInterval delay = (info.hasRetryAfterHeader) ? - [info retryAfterDelayFromNow] : - TNLGlobalServiceUnavailableRetryAfterBackoffValueDefault; - [TNLNetwork serviceUnavailableEncounteredForURL:self->_finalResponse.info.finalURL - retryAfterDelay:delay]; + const TNLHTTPStatusCode statusCode = info.statusCode; + NSDictionary *headers = info.allHTTPHeaderFieldsWithLowerCaseKeys; + NSString *host = nil; + NSURL *URL = self->_hydratedURLRequest.URL; + if ([TNLGlobalConfiguration sharedInstance].shouldBackoffUseOriginalRequestHost) { + host = [self->_originalRequest URL].host; + } + const BOOL shouldSignal = [[TNLGlobalConfiguration sharedInstance].backoffSignaler tnl_shouldSignalBackoffForURL:URL + host:host + statusCode:statusCode + responseHeaders:headers]; + if (shouldSignal) { + [TNLNetwork backoffSignalEncounteredForURL:URL + host:host + responseHTTPHeaders:headers]; } - } if (executingDidChange) { diff --git a/Source/TNLURLStringCoding.m b/Source/TNLURLStringCoding.m index 99ac2e6..151bb50 100644 --- a/Source/TNLURLStringCoding.m +++ b/Source/TNLURLStringCoding.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 7/28/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNL_Project.h" diff --git a/Source/TNL_Project.h b/Source/TNL_Project.h index 7fd64e7..fc23fff 100644 --- a/Source/TNL_Project.h +++ b/Source/TNL_Project.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/24/14. -// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Copyright © 2020 Twitter, Inc. All rights reserved. // #import "TNL_ProjectCommon.h" @@ -104,6 +104,7 @@ NS_INLINE void tnl_dispatch_timer_invalidate(dispatch_source_t __nullable timerS #pragma mark - Threading +FOUNDATION_EXTERN NSOperationQueue *TNLNetworkOperationQueue(void); FOUNDATION_EXTERN dispatch_queue_t tnl_network_queue(void); FOUNDATION_EXTERN dispatch_queue_t tnl_coding_queue(void); diff --git a/Source/TNL_Project.m b/Source/TNL_Project.m index 95369e2..32340da 100644 --- a/Source/TNL_Project.m +++ b/Source/TNL_Project.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/24/14. -// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Copyright © 2020 Twitter, Inc. All rights reserved. // #include @@ -41,6 +41,18 @@ dispatch_source_t tnl_dispatch_timer_create_and_start(dispatch_queue_t queue, #pragma mark - Threading +NSOperationQueue *TNLNetworkOperationQueue() +{ + static NSOperationQueue *sQueue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sQueue = [[NSOperationQueue alloc] init]; + sQueue.name = @"tnl.network.queue"; + sQueue.underlyingQueue = tnl_network_queue(); + }); + return sQueue; +} + dispatch_queue_t tnl_network_queue() { static dispatch_queue_t sQueue; @@ -214,7 +226,7 @@ static void ConnLoad(void) void TNLIncrementObjectCount(Class class) { - dispatch_async(sCountsQueue, ^{ + tnl_dispatch_async_autoreleasing(sCountsQueue, ^{ NSString *className = NSStringFromClass(class); if (className) { NSUInteger count = [sCounts[className] unsignedIntegerValue]; @@ -225,7 +237,7 @@ void TNLIncrementObjectCount(Class class) void TNLDecrementObjectCount(Class class) { - dispatch_async(sCountsQueue, ^{ + tnl_dispatch_async_autoreleasing(sCountsQueue, ^{ NSString *className = NSStringFromClass(class); if (className) { NSUInteger count = [sCounts[className] unsignedIntegerValue]; diff --git a/Source/TNL_ProjectCommon.h b/Source/TNL_ProjectCommon.h index 18843e8..015728b 100644 --- a/Source/TNL_ProjectCommon.h +++ b/Source/TNL_ProjectCommon.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 3/5/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // // This header is kept in sync with other *_Common.h headers from sibling projects. @@ -69,14 +69,16 @@ FOUNDATION_EXTERN BOOL gTwitterNetworkLayerAssertEnabled; do { \ __PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \ if (__builtin_expect(!(condition), 0)) { \ - NSString *__assert_fn__ = [NSString stringWithUTF8String:__PRETTY_FUNCTION__]; \ - __assert_fn__ = __assert_fn__ ? __assert_fn__ : @""; \ - NSString *__assert_file__ = [NSString stringWithUTF8String:TNL_FILE_NAME]; \ - __assert_file__ = __assert_file__ ? __assert_file__ : @""; \ + __TNLAssertTriggering(); \ + NSString *__assert_fn__ = [NSString stringWithUTF8String:__PRETTY_FUNCTION__]; \ + __assert_fn__ = __assert_fn__ ? __assert_fn__ : @""; \ + NSString *__assert_file__ = [NSString stringWithUTF8String:TNL_FILE_NAME]; \ + __assert_file__ = __assert_file__ ? __assert_file__ : @""; \ [[NSAssertionHandler currentHandler] handleFailureInFunction:__assert_fn__ \ - file:__assert_file__ \ - lineNumber:__LINE__ description:(desc), ##__VA_ARGS__]; \ - } \ + file:__assert_file__ \ + lineNumber:__LINE__ \ + description:(desc), ##__VA_ARGS__]; \ + } \ __PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \ } while(0) @@ -89,14 +91,12 @@ do { \ #define TNLAssert(expression) \ ({ if (gTwitterNetworkLayerAssertEnabled) { \ const BOOL __expressionValue = !!(expression); (void)__expressionValue; \ - __TNLAssert(__expressionValue); \ TNLCAssert(__expressionValue, @"assertion failed: (" #expression ")"); \ } }) #define TNLAssertMessage(expression, format, ...) \ ({ if (gTwitterNetworkLayerAssertEnabled) { \ const BOOL __expressionValue = !!(expression); (void)__expressionValue; \ - __TNLAssert(__expressionValue); \ TNLCAssert(__expressionValue, @"assertion failed: (" #expression ") message: %@", [NSString stringWithFormat:format, ##__VA_ARGS__]); \ } }) @@ -141,13 +141,13 @@ do { \ #pragma mark - Debugging Tools #if DEBUG -FOUNDATION_EXTERN void __TNLAssert(BOOL expression); +FOUNDATION_EXTERN void __TNLAssertTriggering(void); FOUNDATION_EXTERN BOOL TNLIsDebuggerAttached(void); FOUNDATION_EXTERN void TNLTriggerDebugSTOP(void); FOUNDATION_EXTERN BOOL TNLIsDebugSTOPOnAssertEnabled(void); FOUNDATION_EXTERN void TNLSetDebugSTOPOnAssertEnabled(BOOL stopOnAssert); #else -#define __TNLAssert(exp) ((void)0) +#define __TNLAssertTriggering() ((void)0) #define TNLIsDebuggerAttached() (NO) #define TNLTriggerDebugSTOP() ((void)0) #define TNLIsDebugSTOPOnAssertEnabled() (NO) @@ -192,7 +192,7 @@ __strong tnl_defer_block_t tnl_macro_concat(tnl_stack_defer_block_, __LINE__) __ #define TNLDeferRelease(ref) tnl_defer(^{ if (ref) { CFRelease(ref); } }) -#pragma twitter stopignorestylecheck +#pragma twitter endignorestylecheck #pragma mark - GCD helpers @@ -227,6 +227,7 @@ __strong tnl_defer_block_t tnl_macro_concat(tnl_stack_defer_block_, __LINE__) __ // } // } +// Should pretty much ALWAYS use this for async dispatch NS_INLINE void tnl_dispatch_async_autoreleasing(dispatch_queue_t queue, dispatch_block_t block) { dispatch_async(queue, ^{ @@ -236,6 +237,17 @@ NS_INLINE void tnl_dispatch_async_autoreleasing(dispatch_queue_t queue, dispatch }); } +// Should pretty much ALWAYS use this for async barrier dispatch +NS_INLINE void tnl_dispatch_barrier_async_autoreleasing(dispatch_queue_t queue, dispatch_block_t block) +{ + dispatch_barrier_async(queue, ^{ + @autoreleasepool { + block(); + } + }); +} + +// Only need this in a tight loop, existing autorelease pool will take effect for dispatch_sync NS_INLINE void tnl_dispatch_sync_autoreleasing(dispatch_queue_t __attribute__((noescape)) queue, dispatch_block_t block) { dispatch_sync(queue, ^{ @@ -328,3 +340,4 @@ NS_INLINE void tnl_dispatch_sync_autoreleasing(dispatch_queue_t __attribute__((n NS_ASSUME_NONNULL_END + diff --git a/Source/TNL_ProjectCommon.m b/Source/TNL_ProjectCommon.m index 2e29003..e426a8b 100644 --- a/Source/TNL_ProjectCommon.m +++ b/Source/TNL_ProjectCommon.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 3/5/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNL_ProjectCommon.h" @@ -112,12 +112,10 @@ void TNLSetDebugSTOPOnAssertEnabled(BOOL stopOnAssert) sIsDebugSTOPEnabled = stopOnAssert; } -void __TNLAssert(BOOL expression) +void __TNLAssertTriggering() { - if (!expression) { - if (TNLIsDebugSTOPOnAssertEnabled() && TNLIsDebuggerAttached()) { - TNLTriggerDebugSTOP(); // trigger debug stop - } + if (TNLIsDebugSTOPOnAssertEnabled() && TNLIsDebuggerAttached()) { + TNLTriggerDebugSTOP(); // trigger debug stop } } diff --git a/Source/TwitterNetworkLayer.h b/Source/TwitterNetworkLayer.h index 74eda97..e28570b 100644 --- a/Source/TwitterNetworkLayer.h +++ b/Source/TwitterNetworkLayer.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 6/9/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #pragma Headers @@ -11,6 +11,7 @@ #import #import #import +#import #import #import #import @@ -55,6 +56,7 @@ #import #import #import +#import #import #import #import diff --git a/TNLCLI/TNLCLIError.h b/TNLCLI/TNLCLIError.h new file mode 100644 index 0000000..ac096fa --- /dev/null +++ b/TNLCLI/TNLCLIError.h @@ -0,0 +1,31 @@ +// +// TNLCLIError.h +// TNLCLI +// +// Created on 9/11/19. +// Copyright © 2020 Twitter. All rights reserved. +// + +@import Foundation; + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, TNLCLIError) +{ + TNLCLIErrorException = -1, + TNLCLIErrorUnknown = 0, + TNLCLIErrorEmptyMainFunctionArguments, + TNLCLIErrorMissingPWDEnvironmentVariable, + TNLCLIErrorMissingRequestURLArgument, + TNLCLIErrorInvalidURLArgument, + TNLCLIErrorArgumentInputFileCannotBeRead, + TNLCLIErrorJSONParseFailure, + TNLCLIErrorResponseBodyCannotPrint, + TNLCLIErrorInvalidRequestConfigurationFileFormat, // needs to be JSON of key=value pairs (all strings, even numeric values!) +}; + +FOUNDATION_EXTERN NSString * const TNLCLIErrorDomain; + +FOUNDATION_EXTERN NSError *TNLCLICreateError(TNLCLIError code, id __nullable userInfoDictionaryOrDescriptionString); + +NS_ASSUME_NONNULL_END diff --git a/TNLCLI/TNLCLIError.m b/TNLCLI/TNLCLIError.m new file mode 100644 index 0000000..08e9597 --- /dev/null +++ b/TNLCLI/TNLCLIError.m @@ -0,0 +1,27 @@ +// +// TNLCLIError.m +// TNLCLI +// +// Created on 9/11/19. +// Copyright © 2020 Twitter. All rights reserved. +// + +#import "TNLCLIError.h" + +NSString * const TNLCLIErrorDomain = @"tnlcli.error"; + +NSError *TNLCLICreateError(TNLCLIError code, id __nullable userInfoDictionaryOrDescriptionString) +{ + NSDictionary *userInfo = nil; + if ([userInfoDictionaryOrDescriptionString isKindOfClass:[NSDictionary class]]) { + userInfo = userInfoDictionaryOrDescriptionString; + } else if ([userInfoDictionaryOrDescriptionString isKindOfClass:[NSString class]]) { + userInfo = @{ + NSDebugDescriptionErrorKey : [userInfoDictionaryOrDescriptionString copy] + }; + } + + return [NSError errorWithDomain:TNLCLIErrorDomain + code:code + userInfo:userInfo]; +} diff --git a/TNLCLI/TNLCLIExecution.h b/TNLCLI/TNLCLIExecution.h new file mode 100644 index 0000000..77899ae --- /dev/null +++ b/TNLCLI/TNLCLIExecution.h @@ -0,0 +1,25 @@ +// +// TNLCLIExecution.h +// tnlcli +// +// Created on 9/12/19. +// Copyright © 2020 Twitter. All rights reserved. +// + +#import "TNLCLIExecutionContext.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface TNLCLIExecution : NSObject + +@property (nonatomic, readonly) TNLCLIExecutionContext *context; + +- (instancetype)initWithContext:(TNLCLIExecutionContext *)context; +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +- (nullable NSError *)execute; + +@end + +NS_ASSUME_NONNULL_END diff --git a/TNLCLI/TNLCLIExecution.m b/TNLCLI/TNLCLIExecution.m new file mode 100644 index 0000000..ef2c554 --- /dev/null +++ b/TNLCLI/TNLCLIExecution.m @@ -0,0 +1,416 @@ +// +// TNLCLIExecution.m +// tnlcli +// +// Created on 9/12/19. +// Copyright © 2020 Twitter. All rights reserved. +// + +#import + +#import "TNLCLIError.h" +#import "TNLCLIExecution.h" +#import "TNLCLIPrint.h" +#import "TNLCLIUtils.h" +#import "TNLGlobalConfiguration+TNLCLI.h" +#import "TNLMutableRequestConfiguration+TNLCLI.h" + +#pragma mark - Static Functions + +#define FAIL(err) \ +({\ + _executionError = (err); \ + TNLCLIPrintError(_executionError); \ + return; \ +}) +#define SOFT_FAIL(err) \ +({\ + NSError *err__ = (err); \ + TNLCLIPrintError(err__); \ + if (!_executionError) { \ + _executionError = err__; \ + } \ +}) + +#pragma mark - TNLCLIExecution + +@interface TNLCLIExecution () +@property (nonatomic, readonly, nullable) NSError *executionError; +@property (nonatomic, readonly, nullable) TNLResponse *response; +- (NSString *)sanitizePath:(NSString *)path; +@end + +@interface TNLCLIExecution (TNLDelegate) +@end + +@implementation TNLCLIExecution + +- (instancetype)initWithContext:(TNLCLIExecutionContext *)context +{ + if (self = [super init]) { + _context = context; + _executionError = context.contextError; + } + return self; +} + +- (nullable NSError *)execute +{ + @try { + [self _execute]; + } @catch (NSException *exception) { + _executionError = TNLCLICreateError(TNLCLIErrorException, + @{ + NSDebugDescriptionErrorKey : @"Exception when executing", + @"exception" : exception + }); + TNLCLIPrintError(_executionError); + tnlcli_fprintf(stderr, "call stack:\n%s\n", exception.callStackSymbols.description.UTF8String); + } + + return _executionError; +} + +- (NSString *)sanitizePath:(NSString *)path +{ + NSString *newPath = [path stringByExpandingTildeInPath]; + if (!newPath.isAbsolutePath) { + newPath = [self.context.currentDirectory stringByAppendingPathComponent:newPath]; + } + return newPath; +} + +- (void)_execute +{ + if (_executionError) { + return; + } + + TNLCLIExecutionContext *context = _context; + + /// Print the version? + + if (context.printVersion) { + tnlcli_printf("%s version %s\n", context.executableName.UTF8String, [TNLGlobalConfiguration version].UTF8String); + if (context.requestURLString.length == 0) { + // just getting the version + return; + } + } + + /// Global Config + + TNLGlobalConfiguration *globalConfig = [TNLGlobalConfiguration sharedInstance]; + globalConfig.assertsEnabled = YES; + globalConfig.logger = self; + [globalConfig addAuthenticationChallengeHandler:self]; + + // Optionally update global config + + for (NSString *globalConfigSetting in context.globalConfigurations) { + NSString *name, *value; + if (TNLCLIParseColonSeparatedKeyValuePair(globalConfigSetting, &name, &value)) { + (void)[globalConfig tnlcli_applySettingWithName:name value:value]; + } else { + TNLCLIPrintWarning([NSString stringWithFormat:@"'%@' is not in the expected format for a global configuration: 'name:value'. Skipping this global configuration.", globalConfigSetting]); + } + } + + /// Construct the request + + TNLMutableRequestConfiguration *configuration = nil; + TNLMutableHTTPRequest *request = nil; + + // Init our request + + request = [[TNLMutableHTTPRequest alloc] initWithURL:[NSURL URLWithString:context.requestURLString]]; + if (!request.URL) { + FAIL(TNLCLICreateError(TNLCLIErrorInvalidURLArgument, + @{ + NSDebugDescriptionErrorKey : @"Request URL argument is not valid", + @"url_arg" : context.requestURLString ?: @"" + })); + } + + // Optionally set method + + if (context.requestMethodValueString) { + request.HTTPMethodValue = TNLHTTPMethodFromString(context.requestMethodValueString); + if (request.HTTPMethodValue == TNLHTTPMethodGET && ![context.requestMethodValueString isEqualToString:@"GET"]) { + TNLCLIPrintWarning([NSString stringWithFormat:@"--request-method arg `%s` is not an HTTP Method, using `GET` instead", context.requestMethodValueString.UTF8String]); + } + } + + // Optionally set headers + + if (context.requestHeadersFilePath) { + NSString *filePath = [self sanitizePath:context.requestHeadersFilePath]; + NSData *requestHeadersData = [NSData dataWithContentsOfFile:filePath]; + if (!requestHeadersData) { + FAIL(TNLCLICreateError(TNLCLIErrorArgumentInputFileCannotBeRead, + @{ + NSDebugDescriptionErrorKey : @"--request-headers-file cannot be read", + @"file_arg" : context.requestHeadersFilePath + })); + } + NSError *error; + NSDictionary *headers = [NSJSONSerialization JSONObjectWithData:requestHeadersData options:0 error:&error]; + if (!headers) { + FAIL(error ?: TNLCLICreateError(TNLCLIErrorUnknown, nil)); + } + if (![headers isKindOfClass:[NSDictionary class]]) { + FAIL(TNLCLICreateError(TNLCLIErrorJSONParseFailure, + @{ + NSDebugDescriptionErrorKey : @"Failed to parse file's JSON as key-value-pairs", + @"file_arg" : context.requestHeadersFilePath + })); + } + request.allHTTPHeaderFields = headers; + } + + // Optionally set more headers + + for (NSString *header in context.requestHeaders) { + NSString *field, *value; + if (TNLCLIParseColonSeparatedKeyValuePair(header, &field, &value)) { + [request setValue:value forHTTPHeaderField:field]; + } else { + TNLCLIPrintWarning([NSString stringWithFormat:@"'%@' is not in the expected format for a header: 'Header: Value'. Skipping this header.", header]); + } + } + + // Optionally set body + + if (context.requestBodyFilePath) { + request.HTTPBodyFilePath = [self sanitizePath:context.requestBodyFilePath]; + } + + // Construct configuration + + if (context.requestConfigurationFilePath) { + NSString *filePath = [self sanitizePath:context.requestConfigurationFilePath]; + NSError *error; + configuration = [TNLMutableRequestConfiguration tnlcli_configurationWithFile:filePath error:&error]; + if (!configuration) { + FAIL(error); + } + } + + if (!configuration) { + configuration = [[TNLMutableRequestConfiguration alloc] init]; + } + + // Optionally update the configuration + + for (NSString *config in context.requestConfigurations) { + NSString *name, *value; + if (TNLCLIParseColonSeparatedKeyValuePair(config, &name, &value)) { + [configuration tnlcli_applySettingWithName:name value:value]; + } else { + TNLCLIPrintWarning([NSString stringWithFormat:@"'%@' is not in the expected format for a request config seting: 'Name:Value'. Skipping this setting.", config]); + } + } + + /// Run our operation + + TNLRequestOperation *operation = [TNLRequestOperation operationWithRequest:request configuration:configuration delegate:self]; + [[TNLRequestOperationQueue defaultOperationQueue] enqueueRequestOperation:operation]; + [operation waitUntilFinishedWithoutBlockingRunLoop]; + + /// Handle our response + + // Was there an error + + if (_response.operationError) { + SOFT_FAIL(_response.operationError); + } + + // Verbose Info + + if (context.verbose) { + tnlcli_printf("** STATS **\n"); + NSDictionary *metricsDescription = [_response.metrics dictionaryDescription:YES]; + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:metricsDescription + options:(NSJSONWritingSortedKeys | NSJSONWritingPrettyPrinted) + error:&error]; + if (jsonData) { + jsonData = TNLCLIEnsureDataIsNullTerminated(jsonData); + tnlcli_printf("%s\n", (const char *)jsonData.bytes); + } else { + tnlcli_printf("Failed To Generate Stats! "); + TNLCLIPrintError(error); + } + } + + // Response headers + + do { + NSMutableDictionary *dictionary = [[_response.info allHTTPHeaderFields] mutableCopy]; + dictionary[@"_tnlcli_StatusCode"] = [@(_response.info.statusCode) stringValue]; + dictionary[@"_tnlcli_URL"] = [_response.info.finalURL absoluteString]; + + NSError *error; + NSData *jsonData = (dictionary) ? [NSJSONSerialization dataWithJSONObject:dictionary + options:NSJSONWritingSortedKeys | NSJSONWritingPrettyPrinted + error:&error] : nil; + if (!jsonData) { + SOFT_FAIL(error); + } else { + jsonData = TNLCLIEnsureDataIsNullTerminated(jsonData); + if (context.verbose || [context.responseHeadersOutputModes containsObject:@"print"]) { + if (context.verbose) { + tnlcli_printf("** RESPONSE HEADERS **\n"); + } + tnlcli_printf("%s\n", (const char *)jsonData.bytes); + } + if ([context.responseHeadersOutputModes containsObject:@"file"] && context.requestBodyFilePath) { + NSString *filePath = [self sanitizePath:context.requestBodyFilePath]; + if (![jsonData writeToFile:filePath options:NSDataWritingAtomic | NSDataWritingWithoutOverwriting error:&error]) { + SOFT_FAIL(error); + } + } + } + } while (0); + + // Response body + + if (_response.info.data || _response.info.temporarySavedFile) { + NSData *data = _response.info.data; + BOOL writeToFile = [context.responseBodyOutputModes containsObject:@"file"] && context.responseBodyTargetFilePath; + const BOOL print = context.verbose || [context.responseBodyOutputModes containsObject:@"print"]; + if (_response.info.temporarySavedFile) { + NSString *path = nil; + if (writeToFile) { + path = [self sanitizePath:context.responseBodyTargetFilePath]; + writeToFile = NO; + } else if (print) { + [[NSFileManager defaultManager] createDirectoryAtPath:NSTemporaryDirectory() withIntermediateDirectories:YES attributes:nil error:NULL]; + path = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSUUID UUID].UUIDString]; + } + + if (path) { + NSError *error; + if (![_response.info.temporarySavedFile moveToPath:path error:&error]) { + SOFT_FAIL(error); + } else { + if (print && !data) { + data = [NSData dataWithContentsOfFile:path]; + } + } + } + } + if (data) { + if (writeToFile) { + NSError *error; + if (![data writeToFile:[self sanitizePath:context.responseBodyTargetFilePath] options:NSDataWritingAtomic | NSDataWritingWithoutOverwriting error:&error]) { + SOFT_FAIL(error); + } + } + if (print) { + data = TNLCLIEnsureDataIsNullTerminated(data); + NSString *printable = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + if (context.verbose) { + tnlcli_printf("** RESPONSE BODY **\n"); + } + if (!printable) { + if (context.verbose && ![context.responseBodyOutputModes containsObject:@"print"]) { + // due to being verbose + tnlcli_printf("Response body is not UTF-8 and cannot be printed.\n"); + } else { + SOFT_FAIL(TNLCLICreateError(TNLCLIErrorResponseBodyCannotPrint, @"The response body is not UTF-8 and therefore cannot be printed")); + } + } else { + tnlcli_printf("\r\n%s\n", printable.UTF8String); + } + } + } + } +} + +@end + +@implementation TNLCLIExecution (TNLDelegate) + +- (void)tnl_logWithLevel:(TNLLogLevel)level + context:(nullable id)context + file:(NSString *)file + function:(NSString *)function + line:(int)line + message:(NSString *)message +{ + static const char * sLevelStrings[] = { + "EMGCY", + "ALERT", + "CRTCL", + "ERROR", + "WARNG", + "Notce", + "Info ", + "Debug" + }; + tnlcli_fprintf((level >= TNLLogLevelNotice) ? stdout : stderr, "%s: %s\n", sLevelStrings[level], message.UTF8String); +} + +- (BOOL)tnl_shouldRedactHTTPHeaderField:(NSString *)headerField +{ + return NO; +} + +- (BOOL)tnl_canLogWithLevel:(TNLLogLevel)level context:(nullable id)context +{ + if (!_context.verbose) { + return NO; + } + return (level <= TNLLogLevelWarning); +} + +- (BOOL)tnl_shouldLogVerbosely +{ + return _context.verbose; +} + +- (void)tnl_requestOperation:(TNLRequestOperation *)op didCompleteWithResponse:(TNLResponse *)response +{ + _response = response; +} + +- (void)tnl_networkLayerDidReceiveAuthChallenge:(NSURLAuthenticationChallenge *)challenge + requestOperation:(TNLRequestOperation *)op + completion:(TNLURLSessionAuthChallengeCompletionBlock)completion +{ + if (self.context.certificateChainDumpDirectory) { + if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { + NSString *host = challenge.protectionSpace.host; + NSString *dumpDir = [self sanitizePath:self.context.certificateChainDumpDirectory]; + NSFileManager *fm = [NSFileManager defaultManager]; + NSError *error; + if (![fm createDirectoryAtPath:dumpDir withIntermediateDirectories:YES attributes:nil error:&error]) { + TNLCLIPrintError(error); + } + + SecTrustRef serverTrust = challenge.protectionSpace.serverTrust; + const CFIndex chainLength = SecTrustGetCertificateCount(serverTrust); + if (chainLength > 0 && self.context.verbose) { + tnlcli_printf("** CERT DUMP **\n"); + } + for (CFIndex i = 0; i < chainLength; i++) { + SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i); + NSData *DERData = (NSData *)CFBridgingRelease(SecCertificateCopyData(certificate)); + NSString *DERFilePath = [dumpDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@_cert_%li.DER", host, i]]; + NSString *summary = (NSString *)CFBridgingRelease(CFCopyDescription(certificate)); + if (![DERData writeToFile:DERFilePath options:NSDataWritingWithoutOverwriting error:&error]) { + NSMutableDictionary *errorInfo = [error.userInfo mutableCopy] ?: [[NSMutableDictionary alloc] init]; + errorInfo[@"cert.description"] = summary; + TNLCLIPrintError([NSError errorWithDomain:error.domain code:error.code userInfo:errorInfo]); + } else if (self.context.verbose) { + tnlcli_printf("'%s' => %s\n", summary.UTF8String, DERFilePath.UTF8String); + } + } + } + } + + completion(NSURLSessionAuthChallengePerformDefaultHandling, nil); +} + +@end diff --git a/TNLCLI/TNLCLIExecutionContext.h b/TNLCLI/TNLCLIExecutionContext.h new file mode 100644 index 0000000..287a44c --- /dev/null +++ b/TNLCLI/TNLCLIExecutionContext.h @@ -0,0 +1,65 @@ +// +// TNLCLIExecutionContext.h +// TNLCLI +// +// Created on 9/11/19. +// Copyright © 2020 Twitter. All rights reserved. +// + +@import Foundation; + +@class TNLResponse; + +NS_ASSUME_NONNULL_BEGIN + +@interface TNLCLIExecutionContext : NSObject + +#pragma mark Context Error + +@property (nonatomic, readonly, nullable) NSError *contextError; + +#pragma mark Execution Info + +@property (nonatomic, readonly, copy, nullable) NSString *executableName; +@property (nonatomic, readonly, copy, nullable) NSString *executableDirectory; +@property (nonatomic, readonly, copy, nullable) NSString *currentDirectory; + +#pragma mark Global Config + +@property (nonatomic, readonly, copy, nullable) NSArray *globalConfigurations; + +#pragma mark Request Info + +@property (nonatomic, readonly, copy, nullable) NSString *requestConfigurationFilePath; +@property (nonatomic, readonly, copy, nullable) NSString *requestHeadersFilePath; +@property (nonatomic, readonly, copy, nullable) NSString *requestBodyFilePath; +@property (nonatomic, readonly, copy, nullable) NSArray *requestHeaders; +@property (nonatomic, readonly, copy, nullable) NSArray *requestConfigurations; +@property (nonatomic, readonly, copy, nullable) NSString *requestMethodValueString; +@property (nonatomic, readonly, copy, nullable) NSString *requestURLString; + +#pragma mark Response Info + +@property (nonatomic, readonly, copy, nullable) NSArray *responseBodyOutputModes; // @"file", @"print", @"file,print" +@property (nonatomic, readonly, copy, nullable) NSString *responseBodyTargetFilePath; + +@property (nonatomic, readonly, copy, nullable) NSArray *responseHeadersOutputModes; // @"file", @"print", @"file,print" +@property (nonatomic, readonly, copy, nullable) NSString *responseHeadersTargetFilePath; + +@property (nonatomic, readonly, copy, nullable) NSString *certificateChainDumpDirectory; + +#pragma mark Other Info + +@property (nonatomic, readonly) BOOL verbose; +@property (nonatomic, readonly) BOOL printVersion; // --version + +#pragma mark Init + +- (instancetype)initWithArgC:(int)argc argV:(const char * __nonnull * __nonnull)argv; +- (instancetype)initWithArgs:(nullable NSArray *)args NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/TNLCLI/TNLCLIExecutionContext.m b/TNLCLI/TNLCLIExecutionContext.m new file mode 100644 index 0000000..0ca0c71 --- /dev/null +++ b/TNLCLI/TNLCLIExecutionContext.m @@ -0,0 +1,159 @@ +// +// TNLCLIExecutionContext.m +// TNLCLI +// +// Created on 9/11/19. +// Copyright © 2020 Twitter. All rights reserved. +// + +#import "TNLCLIError.h" +#import "TNLCLIExecutionContext.h" +#import "TNLCLIPrint.h" + +#pragma mark - Static Functions + +static NSArray *parseArgs(int argc, const char * argv[]) +{ + NSMutableArray *args = [[NSMutableArray alloc] init]; + for (int c = 0; c < argc; c++) { + [args addObject:@(argv[c])]; + } + return [args copy]; +} + +#define FAIL(err) \ +({\ + _contextError = (err); \ + TNLCLIPrintError(_contextError); \ + return; \ +}) +#define SOFT_FAIL(err) \ +({\ + NSError *err__ = (err); \ + TNLCLIPrintError(err__); \ + if (!_contextError) { \ + _contextError = err__; \ + } \ +}) + +#pragma mark - TNLCLIExecutionContext + +@implementation TNLCLIExecutionContext + +#pragma mark Init + +- (instancetype)initWithArgC:(int)argc argV:(const char **)argv +{ + NSArray *args = parseArgs(argc, argv); + return [self initWithArgs:args]; +} + +- (instancetype)initWithArgs:(NSArray *)args +{ + if (self = [super init]) { + [self digestArgs:args]; + } + return self; +} + +- (void)digestArgs:(NSArray *)args +{ + if (args.count == 0) { + FAIL(TNLCLICreateError(TNLCLIErrorEmptyMainFunctionArguments, @"Expected args for main(...) function")); + } + + NSString *str; + str = @(getenv("PWD")); + if (!str) { + FAIL(TNLCLICreateError(TNLCLIErrorMissingPWDEnvironmentVariable, @"Missing PWD environment variable")); + } else { + _currentDirectory = [str stringByExpandingTildeInPath]; + } + + str = args.firstObject; + if (!str.isAbsolutePath) { + str = [_currentDirectory stringByAppendingPathComponent:str]; + } else { + str = [str stringByExpandingTildeInPath]; + } + + _executableName = str.lastPathComponent; + _executableDirectory = [str stringByDeletingLastPathComponent]; + + if (args.count == 1) { + FAIL(TNLCLICreateError(TNLCLIErrorMissingRequestURLArgument, @"Missing `url` for request (final argument to be passed in)")); + } + + if (args.count == 2 && [args[1] isEqualToString:@"--version"]) { + _printVersion = YES; + return; + } + + NSMutableArray *headers = [[NSMutableArray alloc] init]; + NSMutableArray *configs = [[NSMutableArray alloc] init]; + NSMutableArray *globals = [[NSMutableArray alloc] init]; + for (NSUInteger i = 1; i < args.count - 1; ) { + NSString *option = args[i++]; + + if ([option isEqualToString:@"--verbose"]) { + _verbose = YES; + continue; + } + + if ([option isEqualToString:@"--version"]) { + _printVersion = YES; + continue; + } + + NSString *value = args[i++]; + if (i == args.count) { + FAIL(TNLCLICreateError(TNLCLIErrorMissingRequestURLArgument, @"Missing `url` for request (final argument to be passed in)")); + } + +#define CASE(arg, ivar) \ + if ([option isEqualToString: (arg) ]) { \ + ivar = [value copy]; \ + continue; \ + } \ + + CASE(@"--request-config-file", _requestConfigurationFilePath); + CASE(@"--request-headers-file", _requestHeadersFilePath); + CASE(@"--request-body-file", _requestBodyFilePath); + CASE(@"--request-method", _requestMethodValueString); + + CASE(@"--response-body-file", _responseBodyTargetFilePath); + CASE(@"--response-headers-file", _responseHeadersTargetFilePath); + CASE(@"--dump-cert-chain-directory", _certificateChainDumpDirectory); + + if ([option isEqualToString:@"--request-header"]) { + [headers addObject:value]; + continue; + } + if ([option isEqualToString:@"--request-config"]) { + [configs addObject:value]; + continue; + } + if ([option isEqualToString:@"--global-config"]) { + [globals addObject:value]; + continue; + } + if ([option isEqualToString:@"--response-body-mode"]) { + _responseBodyOutputModes = [value componentsSeparatedByString:@","]; + continue; + } + if ([option isEqualToString:@"--response-headers-mode"]) { + _responseHeadersOutputModes = [value componentsSeparatedByString:@","]; + continue; + } + + TNLCLIPrintWarning([NSString stringWithFormat:@"`%@` is an unknown argument. Skipping it and its value `%@`", option, value]); +#undef CASE + } + _requestHeaders = [headers copy]; + _requestConfigurations = [configs copy]; + _globalConfigurations = [globals copy]; + _requestURLString = [args.lastObject copy]; +} + +@end + diff --git a/TNLCLI/TNLCLIPrint.h b/TNLCLI/TNLCLIPrint.h new file mode 100644 index 0000000..caa5b5d --- /dev/null +++ b/TNLCLI/TNLCLIPrint.h @@ -0,0 +1,23 @@ +// +// TNLCLIPrint.h +// tnlcli +// +// Created on 9/12/19. +// Copyright © 2020 Twitter. All rights reserved. +// + +@import Foundation; + +NS_ASSUME_NONNULL_BEGIN + +#define tnlcli_printf printf +#define tnlcli_fprintf fprintf + +FOUNDATION_EXTERN void TNLCLIPrintWarning(NSString *warning); +FOUNDATION_EXTERN void TNLCLIPrintError(NSError *error); +FOUNDATION_EXTERN void TNLCLIPrintUsage(NSString * __nullable cliName); + +FOUNDATION_EXTERN NSData *TNLCLIEnsureDataIsNullTerminated(NSData *data); + + +NS_ASSUME_NONNULL_END diff --git a/TNLCLI/TNLCLIPrint.m b/TNLCLI/TNLCLIPrint.m new file mode 100644 index 0000000..3d40ff8 --- /dev/null +++ b/TNLCLI/TNLCLIPrint.m @@ -0,0 +1,69 @@ +// +// TNLCLIPrint.m +// tnlcli +// +// Created on 9/12/19. +// Copyright © 2020 Twitter. All rights reserved. +// + +#import "TNLCLIPrint.h" + +void TNLCLIPrintWarning(NSString *warning) +{ + fprintf(stderr, "WARNING: %s\n", warning.UTF8String); +} + +void TNLCLIPrintError(NSError *error) +{ + fprintf(stderr, "ERR: %s:%li %s\n\n", error.domain.UTF8String, error.code, error.userInfo.description.UTF8String ?: ""); +} + +NSData *TNLCLIEnsureDataIsNullTerminated(NSData *data) +{ + __block BOOL needsNULLTerminator = NO; + [data enumerateByteRangesUsingBlock:^(const void * _Nonnull bytes, NSRange byteRange, BOOL * _Nonnull stop) { + if (byteRange.location + byteRange.length == data.length) { + const char *cStr = bytes; + char c = cStr[byteRange.length - 1]; + needsNULLTerminator = (c != '\0'); + *stop = YES; + } + }]; + if (needsNULLTerminator) { + @autoreleasepool { + NSMutableData *mData = [data mutableCopy]; + [mData appendBytes:"" length:1]; + data = [mData copy]; + } + } + return data; +} + +void TNLCLIPrintUsage(NSString * __nullable cliName) +{ + // NOTE: when updating the usage, update the README.md too. + + cliName = cliName ?: @"tnlcli"; + tnlcli_fprintf(stderr, "Usage: %s [options] url\n\n", cliName.UTF8String); + tnlcli_fprintf(stderr, "\tExample: %s --request-method HEAD --response-header-mode file,print --response-header-file response_headers.json https://google.com\n\n", cliName.UTF8String); + tnlcli_fprintf(stderr, "Argument Options:\n-----------------\n\n"); + tnlcli_fprintf(stderr, "\t--request-config-file TNLRequestConfiguration as a json file\n"); + tnlcli_fprintf(stderr, "\t--request-headers-file json file of key-value-pairs for using as headers\n"); + tnlcli_fprintf(stderr, "\t--request-body-file file for the HTTP body\n"); + tnlcli_fprintf(stderr, "\n"); + tnlcli_fprintf(stderr, "\t--request-header \"Field: Value\" A header to provide with the request (will override the header if also in the request header file). Can provide multiple headers.\n"); + tnlcli_fprintf(stderr, "\t--request-config \"config: value\" A config setting for the TNLRequestConfiguration of the request (will override the config if also in the request config file). Can provide multiple configs.\n"); + tnlcli_fprintf(stderr, "\t--request-method HTTP Method from Section 9 in HTTP/1.1 spec (RFC 2616), such as GET, POST, HEAD, etc\n"); + tnlcli_fprintf(stderr, "\n"); + tnlcli_fprintf(stderr, "\t--response-body-mode \"file\" or \"print\" or a combo using commas\n"); + tnlcli_fprintf(stderr, "\t--response-body-file file for the response body to save to (requires \"file\" for --response-body-mode\n"); + tnlcli_fprintf(stderr, "\t--response-headers-mode \"file\" or \"print\" or a combo using commas\n"); + tnlcli_fprintf(stderr, "\t--response-headers-file file for the response headers to save to (as json)\n"); + tnlcli_fprintf(stderr, "\n"); + tnlcli_fprintf(stderr, "\t--dump-cert-chain-directory directory for the certification chain to be dumped to (as DER files)\n"); + tnlcli_fprintf(stderr, "\n"); + tnlcli_fprintf(stderr, "\t--verbose Will print verbose information and force the --response-body-mode and --responde-headers-mode to have \"print\".\n"); + tnlcli_fprintf(stderr, "\t--version Will print ther version information.\n"); + tnlcli_fprintf(stderr, "\n"); +} + diff --git a/TNLCLI/TNLCLIUtils.h b/TNLCLI/TNLCLIUtils.h new file mode 100644 index 0000000..9553433 --- /dev/null +++ b/TNLCLI/TNLCLIUtils.h @@ -0,0 +1,20 @@ +// +// TNLCLIUtils.h +// tnlcli +// +// Created on 9/17/19. +// Copyright © 2020 Twitter. All rights reserved. +// + +@import Foundation; + +NS_ASSUME_NONNULL_BEGIN + +FOUNDATION_EXTERN BOOL TNLCLIParseColonSeparatedKeyValuePair(NSString *str, + NSString * __nullable * __nullable keyOut, + NSString * __nullable * __nullable valueOut); + +FOUNDATION_EXTERN NSNumber * __nullable TNLCLINumberValueFromString(NSString *str); +FOUNDATION_EXTERN NSNumber * __nullable TNLCLIBoolNumberValueFromString(NSString *value); + +NS_ASSUME_NONNULL_END diff --git a/TNLCLI/TNLCLIUtils.m b/TNLCLI/TNLCLIUtils.m new file mode 100644 index 0000000..2a608a7 --- /dev/null +++ b/TNLCLI/TNLCLIUtils.m @@ -0,0 +1,61 @@ +// +// TNLCLIUtils.m +// tnlcli +// +// Created on 9/17/19. +// Copyright © 2020 Twitter. All rights reserved. +// + +#import "TNLCLIUtils.h" + +BOOL TNLCLIParseColonSeparatedKeyValuePair(NSString *str, NSString ** keyOut, NSString ** valueOut) +{ + NSString *key, *value; + const NSUInteger indexOfColon = [str rangeOfString:@":"].location; + if (indexOfColon != NSNotFound) { + @autoreleasepool { + key = [[str substringToIndex:indexOfColon] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + value = [[str substringFromIndex:indexOfColon+1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + } + } + + if (keyOut) { + *keyOut = key; + } + if (valueOut) { + *valueOut = value; + } + return (key && value); +} + +NSNumber *TNLCLINumberValueFromString(NSString *value) +{ + NSScanner *scanner = [NSScanner scannerWithString:value]; + double num; + if ([scanner scanDouble:&num] && scanner.atEnd) { + return @(num); + } + return nil; +} + +NSNumber *TNLCLIBoolNumberValueFromString(NSString *value) +{ + value = value.lowercaseString; + + static NSSet *sTrueStrings; + static NSSet *sFalseStrings; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sTrueStrings = [NSSet setWithObjects:@"true", @"yes", @"1", nil]; + sFalseStrings = [NSSet setWithObjects:@"false", @"no", @"0", nil]; + }); + + if ([sTrueStrings containsObject:value]) { + return @YES; + } + if ([sFalseStrings containsObject:value]) { + return @NO; + } + return nil; +} + diff --git a/TNLCLI/TNLGlobalConfiguration+TNLCLI.h b/TNLCLI/TNLGlobalConfiguration+TNLCLI.h new file mode 100644 index 0000000..2911a36 --- /dev/null +++ b/TNLCLI/TNLGlobalConfiguration+TNLCLI.h @@ -0,0 +1,19 @@ +// +// TNLGlobalConfiguration+TNLCLI.h +// tnlcli +// +// Created on 9/17/19. +// Copyright © 2020 Twitter. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface TNLGlobalConfiguration (TNLCLI) + +- (BOOL)tnlcli_applySettingWithName:(NSString *)name value:(NSString *)value; + +@end + +NS_ASSUME_NONNULL_END diff --git a/TNLCLI/TNLGlobalConfiguration+TNLCLI.m b/TNLCLI/TNLGlobalConfiguration+TNLCLI.m new file mode 100644 index 0000000..d30b7a0 --- /dev/null +++ b/TNLCLI/TNLGlobalConfiguration+TNLCLI.m @@ -0,0 +1,42 @@ +// +// TNLGlobalConfiguration+TNLCLI.m +// tnlcli +// +// Created on 9/17/19. +// Copyright © 2020 Twitter. All rights reserved. +// + +#import + +#import "TNLCLIPrint.h" +#import "TNLCLIUtils.h" +#import "TNLGlobalConfiguration+TNLCLI.h" + +@implementation TNLGlobalConfiguration (TNLCLI) + +- (BOOL)tnlcli_applySettingWithName:(NSString *)name value:(NSString *)value +{ + if ([name isEqualToString:@"idleTimeoutMode"]) { + NSNumber *number = TNLCLINumberValueFromString(value); + if (number) { + self.idleTimeoutMode = number.integerValue; + return YES; + } else { + TNLCLIPrintWarning([NSString stringWithFormat:@"'%@' should be an integer value matching the 'TNLGlobalConfigurationIdleTimeoutMode' enumeration for global configuration, but '%@' was provided", name, value]); + } + } else if ([name isEqualToString:@"timeoutIntervalBetweenDataTransfer"]) { + NSNumber *number = TNLCLINumberValueFromString(value); + if (number) { + self.timeoutIntervalBetweenDataTransfer = number.doubleValue; + return YES; + } else { + TNLCLIPrintWarning([NSString stringWithFormat:@"'%@' should be a time interval in seconds as double for global configuration, but '%@' was provided", name, value]); + } + } else { + TNLCLIPrintWarning([NSString stringWithFormat:@"'%@' is not a recognized global configuration setting, ignoring it.", name]); + } + + return NO; +} + +@end diff --git a/TNLCLI/TNLMutableRequestConfiguration+TNLCLI.h b/TNLCLI/TNLMutableRequestConfiguration+TNLCLI.h new file mode 100644 index 0000000..d80ade7 --- /dev/null +++ b/TNLCLI/TNLMutableRequestConfiguration+TNLCLI.h @@ -0,0 +1,22 @@ +// +// TNLMutableRequestConfiguration+TNLCLI.h +// tnlcli +// +// Created on 9/17/19. +// Copyright © 2020 Twitter. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface TNLMutableRequestConfiguration (TNLCLI) + ++ (nullable instancetype)tnlcli_configurationWithFile:(NSString *)filePath error:(NSError * __nullable * __nullable)errorOut; ++ (instancetype)tnlcli_configurationWithDictionary:(nullable NSDictionary *)d; + +- (BOOL)tnlcli_applySettingWithName:(NSString *)name value:(NSString *)value; + +@end + +NS_ASSUME_NONNULL_END diff --git a/TNLCLI/TNLMutableRequestConfiguration+TNLCLI.m b/TNLCLI/TNLMutableRequestConfiguration+TNLCLI.m new file mode 100644 index 0000000..f297dab --- /dev/null +++ b/TNLCLI/TNLMutableRequestConfiguration+TNLCLI.m @@ -0,0 +1,170 @@ +// +// TNLMutableRequestConfiguration+TNLCLI.m +// tnlcli +// +// Created on 9/17/19. +// Copyright © 2020 Twitter. All rights reserved. +// + +#import + +#import "TNLCLIError.h" +#import "TNLCLIPrint.h" +#import "TNLCLIUtils.h" +#import "TNLMutableRequestConfiguration+TNLCLI.h" + + +@implementation TNLMutableRequestConfiguration (TNLCLI) + ++ (instancetype)tnlcli_configurationWithFile:(NSString *)filePath error:(NSError * _Nullable __autoreleasing *)errorOut +{ + @autoreleasepool { + NSError *error; + NSDictionary *d; + NSData *jsonData = [NSData dataWithContentsOfFile:filePath + options:0 + error:&error]; + if (jsonData) { + d = [NSJSONSerialization JSONObjectWithData:jsonData + options:0 + error:&error]; + if (d) { + if ([d isKindOfClass:[NSDictionary class]]) { + if (d.count) { + __block BOOL allStrings = YES; + [d enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull obj, BOOL * _Nonnull stop) { + if (![key isKindOfClass:[NSString class]] || ![obj isKindOfClass:[NSString class]]) { + allStrings = NO; + *stop = YES; + } + }]; + if (!allStrings) { + d = nil; + } + } + } else { + d = nil; + } + } + } + + if (!error && !d) { + error = TNLCLICreateError(TNLCLIErrorInvalidRequestConfigurationFileFormat, @{ @"file" : filePath }); + } + + if (errorOut) { + *errorOut = error; + } + if (error) { + return nil; + } + + return [self tnlcli_configurationWithDictionary:d]; + } +} + ++ (instancetype)tnlcli_configurationWithDictionary:(NSDictionary *)d +{ + TNLMutableRequestConfiguration *config = [[self alloc] init]; + [d enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull obj, BOOL * _Nonnull stop) { + (void)[config tnlcli_applySettingWithName:key value:obj]; + }]; + return config; +} + +- (BOOL)tnlcli_applySettingWithName:(NSString *)name value:(NSString *)value +{ +#define BOOL_SETTING(setting) \ +do { \ + if ([name isEqualToString: @"" #setting ]) { \ + NSNumber *number = TNLCLIBoolNumberValueFromString(value); \ + if (number) { \ + self. setting = number.boolValue; \ + return YES; \ + } \ + TNLCLIPrintWarning([NSString stringWithFormat:@"'%@' should be an BOOL for a `TNLRequestConfiguration`, but '%@' was provided", name, value]); \ + return NO; \ + } \ +} while (0) + +#define NUMBER_SETTING(setting, accessor) \ +do { \ + if ([name isEqualToString: @"" #setting ]) { \ + NSNumber *number = TNLCLINumberValueFromString(value); \ + if (number) { \ + self. setting = [number accessor##Value]; \ + return YES; \ + } \ + TNLCLIPrintWarning([NSString stringWithFormat:@"'%@' should be a " #accessor " for `TNLRequestConfiguration`, but '%@' was provided", name, value]); \ + return NO; \ + } \ +} while (0) + +#define STRING_SETTING(setting) \ +do { \ + if ([name isEqualToString: @"" #setting ]) { \ + self. setting = value; \ + return YES; \ + } \ +} while (0) + + + /// BOOL settings + + BOOL_SETTING(contributeToExecutingNetworkConnectionsCount); + BOOL_SETTING(skipHostSanitization); + BOOL_SETTING(shouldSetCookies); + BOOL_SETTING(allowsCellularAccess); + BOOL_SETTING(discretionary); + BOOL_SETTING(shouldUseExtendedBackgroundIdleMode); + BOOL_SETTING(shouldLaunchAppForBackgroundEvents); + + + /// Double settings + + NUMBER_SETTING(idleTimeout, double); + NUMBER_SETTING(attemptTimeout, double); + NUMBER_SETTING(operationTimeout, double); + NUMBER_SETTING(deferrableInterval, double); + + + /// Integer settings + + NUMBER_SETTING(executionMode, integer); + NUMBER_SETTING(redirectPolicy, integer); + NUMBER_SETTING(responseDataConsumptionMode, integer); + NUMBER_SETTING(protocolOptions, integer); + NUMBER_SETTING(connectivityOptions, integer); + NUMBER_SETTING(responseComputeHashAlgorithm, integer); + // NUMBER_SETTING(multipathServiceType, integer); -- unavailable on Mac + + + /// Unsigned Integer settings + + NUMBER_SETTING(cachePolicy, unsignedInteger); + NUMBER_SETTING(cookieAcceptPolicy, unsignedInteger); + NUMBER_SETTING(networkServiceType, unsignedInteger); + + + /// String settings + + STRING_SETTING(sharedContainerIdentifier); + + + /// Unsupported from key-value-pair settings (aka TODO) + + // @property id retryPolicyProvider; + // @property id contentEncoder; + // @property NSArray> *additionalContentDecoders; + // @property NSURLCredentialStorage *URLCredentialStorage; + // @property NSURLCache *URLCache; + // @property NSHTTPCookieStorage *cookieStorage; + + return NO; + +#undef BOOL_SETTING +#undef NUMBER_SETTING +#undef STRING_SETTING +} + +@end diff --git a/TNLCLI/main.m b/TNLCLI/main.m new file mode 100644 index 0000000..fa609de --- /dev/null +++ b/TNLCLI/main.m @@ -0,0 +1,29 @@ +// +// main.m +// TNLCLI +// +// Created on 9/11/19. +// Copyright © 2020 Twitter. All rights reserved. +// + +#import "TNLCLIExecution.h" +#import "TNLCLIPrint.h" + +@import Foundation; + +int main(int argc, const char * argv[]) +{ + int result = 0; + @autoreleasepool { + + TNLCLIExecutionContext *context = [[TNLCLIExecutionContext alloc] initWithArgC:argc argV:argv]; + TNLCLIExecution *exe = [[TNLCLIExecution alloc] initWithContext:context]; + NSError *error = [exe execute]; + if (error) { + TNLCLIPrintUsage(context.executableName); + result = (int)error.code ?: -1; + } + + } + return result; +} diff --git a/TNLExample/TAPI/TAPI.h b/TNLExample/TAPI/TAPI.h index 66e46c2..1f62d3d 100644 --- a/TNLExample/TAPI/TAPI.h +++ b/TNLExample/TAPI/TAPI.h @@ -3,7 +3,7 @@ // TNLExample // // Created on 5/25/18. -// Copyright © 2018 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // // Twitter API Imports diff --git a/TNLExample/TAPI/TAPIClient.h b/TNLExample/TAPI/TAPIClient.h index 9ad1f49..e85bb24 100644 --- a/TNLExample/TAPI/TAPIClient.h +++ b/TNLExample/TAPI/TAPIClient.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/17/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/TNLExample/TAPI/TAPIClient.m b/TNLExample/TAPI/TAPIClient.m index a4e514b..40a4568 100644 --- a/TNLExample/TAPI/TAPIClient.m +++ b/TNLExample/TAPI/TAPIClient.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/17/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/TNLExample/TAPI/TAPIError.h b/TNLExample/TAPI/TAPIError.h index 97fb554..777c740 100644 --- a/TNLExample/TAPI/TAPIError.h +++ b/TNLExample/TAPI/TAPIError.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/17/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/TNLExample/TAPI/TAPIError.m b/TNLExample/TAPI/TAPIError.m index 5a2f4e2..108bfc9 100644 --- a/TNLExample/TAPI/TAPIError.m +++ b/TNLExample/TAPI/TAPIError.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/17/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TAPIError.h" diff --git a/TNLExample/TAPI/TAPIFavoriteRequests.h b/TNLExample/TAPI/TAPIFavoriteRequests.h index d2edef2..24e51ef 100644 --- a/TNLExample/TAPI/TAPIFavoriteRequests.h +++ b/TNLExample/TAPI/TAPIFavoriteRequests.h @@ -3,7 +3,7 @@ // TNLExample // // Created on 5/24/18. -// Copyright © 2018 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TAPIRequest.h" diff --git a/TNLExample/TAPI/TAPIFavoriteRequests.m b/TNLExample/TAPI/TAPIFavoriteRequests.m index fd11f3e..3a6b529 100644 --- a/TNLExample/TAPI/TAPIFavoriteRequests.m +++ b/TNLExample/TAPI/TAPIFavoriteRequests.m @@ -3,7 +3,7 @@ // TNLExample // // Created on 5/24/18. -// Copyright © 2018 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TAPIFavoriteRequests.h" diff --git a/TNLExample/TAPI/TAPIModel.h b/TNLExample/TAPI/TAPIModel.h index a04c08b..5c9749e 100644 --- a/TNLExample/TAPI/TAPIModel.h +++ b/TNLExample/TAPI/TAPIModel.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/17/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/TNLExample/TAPI/TAPIModel.m b/TNLExample/TAPI/TAPIModel.m index 3bc4daf..a1627c0 100644 --- a/TNLExample/TAPI/TAPIModel.m +++ b/TNLExample/TAPI/TAPIModel.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/17/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/TNLExample/TAPI/TAPIRequest.h b/TNLExample/TAPI/TAPIRequest.h index 587220d..5791919 100644 --- a/TNLExample/TAPI/TAPIRequest.h +++ b/TNLExample/TAPI/TAPIRequest.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/17/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/TNLExample/TAPI/TAPIRequest.m b/TNLExample/TAPI/TAPIRequest.m index b7d81d4..8614186 100644 --- a/TNLExample/TAPI/TAPIRequest.m +++ b/TNLExample/TAPI/TAPIRequest.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/17/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TAPIRequest.h" diff --git a/TNLExample/TAPI/TAPIResponse.h b/TNLExample/TAPI/TAPIResponse.h index 6058c70..bdd45b0 100644 --- a/TNLExample/TAPI/TAPIResponse.h +++ b/TNLExample/TAPI/TAPIResponse.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/17/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/TNLExample/TAPI/TAPIResponse.m b/TNLExample/TAPI/TAPIResponse.m index 30412fb..7d84a18 100644 --- a/TNLExample/TAPI/TAPIResponse.m +++ b/TNLExample/TAPI/TAPIResponse.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/17/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TAPIError.h" diff --git a/TNLExample/TAPI/TAPISearchRequests.h b/TNLExample/TAPI/TAPISearchRequests.h index b6452a1..a370953 100644 --- a/TNLExample/TAPI/TAPISearchRequests.h +++ b/TNLExample/TAPI/TAPISearchRequests.h @@ -3,7 +3,7 @@ // TNLExample // // Created on 5/24/18. -// Copyright © 2018 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TAPIModel.h" diff --git a/TNLExample/TAPI/TAPISearchRequests.m b/TNLExample/TAPI/TAPISearchRequests.m index 3d37135..be0bb7e 100644 --- a/TNLExample/TAPI/TAPISearchRequests.m +++ b/TNLExample/TAPI/TAPISearchRequests.m @@ -3,7 +3,7 @@ // TNLExample // // Created on 5/24/18. -// Copyright © 2018 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TAPIError.h" diff --git a/TNLExample/TAPI/TAPIUploadMediaRequest.h b/TNLExample/TAPI/TAPIUploadMediaRequest.h index f9536fa..52bc899 100644 --- a/TNLExample/TAPI/TAPIUploadMediaRequest.h +++ b/TNLExample/TAPI/TAPIUploadMediaRequest.h @@ -2,8 +2,8 @@ // TAPIUploadMediaRequest.h // TNLExample // -// Created by Nolan O'Brien on 5/30/18. -// Copyright © 2018 Twitter. All rights reserved. +// Created on 5/30/18. +// Copyright © 2020 Twitter. All rights reserved. // #import "TAPIRequest.h" diff --git a/TNLExample/TAPI/TAPIUploadMediaRequest.m b/TNLExample/TAPI/TAPIUploadMediaRequest.m index 6dca930..8a4c358 100644 --- a/TNLExample/TAPI/TAPIUploadMediaRequest.m +++ b/TNLExample/TAPI/TAPIUploadMediaRequest.m @@ -2,8 +2,8 @@ // TAPIUploadMediaRequest.m // TNLExample // -// Created by Nolan O'Brien on 5/30/18. -// Copyright © 2018 Twitter. All rights reserved. +// Created on 5/30/18. +// Copyright © 2020 Twitter. All rights reserved. // #import "TAPIUploadMediaRequest.h" diff --git a/TNLExample/TNLExample.entitlements b/TNLExample/TNLExample.entitlements new file mode 100644 index 0000000..ee95ab7 --- /dev/null +++ b/TNLExample/TNLExample.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/TNLExample/TNLXAppDelegate.h b/TNLExample/TNLXAppDelegate.h index c0134a8..dc36326 100644 --- a/TNLExample/TNLXAppDelegate.h +++ b/TNLExample/TNLXAppDelegate.h @@ -3,7 +3,7 @@ // TNLExample // // Created on 7/24/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // @import UIKit; diff --git a/TNLExample/TNLXAppDelegate.m b/TNLExample/TNLXAppDelegate.m index 5d18978..4e92d99 100644 --- a/TNLExample/TNLXAppDelegate.m +++ b/TNLExample/TNLXAppDelegate.m @@ -3,7 +3,7 @@ // TNLExample // // Created on 7/24/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/TNLExample/TNLXDummy.h b/TNLExample/TNLXDummy.h index ce30e70..87c64b4 100644 --- a/TNLExample/TNLXDummy.h +++ b/TNLExample/TNLXDummy.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/29/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // @import UIKit; diff --git a/TNLExample/TNLXDummy.m b/TNLExample/TNLXDummy.m index 5704374..b0e2a40 100644 --- a/TNLExample/TNLXDummy.m +++ b/TNLExample/TNLXDummy.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/29/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/TNLExample/TNLXImageSupport.h b/TNLExample/TNLXImageSupport.h index 3f57638..6ebe19d 100644 --- a/TNLExample/TNLXImageSupport.h +++ b/TNLExample/TNLXImageSupport.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/18/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/TNLExample/TNLXImageSupport.m b/TNLExample/TNLXImageSupport.m index 0220862..57ed4e6 100644 --- a/TNLExample/TNLXImageSupport.m +++ b/TNLExample/TNLXImageSupport.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/18/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLXImageSupport.h" diff --git a/TNLExample/TNLXImageTableViewController.h b/TNLExample/TNLXImageTableViewController.h index 68cd17e..49d51c5 100644 --- a/TNLExample/TNLXImageTableViewController.h +++ b/TNLExample/TNLXImageTableViewController.h @@ -3,7 +3,7 @@ // TNLExample // // Created on 7/24/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // @import UIKit; diff --git a/TNLExample/TNLXImageTableViewController.m b/TNLExample/TNLXImageTableViewController.m index 096aff0..a8fc834 100644 --- a/TNLExample/TNLXImageTableViewController.m +++ b/TNLExample/TNLXImageTableViewController.m @@ -3,7 +3,7 @@ // TNLExample // // Created on 7/24/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/TNLExample/TNLXImageViewController.h b/TNLExample/TNLXImageViewController.h index 4fcdfa3..c7f56bc 100644 --- a/TNLExample/TNLXImageViewController.h +++ b/TNLExample/TNLXImageViewController.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/18/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // @import UIKit; diff --git a/TNLExample/TNLXImageViewController.m b/TNLExample/TNLXImageViewController.m index 17bdf90..fdb3609 100644 --- a/TNLExample/TNLXImageViewController.m +++ b/TNLExample/TNLXImageViewController.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/18/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/TNLExample/TNLXLotsOfRequestsViewController.h b/TNLExample/TNLXLotsOfRequestsViewController.h index 67066bc..6c70a93 100644 --- a/TNLExample/TNLXLotsOfRequestsViewController.h +++ b/TNLExample/TNLXLotsOfRequestsViewController.h @@ -3,7 +3,7 @@ // TNLExample // // Created on 7/24/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // @import UIKit; diff --git a/TNLExample/TNLXLotsOfRequestsViewController.m b/TNLExample/TNLXLotsOfRequestsViewController.m index 2353288..2c34c97 100644 --- a/TNLExample/TNLXLotsOfRequestsViewController.m +++ b/TNLExample/TNLXLotsOfRequestsViewController.m @@ -3,7 +3,7 @@ // TNLExample // // Created on 7/24/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/TNLExample/TNLXMultipartFormData.h b/TNLExample/TNLXMultipartFormData.h index d121b22..292f6a6 100644 --- a/TNLExample/TNLXMultipartFormData.h +++ b/TNLExample/TNLXMultipartFormData.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/22/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLRequest.h" diff --git a/TNLExample/TNLXMultipartFormData.m b/TNLExample/TNLXMultipartFormData.m index b224912..4ff39db 100644 --- a/TNLExample/TNLXMultipartFormData.m +++ b/TNLExample/TNLXMultipartFormData.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/22/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSDictionary+TNLAdditions.h" diff --git a/TNLExample/TNLXNetworkHeuristicObserver.h b/TNLExample/TNLXNetworkHeuristicObserver.h index ad3ea1d..87a4d56 100644 --- a/TNLExample/TNLXNetworkHeuristicObserver.h +++ b/TNLExample/TNLXNetworkHeuristicObserver.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 1/26/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/TNLExample/TNLXNetworkHeuristicObserver.m b/TNLExample/TNLXNetworkHeuristicObserver.m index 7f38ee5..249e29b 100644 --- a/TNLExample/TNLXNetworkHeuristicObserver.m +++ b/TNLExample/TNLXNetworkHeuristicObserver.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 1/26/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLXNetworkHeuristicObserver.h" diff --git a/TNLExample/TNLXPlaygroundViewController.h b/TNLExample/TNLXPlaygroundViewController.h index a1e8a6b..4555dd4 100644 --- a/TNLExample/TNLXPlaygroundViewController.h +++ b/TNLExample/TNLXPlaygroundViewController.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/23/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // @import UIKit; diff --git a/TNLExample/TNLXPlaygroundViewController.m b/TNLExample/TNLXPlaygroundViewController.m index e1832e0..bee7091 100644 --- a/TNLExample/TNLXPlaygroundViewController.m +++ b/TNLExample/TNLXPlaygroundViewController.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 8/23/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/TNLExample/en.lproj/InfoPlist.strings b/TNLExample/en.lproj/InfoPlist.strings deleted file mode 100644 index 477b28f..0000000 --- a/TNLExample/en.lproj/InfoPlist.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Localized versions of Info.plist keys */ - diff --git a/TNLExample/main.m b/TNLExample/main.m index 9880bcc..4b1f27a 100644 --- a/TNLExample/main.m +++ b/TNLExample/main.m @@ -3,7 +3,7 @@ // TNLExample // // Created on 7/24/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLXAppDelegate.h" diff --git a/TwitterNetworkLayer.xcodeproj/project.pbxproj b/TwitterNetworkLayer.xcodeproj/project.pbxproj index dd803d8..5fa785e 100644 --- a/TwitterNetworkLayer.xcodeproj/project.pbxproj +++ b/TwitterNetworkLayer.xcodeproj/project.pbxproj @@ -65,6 +65,9 @@ 8B490DDD22A82B9D002D2296 /* NSCoder+TNLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B490DD622A82B9D002D2296 /* NSCoder+TNLAdditions.m */; }; 8B490DDE22A82B9D002D2296 /* NSCoder+TNLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B490DD622A82B9D002D2296 /* NSCoder+TNLAdditions.m */; }; 8B4AA7621E1F297C00BF7EA0 /* CoreTelephony.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B4AA75D1E1F28DF00BF7EA0 /* CoreTelephony.framework */; }; + 8B4AF738245A359A00ABB8D5 /* TNLCommunicationAgentTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B4AF737245A359A00ABB8D5 /* TNLCommunicationAgentTest.m */; }; + 8B4AF739245A359A00ABB8D5 /* TNLCommunicationAgentTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B4AF737245A359A00ABB8D5 /* TNLCommunicationAgentTest.m */; }; + 8B4AF73A245A359A00ABB8D5 /* TNLCommunicationAgentTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B4AF737245A359A00ABB8D5 /* TNLCommunicationAgentTest.m */; }; 8B4CBEA91A169A6800230318 /* TNLRequestRetryPolicyConfigurationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B4CBEA81A169A6800230318 /* TNLRequestRetryPolicyConfigurationTest.m */; }; 8B4DEFF91986AA64008A31EB /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B4DEFF81986AA64008A31EB /* SystemConfiguration.framework */; }; 8B4DEFFC1986AE55008A31EB /* TNLURLCoding.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B4DEFFA1986AE55008A31EB /* TNLURLCoding.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -102,6 +105,8 @@ 8B84348A1A13B8E500D006DA /* TNLResponseTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B8434891A13B8E500D006DA /* TNLResponseTest.m */; }; 8B84348C1A13BF3C00D006DA /* TNLRequestOperationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B84348B1A13BF3C00D006DA /* TNLRequestOperationTest.m */; }; 8B84348E1A1509F500D006DA /* NSURLCache+TNLAdditionsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B84348D1A1509F500D006DA /* NSURLCache+TNLAdditionsTest.m */; }; + 8B84E5C0232AC621001CC260 /* TNLCLIExecution.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B84E5BF232AC621001CC260 /* TNLCLIExecution.m */; }; + 8B84E5C3232ACB10001CC260 /* TNLCLIPrint.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B84E5C2232ACB10001CC260 /* TNLCLIPrint.m */; }; 8B86BF3A1A2D0998005AE96B /* TNLGlobalConfiguration_Project.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B86BF381A2D0998005AE96B /* TNLGlobalConfiguration_Project.h */; }; 8B879D8619F1B5F500FE95FF /* TAPIRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B879D8519F1B5F500FE95FF /* TAPIRequest.m */; }; 8B879D8919F1B74E00FE95FF /* TAPIError.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B879D8819F1B74E00FE95FF /* TAPIError.m */; }; @@ -251,6 +256,10 @@ 8BB26ABB1D8B142E00D1AB5A /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B6DCB4B1974598300235576 /* CFNetwork.framework */; }; 8BB26ABC1D8B143900D1AB5A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B6DCB491974583B00235576 /* UIKit.framework */; }; 8BB8B87D1A1E710E0093AE63 /* TNLRequestDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BB8B87B1A1E710E0093AE63 /* TNLRequestDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8BBF551323297F3900C94709 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BBF551223297F3900C94709 /* main.m */; }; + 8BBF551923297F5800C94709 /* TwitterNetworkLayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF4AA13B1EE61D47001647B5 /* TwitterNetworkLayer.framework */; }; + 8BBF551C23297FEE00C94709 /* TNLCLIExecutionContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BBF551B23297FEE00C94709 /* TNLCLIExecutionContext.m */; }; + 8BBF551F232980FC00C94709 /* TNLCLIError.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BBF551E232980FC00C94709 /* TNLCLIError.m */; }; 8BC1AC3D19A27559008687EE /* TNLXImageSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BC1AC3C19A27559008687EE /* TNLXImageSupport.m */; }; 8BCA626419C356AE00F3F8CA /* TNLRequestOperationState.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BCA626319C356AE00F3F8CA /* TNLRequestOperationState.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8BCAF8C519F716370043EB22 /* TNLRequestOperationCancelSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BCAF8C319F716370043EB22 /* TNLRequestOperationCancelSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -266,6 +275,9 @@ 8BD500E71D8765F200D828C7 /* TNLURLStringCoding.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BD500E61D8765F200D828C7 /* TNLURLStringCoding.m */; }; 8BD500F51D87762200D828C7 /* TNL_ProjectCommon.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BD500F31D87762200D828C7 /* TNL_ProjectCommon.h */; }; 8BD500F61D87762200D828C7 /* TNL_ProjectCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BD500F41D87762200D828C7 /* TNL_ProjectCommon.m */; }; + 8BD5F4C42331DE3A00C46FAA /* TNLCLIUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BD5F4C32331DE3A00C46FAA /* TNLCLIUtils.m */; }; + 8BD5F4C72331DF6800C46FAA /* TNLGlobalConfiguration+TNLCLI.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BD5F4C62331DF6800C46FAA /* TNLGlobalConfiguration+TNLCLI.m */; }; + 8BD5F4CA2331E0ED00C46FAA /* TNLMutableRequestConfiguration+TNLCLI.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BD5F4C92331E0ED00C46FAA /* TNLMutableRequestConfiguration+TNLCLI.m */; }; 8BD8715D22AD82360011DACA /* Network.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8BCC9EEF22AC36C400A5D1C8 /* Network.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 8BD8715E22AD823A0011DACA /* Network.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8BCC9EEF22AC36C400A5D1C8 /* Network.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 8BDA9D2D197881DE00678D90 /* TNLError.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BDA9D2B197881DE00678D90 /* TNLError.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -276,11 +288,18 @@ 8BDB97E31D24D28700861951 /* TwitterNetworkLayer.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8BE402C61946743D00C7241E /* TwitterNetworkLayer.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 8BDC0E3C1BDFE15C0077F8FC /* TNLLRUCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BDC0E391BDFE15C0077F8FC /* TNLLRUCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8BDC0E3E1BDFE15C0077F8FC /* TNLLRUCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BDC0E3A1BDFE15C0077F8FC /* TNLLRUCache.m */; }; + 8BDC839E243408E70001BA82 /* TNLBackoff.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BDC839D243408E70001BA82 /* TNLBackoff.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8BDC839F243408E70001BA82 /* TNLBackoff.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BDC839D243408E70001BA82 /* TNLBackoff.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8BDC83A0243408E70001BA82 /* TNLBackoff.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BDC839D243408E70001BA82 /* TNLBackoff.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8BDC83A2243449220001BA82 /* TNLBackoff.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BDC83A1243449220001BA82 /* TNLBackoff.m */; }; + 8BDC83A3243449220001BA82 /* TNLBackoff.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BDC83A1243449220001BA82 /* TNLBackoff.m */; }; + 8BDC83A4243449220001BA82 /* TNLBackoff.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BDC83A1243449220001BA82 /* TNLBackoff.m */; }; + 8BDC83A5243449220001BA82 /* TNLBackoff.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BDC83A1243449220001BA82 /* TNLBackoff.m */; }; + 8BDC83A6243449280001BA82 /* TNLBackoff.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BDC839D243408E70001BA82 /* TNLBackoff.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8BDDDF1D19C0D0BB00D7C0CD /* TNLXMultipartFormData.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BDDDF1C19C0D0BB00D7C0CD /* TNLXMultipartFormData.m */; }; 8BDFFC0519814D3100F7F670 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8BE402C91946743D00C7241E /* Foundation.framework */; }; 8BDFFC0719814D3100F7F670 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8BDFFC0619814D3100F7F670 /* CoreGraphics.framework */; }; 8BDFFC0819814D3100F7F670 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B6DCB491974583B00235576 /* UIKit.framework */; }; - 8BDFFC0E19814D3100F7F670 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8BDFFC0C19814D3100F7F670 /* InfoPlist.strings */; }; 8BDFFC1019814D3100F7F670 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BDFFC0F19814D3100F7F670 /* main.m */; }; 8BDFFC1419814D3100F7F670 /* TNLXAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BDFFC1319814D3100F7F670 /* TNLXAppDelegate.m */; }; 8BDFFC1719814D3100F7F670 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8BDFFC1519814D3100F7F670 /* Main.storyboard */; }; @@ -319,6 +338,14 @@ 8BE68D021B7E421100A0F853 /* NSOperationQueue+TNLSafety.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BE68CFE1B7E421100A0F853 /* NSOperationQueue+TNLSafety.m */; }; 8BE857661DD396B100F79F3D /* NSURLRequest+TNLAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BE857641DD396B100F79F3D /* NSURLRequest+TNLAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8BE857671DD396B100F79F3D /* NSURLRequest+TNLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BE857651DD396B100F79F3D /* NSURLRequest+TNLAdditions.m */; }; + 8BEB80B224216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BEB80B024216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8BEB80B324216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BEB80B024216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8BEB80B424216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BEB80B024216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8BEB80B524216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BEB80B024216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8BEB80B624216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BEB80B124216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.m */; }; + 8BEB80B724216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BEB80B124216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.m */; }; + 8BEB80B824216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BEB80B124216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.m */; }; + 8BEB80B924216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BEB80B124216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.m */; }; 8BED1FC11A89480C00279141 /* TNLRequestRedirecter.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BED1FBF1A89480C00279141 /* TNLRequestRedirecter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8BEE98C31ADC5E3100A58A92 /* TNLHTTPHeaderProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BEE98C11ADC5E3100A58A92 /* TNLHTTPHeaderProvider.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8BF3D2B71A76EAF800DABD9D /* TNLXNetworkHeuristicObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF3D2B61A76EAF700DABD9D /* TNLXNetworkHeuristicObserver.m */; }; @@ -656,6 +683,13 @@ remoteGlobalIDString = 8BFDF90C2135AB2C002F6A80; remoteInfo = "TwitterNetworkLayer tvOS"; }; + 8BBF551723297F4E00C94709 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8BE402BE1946743D00C7241E /* Project object */; + proxyType = 1; + remoteGlobalIDString = BF4AA0C01EE61D46001647B5; + remoteInfo = "TwitterNetworkLayer macOS"; + }; BF4765F81EEEFC5B00A23506 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 8BE402BE1946743D00C7241E /* Project object */; @@ -666,6 +700,15 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 8BBF550E23297F3900C94709 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; 8BDB97E71D24D28800861951 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -732,6 +775,8 @@ 8B490DD522A82B9D002D2296 /* NSCoder+TNLAdditions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSCoder+TNLAdditions.h"; sourceTree = ""; }; 8B490DD622A82B9D002D2296 /* NSCoder+TNLAdditions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSCoder+TNLAdditions.m"; sourceTree = ""; }; 8B4AA75D1E1F28DF00BF7EA0 /* CoreTelephony.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreTelephony.framework; path = System/Library/Frameworks/CoreTelephony.framework; sourceTree = SDKROOT; }; + 8B4AF736245A341E00ABB8D5 /* TNLExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TNLExample.entitlements; sourceTree = ""; }; + 8B4AF737245A359A00ABB8D5 /* TNLCommunicationAgentTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TNLCommunicationAgentTest.m; sourceTree = ""; }; 8B4CBEA81A169A6800230318 /* TNLRequestRetryPolicyConfigurationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TNLRequestRetryPolicyConfigurationTest.m; sourceTree = ""; }; 8B4DEFF81986AA64008A31EB /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; 8B4DEFFA1986AE55008A31EB /* TNLURLCoding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TNLURLCoding.h; sourceTree = ""; }; @@ -771,6 +816,10 @@ 8B8434891A13B8E500D006DA /* TNLResponseTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TNLResponseTest.m; sourceTree = ""; }; 8B84348B1A13BF3C00D006DA /* TNLRequestOperationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TNLRequestOperationTest.m; sourceTree = ""; }; 8B84348D1A1509F500D006DA /* NSURLCache+TNLAdditionsTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURLCache+TNLAdditionsTest.m"; sourceTree = ""; }; + 8B84E5BE232AC621001CC260 /* TNLCLIExecution.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TNLCLIExecution.h; sourceTree = ""; }; + 8B84E5BF232AC621001CC260 /* TNLCLIExecution.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TNLCLIExecution.m; sourceTree = ""; }; + 8B84E5C1232ACB10001CC260 /* TNLCLIPrint.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TNLCLIPrint.h; sourceTree = ""; }; + 8B84E5C2232ACB10001CC260 /* TNLCLIPrint.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TNLCLIPrint.m; sourceTree = ""; }; 8B86BF381A2D0998005AE96B /* TNLGlobalConfiguration_Project.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TNLGlobalConfiguration_Project.h; sourceTree = ""; }; 8B879D8419F1B5F500FE95FF /* TAPIRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TAPIRequest.h; path = TAPI/TAPIRequest.h; sourceTree = ""; }; 8B879D8519F1B5F500FE95FF /* TAPIRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TAPIRequest.m; path = TAPI/TAPIRequest.m; sourceTree = ""; }; @@ -805,6 +854,12 @@ 8BB1190D1D83537C00E75CD9 /* NSData+TNLAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+TNLAdditions.h"; sourceTree = ""; }; 8BB1190E1D83537C00E75CD9 /* NSData+TNLAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+TNLAdditions.m"; sourceTree = ""; }; 8BB8B87B1A1E710E0093AE63 /* TNLRequestDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TNLRequestDelegate.h; sourceTree = ""; }; + 8BBF551023297F3900C94709 /* tnlcli */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = tnlcli; sourceTree = BUILT_PRODUCTS_DIR; }; + 8BBF551223297F3900C94709 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 8BBF551A23297FEE00C94709 /* TNLCLIExecutionContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TNLCLIExecutionContext.h; sourceTree = ""; }; + 8BBF551B23297FEE00C94709 /* TNLCLIExecutionContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TNLCLIExecutionContext.m; sourceTree = ""; }; + 8BBF551D232980FC00C94709 /* TNLCLIError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TNLCLIError.h; sourceTree = ""; }; + 8BBF551E232980FC00C94709 /* TNLCLIError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TNLCLIError.m; sourceTree = ""; }; 8BC1AC3B19A27559008687EE /* TNLXImageSupport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TNLXImageSupport.h; sourceTree = ""; }; 8BC1AC3C19A27559008687EE /* TNLXImageSupport.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TNLXImageSupport.m; sourceTree = ""; }; 8BCA626319C356AE00F3F8CA /* TNLRequestOperationState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TNLRequestOperationState.h; sourceTree = ""; }; @@ -820,18 +875,25 @@ 8BD500E61D8765F200D828C7 /* TNLURLStringCoding.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TNLURLStringCoding.m; sourceTree = ""; }; 8BD500F31D87762200D828C7 /* TNL_ProjectCommon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TNL_ProjectCommon.h; sourceTree = ""; }; 8BD500F41D87762200D828C7 /* TNL_ProjectCommon.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TNL_ProjectCommon.m; sourceTree = ""; }; + 8BD5F4C22331DE3A00C46FAA /* TNLCLIUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TNLCLIUtils.h; sourceTree = ""; }; + 8BD5F4C32331DE3A00C46FAA /* TNLCLIUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TNLCLIUtils.m; sourceTree = ""; }; + 8BD5F4C52331DF6800C46FAA /* TNLGlobalConfiguration+TNLCLI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TNLGlobalConfiguration+TNLCLI.h"; sourceTree = ""; }; + 8BD5F4C62331DF6800C46FAA /* TNLGlobalConfiguration+TNLCLI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "TNLGlobalConfiguration+TNLCLI.m"; sourceTree = ""; }; + 8BD5F4C82331E0ED00C46FAA /* TNLMutableRequestConfiguration+TNLCLI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TNLMutableRequestConfiguration+TNLCLI.h"; sourceTree = ""; }; + 8BD5F4C92331E0ED00C46FAA /* TNLMutableRequestConfiguration+TNLCLI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "TNLMutableRequestConfiguration+TNLCLI.m"; sourceTree = ""; }; 8BDA9D2B197881DE00678D90 /* TNLError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TNLError.h; sourceTree = ""; }; 8BDA9D2C197881DE00678D90 /* TNLError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TNLError.m; sourceTree = ""; }; 8BDA9D311978822300678D90 /* TNLPriority.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TNLPriority.h; sourceTree = ""; }; 8BDA9D321978822300678D90 /* TNLPriority.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TNLPriority.m; sourceTree = ""; }; 8BDC0E391BDFE15C0077F8FC /* TNLLRUCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TNLLRUCache.h; sourceTree = ""; }; 8BDC0E3A1BDFE15C0077F8FC /* TNLLRUCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TNLLRUCache.m; sourceTree = ""; }; + 8BDC839D243408E70001BA82 /* TNLBackoff.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TNLBackoff.h; sourceTree = ""; }; + 8BDC83A1243449220001BA82 /* TNLBackoff.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TNLBackoff.m; sourceTree = ""; }; 8BDDDF1B19C0D0BB00D7C0CD /* TNLXMultipartFormData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TNLXMultipartFormData.h; sourceTree = ""; }; 8BDDDF1C19C0D0BB00D7C0CD /* TNLXMultipartFormData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TNLXMultipartFormData.m; sourceTree = ""; }; 8BDFFC0419814D3100F7F670 /* TNLExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TNLExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 8BDFFC0619814D3100F7F670 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 8BDFFC0B19814D3100F7F670 /* TNLExample-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "TNLExample-Info.plist"; sourceTree = ""; }; - 8BDFFC0D19814D3100F7F670 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 8BDFFC0F19814D3100F7F670 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 8BDFFC1219814D3100F7F670 /* TNLXAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TNLXAppDelegate.h; sourceTree = ""; }; 8BDFFC1319814D3100F7F670 /* TNLXAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TNLXAppDelegate.m; sourceTree = ""; }; @@ -872,6 +934,8 @@ 8BE68CFE1B7E421100A0F853 /* NSOperationQueue+TNLSafety.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSOperationQueue+TNLSafety.m"; sourceTree = ""; }; 8BE857641DD396B100F79F3D /* NSURLRequest+TNLAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURLRequest+TNLAdditions.h"; sourceTree = ""; }; 8BE857651DD396B100F79F3D /* NSURLRequest+TNLAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURLRequest+TNLAdditions.m"; sourceTree = ""; }; + 8BEB80B024216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSURLAuthenticationChallenge+TNLAdditions.h"; sourceTree = ""; }; + 8BEB80B124216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSURLAuthenticationChallenge+TNLAdditions.m"; sourceTree = ""; }; 8BED1FBF1A89480C00279141 /* TNLRequestRedirecter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TNLRequestRedirecter.h; sourceTree = ""; }; 8BEE98C11ADC5E3100A58A92 /* TNLHTTPHeaderProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TNLHTTPHeaderProvider.h; sourceTree = ""; }; 8BF3D2B51A76EAF700DABD9D /* TNLXNetworkHeuristicObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TNLXNetworkHeuristicObserver.h; sourceTree = ""; }; @@ -906,6 +970,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 8BBF550D23297F3900C94709 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8BBF551923297F5800C94709 /* TwitterNetworkLayer.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 8BDFFC0119814D3100F7F670 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1079,6 +1151,28 @@ name = Private; sourceTree = ""; }; + 8BBF551123297F3900C94709 /* TNLCLI */ = { + isa = PBXGroup; + children = ( + 8BBF551223297F3900C94709 /* main.m */, + 8BBF551D232980FC00C94709 /* TNLCLIError.h */, + 8BBF551E232980FC00C94709 /* TNLCLIError.m */, + 8B84E5BE232AC621001CC260 /* TNLCLIExecution.h */, + 8B84E5BF232AC621001CC260 /* TNLCLIExecution.m */, + 8BBF551A23297FEE00C94709 /* TNLCLIExecutionContext.h */, + 8BBF551B23297FEE00C94709 /* TNLCLIExecutionContext.m */, + 8B84E5C1232ACB10001CC260 /* TNLCLIPrint.h */, + 8B84E5C2232ACB10001CC260 /* TNLCLIPrint.m */, + 8BD5F4C22331DE3A00C46FAA /* TNLCLIUtils.h */, + 8BD5F4C32331DE3A00C46FAA /* TNLCLIUtils.m */, + 8BD5F4C52331DF6800C46FAA /* TNLGlobalConfiguration+TNLCLI.h */, + 8BD5F4C62331DF6800C46FAA /* TNLGlobalConfiguration+TNLCLI.m */, + 8BD5F4C82331E0ED00C46FAA /* TNLMutableRequestConfiguration+TNLCLI.h */, + 8BD5F4C92331E0ED00C46FAA /* TNLMutableRequestConfiguration+TNLCLI.m */, + ); + path = TNLCLI; + sourceTree = ""; + }; 8BDFFC0919814D3100F7F670 /* TNLExample */ = { isa = PBXGroup; children = ( @@ -1087,6 +1181,7 @@ 8BDFFC1519814D3100F7F670 /* Main.storyboard */, 8BDFFC0A19814D3100F7F670 /* Supporting Files */, 8B879D8019F1B5CB00FE95FF /* TAPI */, + 8B4AF736245A341E00ABB8D5 /* TNLExample.entitlements */, 8BDFFC1219814D3100F7F670 /* TNLXAppDelegate.h */, 8BDFFC1319814D3100F7F670 /* TNLXAppDelegate.m */, 8B2EC95019B13F1700EE8816 /* TNLXDummy.h */, @@ -1109,7 +1204,6 @@ isa = PBXGroup; children = ( 8BDFFC0B19814D3100F7F670 /* TNLExample-Info.plist */, - 8BDFFC0C19814D3100F7F670 /* InfoPlist.strings */, 8BDFFC0F19814D3100F7F670 /* main.m */, ); name = "Supporting Files"; @@ -1125,6 +1219,7 @@ 8BE402C71946743D00C7241E /* Products */, 8B109D6B20B63F15000D10AA /* README.md */, 8BE402CB1946743E00C7241E /* Source */, + 8BBF551123297F3900C94709 /* TNLCLI */, 8BDFFC0919814D3100F7F670 /* TNLExample */, 8BE402DF1946743E00C7241E /* TwitterNetworkLayerTests */, ); @@ -1141,6 +1236,7 @@ 8BFDF98D2135AB2C002F6A80 /* TwitterNetworkLayer.framework */, 8BFDF9BD2135ACDB002F6A80 /* TwitterNetworkLayerTests tvOS.xctest */, 8B9EBE362135B4B100E6E466 /* TwitterNetworkLayer.framework */, + 8BBF551023297F3900C94709 /* tnlcli */, ); name = Products; sourceTree = ""; @@ -1187,6 +1283,8 @@ 8BE68CFE1B7E421100A0F853 /* NSOperationQueue+TNLSafety.m */, 8B87BA3D1E09DA20005A8926 /* NSURL+TNLAdditions.h */, 8B87BA3E1E09DA20005A8926 /* NSURL+TNLAdditions.m */, + 8BEB80B024216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.h */, + 8BEB80B124216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.m */, 8B8434831A13B77700D006DA /* NSURLCache+TNLAdditions.h */, 8B8434841A13B77700D006DA /* NSURLCache+TNLAdditions.m */, 8BA336D81A3271A0005D7B35 /* NSURLCredentialStorage+TNLAdditions.h */, @@ -1208,6 +1306,8 @@ 8BF953DC1A67DD3F00E9C1AA /* TNLAttemptMetrics.m */, 8B153161207D1DA400A1288F /* TNLAuthenticationChallengeHandler.h */, 8B2924BC1992E42900AC139A /* TNLBackgroundURLSessionTaskOperationManager.m */, + 8BDC839D243408E70001BA82 /* TNLBackoff.h */, + 8BDC83A1243449220001BA82 /* TNLBackoff.m */, 8B00D5A91CFF512100D1728D /* TNLCommunicationAgent.h */, 8B00D5AA1CFF512100D1728D /* TNLCommunicationAgent.m */, 8B6E34241DE0B755004A35C7 /* TNLContentCoding.h */, @@ -1283,6 +1383,7 @@ 8BE402E01946743E00C7241E /* Supporting Files */, 5C7E65741B0298670037AD91 /* TNLAttemptMetaDataTest.m */, 8B68AA5C1D95BF2E00AFD0C8 /* TNLAutoDependencyTest.m */, + 8B4AF737245A359A00ABB8D5 /* TNLCommunicationAgentTest.m */, 8B6E34261DE35F71004A35C7 /* TNLContentEncodingTests.m */, 8B986C641BE3EF1D0053BB14 /* TNLHTTPTests.m */, 8B8A684219FF13F0008623E8 /* TNLNetworkTests.m */, @@ -1354,6 +1455,7 @@ 8B9EBE0B2135B4B100E6E466 /* TNL_ProjectCommon.h in Headers */, 8B9EBE0C2135B4B100E6E466 /* TNLNetworkObserver.h in Headers */, 8B9EBE0D2135B4B100E6E466 /* TNLTiming.h in Headers */, + 8BEB80B524216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.h in Headers */, 8B9EBE0E2135B4B100E6E466 /* TNLRequestOperationCancelSource.h in Headers */, 8B9EBE0F2135B4B100E6E466 /* TNLError.h in Headers */, 8B9EBE102135B4B100E6E466 /* TNLAttemptMetrics_Project.h in Headers */, @@ -1382,6 +1484,7 @@ 8B9EBE272135B4B100E6E466 /* TNLRequestOperationQueue.h in Headers */, 8B9EBE282135B4B100E6E466 /* TNLRequestOperationState.h in Headers */, 8B9EBE292135B4B100E6E466 /* TNLRequestRedirecter.h in Headers */, + 8BDC83A6243449280001BA82 /* TNLBackoff.h in Headers */, 8B9EBE2A2135B4B100E6E466 /* TNLCommunicationAgent_Project.h in Headers */, 8B9EBE2B2135B4B100E6E466 /* TNLNetwork.h in Headers */, 8B9EBE2C2135B4B100E6E466 /* TNLResponse_Project.h in Headers */, @@ -1430,6 +1533,7 @@ 8BD500F51D87762200D828C7 /* TNL_ProjectCommon.h in Headers */, 8B82A5A71948D3E900A16237 /* TNLNetworkObserver.h in Headers */, 8B5141231CE530E100830987 /* TNLTiming.h in Headers */, + 8BEB80B224216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.h in Headers */, 8BCAF8C519F716370043EB22 /* TNLRequestOperationCancelSource.h in Headers */, 8BDA9D2D197881DE00678D90 /* TNLError.h in Headers */, 8BF953E31A67E73E00E9C1AA /* TNLAttemptMetrics_Project.h in Headers */, @@ -1456,6 +1560,7 @@ 8B86BF3A1A2D0998005AE96B /* TNLGlobalConfiguration_Project.h in Headers */, 8B9038E219799D7C001A3DDD /* TNLTemporaryFile_Project.h in Headers */, 8BE403211946794300C7241E /* TNLRequestOperationQueue.h in Headers */, + 8BDC839E243408E70001BA82 /* TNLBackoff.h in Headers */, 8BCA626419C356AE00F3F8CA /* TNLRequestOperationState.h in Headers */, 8BED1FC11A89480C00279141 /* TNLRequestRedirecter.h in Headers */, 8B5DBBFE206D8F9D007EF65B /* TNLCommunicationAgent_Project.h in Headers */, @@ -1506,6 +1611,7 @@ 8BFDF9622135AB2C002F6A80 /* TNL_ProjectCommon.h in Headers */, 8BFDF9632135AB2C002F6A80 /* TNLNetworkObserver.h in Headers */, 8BFDF9642135AB2C002F6A80 /* TNLTiming.h in Headers */, + 8BEB80B424216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.h in Headers */, 8BFDF9652135AB2C002F6A80 /* TNLRequestOperationCancelSource.h in Headers */, 8BFDF9662135AB2C002F6A80 /* TNLError.h in Headers */, 8BFDF9672135AB2C002F6A80 /* TNLAttemptMetrics_Project.h in Headers */, @@ -1532,6 +1638,7 @@ 8BFDF97C2135AB2C002F6A80 /* TNLGlobalConfiguration_Project.h in Headers */, 8BFDF97D2135AB2C002F6A80 /* TNLTemporaryFile_Project.h in Headers */, 8BFDF97E2135AB2C002F6A80 /* TNLRequestOperationQueue.h in Headers */, + 8BDC83A0243408E70001BA82 /* TNLBackoff.h in Headers */, 8BFDF97F2135AB2C002F6A80 /* TNLRequestOperationState.h in Headers */, 8BFDF9802135AB2C002F6A80 /* TNLRequestRedirecter.h in Headers */, 8BFDF9812135AB2C002F6A80 /* TNLCommunicationAgent_Project.h in Headers */, @@ -1598,6 +1705,7 @@ BF4AA1241EE61D46001647B5 /* NSURLResponse+TNLAdditions.h in Headers */, BF4AA1251EE61D46001647B5 /* TNLURLCoding.h in Headers */, BF4AA1261EE61D46001647B5 /* TNLPseudoURLProtocol.h in Headers */, + 8BEB80B324216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.h in Headers */, BF4AA1271EE61D46001647B5 /* TNLLogger.h in Headers */, 8BD083CB1FD9C20E0090B7C3 /* TNLTimeoutOperation.h in Headers */, BF4AA1281EE61D46001647B5 /* TwitterNetworkLayer.h in Headers */, @@ -1613,6 +1721,7 @@ BF4AA1311EE61D46001647B5 /* TNLNetwork.h in Headers */, BF4AA1321EE61D46001647B5 /* TNLResponse_Project.h in Headers */, BF4AA1331EE61D46001647B5 /* TNLLRUCache.h in Headers */, + 8BDC839F243408E70001BA82 /* TNLBackoff.h in Headers */, 8B153163207D1DA400A1288F /* TNLAuthenticationChallengeHandler.h in Headers */, BF4AA1341EE61D46001647B5 /* TNLRequestConfiguration_Project.h in Headers */, BF4AA1351EE61D46001647B5 /* NSURL+TNLAdditions.h in Headers */, @@ -1641,6 +1750,24 @@ productReference = 8B9EBE362135B4B100E6E466 /* TwitterNetworkLayer.framework */; productType = "com.apple.product-type.framework"; }; + 8BBF550F23297F3900C94709 /* tnlcli */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8BBF551623297F3900C94709 /* Build configuration list for PBXNativeTarget "tnlcli" */; + buildPhases = ( + 8BBF550C23297F3900C94709 /* Sources */, + 8BBF550D23297F3900C94709 /* Frameworks */, + 8BBF550E23297F3900C94709 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + 8BBF551823297F4E00C94709 /* PBXTargetDependency */, + ); + name = tnlcli; + productName = TNLCLI; + productReference = 8BBF551023297F3900C94709 /* tnlcli */; + productType = "com.apple.product-type.tool"; + }; 8BDFFC0319814D3100F7F670 /* TNLExample */ = { isa = PBXNativeTarget; buildConfigurationList = 8BDFFC3719814D3100F7F670 /* Build configuration list for PBXNativeTarget "TNLExample" */; @@ -1772,12 +1899,16 @@ isa = PBXProject; attributes = { CLASSPREFIX = TNL; - LastUpgradeCheck = 1030; + LastUpgradeCheck = 1140; ORGANIZATIONNAME = Twitter; TargetAttributes = { 8B9EBDB52135B4B100E6E466 = { ProvisioningStyle = Manual; }; + 8BBF550F23297F3900C94709 = { + CreatedOnToolsVersion = 11.0; + ProvisioningStyle = Automatic; + }; 8BDFFC0319814D3100F7F670 = { ProvisioningStyle = Manual; }; @@ -1823,6 +1954,7 @@ 8BFDF90C2135AB2C002F6A80 /* TwitterNetworkLayer tvOS */, 8BFDF98F2135ACDB002F6A80 /* TwitterNetworkLayerTests tvOS */, 8B9EBDB52135B4B100E6E466 /* TwitterNetworkLayer watchOS */, + 8BBF550F23297F3900C94709 /* tnlcli */, ); }; /* End PBXProject section */ @@ -1833,7 +1965,6 @@ buildActionMask = 2147483647; files = ( 8BDFFC1F19814D3100F7F670 /* Images.xcassets in Resources */, - 8BDFFC0E19814D3100F7F670 /* InfoPlist.strings in Resources */, 8BDFFC1719814D3100F7F670 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1899,6 +2030,7 @@ 8B9EBDCC2135B4B100E6E466 /* NSCachedURLResponse+TNLAdditions.m in Sources */, 8B9EBDCD2135B4B100E6E466 /* TNLLRUCache.m in Sources */, 8B9EBDCE2135B4B100E6E466 /* TNLParameterCollection.m in Sources */, + 8BDC83A5243449220001BA82 /* TNLBackoff.m in Sources */, 8B9EBDCF2135B4B100E6E466 /* TNLPseudoURLProtocol.m in Sources */, 8B9EBDD02135B4B100E6E466 /* TNL_ProjectCommon.m in Sources */, 8B9EBDD12135B4B100E6E466 /* TNLPriority.m in Sources */, @@ -1910,6 +2042,7 @@ 8B9EBDD62135B4B100E6E466 /* TNLAttemptMetrics.m in Sources */, 8B9EBDD72135B4B100E6E466 /* TNLCommunicationAgent.m in Sources */, 8B9EBDD82135B4B100E6E466 /* TNLTemporaryFile.m in Sources */, + 8BEB80B924216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.m in Sources */, 8B9EBDD92135B4B100E6E466 /* TNLRequestOperationQueue.m in Sources */, 8B9EBDDA2135B4B100E6E466 /* TNLHTTP.m in Sources */, 8B9EBDDB2135B4B100E6E466 /* TNLError.m in Sources */, @@ -1923,6 +2056,21 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 8BBF550C23297F3900C94709 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8BBF551C23297FEE00C94709 /* TNLCLIExecutionContext.m in Sources */, + 8BBF551323297F3900C94709 /* main.m in Sources */, + 8B84E5C0232AC621001CC260 /* TNLCLIExecution.m in Sources */, + 8BBF551F232980FC00C94709 /* TNLCLIError.m in Sources */, + 8BD5F4CA2331E0ED00C46FAA /* TNLMutableRequestConfiguration+TNLCLI.m in Sources */, + 8BD5F4C72331DF6800C46FAA /* TNLGlobalConfiguration+TNLCLI.m in Sources */, + 8B84E5C3232ACB10001CC260 /* TNLCLIPrint.m in Sources */, + 8BD5F4C42331DE3A00C46FAA /* TNLCLIUtils.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 8BDFFC0019814D3100F7F670 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1976,6 +2124,7 @@ 8B277E4C1C022B5C00279EA9 /* NSCachedURLResponse+TNLAdditions.m in Sources */, 8BDC0E3E1BDFE15C0077F8FC /* TNLLRUCache.m in Sources */, 8B4E017219FB0632004D3CED /* TNLParameterCollection.m in Sources */, + 8BDC83A2243449220001BA82 /* TNLBackoff.m in Sources */, 8B0AFAA91A018EBE00C8C81F /* TNLPseudoURLProtocol.m in Sources */, 8BD500F61D87762200D828C7 /* TNL_ProjectCommon.m in Sources */, 8BDA9D351978822300678D90 /* TNLPriority.m in Sources */, @@ -1987,6 +2136,7 @@ 8BF953E01A67DD3F00E9C1AA /* TNLAttemptMetrics.m in Sources */, 8B00D5AC1CFF512100D1728D /* TNLCommunicationAgent.m in Sources */, 8B9038DE19799D4A001A3DDD /* TNLTemporaryFile.m in Sources */, + 8BEB80B624216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.m in Sources */, 8BE403221946794300C7241E /* TNLRequestOperationQueue.m in Sources */, 8BE4032619467A1400C7241E /* TNLHTTP.m in Sources */, 8BDA9D2F197881DE00678D90 /* TNLError.m in Sources */, @@ -2014,6 +2164,7 @@ 8B8A683E19FEEB51008623E8 /* TNLParameterCollectionTests.m in Sources */, 8B84348C1A13BF3C00D006DA /* TNLRequestOperationTest.m in Sources */, 8B84348E1A1509F500D006DA /* NSURLCache+TNLAdditionsTest.m in Sources */, + 8B4AF738245A359A00ABB8D5 /* TNLCommunicationAgentTest.m in Sources */, 8BFF0A6319FFEFED001F42B7 /* NSURLSessionConfiguration+TNLAdditionsTest.m in Sources */, 8B5F44861A1903A100720DEA /* TNLXMultipartFormData.m in Sources */, 8B4CBEA91A169A6800230318 /* TNLRequestRetryPolicyConfigurationTest.m in Sources */, @@ -2059,6 +2210,7 @@ 8BFDF9232135AB2C002F6A80 /* NSCachedURLResponse+TNLAdditions.m in Sources */, 8BFDF9242135AB2C002F6A80 /* TNLLRUCache.m in Sources */, 8BFDF9252135AB2C002F6A80 /* TNLParameterCollection.m in Sources */, + 8BDC83A4243449220001BA82 /* TNLBackoff.m in Sources */, 8BFDF9262135AB2C002F6A80 /* TNLPseudoURLProtocol.m in Sources */, 8BFDF9272135AB2C002F6A80 /* TNL_ProjectCommon.m in Sources */, 8BFDF9282135AB2C002F6A80 /* TNLPriority.m in Sources */, @@ -2070,6 +2222,7 @@ 8BFDF92D2135AB2C002F6A80 /* TNLAttemptMetrics.m in Sources */, 8BFDF92E2135AB2C002F6A80 /* TNLCommunicationAgent.m in Sources */, 8BFDF92F2135AB2C002F6A80 /* TNLTemporaryFile.m in Sources */, + 8BEB80B824216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.m in Sources */, 8BFDF9302135AB2C002F6A80 /* TNLRequestOperationQueue.m in Sources */, 8BFDF9312135AB2C002F6A80 /* TNLHTTP.m in Sources */, 8BFDF9322135AB2C002F6A80 /* TNLError.m in Sources */, @@ -2097,6 +2250,7 @@ 8BFDF9992135ACDB002F6A80 /* TNLParameterCollectionTests.m in Sources */, 8BFDF99A2135ACDB002F6A80 /* TNLRequestOperationTest.m in Sources */, 8BFDF99B2135ACDB002F6A80 /* NSURLCache+TNLAdditionsTest.m in Sources */, + 8B4AF73A245A359A00ABB8D5 /* TNLCommunicationAgentTest.m in Sources */, 8BFDF99C2135ACDB002F6A80 /* NSURLSessionConfiguration+TNLAdditionsTest.m in Sources */, 8BFDF99D2135ACDB002F6A80 /* TNLXMultipartFormData.m in Sources */, 8BFDF99E2135ACDB002F6A80 /* TNLRequestRetryPolicyConfigurationTest.m in Sources */, @@ -2142,6 +2296,7 @@ BF4AA0D71EE61D46001647B5 /* NSCachedURLResponse+TNLAdditions.m in Sources */, BF4AA0D81EE61D46001647B5 /* TNLLRUCache.m in Sources */, BF4AA0D91EE61D46001647B5 /* TNLParameterCollection.m in Sources */, + 8BDC83A3243449220001BA82 /* TNLBackoff.m in Sources */, BF4AA0DA1EE61D46001647B5 /* TNLPseudoURLProtocol.m in Sources */, BF4AA0DB1EE61D46001647B5 /* TNL_ProjectCommon.m in Sources */, BF4AA0DC1EE61D46001647B5 /* TNLPriority.m in Sources */, @@ -2153,6 +2308,7 @@ BF4AA0E11EE61D46001647B5 /* TNLAttemptMetrics.m in Sources */, BF4AA0E21EE61D46001647B5 /* TNLCommunicationAgent.m in Sources */, BF4AA0E31EE61D46001647B5 /* TNLTemporaryFile.m in Sources */, + 8BEB80B724216AF900FEF7BC /* NSURLAuthenticationChallenge+TNLAdditions.m in Sources */, BF4AA0E41EE61D46001647B5 /* TNLRequestOperationQueue.m in Sources */, BF4AA0E51EE61D46001647B5 /* TNLHTTP.m in Sources */, BF4AA0E61EE61D46001647B5 /* TNLError.m in Sources */, @@ -2180,6 +2336,7 @@ BF4AA1481EE626ED001647B5 /* TNLParameterCollectionTests.m in Sources */, BF4AA1491EE626ED001647B5 /* TNLRequestOperationTest.m in Sources */, BF4AA14A1EE626ED001647B5 /* NSURLCache+TNLAdditionsTest.m in Sources */, + 8B4AF739245A359A00ABB8D5 /* TNLCommunicationAgentTest.m in Sources */, BF4AA14B1EE626ED001647B5 /* NSURLSessionConfiguration+TNLAdditionsTest.m in Sources */, BF4AA14C1EE626ED001647B5 /* TNLXMultipartFormData.m in Sources */, BF4AA14D1EE626ED001647B5 /* TNLRequestRetryPolicyConfigurationTest.m in Sources */, @@ -2215,6 +2372,11 @@ target = 8BFDF90C2135AB2C002F6A80 /* TwitterNetworkLayer tvOS */; targetProxy = 8B9EBDB32135B2D700E6E466 /* PBXContainerItemProxy */; }; + 8BBF551823297F4E00C94709 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = BF4AA0C01EE61D46001647B5 /* TwitterNetworkLayer macOS */; + targetProxy = 8BBF551723297F4E00C94709 /* PBXContainerItemProxy */; + }; BF4765F91EEEFC5B00A23506 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = BF4AA0C01EE61D46001647B5 /* TwitterNetworkLayer macOS */; @@ -2223,14 +2385,6 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - 8BDFFC0C19814D3100F7F670 /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - 8BDFFC0D19814D3100F7F670 /* en */, - ); - name = InfoPlist.strings; - sourceTree = ""; - }; 8BDFFC1519814D3100F7F670 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -2278,6 +2432,63 @@ }; name = Release; }; + 8BBF551423297F3900C94709 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = NO; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + LD_RUNPATH_SEARCH_PATHS = "@executable_path . /usr/local/lib"; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + }; + name = Debug; + }; + 8BBF551523297F3900C94709 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = NO; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + LD_RUNPATH_SEARCH_PATHS = "@executable_path . /usr/local/lib"; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + }; + name = Release; + }; 8BDFFC3219814D3100F7F670 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2301,6 +2512,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "com.twitter.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTS_MACCATALYST = YES; TARGETED_DEVICE_FAMILY = "1,2"; WRAPPER_EXTENSION = app; }; @@ -2326,6 +2538,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "com.twitter.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTS_MACCATALYST = YES; TARGETED_DEVICE_FAMILY = "1,2"; WRAPPER_EXTENSION = app; }; @@ -2363,8 +2576,9 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2.8; + CURRENT_PROJECT_VERSION = 2.14; DEAD_CODE_STRIPPING = NO; + DEBUG_INFORMATION_FORMAT = dwarf; DYLIB_CURRENT_VERSION = "$(CURRENT_PROJECT_VERSION)"; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_BITCODE = NO; @@ -2430,7 +2644,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2.8; + CURRENT_PROJECT_VERSION = 2.14; DEAD_CODE_STRIPPING = NO; DYLIB_CURRENT_VERSION = "$(CURRENT_PROJECT_VERSION)"; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -2478,6 +2692,7 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_MODULES_AUTOLINK = NO; + "DEBUG_INFORMATION_FORMAT[sdk=macosx*]" = "dwarf-with-dsym"; DEFINES_MODULE = YES; INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = com.twitter.TwitterNetworkLayer; @@ -2490,6 +2705,7 @@ 8BE402ED1946743E00C7241E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; DEBUG_INFORMATION_FORMAT = dwarf; INFOPLIST_FILE = "TwitterNetworkLayerTests/TwitterNetworkLayerTests-Info.plist"; OTHER_LDFLAGS = ( @@ -2506,6 +2722,7 @@ 8BE402EE1946743E00C7241E /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; DEBUG_INFORMATION_FORMAT = dwarf; INFOPLIST_FILE = "TwitterNetworkLayerTests/TwitterNetworkLayerTests-Info.plist"; ONLY_ACTIVE_ARCH = YES; @@ -2654,6 +2871,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 8BBF551623297F3900C94709 /* Build configuration list for PBXNativeTarget "tnlcli" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8BBF551423297F3900C94709 /* Debug */, + 8BBF551523297F3900C94709 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 8BDFFC3719814D3100F7F670 /* Build configuration list for PBXNativeTarget "TNLExample" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/TwitterNetworkLayer.xcodeproj/xcshareddata/xcschemes/TNLCLI.xcscheme b/TwitterNetworkLayer.xcodeproj/xcshareddata/xcschemes/TNLCLI.xcscheme new file mode 100644 index 0000000..7588678 --- /dev/null +++ b/TwitterNetworkLayer.xcodeproj/xcshareddata/xcschemes/TNLCLI.xcscheme @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TwitterNetworkLayer.xcodeproj/xcshareddata/xcschemes/TNLExample.xcscheme b/TwitterNetworkLayer.xcodeproj/xcshareddata/xcschemes/TNLExample.xcscheme index a75b045..e5d50a1 100644 --- a/TwitterNetworkLayer.xcodeproj/xcshareddata/xcschemes/TNLExample.xcscheme +++ b/TwitterNetworkLayer.xcodeproj/xcshareddata/xcschemes/TNLExample.xcscheme @@ -1,10 +1,10 @@ + buildImplicitDependencies = "NO"> + + + + - - - - - - - - + buildImplicitDependencies = "NO"> + + + + - - - - - - - - + buildImplicitDependencies = "NO"> - - - - + + - - + buildImplicitDependencies = "NO"> diff --git a/TwitterNetworkLayerTests/NSURLCache+TNLAdditionsTest.m b/TwitterNetworkLayerTests/NSURLCache+TNLAdditionsTest.m index d8267c3..3c4c782 100644 --- a/TwitterNetworkLayerTests/NSURLCache+TNLAdditionsTest.m +++ b/TwitterNetworkLayerTests/NSURLCache+TNLAdditionsTest.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/28/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSURLCache+TNLAdditions.h" @@ -73,7 +73,20 @@ - (void)testCacheHitDetection NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:NSStringFromSelector(_cmd)]; [[NSFileManager defaultManager] removeItemAtPath:path error:NULL]; - NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:1024*1024*10 diskCapacity:1024*1024*10 diskPath:path]; + NSURLCache *cache; + if (tnl_available_ios_13) { + cache = [[NSURLCache alloc] initWithMemoryCapacity:1024*1024*10 + diskCapacity:1024*1024*10 + directoryURL:[NSURL URLWithString:path]]; + } +#if !TARGET_OS_MACCATALYST + else { + cache = [[NSURLCache alloc] initWithMemoryCapacity:1024*1024*10 + diskCapacity:1024*1024*10 + diskPath:path]; + + } +#endif [cache removeAllCachedResponses]; [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]]; // give cache time to purge tnl_defer(^{ diff --git a/TwitterNetworkLayerTests/NSURLSessionConfiguration+TNLAdditionsTest.m b/TwitterNetworkLayerTests/NSURLSessionConfiguration+TNLAdditionsTest.m index 31f2e15..bf45060 100644 --- a/TwitterNetworkLayerTests/NSURLSessionConfiguration+TNLAdditionsTest.m +++ b/TwitterNetworkLayerTests/NSURLSessionConfiguration+TNLAdditionsTest.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/28/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSURLSessionConfiguration+TNLAdditions.h" diff --git a/TwitterNetworkLayerTests/TNLAttemptMetaDataTest.m b/TwitterNetworkLayerTests/TNLAttemptMetaDataTest.m index 4be2d88..bf9a238 100644 --- a/TwitterNetworkLayerTests/TNLAttemptMetaDataTest.m +++ b/TwitterNetworkLayerTests/TNLAttemptMetaDataTest.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created by Kevin Goodier on 05/12/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNL_Project.h" diff --git a/TwitterNetworkLayerTests/TNLAutoDependencyTest.m b/TwitterNetworkLayerTests/TNLAutoDependencyTest.m index 43bed7e..4de250f 100644 --- a/TwitterNetworkLayerTests/TNLAutoDependencyTest.m +++ b/TwitterNetworkLayerTests/TNLAutoDependencyTest.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 9/23/16. -// Copyright © 2016 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/TwitterNetworkLayerTests/TNLCommunicationAgentTest.m b/TwitterNetworkLayerTests/TNLCommunicationAgentTest.m new file mode 100644 index 0000000..f46b029 --- /dev/null +++ b/TwitterNetworkLayerTests/TNLCommunicationAgentTest.m @@ -0,0 +1,59 @@ +// +// TNLCommunicationAgentTest.m +// TwitterNetworkLayer +// +// Created by Nolan on 4/29/20. +// Copyright © 2020 Twitter. All rights reserved. +// + +#import "TNLCommunicationAgent.h" + +@import XCTest; + +#define TEST_COMM_AGENT 0 + +@interface TNLCommunicationAgentTest : XCTestCase +@end + +// This "test" mostly just excercises the communication agent and logs the state. +// It is best to avoid the network interfaces of a machine during unit tests, +// so this "test" is disable but available for local testing by setting TEST_COMM_AGENT to 1. +@implementation TNLCommunicationAgentTest + +#if TEST_COMM_AGENT +- (void)testCommunicationAgent +{ + TNLCommunicationAgent *agent = [[TNLCommunicationAgent alloc] initWithInternetReachabilityHost:@"api.twitter.com"]; + + XCTestExpectation *reachExpectation = [self expectationWithDescription:@"reachability"]; + [agent identifyReachability:^(TNLNetworkReachabilityFlags flags, TNLNetworkReachabilityStatus status) { + [reachExpectation fulfill]; + }]; + XCTestExpectation *carrierExpectation = [self expectationWithDescription:@"carrier"]; + [agent identifyCarrierInfo:^(id _Nullable info) { + [carrierExpectation fulfill]; + }]; + XCTestExpectation *radioExpectation = [self expectationWithDescription:@"radio"]; + [agent identifyWWANRadioAccessTechnology:^(NSString * _Nullable info) { + [radioExpectation fulfill]; + }]; + XCTestExpectation *captivePortalExpectation = [self expectationWithDescription:@"captive.portal"]; + [agent identifyCaptivePortalStatus:^(TNLCaptivePortalStatus status) { + [captivePortalExpectation fulfill]; + }]; + + [self waitForExpectations:@[reachExpectation, + carrierExpectation, + radioExpectation, + captivePortalExpectation] + timeout:10.0]; + + NSLog(@"Reach.Status: %@", TNLNetworkReachabilityStatusToString(agent.currentReachabilityStatus)); + NSLog(@"Reach.Flags: %@", TNLDebugStringFromNetworkReachabilityFlags(agent.currentReachabilityFlags)); + NSLog(@"Radio.Tech: %@", agent.currentWWANRadioAccessTechnology); + NSLog(@"Carrier: %@", agent.currentCarrierInfo); + NSLog(@"Captive: %@", TNLCaptivePortalStatusToString(agent.currentCaptivePortalStatus)); +} +#endif // TEST_COMM_AGENT + +@end diff --git a/TwitterNetworkLayerTests/TNLContentEncodingTests.m b/TwitterNetworkLayerTests/TNLContentEncodingTests.m index fcd5978..71e0556 100644 --- a/TwitterNetworkLayerTests/TNLContentEncodingTests.m +++ b/TwitterNetworkLayerTests/TNLContentEncodingTests.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 11/21/16. -// Copyright © 2016 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/TwitterNetworkLayerTests/TNLHTTPTests.m b/TwitterNetworkLayerTests/TNLHTTPTests.m index 17abb86..042bceb 100644 --- a/TwitterNetworkLayerTests/TNLHTTPTests.m +++ b/TwitterNetworkLayerTests/TNLHTTPTests.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/30/15. -// Copyright © 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLHTTP.h" diff --git a/TwitterNetworkLayerTests/TNLNetworkTests.m b/TwitterNetworkLayerTests/TNLNetworkTests.m index 99a5b83..659d128 100644 --- a/TwitterNetworkLayerTests/TNLNetworkTests.m +++ b/TwitterNetworkLayerTests/TNLNetworkTests.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/27/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLNetwork.h" diff --git a/TwitterNetworkLayerTests/TNLParameterCollectionTests.m b/TwitterNetworkLayerTests/TNLParameterCollectionTests.m index c8e1b66..cceffc4 100644 --- a/TwitterNetworkLayerTests/TNLParameterCollectionTests.m +++ b/TwitterNetworkLayerTests/TNLParameterCollectionTests.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/27/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #include diff --git a/TwitterNetworkLayerTests/TNLPseudoRequestOperationTest.m b/TwitterNetworkLayerTests/TNLPseudoRequestOperationTest.m index d0f7adc..5fef05b 100644 --- a/TwitterNetworkLayerTests/TNLPseudoRequestOperationTest.m +++ b/TwitterNetworkLayerTests/TNLPseudoRequestOperationTest.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/29/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSDictionary+TNLAdditions.h" diff --git a/TwitterNetworkLayerTests/TNLRequestConfigurationTest.m b/TwitterNetworkLayerTests/TNLRequestConfigurationTest.m index 2897c86..cb924fb 100644 --- a/TwitterNetworkLayerTests/TNLRequestConfigurationTest.m +++ b/TwitterNetworkLayerTests/TNLRequestConfigurationTest.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 11/11/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSHTTPCookieStorage+TNLAdditions.h" diff --git a/TwitterNetworkLayerTests/TNLRequestOperationQueueTest.m b/TwitterNetworkLayerTests/TNLRequestOperationQueueTest.m index 3b77b82..39ad4a6 100644 --- a/TwitterNetworkLayerTests/TNLRequestOperationQueueTest.m +++ b/TwitterNetworkLayerTests/TNLRequestOperationQueueTest.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 5/1/15. -// Copyright (c) 2015 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLRequestOperationQueue.h" diff --git a/TwitterNetworkLayerTests/TNLRequestOperationTest.m b/TwitterNetworkLayerTests/TNLRequestOperationTest.m index 62c2721..a19fbaa 100644 --- a/TwitterNetworkLayerTests/TNLRequestOperationTest.m +++ b/TwitterNetworkLayerTests/TNLRequestOperationTest.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 11/12/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #include @@ -17,6 +17,7 @@ #import "TNLRequestOperation_Project.h" #import "TNLRequestOperationCancelSource.h" #import "TNLRequestOperationQueue_Project.h" +#import "TNLRequestRetryPolicyProvider.h" #import "TNLTemporaryFile_Project.h" @import ObjectiveC.runtime; @@ -101,6 +102,9 @@ @interface TestJSONResponse : TNLResponse @property (nonatomic, readonly) BOOL responseBodyWasInFile; @end +@interface TestRetryOnceOn503Response : NSObject +@end + @interface TestTNLRequestDelegate : NSObject @property (atomic, readonly) NSArray *observedCallbacks; @end @@ -184,6 +188,11 @@ - (void)unregisterRequest:(TNLHTTPRequest *)request } - (void)registerRequest:(TNLHTTPRequest *)request args:(NSDictionary *)args +{ + [self registerRequest:request statusCode:200 args:args]; +} + +- (void)registerRequest:(TNLHTTPRequest *)request statusCode:(NSInteger)statusCode args:(NSDictionary *)args { #if RUN_TESTS_WITH_CANNED_RESPONSES @@ -204,7 +213,7 @@ - (void)registerRequest:(TNLHTTPRequest *)request args:(NSDictionary *)args NSDictionary *responseBodyJSON = @{ @"args" : args ?: @{}, @"headers" : headers, @"url" : endpoint.absoluteString, @"json" : (hasBody) ? kBODY_DICTIONARY : [NSNull null] }; NSData *body = [NSJSONSerialization dataWithJSONObject:responseBodyJSON options:0 error:NULL]; - NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:endpoint statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:@{ @"Content-Type" : @"application/json" }]; + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:endpoint statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:@{ @"Content-Type" : @"application/json" }]; [TNLPseudoURLProtocol registerURLResponse:response body:body withEndpoint:endpoint]; [_registeredEndpoints addObject:endpoint]; @@ -1161,6 +1170,37 @@ - (void)testOrderOfCallbacks XCTAssertEqualObjects(expectedCallbacks, observedCallbacks); } +- (void)testOrderOfCallbacksWithRetry +{ + NSDictionary *args = @{@"method":@"status/503"}; + TNLMutableHTTPRequest *request = [self httpBinRequest:args]; + request.HTTPMethodValue = TNLHTTPMethodGET; + [self registerRequest:request statusCode:503 args:args]; + + TNLMutableRequestConfiguration *config = self.config; + config.retryPolicyProvider = [[TestRetryOnceOn503Response alloc] init]; + + TestTNLRequestDelegate *delegate = [[TestTNLRequestDelegate alloc] init]; + TNLRequestOperation *op = [TNLRequestOperation operationWithRequest:request responseClass:[TestJSONResponse class] configuration:config delegate:delegate]; + op.context = delegate; + + [[TNLRequestOperationQueue defaultOperationQueue] enqueueRequestOperation:op]; + [op waitUntilFinishedWithoutBlockingRunLoop]; + [self unregisterRequest:request]; + + NSArray *expectedCallbacks = @[ + NSStringFromSelector(@selector(tnl_requestOperation:didReceiveURLResponse:)), + NSStringFromSelector(@selector(tnl_requestOperation:didCompleteAttemptWithResponse:disposition:)), + NSStringFromSelector(@selector(tnl_requestOperation:willStartRetryFromResponse:policyProvider:afterDelay:)), + NSStringFromSelector(@selector(tnl_requestOperation:didStartRetryFromResponse:policyProvider:)), + NSStringFromSelector(@selector(tnl_requestOperation:didReceiveURLResponse:)), + NSStringFromSelector(@selector(tnl_requestOperation:didCompleteAttemptWithResponse:disposition:)), + NSStringFromSelector(@selector(tnl_requestOperation:didCompleteWithResponse:)) + ]; + NSArray *observedCallbacks = delegate.observedCallbacks; + XCTAssertEqualObjects(expectedCallbacks, observedCallbacks); +} + @end @implementation TestJSONResponse @@ -1193,6 +1233,15 @@ - (void)prepare @end +@implementation TestRetryOnceOn503Response + +- (BOOL)tnl_shouldRetryRequestOperation:(TNLRequestOperation *)op withResponse:(TNLResponse *)response +{ + return op.retryCount <= 0 && response.info.statusCode == 503; +} + +@end + @implementation TestTNLRequestDelegate { dispatch_queue_t _slowQueue; @@ -1241,6 +1290,22 @@ - (void)tnl_requestOperation:(TNLRequestOperation *)op didCompleteAttemptWithRes }); } +- (void)tnl_requestOperation:(TNLRequestOperation *)op willStartRetryFromResponse:(TNLResponse *)responseBeforeRetry policyProvider:(id)policyProvider afterDelay:(NSTimeInterval)delay +{ + SEL cmd = _cmd; + dispatch_sync(_fastQueue, ^{ + [self trackSelector:cmd]; + }); +} + +- (void)tnl_requestOperation:(TNLRequestOperation *)op didStartRetryFromResponse:(TNLResponse *)responseBeforeRetry policyProvider:(id)policyProvider +{ + SEL cmd = _cmd; + dispatch_sync(_fastQueue, ^{ + [self trackSelector:cmd]; + }); +} + - (void)tnl_requestOperation:(TNLRequestOperation *)op didCompleteWithResponse:(TNLResponse *)response { [self trackSelector:_cmd]; diff --git a/TwitterNetworkLayerTests/TNLRequestRetryPolicyConfigurationTest.m b/TwitterNetworkLayerTests/TNLRequestRetryPolicyConfigurationTest.m index 485c785..f420f2a 100644 --- a/TwitterNetworkLayerTests/TNLRequestRetryPolicyConfigurationTest.m +++ b/TwitterNetworkLayerTests/TNLRequestRetryPolicyConfigurationTest.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 11/14/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLHTTPRequest.h" diff --git a/TwitterNetworkLayerTests/TNLRequestRetryPolicyTest.m b/TwitterNetworkLayerTests/TNLRequestRetryPolicyTest.m index b256fd3..8929552 100644 --- a/TwitterNetworkLayerTests/TNLRequestRetryPolicyTest.m +++ b/TwitterNetworkLayerTests/TNLRequestRetryPolicyTest.m @@ -3,7 +3,7 @@ // TwitterNetworkLayerTests // // Created on 8/24/18. -// Copyright © 2018 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNL_Project.h" @@ -12,23 +12,67 @@ @import TwitterNetworkLayer; @import XCTest; +#define kDELAY_TIME_INTERVAL (0.2) + @interface BrokenContentEncoder : NSObject @end @interface TestRetryPolicy : NSObject +@property (nonatomic) NSTimeInterval retryDelay; +@property (nonatomic) NSUInteger maxAttempts; +@end + +@interface TimerOperation : NSOperation +- (instancetype)initWithTimeout:(NSTimeInterval)timeout; @end -@interface TNLRequestRetryPolicyTest : XCTestCase +@interface TNLRequestRetryPolicyTest : XCTestCase @end @implementation TNLRequestRetryPolicyTest +{ + TNLResponse *_response; + NSTimeInterval _dependencyDuration; + NSTimeInterval _dependencyDelay; + NSTimeInterval _retryDelay; + dispatch_block_t _onNextRetry; +} + +- (void)tearDown +{ + [super tearDown]; + _response = nil; + _dependencyDuration = 0; + _dependencyDelay = 0; + _retryDelay = 0; + _onNextRetry = nil; +} - (void)testRetryPolicy { + for (_retryDelay = 0.0; _retryDelay < (kDELAY_TIME_INTERVAL * 1.5); _retryDelay += kDELAY_TIME_INTERVAL) { + for (_dependencyDuration = 0.0; _dependencyDuration < (kDELAY_TIME_INTERVAL * 1.5); _dependencyDuration += kDELAY_TIME_INTERVAL) { + for (_dependencyDelay = 0.0; _dependencyDelay < (kDELAY_TIME_INTERVAL * 1.5); _dependencyDelay += kDELAY_TIME_INTERVAL) { + NSString *cmdStr = NSStringFromSelector(_cmd); + NSLog(@"%@: retry-delay=%fs, dependency-duration=%fs, dependency-delay=%fs", cmdStr, _retryDelay, _dependencyDuration, _dependencyDelay); + const CFAbsoluteTime start = CFAbsoluteTimeGetCurrent(); + [self _runRetryPolicyTest]; + const NSTimeInterval duration = CFAbsoluteTimeGetCurrent() - start; + NSLog(@"%@: run=%fs", cmdStr, duration); + _response = nil; + } + } + } +} + +- (void)_runRetryPolicyTest +{ + TestRetryPolicy *retryPolicy = [[TestRetryPolicy alloc] initWithConfiguration:[TNLRequestRetryPolicyConfiguration standardConfiguration]]; + retryPolicy.retryDelay = _retryDelay; NSURL *URL = [NSURL URLWithString:@"https://fake.domain.com/post/results"]; TNLMutableRequestConfiguration *config = [TNLMutableRequestConfiguration defaultConfiguration]; config.contentEncoder = [[BrokenContentEncoder alloc] init]; - config.retryPolicyProvider = [[TestRetryPolicy alloc] initWithConfiguration:[TNLRequestRetryPolicyConfiguration standardConfiguration]]; + config.retryPolicyProvider = retryPolicy; config.protocolOptions = TNLRequestProtocolOptionPseudo; NSHTTPURLResponse *URLResponse = [[NSHTTPURLResponse alloc] initWithURL:URL @@ -43,7 +87,6 @@ - (void)testRetryPolicy [TNLPseudoURLProtocol unregisterEndpoint:URL]; }); - __block TNLResponse *response = nil; NSDictionary *results = @{ @"player1" : @{ @"name" : @"Montoya", @@ -62,19 +105,21 @@ - (void)testRetryPolicy HTTPBody:requestBody]; TNLRequestOperation *op = [TNLRequestOperation operationWithRequest:request configuration:config - completion:^(TNLRequestOperation * _Nonnull operation, TNLResponse * _Nonnull opResponse) { - response = opResponse; - }]; + delegate:self]; [[TNLRequestOperationQueue defaultOperationQueue] enqueueRequestOperation:op]; [op waitUntilFinishedWithoutBlockingRunLoop]; - XCTAssertNotNil(response); - XCTAssertEqual((int)response.info.statusCode, 200); - XCTAssertNil(response.operationError); - XCTAssertEqual((int)response.metrics.attemptCount, 2); + XCTAssertNotNil(_response); + XCTAssertEqual((int)_response.info.statusCode, 200); + XCTAssertNil(_response.operationError); + XCTAssertEqual((int)_response.metrics.attemptCount, 2); + XCTAssertGreaterThan(_response.metrics.totalDuration, MAX(_retryDelay, _dependencyDuration + _dependencyDelay)); +#if 0 // disable this since the CI machines can have really slow performance -- feel free to enable when running locally + XCTAssertLessThan(_response.metrics.totalDuration, MAX(_retryDelay, _dependencyDuration + _dependencyDelay) + kDELAY_TIME_INTERVAL); +#endif do { - TNLAttemptMetrics *firstAttemptMetrics = response.metrics.attemptMetrics.firstObject; + TNLAttemptMetrics *firstAttemptMetrics = _response.metrics.attemptMetrics.firstObject; XCTAssertNotNil(firstAttemptMetrics); XCTAssertNotNil(firstAttemptMetrics.operationError); XCTAssertEqualObjects(firstAttemptMetrics.operationError.domain, TNLErrorDomain); @@ -85,7 +130,7 @@ - (void)testRetryPolicy NSData *requestBodyBase64 = [requestBody base64EncodedDataWithOptions:(NSDataBase64Encoding64CharacterLineLength | NSDataBase64EncodingEndLineWithCarriageReturn | NSDataBase64EncodingEndLineWithLineFeed)]; - TNLAttemptMetrics *lastAttemptMetrics = response.metrics.attemptMetrics.lastObject; + TNLAttemptMetrics *lastAttemptMetrics = _response.metrics.attemptMetrics.lastObject; XCTAssertNotNil(lastAttemptMetrics); XCTAssertNil(lastAttemptMetrics.operationError); XCTAssertEqualObjects([lastAttemptMetrics.URLRequest valueForHTTPHeaderField:@"Content-Encoding"], @"base64"); @@ -93,6 +138,164 @@ - (void)testRetryPolicy } while (0); } +#pragma mark Test specific scenarios + +- (void)testSuccessfulRequestDoesNotRetry +{ + NSURL *URL = [NSURL URLWithString:@"https://fake.domain.com/"]; + TNLMutableHTTPRequest *request = [TNLMutableHTTPRequest GETRequestWithURL:URL HTTPHeaderFields:nil]; + + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:nil]; + [TNLPseudoURLProtocol registerURLResponse:response body:nil withEndpoint:URL]; + tnl_defer(^{ + [TNLPseudoURLProtocol unregisterEndpoint:URL]; + }); + + TNLRequestRetryPolicyConfiguration *retryConfig = [[TNLRequestRetryPolicyConfiguration alloc] initWithAllMethodsRetriableAndRetriableStatusCodes:@[@200] URLErrorCodes:nil POSIXErrorCodes:nil]; + TestRetryPolicy *retryPolicy = [[TestRetryPolicy alloc] initWithConfiguration:retryConfig]; + + TNLMutableRequestConfiguration *config = [TNLMutableRequestConfiguration defaultConfiguration]; + config.retryPolicyProvider = retryPolicy; + config.protocolOptions = TNLRequestProtocolOptionPseudo; + + TNLRequestOperation *operation = [TNLRequestOperation operationWithRequest:request configuration:config delegate:self]; + [[TNLRequestOperationQueue defaultOperationQueue] enqueueRequestOperation:operation]; + [operation waitUntilFinishedWithoutBlockingRunLoop]; + + XCTAssertEqual(operation.attemptCount, 1); + XCTAssertEqual(operation.retryCount, 0); + XCTAssertEqual(operation.response.info.statusCode, 200); +} + +- (void)testRetryWhenSubsequentRequestSucceeds +{ + NSURL *URL = [NSURL URLWithString:@"https://fake.domain.com/"]; + TNLMutableHTTPRequest *request = [TNLMutableHTTPRequest GETRequestWithURL:URL HTTPHeaderFields:nil]; + + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:URL statusCode:503 HTTPVersion:@"HTTP/1.1" headerFields:nil]; + NSHTTPURLResponse *successResponse = [[NSHTTPURLResponse alloc] initWithURL:URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:nil]; + [TNLPseudoURLProtocol registerURLResponse:response body:nil withEndpoint:URL]; + _onNextRetry = ^{ + [TNLPseudoURLProtocol registerURLResponse:successResponse body:nil withEndpoint:URL]; + }; + tnl_defer(^{ + [TNLPseudoURLProtocol unregisterEndpoint:URL]; + }); + + TestRetryPolicy *retryPolicy = [[TestRetryPolicy alloc] initWithConfiguration:[TNLRequestRetryPolicyConfiguration standardConfiguration]]; + + TNLMutableRequestConfiguration *config = [TNLMutableRequestConfiguration defaultConfiguration]; + config.retryPolicyProvider = retryPolicy; + config.protocolOptions = TNLRequestProtocolOptionPseudo; + + TNLRequestOperation *operation = [TNLRequestOperation operationWithRequest:request configuration:config delegate:self]; + [[TNLRequestOperationQueue defaultOperationQueue] enqueueRequestOperation:operation]; + [operation waitUntilFinishedWithoutBlockingRunLoop]; + + XCTAssertEqual(operation.attemptCount, 2); + XCTAssertEqual(operation.retryCount, 1); + XCTAssertEqual(operation.response.info.statusCode, 200); +} + +- (void)testOperationTimeoutCancelsRetry +{ + // The actual timeout here does not matter, as long as it is enough for + // the initial attempt to complete. If the retryDelay is longer than the + // operationTimeout the retry should not be attempted. + static const NSTimeInterval operationTimeout = 30.0; + static const NSTimeInterval retryDelay = 40.0; + + NSURL *URL = [NSURL URLWithString:@"https://fake.domain.com/"]; + TNLMutableHTTPRequest *request = [TNLMutableHTTPRequest GETRequestWithURL:URL HTTPHeaderFields:nil]; + + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:URL statusCode:503 HTTPVersion:@"HTTP/1.1" headerFields:nil]; + [TNLPseudoURLProtocol registerURLResponse:response body:nil withEndpoint:URL]; + tnl_defer(^{ + [TNLPseudoURLProtocol unregisterEndpoint:URL]; + }); + + TestRetryPolicy *retryPolicy = [[TestRetryPolicy alloc] initWithConfiguration:[TNLRequestRetryPolicyConfiguration standardConfiguration]]; + retryPolicy.retryDelay = retryDelay; + + TNLMutableRequestConfiguration *config = [TNLMutableRequestConfiguration defaultConfiguration]; + config.retryPolicyProvider = retryPolicy; + config.protocolOptions = TNLRequestProtocolOptionPseudo; + config.operationTimeout = operationTimeout; + config.attemptTimeout = operationTimeout; + config.idleTimeout = operationTimeout; + + TNLRequestOperation *operation = [TNLRequestOperation operationWithRequest:request configuration:config delegate:self]; + [[TNLRequestOperationQueue defaultOperationQueue] enqueueRequestOperation:operation]; + [operation waitUntilFinishedWithoutBlockingRunLoop]; + + XCTAssertEqual(operation.attemptCount, 1); + XCTAssertEqual(operation.retryCount, 0); + XCTAssertEqual(operation.response.info.statusCode, 503); +} + +- (void)testRetryWithNoOperationTimeout +{ + // The operation should retry if the `operationTimeout` is unlimited (less than 0.1 per docs). + // There used to be a bug where `operationTimeout` less than 0.1 would NEVER retry. + // This test prevents regression from fixing this particular bug. + + NSURL *URL = [NSURL URLWithString:@"https://fake.domain.com/"]; + TNLMutableHTTPRequest *request = [TNLMutableHTTPRequest GETRequestWithURL:URL HTTPHeaderFields:nil]; + + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:URL statusCode:503 HTTPVersion:@"HTTP/1.1" headerFields:nil]; + [TNLPseudoURLProtocol registerURLResponse:response body:nil withEndpoint:URL]; + tnl_defer(^{ + [TNLPseudoURLProtocol unregisterEndpoint:URL]; + }); + + TestRetryPolicy *retryPolicy = [[TestRetryPolicy alloc] initWithConfiguration:[TNLRequestRetryPolicyConfiguration standardConfiguration]]; + retryPolicy.maxAttempts = 1; + + TNLMutableRequestConfiguration *config = [TNLMutableRequestConfiguration defaultConfiguration]; + config.retryPolicyProvider = retryPolicy; + config.protocolOptions = TNLRequestProtocolOptionPseudo; + config.operationTimeout = -1; + + TNLRequestOperation *operation = [TNLRequestOperation operationWithRequest:request configuration:config delegate:self]; + [[TNLRequestOperationQueue defaultOperationQueue] enqueueRequestOperation:operation]; + [operation waitUntilFinishedWithoutBlockingRunLoop]; + + XCTAssertEqual(operation.attemptCount, 2); + XCTAssertEqual(operation.retryCount, 1); + XCTAssertEqual(operation.response.info.statusCode, 503); +} + +#pragma mark - TNLRequestDelegate + +- (void)tnl_requestOperation:(TNLRequestOperation *)op + willStartRetryFromResponse:(TNLResponse *)responseBeforeRetry + policyProvider:(id)policyProvider + afterDelay:(NSTimeInterval)delay +{ + if (_onNextRetry) { + _onNextRetry(); + _onNextRetry = nil; + } + + if (_dependencyDelay < 0.1 && _dependencyDuration < 0.1) { + return; + } + + NSOperation *timeoutOp = [[TimerOperation alloc] initWithTimeout:_dependencyDuration]; + [op addDependency:timeoutOp]; + NSOperation *delayOp = [[TimerOperation alloc] initWithTimeout:_dependencyDelay]; + [timeoutOp addDependency:delayOp]; + NSOperationQueue *q = [NSOperationQueue currentQueue] ?: [NSOperationQueue mainQueue]; + [q addOperation:timeoutOp]; + [q addOperation:delayOp]; +} + +- (void)tnl_requestOperation:(TNLRequestOperation *)op + didCompleteWithResponse:(TNLResponse *)response +{ + _response = response; +} + @end @implementation BrokenContentEncoder @@ -136,6 +339,10 @@ - (nullable TNLRequestRetryPolicyConfiguration *)configuration - (BOOL)tnl_shouldRetryRequestOperation:(TNLRequestOperation *)op withResponse:(TNLResponse *)response { + if (_maxAttempts && op.attemptCount > _maxAttempts) { + return NO; + } + if ([response.operationError.domain isEqualToString:TNLErrorDomain] && response.operationError.code == TNLErrorCodeRequestOperationRequestContentEncodingFailed) { if (response.metrics.attemptCount > 3) { return NO; @@ -149,7 +356,7 @@ - (BOOL)tnl_shouldRetryRequestOperation:(TNLRequestOperation *)op - (NSTimeInterval)tnl_delayBeforeRetryForRequestOperation:(TNLRequestOperation *)op withResponse:(TNLResponse *)response { - return 0.5; + return _retryDelay; } - (nullable TNLRequestConfiguration *)tnl_configurationOfRetryForRequestOperation:(TNLRequestOperation *)op @@ -180,3 +387,68 @@ - (nullable NSString *)tnl_retryPolicyIdentifier } @end + +@implementation TimerOperation +{ + NSTimeInterval _timeout; + BOOL _finished; + BOOL _executing; +} + +- (instancetype)initWithTimeout:(NSTimeInterval)timeout +{ + if (self = [super init]) { + _timeout = timeout; + } + return self; +} + +- (void)start +{ + if ([self isCancelled]) { + [self willChangeValueForKey:@"isFinished"]; + _finished = YES; + [self didChangeValueForKey:@"isFinished"]; + return; + } + + [self willChangeValueForKey:@"isExecuting"]; + _executing = YES; + [self didChangeValueForKey:@"isExecuting"]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_timeout * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self completeOperation]; + }); +} + +- (BOOL)isExecuting +{ + return _executing; +} + +- (BOOL)isFinished +{ + return _finished; +} + +- (BOOL)isConcurrent +{ + return YES; +} + +- (BOOL)isAsynchronous +{ + return YES; +} + +- (void)completeOperation +{ + [self willChangeValueForKey:@"isFinished"]; + [self willChangeValueForKey:@"isExecuting"]; + _executing = NO; + _finished = YES; + [self didChangeValueForKey:@"isExecuting"]; + [self didChangeValueForKey:@"isFinished"]; +} + +@end + diff --git a/TwitterNetworkLayerTests/TNLRequestTests.m b/TwitterNetworkLayerTests/TNLRequestTests.m index 5350779..4f10608 100644 --- a/TwitterNetworkLayerTests/TNLRequestTests.m +++ b/TwitterNetworkLayerTests/TNLRequestTests.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/28/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLError.h" diff --git a/TwitterNetworkLayerTests/TNLResponseTest.m b/TwitterNetworkLayerTests/TNLResponseTest.m index c885bbb..bff8905 100644 --- a/TwitterNetworkLayerTests/TNLResponseTest.m +++ b/TwitterNetworkLayerTests/TNLResponseTest.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 11/12/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "NSURLResponse+TNLAdditions.h" @@ -172,8 +172,10 @@ - (void)testNSCoding if (tnl_available_ios_11) { error = nil; archive = [NSKeyedArchiver archivedDataWithRootObject:response requiringSecureCoding:YES error:&error]; +#if !TARGET_OS_MACCATALYST } else { archive = [NSKeyedArchiver archivedDataWithRootObject:response]; +#endif } XCTAssertNotEqual(0UL, archive.length); @@ -181,8 +183,10 @@ - (void)testNSCoding if (tnl_available_ios_11) { error = nil; decodedResponse = [NSKeyedUnarchiver unarchivedObjectOfClass:[TNLResponse class] fromData:archive error:&error]; +#if !TARGET_OS_MACCATALYST } else { decodedResponse = [NSKeyedUnarchiver unarchiveObjectWithData:archive]; +#endif } XCTAssertTrue([decodedResponse isKindOfClass:[TNLResponse class]]); @@ -227,8 +231,10 @@ - (void)testNSCoding if (tnl_available_ios_11) { error = nil; archive = [NSKeyedArchiver archivedDataWithRootObject:response requiringSecureCoding:YES error:&error]; +#if !TARGET_OS_MACCATALYST } else { archive = [NSKeyedArchiver archivedDataWithRootObject:response]; +#endif } XCTAssertNotEqual(0UL, archive.length); @@ -236,8 +242,10 @@ - (void)testNSCoding if (tnl_available_ios_11) { error = nil; decodedResponse = [NSKeyedUnarchiver unarchivedObjectOfClass:[TNLResponse class] fromData:archive error:&error]; +#if !TARGET_OS_MACCATALYST } else { decodedResponse = [NSKeyedUnarchiver unarchiveObjectWithData:archive]; +#endif } XCTAssertTrue([decodedResponse isKindOfClass:[TNLResponse class]]); diff --git a/TwitterNetworkLayerTests/TNLTemporaryFileTest.m b/TwitterNetworkLayerTests/TNLTemporaryFileTest.m index aaf3e88..12d2599 100644 --- a/TwitterNetworkLayerTests/TNLTemporaryFileTest.m +++ b/TwitterNetworkLayerTests/TNLTemporaryFileTest.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 10/28/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import "TNLTemporaryFile_Project.h" diff --git a/TwitterNetworkLayerTests/TNLURLCodingTest.m b/TwitterNetworkLayerTests/TNLURLCodingTest.m index 91f31b9..004bf6c 100644 --- a/TwitterNetworkLayerTests/TNLURLCodingTest.m +++ b/TwitterNetworkLayerTests/TNLURLCodingTest.m @@ -3,9 +3,10 @@ // TwitterNetworkLayer // // Created on 11/12/14. -// Copyright (c) 2014 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // +#import "NSNumber+TNLURLCoding.h" #import "TNLURLCoding.h" @import XCTest; @@ -77,4 +78,101 @@ - (void)testURLParameterParsing XCTAssertEqualObjects(query, @"ok=not-empty"); } +- (void)testNumberCoding +{ + NSArray *numbers = @[ + @((BOOL)YES), + @((BOOL)NO), + @((BOOL)7), + + @((uint8_t)0), + @((uint8_t)UINT8_MAX), + @((int8_t)INT8_MIN), + @((int8_t)0), + @((int8_t)INT8_MAX), + + @((uint16_t)0), + @((uint16_t)UINT16_MAX), + @((int16_t)INT16_MIN), + @((int16_t)0), + @((int16_t)INT16_MAX), + + @((uint32_t)0), + @((uint32_t)UINT32_MAX), + @((int32_t)INT32_MIN), + @((int32_t)0), + @((int32_t)INT32_MAX), + + @((uint64_t)0), + @((uint64_t)UINT64_MAX), + @((int64_t)INT64_MIN), + @((int64_t)0), + @((int64_t)INT64_MAX), + + @(FLT_MIN), + @(0.f), + @(FLT_MAX), + @((float)M_PI), + + @(DBL_MIN), + @(0.f), + @(DBL_MAX), + @(M_PI), + ]; + + NSTimeInterval nsDuration, tnlDuration; + const NSUInteger iterations = 20000; +#define PRINT_NUMBERS 0 + + { +#if PRINT_NUMBERS + BOOL didPrint = NO; +#endif + const CFAbsoluteTime nsStart = CFAbsoluteTimeGetCurrent(); + for (NSUInteger i = 0; i < iterations; i++) { + for (NSNumber *number in numbers) { + NSString *value = [number stringValue]; +#if PRINT_NUMBERS + if (!didPrint) { + NSLog(@"%@", value); + } +#endif + (void)value; + } +#if PRINT_NUMBERS + didPrint = YES; +#endif + } + const CFAbsoluteTime nsEnd = CFAbsoluteTimeGetCurrent(); + nsDuration = nsEnd - nsStart; + NSLog(@"-[NSNumber stringValue] = %fs", nsDuration); + } + + { +#if PRINT_NUMBERS + BOOL didPrint = NO; +#endif + const CFAbsoluteTime tnlStart = CFAbsoluteTimeGetCurrent(); + for (NSUInteger i = 0; i < iterations; i++) { + for (NSNumber *number in numbers) { + NSString *value = [number tnl_quickStringValue]; +#if PRINT_NUMBERS + if (!didPrint) { + NSLog(@"%@", value); + } +#endif + (void)value; + } +#if PRINT_NUMBERS + didPrint = YES; +#endif + } + const CFAbsoluteTime tnlEnd = CFAbsoluteTimeGetCurrent(); + tnlDuration = tnlEnd - tnlStart; + NSLog(@"-[NSNumber tnl_quickStringValue] = %fs", tnlDuration); + } + + XCTAssertLessThan(tnlDuration, nsDuration, @"-[NSNumber tnl_quickStringValue] ought to be faster than -[NSNumber stringValue]!"); +} + @end diff --git a/TwitterNetworkLayerTests/TNLURLSessionManagerTest.m b/TwitterNetworkLayerTests/TNLURLSessionManagerTest.m index 012da94..0f70049 100644 --- a/TwitterNetworkLayerTests/TNLURLSessionManagerTest.m +++ b/TwitterNetworkLayerTests/TNLURLSessionManagerTest.m @@ -2,8 +2,8 @@ // TNLURLSessionManagerTest.m // TwitterNetworkLayer // -// Created by Nolan on 4/1/19. -// Copyright © 2019 Twitter. All rights reserved. +// Created on 4/1/19. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/TwitterNetworkLayerTests/TNLXContentEncoding.h b/TwitterNetworkLayerTests/TNLXContentEncoding.h index 49c3f14..45ab3f1 100644 --- a/TwitterNetworkLayerTests/TNLXContentEncoding.h +++ b/TwitterNetworkLayerTests/TNLXContentEncoding.h @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 11/21/16. -// Copyright © 2016 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #import diff --git a/TwitterNetworkLayerTests/TNLXContentEncoding.m b/TwitterNetworkLayerTests/TNLXContentEncoding.m index 51456c1..f92a7c9 100644 --- a/TwitterNetworkLayerTests/TNLXContentEncoding.m +++ b/TwitterNetworkLayerTests/TNLXContentEncoding.m @@ -3,7 +3,7 @@ // TwitterNetworkLayer // // Created on 11/21/16. -// Copyright © 2016 Twitter. All rights reserved. +// Copyright © 2020 Twitter. All rights reserved. // #include