diff --git a/Classes/BITSender.h b/Classes/BITSender.h new file mode 100644 index 0000000000..f79ca6613e --- /dev/null +++ b/Classes/BITSender.h @@ -0,0 +1,115 @@ +#import +#import "HockeySDK.h" + +#if HOCKEYSDK_FEATURE_TELEMETRY + +#import "HockeySDKNullability.h" +@class BITPersistence; + +NS_ASSUME_NONNULL_BEGIN +/** + * Utility class that's responsible for sending a bundle of data to the server + */ +@interface BITSender : NSObject + +///----------------------------------------------------------------------------- +/// @name Initialize instance +///----------------------------------------------------------------------------- + +/** + * Initializes a sender instance with a given persistence object. + * + * @param persistence used for loading files before sending them out + * @param serverURL the endpoint URL for telemetry data + * @return an initialized sender instance + */ +- (instancetype)initWithPersistence:(BITPersistence *)persistence serverURL:(NSURL *)serverURL; + +/** + * Access to the internal operation queue + */ +@property (nonatomic, strong) NSOperationQueue *operationQueue; + +/** + * A queue which is used to handle BITHTTPOperation completion blocks. + */ +@property (nonatomic, strong) dispatch_queue_t senderQueue; + +/** + * The endpoint url of the telemetry server. + */ +@property (nonatomic, copy) NSString *endpointPath; + +/** + * The max number of request that can run at a time. + */ +@property NSUInteger maxRequestCount; + +/** + * The number of requests that are currently running. + */ +@property NSUInteger runningRequestsCount; + +/** + * BaseURL to which relative paths are appended. + */ +@property (nonatomic, strong, readonly) NSURL *serverURL; + +/** + * The persistence instance used for loading files before sending them out. + */ +@property (nonatomic, strong, readonly) BITPersistence *persistence; + +///----------------------------------------------------------------------------- +/// @name Sending data +///----------------------------------------------------------------------------- + +/** + * Triggers sending the saved data. Does nothing if nothing has been persisted, yet. This method should be called by BITTelemetryManager on app start. + */ +- (void)sendSavedData; + +/** + * Creates a HTTP operation and puts it to the queue. + * + * @param request a request for sending a data object to the telemetry server + * @param path path to the file which should be sent + * @param urlSessionSupported a flag which determines whether to use NSURLConnection or NSURLSession for sending out data + */ +- (void)sendRequest:(NSURLRequest * __nonnull)request path:(NSString * __nonnull)path urlSessionSupported:(BOOL)isUrlSessionSupported; + +///----------------------------------------------------------------------------- +/// @name Helper +///----------------------------------------------------------------------------- + +/** + * Returns a request for sending data to the telemetry sender. + * + * @param data the data which should be sent + * + * @return a request which contains the given data + */ +- (NSURLRequest *)requestForData:(NSData *)data withContentType:(NSString *)contentType; + +/** + * Returns if data should be deleted based on a given status code. + * + * @param statusCode the status code which is part of the response object + * + * @return YES if data should be deleted, NO if the payload should be sent at a later time again. + */ +- (BOOL)shouldDeleteDataWithStatusCode:(NSInteger)statusCode; + +/** + * This method tries to detect whether the given data object is regular JSON or JSON Stream and returns the appropriate HTTP content type. + * + * @param data The data object whose content type should be returned. + * + * @returns "application/json" if the data is regular JSON or "application/x-json-stream" if it is JSON Stream. Defaults to "application/json". + */ +- (NSString *)contentTypeForData:(NSData *)data; + +@end +NS_ASSUME_NONNULL_END + +#endif /* HOCKEYSDK_FEATURE_TELEMETRY */ diff --git a/Classes/BITSender.m b/Classes/BITSender.m new file mode 100644 index 0000000000..dab56eb926 --- /dev/null +++ b/Classes/BITSender.m @@ -0,0 +1,186 @@ +#import "BITSender.h" +#import "BITPersistencePrivate.h" +#import "BITGZIP.h" +#import "HockeySDKPrivate.h" +#import "BITHTTPOperation.h" +#import "BITHockeyHelper.h" + +static char const *kPersistenceQueueString = "net.hockeyapp.senderQueue"; +static NSUInteger const defaultRequestLimit = 10; + +@implementation BITSender + +@synthesize runningRequestsCount = _runningRequestsCount; +@synthesize persistence = _persistence; + +#pragma mark - Initialize instance + +- (instancetype)initWithPersistence:(BITPersistence *)persistence serverURL:(NSURL *)serverURL { + if ((self = [super init])) { + _senderQueue = dispatch_queue_create(kPersistenceQueueString, DISPATCH_QUEUE_CONCURRENT); + _maxRequestCount = defaultRequestLimit; + _serverURL = serverURL; + _persistence = persistence; + [self registerObservers]; + } + return self; +} + +#pragma mark - Handle persistence events + +- (void)registerObservers{ + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + __weak typeof(self) weakSelf = self; + [center addObserverForName:BITPersistenceSuccessNotification + object:nil + queue:nil + usingBlock:^(NSNotification *notification) { + typeof(self) strongSelf = weakSelf; + [strongSelf sendSavedData]; + }]; +} + +#pragma mark - Sending + +- (void)sendSavedData{ + @synchronized(self){ + if(_runningRequestsCount < _maxRequestCount){ + _runningRequestsCount++; + }else{ + return; + } + } + __weak typeof(self) weakSelf = self; + dispatch_async(self.senderQueue, ^{ + typeof(self) strongSelf = weakSelf; + NSString *path = [self.persistence requestNextPath]; + NSData *data = [self.persistence dataAtPath:path]; + [strongSelf sendData:data withPath:path]; + }); +} + +- (void)sendData:(NSData * __nonnull)data withPath:(NSString * __nonnull)path { + if(data && data.length > 0) { + NSString *contentType = [self contentTypeForData:data]; + + NSData *gzippedData = [data gzippedData]; + NSURLRequest *request = [self requestForData:gzippedData withContentType:contentType]; + id nsurlsessionClass = NSClassFromString(@"NSURLSessionUploadTask"); + BOOL isUrlSessionSupported = (nsurlsessionClass && !bit_isRunningInAppExtension()); + + [self sendRequest:request path:path urlSessionSupported:isUrlSessionSupported]; + } else { + self.runningRequestsCount -= 1; + } +} + +- (void)sendRequest:(NSURLRequest * __nonnull)request path:(NSString * __nonnull)path urlSessionSupported:(BOOL)isUrlSessionSupported{ + if(!path || !request) return; + __weak typeof(self) weakSelf = self; + + if(!isUrlSessionSupported) { + NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration]; + + NSURLSessionDataTask *task = [session dataTaskWithRequest:request + completionHandler: ^(NSData *data, NSURLResponse *response, NSError *error) { + typeof (self) strongSelf = weakSelf; + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + NSInteger statusCode = httpResponse.statusCode; + [strongSelf handleResponseWithStatusCode:statusCode responseData:data filePath:path error:error]; + }]; + [task resume]; + }else{ + BITHTTPOperation *operation = [BITHTTPOperation operationWithRequest:request]; + [operation setCompletion:^(BITHTTPOperation *operation, NSData *responseData, NSError *error) { + typeof(self) strongSelf = weakSelf; + NSInteger statusCode = [operation.response statusCode]; + [strongSelf handleResponseWithStatusCode:statusCode responseData:responseData filePath:path error:error]; + }]; + + [self.operationQueue addOperation:operation]; + } +} + +- (void)handleResponseWithStatusCode:(NSInteger)statusCode responseData:(NSData *)responseData filePath:(NSString *)filePath error:(NSError *)error{ + self.runningRequestsCount -= 1; + + if(responseData && [self shouldDeleteDataWithStatusCode:statusCode]) { + //we delete data that was either sent successfully or if we have a non-recoverable error + BITHockeyLog(@"Sent data with status code: %ld", (long) statusCode); + BITHockeyLog(@"Response data:\n%@", [NSJSONSerialization JSONObjectWithData:responseData options:0 error:nil]); + [self.persistence deleteFileAtPath:filePath]; + [self sendSavedData]; + } else { + BITHockeyLog(@"Sending telemetry data failed"); + BITHockeyLog(@"Error description: %@", error.localizedDescription); + [self.persistence giveBackRequestedPath:filePath]; + } +} + +#pragma mark - Helper + +- (NSURLRequest *)requestForData:(NSData * __nonnull)data withContentType:(NSString * __nonnull)contentType { + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:self.serverURL]; + request.HTTPMethod = @"POST"; + + request.HTTPBody = data; + request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; + + NSDictionary *headers = @{@"Charset": @"UTF-8", + @"Content-Encoding": @"gzip", + @"Content-Type": contentType, + @"Accept-Encoding": @"gzip"}; + [request setAllHTTPHeaderFields:headers]; + + return request; +} + +//some status codes represent recoverable error codes +//we try sending again some point later +- (BOOL)shouldDeleteDataWithStatusCode:(NSInteger)statusCode { + NSArray *recoverableStatusCodes = @[@429, @408, @500, @503, @511]; + + return ![recoverableStatusCodes containsObject:@(statusCode)]; +} + +- (NSString *)contentTypeForData:(NSData *)data { + NSString *contentType; + static const uint8_t LINEBREAK_SIGNATURE = (0x0a); + UInt8 lastByte = 0; + if (data && data.length > 0) { + [data getBytes:&lastByte range:NSMakeRange(data.length-1, 1)]; + } + + if (data && (data.length > sizeof(uint8_t)) && (lastByte == LINEBREAK_SIGNATURE)) { + contentType = @"application/x-json-stream"; + } else { + contentType = @"application/json"; + } + return contentType; +} + +#pragma mark - Getter/Setter + +- (NSOperationQueue *)operationQueue { + if(nil == _operationQueue) { + _operationQueue = [[NSOperationQueue alloc] init]; + _operationQueue.maxConcurrentOperationCount = defaultRequestLimit; + } + return _operationQueue; +} + +- (NSUInteger)runningRequestsCount { + @synchronized(self) { + return _runningRequestsCount; + } +} + +- (void)setRunningRequestsCount:(NSUInteger)runningRequestsCount { + @synchronized(self) { + _runningRequestsCount = runningRequestsCount; + } +} + +@end diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index a35e4e5e95..a424bce14a 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -69,6 +69,10 @@ 1B460AB71B8E72FD0000C344 /* BITTelemetryManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1B460AB31B8E72FD0000C344 /* BITTelemetryManagerTests.m */; }; 1B50A9161B9FA82800ADECD1 /* BITTelemetryContextTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1B50A9151B9FA82800ADECD1 /* BITTelemetryContextTests.m */; }; 1B50A9171B9FA82800ADECD1 /* BITTelemetryContextTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1B50A9151B9FA82800ADECD1 /* BITTelemetryContextTests.m */; }; + 1B57ABE31BA09B4C0040F078 /* BITSender.h in Headers */ = {isa = PBXBuildFile; fileRef = 1B57ABE01BA09B4C0040F078 /* BITSender.h */; }; + 1B57ABE41BA09B4C0040F078 /* BITSender.h in Headers */ = {isa = PBXBuildFile; fileRef = 1B57ABE01BA09B4C0040F078 /* BITSender.h */; }; + 1B57ABE51BA09B4C0040F078 /* BITSender.m in Sources */ = {isa = PBXBuildFile; fileRef = 1B57ABE11BA09B4C0040F078 /* BITSender.m */; }; + 1B57ABE61BA09B4C0040F078 /* BITSender.m in Sources */ = {isa = PBXBuildFile; fileRef = 1B57ABE11BA09B4C0040F078 /* BITSender.m */; }; 1B57ABEC1BA0C8850040F078 /* BITCategoryContainer.h in Headers */ = {isa = PBXBuildFile; fileRef = 1B57ABE91BA0C8850040F078 /* BITCategoryContainer.h */; }; 1B57ABED1BA0C8850040F078 /* BITCategoryContainer.h in Headers */ = {isa = PBXBuildFile; fileRef = 1B57ABE91BA0C8850040F078 /* BITCategoryContainer.h */; }; 1B57ABEE1BA0C8850040F078 /* BITCategoryContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1B57ABEA1BA0C8850040F078 /* BITCategoryContainer.m */; }; @@ -511,6 +515,8 @@ 1B460AB01B8E64AF0000C344 /* libOCMock.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libOCMock.a; sourceTree = ""; }; 1B460AB31B8E72FD0000C344 /* BITTelemetryManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITTelemetryManagerTests.m; sourceTree = ""; }; 1B50A9151B9FA82800ADECD1 /* BITTelemetryContextTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITTelemetryContextTests.m; sourceTree = ""; }; + 1B57ABE01BA09B4C0040F078 /* BITSender.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITSender.h; sourceTree = ""; }; + 1B57ABE11BA09B4C0040F078 /* BITSender.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITSender.m; sourceTree = ""; }; 1B57ABE91BA0C8850040F078 /* BITCategoryContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITCategoryContainer.h; sourceTree = ""; }; 1B57ABEA1BA0C8850040F078 /* BITCategoryContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITCategoryContainer.m; sourceTree = ""; }; 1B57ABEB1BA0C8850040F078 /* BITGZIP.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITGZIP.h; sourceTree = ""; }; @@ -845,6 +851,8 @@ 1B330A6F1B98E267007844AB /* BITTelemetryContext.m */, 1BD33EAD1B950DC700C3368B /* BITChannel.h */, 1BD33EAE1B950DC700C3368B /* BITChannel.m */, + 1B57ABE01BA09B4C0040F078 /* BITSender.h */, + 1B57ABE11BA09B4C0040F078 /* BITSender.m */, 1B57ABE91BA0C8850040F078 /* BITCategoryContainer.h */, 1B57ABEA1BA0C8850040F078 /* BITCategoryContainer.m */, 1B57ABEB1BA0C8850040F078 /* BITGZIP.h */, @@ -1365,6 +1373,7 @@ 1E49A4BE161222B900463151 /* BITHockeyHelper.h in Headers */, 973EC8BB18BDE29800DBFFBB /* BITArrowImageAnnotation.h in Headers */, 1E49A4C4161222B900463151 /* BITAppStoreHeader.h in Headers */, + 1B57ABE31BA09B4C0040F078 /* BITSender.h in Headers */, 1E49A4CA161222B900463151 /* BITStoreButton.h in Headers */, 973EC8B718BCA8A200DBFFBB /* BITRectangleImageAnnotation.h in Headers */, 1E49A4D0161222B900463151 /* BITWebTableViewCell.h in Headers */, @@ -1446,6 +1455,7 @@ 1EB617A01B0A31860035A986 /* BITFeedbackManagerDelegate.h in Headers */, 1EB6179F1B0A31810035A986 /* BITFeedbackManager.h in Headers */, 1EB617691B0A30C00035A986 /* BITAppStoreHeader.h in Headers */, + 1B57ABE41BA09B4C0040F078 /* BITSender.h in Headers */, 1EB6176B1B0A30C60035A986 /* BITStoreButton.h in Headers */, 1EB617A21B0A318E0035A986 /* BITActivityIndicatorButton.h in Headers */, 1EB617A31B0A31910035A986 /* BITAppVersionMetaInfo.h in Headers */, @@ -1807,6 +1817,7 @@ 846A90211B20B0EB0076BB80 /* BITCrashCXXExceptionHandler.mm in Sources */, 1B87EFC51B8D2FBA0007C96B /* BITTelemetryObject.m in Sources */, 1EACC97C162F041E007578C5 /* BITAttributedLabel.m in Sources */, + 1B57ABE51BA09B4C0040F078 /* BITSender.m in Sources */, 973EC8BC18BDE29800DBFFBB /* BITArrowImageAnnotation.m in Sources */, 973EC8B418BCA7BC00DBFFBB /* BITImageAnnotationViewController.m in Sources */, 1E0FEE29173BDB260061331F /* BITKeychainUtils.m in Sources */, @@ -1909,6 +1920,7 @@ 846A90221B20B0EB0076BB80 /* BITCrashCXXExceptionHandler.mm in Sources */, 1B87EFC61B8D2FBA0007C96B /* BITTelemetryObject.m in Sources */, 1EB6178A1B0A31510035A986 /* BITFeedbackComposeViewController.m in Sources */, + 1B57ABE61BA09B4C0040F078 /* BITSender.m in Sources */, 1EB617701B0A30D70035A986 /* BITHockeyAttachment.m in Sources */, 1EB617881B0A31510035A986 /* BITFeedbackMessage.m in Sources */, 1EB617951B0A31510035A986 /* BITHockeyManager.m in Sources */,