diff --git a/Classes/BITCrashDetails.h b/Classes/BITCrashDetails.h new file mode 100644 index 0000000000..4cd8fb7321 --- /dev/null +++ b/Classes/BITCrashDetails.h @@ -0,0 +1,95 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +@interface BITCrashDetails : NSObject + +/** + * UUID for the crash report + */ +@property (nonatomic, readonly, strong) NSString *incidentIdentifier; + +/** + * UUID for the app installation on the device + */ +@property (nonatomic, readonly, strong) NSString *reporterKey; + +/** + * Signal that caused the crash + */ +@property (nonatomic, readonly, strong) NSString *signal; + +/** + * Exception name that triggered the crash, nil if the crash was not caused by an exception + */ +@property (nonatomic, readonly, strong) NSString *exceptionName; + +/** + * Exception reason, nil if the crash was not caused by an exception + */ +@property (nonatomic, readonly, strong) NSString *exceptionReason; + +/** + * Date and time the app started, nil if unknown + */ +@property (nonatomic, readonly, strong) NSDate *appStartTime; + +/** + * Date and time the crash occured, nil if unknown + */ +@property (nonatomic, readonly, strong) NSDate *crashTime; + +/** + * CFBundleVersion value of the app that crashed + */ +@property (nonatomic, readonly, strong) NSString *appBuild; + +/** + Indicates if the app was killed while being in foreground from the iOS + + If `[BITCrashManager enableAppNotTerminatingCleanlyDetection]` is enabled, use this on startup + to check if the app starts the first time after it was killed by iOS in the previous session. + + This can happen if it consumed too much memory or the watchdog killed the app because it + took too long to startup or blocks the main thread for too long, or other reasons. See Apple + documentation: https://developer.apple.com/library/ios/qa/qa1693/_index.html + + See `[BITCrashManager enableDectionAppKillWhileInForeground]` for more details about which kind of kills can be detected. + + @warning This property only has a correct value, once `[BITHockeyManager startManager]` was + invoked! In addition, it is automatically disabled while a debugger session is active! + + @see `[BITCrashManager enableAppNotTerminatingCleanlyDetection]` + @see `[BITCrashManager didReceiveMemoryWarningInLastSession]` + + @return YES if the details represent an app kill instead of a crash + */ +- (BOOL)isAppKill; + +@end diff --git a/Classes/BITCrashDetails.m b/Classes/BITCrashDetails.m new file mode 100644 index 0000000000..b7f5b52c84 --- /dev/null +++ b/Classes/BITCrashDetails.m @@ -0,0 +1,67 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "BITCrashDetails.h" +#import "BITCrashDetailsPrivate.h" + +NSString *const kBITCrashKillSignal = @"SIGKILL"; + +@implementation BITCrashDetails + +- (instancetype)initWithIncidentIdentifier:(NSString *)incidentIdentifier + reporterKey:(NSString *)reporterKey + signal:(NSString *)signal + exceptionName:(NSString *)exceptionName + exceptionReason:(NSString *)exceptionReason + appStartTime:(NSDate *)appStartTime + crashTime:(NSDate *)crashTime + appBuild:(NSString *)appBuild +{ + if ((self = [super init])) { + _incidentIdentifier = incidentIdentifier; + _reporterKey = reporterKey; + _signal = signal; + _exceptionName = exceptionName; + _exceptionReason = exceptionReason; + _appStartTime = appStartTime; + _crashTime = crashTime; + _appBuild = appBuild; + } + return self; +} + +- (BOOL)isAppKill { + BOOL result = NO; + + if (_signal && [[_signal uppercaseString] isEqualToString:kBITCrashKillSignal]) + result = YES; + + return result; +} + +@end diff --git a/Classes/BITCrashDetailsPrivate.h b/Classes/BITCrashDetailsPrivate.h new file mode 100644 index 0000000000..6c6fa241a1 --- /dev/null +++ b/Classes/BITCrashDetailsPrivate.h @@ -0,0 +1,46 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +extern NSString *const __attribute__((unused)) kBITCrashKillSignal; + +@interface BITCrashDetails () { + +} + +- (instancetype)initWithIncidentIdentifier:(NSString *)incidentIdentifier + reporterKey:(NSString *)reporterKey + signal:(NSString *)signal + exceptionName:(NSString *)exceptionName + exceptionReason:(NSString *)exceptionReason + appStartTime:(NSDate *)appStartTime + crashTime:(NSDate *)crashTime + appBuild:(NSString *)appBuild; + +@end diff --git a/Classes/BITCrashManager.h b/Classes/BITCrashManager.h index bf651da527..3a72a5fecc 100644 --- a/Classes/BITCrashManager.h +++ b/Classes/BITCrashManager.h @@ -32,12 +32,14 @@ #import "BITHockeyBaseManager.h" -// We need this check depending on integrating as a subproject or using the binary distribution -#if __has_include("CrashReporter.h") -#import "CrashReporter.h" -#else -#import -#endif +@class BITCrashDetails; +@class BITCrashMetaData; + + +/** + * Custom block that handles the alert that prompts the user whether he wants to send crash reports + */ +typedef void(^BITCustomAlertViewHandler)(); /** @@ -59,6 +61,54 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { }; +/** + * Prototype of a callback function used to execute additional user code. Called upon completion of crash + * handling, after the crash report has been written to disk. + * + * @param context The API client's supplied context value. + * + * @see `BITCrashManagerCallbacks` + * @see `[BITCrashManager setCrashCallbacks:]` + */ +typedef void (*BITCrashManagerPostCrashSignalCallback)(void *context); + +/** + * This structure contains callbacks supported by `BITCrashManager` to allow the host application to perform + * additional tasks prior to program termination after a crash has occured. + * + * @see `BITCrashManagerPostCrashSignalCallback` + * @see `[BITCrashManager setCrashCallbacks:]` + */ +typedef struct BITCrashManagerCallbacks { + /** An arbitrary user-supplied context value. This value may be NULL. */ + void *context; + + /** + * The callback used to report caught signal information. + */ + BITCrashManagerPostCrashSignalCallback handleSignal; +} BITCrashManagerCallbacks; + +/** + * Crash Manager alert user input + */ +typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { + /** + * User chose not to send the crash report + */ + BITCrashManagerUserInputDontSend = 0, + /** + * User wants the crash report to be sent + */ + BITCrashManagerUserInputSend = 1, + /** + * User chose to always send crash reports + */ + BITCrashManagerUserInputAlwaysSend = 2 + +}; + + @protocol BITCrashManagerDelegate; /** @@ -180,6 +230,46 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { @property (nonatomic, assign, getter=isOnDeviceSymbolicationEnabled) BOOL enableOnDeviceSymbolication; +/** + * EXPERIMENTAL: Enable heuristics to detect the app not terminating cleanly + * + * This allows it to get a crash report if the app got killed while being in the foreground + * because of now of the following reasons: + * - The main thread was blocked for too long + * - The app took too long to start up + * - The app tried to allocate too much memory. If iOS did send a memory warning before killing the app because of this reason, `didReceiveMemoryWarningInLastSession` returns `YES`. + * - Permitted background duration if main thread is running in an endless loop + * - App failed to resume in time if main thread is running in an endless loop + * - If `enableMachExceptionHandler` is not activated, crashed due to stackoverflow will also be reported + * + * The following kills can _NOT_ be detected: + * - Terminating the app takes too long + * - Permitted background duration too long for all other cases + * - App failed to resume in time for all other cases + * - possibly more cases + * + * Crash reports triggered by this mechanisms do _NOT_ contain any stack traces since the time of the kill + * cannot be intercepted and hence no stack trace of the time of the kill event can't be gathered. + * + * The heuristic is implemented as follows: + * If the app never gets a `UIApplicationDidEnterBackgroundNotification` or `UIApplicationWillTerminateNotification` + * notification, PLCrashReporter doesn't detect a crash itself, and the app starts up again, it is assumed that + * the app got either killed by iOS while being in foreground or a crash occured that couldn't be detected. + * + * Default: _NO_ + * + * @warning This is a heuristic and it _MAY_ report false positives! It has been tested with iOS 6.1 and iOS 7. + * Depending on Apple changing notification events, new iOS version may cause more false positives! + * + * @see lastSessionCrashDetails + * @see didReceiveMemoryWarningInLastSession + * @see `BITCrashManagerDelegate considerAppNotTerminatedCleanlyReportForCrashManager:` + * @see [Apple Technical Note TN2151](https://developer.apple.com/library/ios/technotes/tn2151/_index.html) + * @see [Apple Technical Q&A QA1693](https://developer.apple.com/library/ios/qa/qa1693/_index.html) + */ +@property (nonatomic, assign, getter = isAppNotTerminatingCleanlyDetectionEnabled) BOOL enableAppNotTerminatingCleanlyDetection; + + /** * Set the callbacks that will be executed prior to program termination after a crash has occurred * @@ -194,15 +284,18 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { * * _Async-Safe Functions_ * - * A subset of functions are defined to be async-safe by the OS, and are safely callable from within a signal handler. If you do implement a custom post-crash handler, it must be async-safe. A table of POSIX-defined async-safe functions and additional information is available from the CERT programming guide - SIG30-C, see https://www.securecoding.cert.org/confluence/display/seccode/SIG30-C.+Call+only+asynchronous-safe+functions+within+signal+handlers + * A subset of functions are defined to be async-safe by the OS, and are safely callable from within a signal handler. If you do implement a custom post-crash handler, it must be async-safe. A table of POSIX-defined async-safe functions and additional information is available from the [CERT programming guide - SIG30-C](https://www.securecoding.cert.org/confluence/display/seccode/SIG30-C.+Call+only+asynchronous-safe+functions+within+signal+handlers). * * Most notably, the Objective-C runtime itself is not async-safe, and Objective-C may not be used within a signal handler. * * Documentation taken from PLCrashReporter: https://www.plcrashreporter.org/documentation/api/v1.2-rc2/async_safety.html * + * @see `BITCrashManagerPostCrashSignalCallback` + * @see `BITCrashManagerCallbacks` + * * @param callbacks A pointer to an initialized PLCrashReporterCallback structure, see https://www.plcrashreporter.org/documentation/api/v1.2-rc2/struct_p_l_crash_reporter_callbacks.html */ -- (void)setCrashCallbacks: (PLCrashReporterCallbacks *) callbacks; +- (void)setCrashCallbacks: (BITCrashManagerCallbacks *) callbacks; /** @@ -234,9 +327,60 @@ typedef NS_ENUM(NSUInteger, BITCrashManagerStatus) { @warning This property only has a correct value, once `[BITHockeyManager startManager]` was invoked! + + @see lastSessionCrashDetails */ @property (nonatomic, readonly) BOOL didCrashInLastSession; +/** + Provides an interface to pass user input from a custom alert to a crash report + + @param userInput Defines the users action wether to send, always send, or not to send the crash report. + @param userProvidedMetaData The content of this optional BITCrashMetaData instance will be attached to the crash report and allows to ask the user for e.g. additional comments or info. + + @return Returns YES if the input is a valid option and successfully triggered further processing of the crash report + + @see BITCrashManagerUserInput + @see BITCrashMetaData + */ +- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedMetaData:(BITCrashMetaData *)userProvidedMetaData; + +/** + Lets you set a custom block which handles showing a custom UI and asking the user + whether he wants to send the crash report. + + @param alertViewHandler A block that is responsible for loading, presenting and and dismissing your custom user interface which prompts the user if he wants to send crash reports. The block is also responsible for triggering further processing of the crash reports. + + @warning Block needs to call the `handleUserInput:withUserProvidedCrashDescription` method! + */ +- (void) setAlertViewHandler:(BITCustomAlertViewHandler)alertViewHandler; + +/** + * Provides details about the crash that occured in the last app session + */ +@property (nonatomic, readonly) BITCrashDetails *lastSessionCrashDetails; + + +/** + Indicates if the app did receive a low memory warning in the last session + + It may happen that low memory warning where send but couldn't be logged, since iOS + killed the app before updating the flag in the filesystem did complete. + + This property may be true in case of low memory kills, but it doesn't have to be! Apps + can also be killed without the app ever receiving a low memory warning. + + Also the app could have received a low memory warning, but the reason for being killed was + actually different. + + @warning This property only has a correct value, once `[BITHockeyManager startManager]` was + invoked! + + @see enableAppNotTerminatingCleanlyDetection + @see lastSessionCrashDetails + */ +@property (nonatomic, readonly) BOOL didReceiveMemoryWarningInLastSession; + /** Provides the time between startup and crash in seconds diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index c9dc2ec7ca..255e84ea56 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -43,6 +43,7 @@ #import "BITHockeyBaseManagerPrivate.h" #import "BITCrashManagerPrivate.h" #import "BITCrashReportTextFormatter.h" +#import "BITCrashDetailsPrivate.h" #include @@ -62,35 +63,62 @@ NSString *const KBITAttachmentDictAttachment = @"attachment"; NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; +NSString *const kBITAppWentIntoBackgroundSafely = @"BITAppWentIntoBackgroundSafely"; +NSString *const kBITAppDidReceiveLowMemoryNotification = @"BITAppDidReceiveLowMemoryNotification"; +NSString *const kBITAppVersion = @"BITAppVersion"; +NSString *const kBITAppOSVersion = @"BITAppOSVersion"; +NSString *const kBITAppUUIDs = @"BITAppUUIDs"; -@interface BITCrashManager () +NSString *const kBITFakeCrashUUID = @"BITFakeCrashUUID"; +NSString *const kBITFakeCrashAppVersion = @"BITFakeCrashAppVersion"; +NSString *const kBITFakeCrashAppBundleIdentifier = @"BITFakeCrashAppBundleIdentifier"; +NSString *const kBITFakeCrashOSVersion = @"BITFakeCrashOSVersion"; +NSString *const kBITFakeCrashDeviceModel = @"BITFakeCrashDeviceModel"; +NSString *const kBITFakeCrashAppBinaryUUID = @"BITFakeCrashAppBinaryUUID"; +NSString *const kBITFakeCrashReport = @"BITFakeCrashAppString"; -@property (nonatomic, strong) NSFileManager *fileManager; -@end +static BITCrashManagerCallbacks bitCrashCallbacks = { + .context = NULL, + .handleSignal = NULL +}; + +// proxy implementation for PLCrashReporter to keep our interface stable while this can change +static void plcr_post_crash_callback (siginfo_t *info, ucontext_t *uap, void *context) { + if (bitCrashCallbacks.handleSignal != NULL) + bitCrashCallbacks.handleSignal(context); +} + +static PLCrashReporterCallbacks plCrashCallbacks = { + .version = 0, + .context = NULL, + .handleSignal = plcr_post_crash_callback +}; + @implementation BITCrashManager { NSMutableDictionary *_approvedCrashReports; NSMutableArray *_crashFiles; - NSString *_crashesDir; + NSString *_lastCrashFilename; NSString *_settingsFile; NSString *_analyzerInProgressFile; NSFileManager *_fileManager; - + PLCrashReporterCallbacks *_crashCallBacks; BOOL _crashIdenticalCurrentVersion; - NSMutableData *_responseData; - NSInteger _statusCode; - - NSURLConnection *_urlConnection; - BOOL _sendingInProgress; BOOL _isSetup; + BOOL _didLogLowMemoryWarning; + id _appDidBecomeActiveObserver; + id _appWillTerminateObserver; + id _appDidEnterBackgroundObserver; + id _appWillEnterForegroundObserver; + id _appDidReceiveLowMemoryWarningObserver; id _networkDidBecomeReachableObserver; } @@ -106,14 +134,13 @@ NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; _crashCallBacks = nil; _crashIdenticalCurrentVersion = YES; - _urlConnection = nil; - _responseData = nil; - _sendingInProgress = NO; _didCrashInLastSession = NO; _timeintervalCrashInLastSessionOccured = -1; + _didLogLowMemoryWarning = NO; _approvedCrashReports = [[NSMutableDictionary alloc] init]; + _alertViewHandler = nil; _fileManager = [[NSFileManager alloc] init]; _crashFiles = [[NSMutableArray alloc] init]; @@ -151,8 +178,6 @@ NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; - (void) dealloc { [self unregisterObservers]; - - [_urlConnection cancel]; } @@ -174,9 +199,9 @@ NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; NSString *errorString = nil; NSMutableDictionary *rootObj = [NSMutableDictionary dictionaryWithCapacity:2]; - if (_approvedCrashReports && [_approvedCrashReports count] > 0) + if (_approvedCrashReports && [_approvedCrashReports count] > 0) { [rootObj setObject:_approvedCrashReports forKey:kBITCrashApprovedReports]; - + } NSData *plist = [NSPropertyListSerialization dataFromPropertyList:(id)rootObj format:NSPropertyListBinaryFormat_v1_0 errorDescription:&errorString]; @@ -214,28 +239,43 @@ NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; } } + /** - * Remove all crash reports and stored meta data for each from the file system and keychain + * Remove a cached crash report + * + * @param filename The base filename of the crash report */ -- (void)cleanCrashReports { +- (void)cleanCrashReportWithFilename:(NSString *)filename { + if (!filename) return; + NSError *error = NULL; - for (NSUInteger i=0; i < [_crashFiles count]; i++) { - [_fileManager removeItemAtPath:[_crashFiles objectAtIndex:i] error:&error]; - [_fileManager removeItemAtPath:[[_crashFiles objectAtIndex:i] stringByAppendingString:@".data"] error:&error]; - [_fileManager removeItemAtPath:[[_crashFiles objectAtIndex:i] stringByAppendingString:@".meta"] error:&error]; - - NSString *cacheFilename = [[_crashFiles objectAtIndex:i] lastPathComponent]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]]; - } - [_crashFiles removeAllObjects]; - [_approvedCrashReports removeAllObjects]; + [_fileManager removeItemAtPath:filename error:&error]; + [_fileManager removeItemAtPath:[filename stringByAppendingString:@".data"] error:&error]; + [_fileManager removeItemAtPath:[filename stringByAppendingString:@".meta"] error:&error]; + [_fileManager removeItemAtPath:[filename stringByAppendingString:@".desc"] error:&error]; + + NSString *cacheFilename = [filename lastPathComponent]; + [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]]; + [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]]; + [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]]; + + [_crashFiles removeObject:filename]; + [_approvedCrashReports removeObjectForKey:filename]; [self saveSettings]; } +/** + * Remove all crash reports and stored meta data for each from the file system and keychain + * + * This is currently only used as a helper method for tests + */ +- (void)cleanCrashReports { + for (NSUInteger i=0; i < [_crashFiles count]; i++) { + [self cleanCrashReportWithFilename:[_crashFiles objectAtIndex:i]]; + } +} - (void)persistAttachment:(BITCrashAttachment *)attachment withFilename:(NSString *)filename { NSString *attachmentFilename = [filename stringByAppendingString:@".data"]; @@ -249,10 +289,33 @@ NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; [data writeToFile:attachmentFilename atomically:YES]; } +- (void)persistUserProvidedMetaData:(BITCrashMetaData *)userProvidedMetaData { + if (!userProvidedMetaData) return; + + if (userProvidedMetaData.userDescription && [userProvidedMetaData.userDescription length] > 0) { + NSError *error; + [userProvidedMetaData.userDescription writeToFile:[NSString stringWithFormat:@"%@.desc", [_crashesDir stringByAppendingPathComponent: _lastCrashFilename]] atomically:YES encoding:NSUTF8StringEncoding error:&error]; + } + + if (userProvidedMetaData.userName && [userProvidedMetaData.userName length] > 0) { + [self addStringValueToKeychain:userProvidedMetaData.userName forKey:[NSString stringWithFormat:@"%@.%@", _lastCrashFilename, kBITCrashMetaUserName]]; + + } + + if (userProvidedMetaData.userEmail && [userProvidedMetaData.userEmail length] > 0) { + [self addStringValueToKeychain:userProvidedMetaData.userEmail forKey:[NSString stringWithFormat:@"%@.%@", _lastCrashFilename, kBITCrashMetaUserEmail]]; + } + + if (userProvidedMetaData.userID && [userProvidedMetaData.userID length] > 0) { + [self addStringValueToKeychain:userProvidedMetaData.userID forKey:[NSString stringWithFormat:@"%@.%@", _lastCrashFilename, kBITCrashMetaUserID]]; + + } +} + /** * Read the attachment data from the stored file * - * @param filename The crash report id + * @param filename The crash report file path * * @return an BITCrashAttachment instance or nil */ @@ -333,20 +396,117 @@ NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; [strongSelf triggerDelayedProcessing]; }]; } + + if (nil == _appWillTerminateObserver) { + _appWillTerminateObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillTerminateNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf leavingAppSafely]; + }]; + } + + if (nil == _appDidEnterBackgroundObserver) { + _appDidEnterBackgroundObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf leavingAppSafely]; + }]; + } + + if (nil == _appWillEnterForegroundObserver) { + _appWillEnterForegroundObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillEnterForegroundNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf appEnteredForeground]; + }]; + } + + if (nil == _appDidReceiveLowMemoryWarningObserver) { + _appDidReceiveLowMemoryWarningObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *note) { + // we only need to log this once + if (!_didLogLowMemoryWarning) { + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kBITAppDidReceiveLowMemoryNotification]; + [[NSUserDefaults standardUserDefaults] synchronize]; + _didLogLowMemoryWarning = YES; + } + }]; + } } - (void) unregisterObservers { - if(_appDidBecomeActiveObserver) { - [[NSNotificationCenter defaultCenter] removeObserver:_appDidBecomeActiveObserver]; - _appDidBecomeActiveObserver = nil; - } + [self unregisterObserver:_appDidBecomeActiveObserver]; + [self unregisterObserver:_appWillTerminateObserver]; + [self unregisterObserver:_appDidEnterBackgroundObserver]; + [self unregisterObserver:_appWillEnterForegroundObserver]; + [self unregisterObserver:_appDidReceiveLowMemoryWarningObserver]; - if(_networkDidBecomeReachableObserver) { - [[NSNotificationCenter defaultCenter] removeObserver:_networkDidBecomeReachableObserver]; - _networkDidBecomeReachableObserver = nil; + [self unregisterObserver:_networkDidBecomeReachableObserver]; +} + +- (void) unregisterObserver:(id)observer { + if (observer) { + [[NSNotificationCenter defaultCenter] removeObserver:observer]; + observer = nil; } } +- (void)leavingAppSafely { + if (self.isAppNotTerminatingCleanlyDetectionEnabled) + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kBITAppWentIntoBackgroundSafely]; +} + +- (void)appEnteredForeground { + // we disable kill detection while the debugger is running, since we'd get only false positives if the app is terminated by the user using the debugger + if (self.isDebuggerAttached) { + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kBITAppWentIntoBackgroundSafely]; + } else if (self.isAppNotTerminatingCleanlyDetectionEnabled) { + [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kBITAppWentIntoBackgroundSafely]; + + static dispatch_once_t predAppData; + + dispatch_once(&predAppData, ^{ + id bundleVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; + if (bundleVersion && [bundleVersion isKindOfClass:[NSString class]]) + [[NSUserDefaults standardUserDefaults] setObject:bundleVersion forKey:kBITAppVersion]; + [[NSUserDefaults standardUserDefaults] setObject:[[UIDevice currentDevice] systemVersion] forKey:kBITAppOSVersion]; + + NSString *uuidString =[NSString stringWithFormat:@"%@", + [self deviceArchitecture], + [self executableUUID] + ]; + + [[NSUserDefaults standardUserDefaults] setObject:uuidString forKey:kBITAppUUIDs]; + }); + } +} + +- (NSString *)deviceArchitecture { + NSString *archName = @"???"; + + size_t size; + cpu_type_t type; + cpu_subtype_t subtype; + size = sizeof(type); + if (sysctlbyname("hw.cputype", &type, &size, NULL, 0)) + return archName; + + size = sizeof(subtype); + if (sysctlbyname("hw.cpusubtype", &subtype, &size, NULL, 0)) + return archName; + + archName = [BITCrashReportTextFormatter bit_archNameFromCPUType:type subType:subtype] ?: @"???"; + + return archName; +} /** * Get the userID from the delegate which should be stored with the crash report @@ -438,8 +598,27 @@ NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; #pragma mark - Public -- (void)setCrashCallbacks: (PLCrashReporterCallbacks *) callbacks { - _crashCallBacks = callbacks; +/** + * Set the callback for PLCrashReporter + * + * @param callbacks BITCrashManagerCallbacks instance + */ +- (void)setCrashCallbacks: (BITCrashManagerCallbacks *) callbacks { + if (!callbacks) return; + + // set our proxy callback struct + bitCrashCallbacks.context = callbacks->context; + bitCrashCallbacks.handleSignal = callbacks->handleSignal; + + // set the PLCrashReporterCallbacks struct + plCrashCallbacks.context = callbacks->context; + + _crashCallBacks = &plCrashCallbacks; +} + + +- (void)setAlertViewHandler:(BITCustomAlertViewHandler)alertViewHandler{ + _alertViewHandler = alertViewHandler; } /** @@ -487,6 +666,82 @@ NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; } } +/** + * Write a meta file for a new crash report + * + * @param filename the crash reports temp filename + */ +- (void)storeMetaDataForCrashReportFilename:(NSString *)filename { + NSError *error = NULL; + NSMutableDictionary *metaDict = [NSMutableDictionary dictionaryWithCapacity:4]; + NSString *applicationLog = @""; + NSString *errorString = nil; + + [self addStringValueToKeychain:[self userNameForCrashReport] forKey:[NSString stringWithFormat:@"%@.%@", filename, kBITCrashMetaUserName]]; + [self addStringValueToKeychain:[self userEmailForCrashReport] forKey:[NSString stringWithFormat:@"%@.%@", filename, kBITCrashMetaUserEmail]]; + [self addStringValueToKeychain:[self userIDForCrashReport] forKey:[NSString stringWithFormat:@"%@.%@", filename, kBITCrashMetaUserID]]; + + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(applicationLogForCrashManager:)]) { + applicationLog = [self.delegate applicationLogForCrashManager:self] ?: @""; + } + [metaDict setObject:applicationLog forKey:kBITCrashMetaApplicationLog]; + + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(attachmentForCrashManager:)]) { + BITCrashAttachment *attachment = [self.delegate attachmentForCrashManager:self]; + + if (attachment) { + [self persistAttachment:attachment withFilename:[_crashesDir stringByAppendingPathComponent: filename]]; + } + } + + NSData *plist = [NSPropertyListSerialization dataFromPropertyList:(id)metaDict + format:NSPropertyListBinaryFormat_v1_0 + errorDescription:&errorString]; + if (plist) { + [plist writeToFile:[_crashesDir stringByAppendingPathComponent: [filename stringByAppendingPathExtension:@"meta"]] atomically:YES]; + } else { + BITHockeyLog(@"ERROR: Writing crash meta data failed. %@", error); + } +} + +- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedMetaData:(BITCrashMetaData *)userProvidedMetaData { + switch (userInput) { + case BITCrashManagerUserInputDontSend: + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillCancelSendingCrashReport:)]) { + [self.delegate crashManagerWillCancelSendingCrashReport:self]; + } + + if (_lastCrashFilename) + [self cleanCrashReportWithFilename:[_crashesDir stringByAppendingPathComponent: _lastCrashFilename]]; + + return YES; + + case BITCrashManagerUserInputSend: + if (userProvidedMetaData) + [self persistUserProvidedMetaData:userProvidedMetaData]; + + [self sendNextCrashReport]; + return YES; + + case BITCrashManagerUserInputAlwaysSend: + _crashManagerStatus = BITCrashManagerStatusAutoSend; + [[NSUserDefaults standardUserDefaults] setInteger:_crashManagerStatus forKey:kBITCrashManagerStatus]; + [[NSUserDefaults standardUserDefaults] synchronize]; + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillSendCrashReportsAlways:)]) { + [self.delegate crashManagerWillSendCrashReportsAlways:self]; + } + + if (userProvidedMetaData) + [self persistUserProvidedMetaData:userProvidedMetaData]; + + [self sendNextCrashReport]; + return YES; + + default: + return NO; + } + +} #pragma mark - PLCrashReporter @@ -513,6 +768,7 @@ NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; NSData *crashData = [[NSData alloc] initWithData:[self.plCrashReporter loadPendingCrashReportDataAndReturnError: &error]]; NSString *cacheFilename = [NSString stringWithFormat: @"%.0f", [NSDate timeIntervalSinceReferenceDate]]; + _lastCrashFilename = cacheFilename; if (crashData == nil) { BITHockeyLog(@"ERROR: Could not load crash report: %@", error); @@ -523,44 +779,36 @@ NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; if (report == nil) { BITHockeyLog(@"WARNING: Could not parse crash report"); } else { + NSDate *appStartTime = nil; + NSDate *appCrashTime = nil; if ([report.processInfo respondsToSelector:@selector(processStartTime)]) { if (report.systemInfo.timestamp && report.processInfo.processStartTime) { + appStartTime = report.processInfo.processStartTime; + appCrashTime =report.systemInfo.timestamp; _timeintervalCrashInLastSessionOccured = [report.systemInfo.timestamp timeIntervalSinceDate:report.processInfo.processStartTime]; } } [crashData writeToFile:[_crashesDir stringByAppendingPathComponent: cacheFilename] atomically:YES]; - // write the meta file - NSMutableDictionary *metaDict = [NSMutableDictionary dictionaryWithCapacity:4]; - NSString *applicationLog = @""; - NSString *errorString = nil; + [self storeMetaDataForCrashReportFilename:cacheFilename]; - [self addStringValueToKeychain:[self userNameForCrashReport] forKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]]; - [self addStringValueToKeychain:[self userEmailForCrashReport] forKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]]; - [self addStringValueToKeychain:[self userIDForCrashReport] forKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]]; - - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(applicationLogForCrashManager:)]) { - applicationLog = [self.delegate applicationLogForCrashManager:self] ?: @""; - } - [metaDict setObject:applicationLog forKey:kBITCrashMetaApplicationLog]; - - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(attachmentForCrashManager:)]) { - BITCrashAttachment *attachment = [self.delegate attachmentForCrashManager:self]; - - if (attachment) { - [self persistAttachment:attachment withFilename:[_crashesDir stringByAppendingPathComponent: cacheFilename]]; - } + NSString *incidentIdentifier = @"???"; + if (report.uuidRef != NULL) { + incidentIdentifier = (NSString *) CFBridgingRelease(CFUUIDCreateString(NULL, report.uuidRef)); } - NSData *plist = [NSPropertyListSerialization dataFromPropertyList:(id)metaDict - format:NSPropertyListBinaryFormat_v1_0 - errorDescription:&errorString]; - if (plist) { - [plist writeToFile:[NSString stringWithFormat:@"%@.meta", [_crashesDir stringByAppendingPathComponent: cacheFilename]] atomically:YES]; - } else { - BITHockeyLog(@"ERROR: Writing crash meta data failed. %@", error); - } + NSString *reporterKey = bit_appAnonID() ?: @""; + + _lastSessionCrashDetails = [[BITCrashDetails alloc] initWithIncidentIdentifier:incidentIdentifier + reporterKey:reporterKey + signal:report.signalInfo.name + exceptionName:report.exceptionInfo.exceptionName + exceptionReason:report.exceptionInfo.exceptionReason + appStartTime:appStartTime + crashTime:appCrashTime + appBuild:report.applicationInfo.applicationVersion + ]; } } } @@ -596,7 +844,7 @@ NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; /** * Check if there are any new crash reports that are not yet processed * - * @return `YES` if ther eis at least one new crash report found, `NO` otherwise + * @return `YES` if there is at least one new crash report found, `NO` otherwise */ - (BOOL)hasPendingCrashReport { if (_crashManagerStatus == BITCrashManagerStatusDisabled) return NO; @@ -614,7 +862,8 @@ NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; ![file hasSuffix:@".analyzer"] && ![file hasSuffix:@".plist"] && ![file hasSuffix:@".data"] && - ![file hasSuffix:@".meta"]) { + ![file hasSuffix:@".meta"] && + ![file hasSuffix:@".desc"]) { [_crashFiles addObject:[_crashesDir stringByAppendingPathComponent: file]]; } } @@ -671,7 +920,7 @@ NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; if (!_sendingInProgress && [self hasPendingCrashReport]) { _sendingInProgress = YES; if (!BITHockeyBundle()) { - [self sendCrashReports]; + [self sendNextCrashReport]; } else if (_crashManagerStatus != BITCrashManagerStatusAutoSend && [self hasNonApprovedCrashReports]) { if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillShowSubmitCrashReportAlert:)]) { @@ -692,19 +941,23 @@ NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; alertDescription = [NSString stringWithFormat:BITHockeyLocalizedString(@"CrashDataFoundDescription"), appName]; } - UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:[NSString stringWithFormat:BITHockeyLocalizedString(@"CrashDataFoundTitle"), appName] - message:alertDescription - delegate:self - cancelButtonTitle:BITHockeyLocalizedString(@"CrashDontSendReport") - otherButtonTitles:BITHockeyLocalizedString(@"CrashSendReport"), nil]; - - if (self.shouldShowAlwaysButton) { - [alertView addButtonWithTitle:BITHockeyLocalizedString(@"CrashSendReportAlways")]; + if (_alertViewHandler) { + _alertViewHandler(); + } else { + UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:[NSString stringWithFormat:BITHockeyLocalizedString(@"CrashDataFoundTitle"), appName] + message:alertDescription + delegate:self + cancelButtonTitle:BITHockeyLocalizedString(@"CrashDontSendReport") + otherButtonTitles:BITHockeyLocalizedString(@"CrashSendReport"), nil]; + + if (self.shouldShowAlwaysButton) { + [alertView addButtonWithTitle:BITHockeyLocalizedString(@"CrashSendReportAlways")]; + } + + [alertView show]; } - - [alertView show]; } else { - [self sendCrashReports]; + [self sendNextCrashReport]; } } } @@ -799,131 +1052,272 @@ NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; _isSetup = YES; }); } + + if ([[NSUserDefaults standardUserDefaults] valueForKey:kBITAppDidReceiveLowMemoryNotification]) + _didReceiveMemoryWarningInLastSession = [[NSUserDefaults standardUserDefaults] boolForKey:kBITAppDidReceiveLowMemoryNotification]; + + if (!_didCrashInLastSession && self.isAppNotTerminatingCleanlyDetectionEnabled) { + BOOL didAppSwitchToBackgroundSafely = YES; + + if ([[NSUserDefaults standardUserDefaults] valueForKey:kBITAppWentIntoBackgroundSafely]) + didAppSwitchToBackgroundSafely = [[NSUserDefaults standardUserDefaults] boolForKey:kBITAppWentIntoBackgroundSafely]; + + if (!didAppSwitchToBackgroundSafely) { + BOOL considerReport = YES; + + if (self.delegate && + [self.delegate respondsToSelector:@selector(considerAppNotTerminatedCleanlyReportForCrashManager:)]) { + considerReport = [self.delegate considerAppNotTerminatedCleanlyReportForCrashManager:self]; + } + + if (considerReport) { + [self createCrashReportForAppKill]; + + _didCrashInLastSession = YES; + } + } + } + [self appEnteredForeground]; + [[NSUserDefaults standardUserDefaults] setBool:NO forKey:kBITAppDidReceiveLowMemoryNotification]; + [[NSUserDefaults standardUserDefaults] synchronize]; [self triggerDelayedProcessing]; } +/** + * Creates a fake crash report because the app was killed while being in foreground + */ +- (void)createCrashReportForAppKill { + NSString *fakeReportUUID = bit_UUID(); + NSString *fakeReporterKey = bit_appAnonID() ?: @"???"; + + NSString *fakeReportAppVersion = [[NSUserDefaults standardUserDefaults] objectForKey:kBITAppVersion]; + if (!fakeReportAppVersion) + return; + + NSString *fakeReportOSVersion = [[NSUserDefaults standardUserDefaults] objectForKey:kBITAppOSVersion] ?: [[UIDevice currentDevice] systemVersion]; + NSString *fakeReportAppBundleIdentifier = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"]; + NSString *fakeReportDeviceModel = [self getDevicePlatform] ?: @"Unknown"; + NSString *fakeReportAppUUIDs = [[NSUserDefaults standardUserDefaults] objectForKey:kBITAppUUIDs] ?: @""; + + NSString *fakeSignalName = kBITCrashKillSignal; + + NSMutableString *fakeReportString = [NSMutableString string]; + + [fakeReportString appendFormat:@"Incident Identifier: %@\n", fakeReportUUID]; + [fakeReportString appendFormat:@"CrashReporter Key: %@\n", fakeReporterKey]; + [fakeReportString appendFormat:@"Hardware Model: %@\n", fakeReportDeviceModel]; + [fakeReportString appendFormat:@"Identifier: %@\n", fakeReportAppBundleIdentifier]; + [fakeReportString appendFormat:@"Version: %@\n", fakeReportAppVersion]; + [fakeReportString appendString:@"Code Type: ARM\n"]; + [fakeReportString appendString:@"\n"]; + + NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; + NSDateFormatter *rfc3339Formatter = [[NSDateFormatter alloc] init]; + [rfc3339Formatter setLocale:enUSPOSIXLocale]; + [rfc3339Formatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"]; + [rfc3339Formatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + NSString *fakeCrashTimestamp = [rfc3339Formatter stringFromDate:[NSDate date]]; + + // we use the current date, since we don't know when the kill actually happened + [fakeReportString appendFormat:@"Date/Time: %@\n", fakeCrashTimestamp]; + [fakeReportString appendFormat:@"OS Version: %@\n", fakeReportOSVersion]; + [fakeReportString appendString:@"Report Version: 104\n"]; + [fakeReportString appendString:@"\n"]; + [fakeReportString appendFormat:@"Exception Type: %@\n", fakeSignalName]; + [fakeReportString appendString:@"Exception Codes: 00000020 at 0x8badf00d\n"]; + [fakeReportString appendString:@"\n"]; + [fakeReportString appendString:@"Application Specific Information:\n"]; + [fakeReportString appendString:@"The application did not terminate cleanly but no crash occured."]; + if (self.didReceiveMemoryWarningInLastSession) { + [fakeReportString appendString:@" The app received at least one Low Memory Warning."]; + } + [fakeReportString appendString:@"\n\n"]; + + NSString *fakeReportFilename = [NSString stringWithFormat: @"%.0f", [NSDate timeIntervalSinceReferenceDate]]; + + NSString *errorString = nil; + + NSMutableDictionary *rootObj = [NSMutableDictionary dictionaryWithCapacity:2]; + [rootObj setObject:fakeReportUUID forKey:kBITFakeCrashUUID]; + [rootObj setObject:fakeReportAppVersion forKey:kBITFakeCrashAppVersion]; + [rootObj setObject:fakeReportAppBundleIdentifier forKey:kBITFakeCrashAppBundleIdentifier]; + [rootObj setObject:fakeReportOSVersion forKey:kBITFakeCrashOSVersion]; + [rootObj setObject:fakeReportDeviceModel forKey:kBITFakeCrashDeviceModel]; + [rootObj setObject:fakeReportAppUUIDs forKey:kBITFakeCrashAppBinaryUUID]; + [rootObj setObject:fakeReportString forKey:kBITFakeCrashReport]; + + _lastSessionCrashDetails = [[BITCrashDetails alloc] initWithIncidentIdentifier:fakeReportUUID + reporterKey:fakeReporterKey + signal:fakeSignalName + exceptionName:nil + exceptionReason:nil + appStartTime:nil + crashTime:nil + appBuild:fakeReportAppVersion + ]; + + NSData *plist = [NSPropertyListSerialization dataFromPropertyList:(id)rootObj + format:NSPropertyListBinaryFormat_v1_0 + errorDescription:&errorString]; + if (plist) { + if ([plist writeToFile:[_crashesDir stringByAppendingPathComponent:[fakeReportFilename stringByAppendingPathExtension:@"fake"]] atomically:YES]) { + [self storeMetaDataForCrashReportFilename:fakeReportFilename]; + } + } else { + BITHockeyLog(@"ERROR: Writing fake crash report. %@", errorString); + } +} + /** * Send all approved crash reports * * Gathers all collected data and constructs the XML structure and starts the sending process */ -- (void)sendCrashReports { +- (void)sendNextCrashReport { NSError *error = NULL; - - NSMutableString *crashes = nil; - NSMutableArray *attachments = [NSMutableArray array]; + _crashIdenticalCurrentVersion = NO; - for (NSUInteger i=0; i < [_crashFiles count]; i++) { - NSString *filename = [_crashFiles objectAtIndex:i]; - NSString *cacheFilename = [filename lastPathComponent]; - NSData *crashData = [NSData dataWithContentsOfFile:filename]; - - if ([crashData length] > 0) { - BITPLCrashReport *report = [[BITPLCrashReport alloc] initWithData:crashData error:&error]; - - if (report == nil) { - BITHockeyLog(@"WARNING: Could not parse crash report"); - // we cannot do anything with this report, so delete it - [_fileManager removeItemAtPath:filename error:&error]; - [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.data", filename] error:&error]; - [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.meta", filename] error:&error]; - - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]]; - continue; + if ([_crashFiles count] == 0) + return; + + NSString *crashXML = nil; + BITCrashAttachment *attachment = nil; + + NSString *filename = [_crashFiles objectAtIndex:0]; + NSString *cacheFilename = [filename lastPathComponent]; + NSData *crashData = [NSData dataWithContentsOfFile:filename]; + + if ([crashData length] > 0) { + BITPLCrashReport *report = nil; + NSString *crashUUID = @""; + NSString *installString = nil; + NSString *crashLogString = nil; + NSString *appBundleIdentifier = nil; + NSString *appBundleVersion = nil; + NSString *osVersion = nil; + NSString *deviceModel = nil; + NSString *appBinaryUUIDs = nil; + NSString *metaFilename = nil; + + NSString *errorString = nil; + NSPropertyListFormat format; + + if ([[cacheFilename pathExtension] isEqualToString:@"fake"]) { + NSDictionary *fakeReportDict = (NSDictionary *)[NSPropertyListSerialization + propertyListFromData:crashData + mutabilityOption:NSPropertyListMutableContainersAndLeaves + format:&format + errorDescription:&errorString]; + + crashLogString = [fakeReportDict objectForKey:kBITFakeCrashReport]; + crashUUID = [fakeReportDict objectForKey:kBITFakeCrashUUID]; + appBundleIdentifier = [fakeReportDict objectForKey:kBITFakeCrashAppBundleIdentifier]; + appBundleVersion = [fakeReportDict objectForKey:kBITFakeCrashAppVersion]; + appBinaryUUIDs = [fakeReportDict objectForKey:kBITFakeCrashAppBinaryUUID]; + deviceModel = [fakeReportDict objectForKey:kBITFakeCrashDeviceModel]; + osVersion = [fakeReportDict objectForKey:kBITFakeCrashOSVersion]; + + metaFilename = [cacheFilename stringByReplacingOccurrencesOfString:@".fake" withString:@".meta"]; + if ([appBundleVersion compare:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]] == NSOrderedSame) { + _crashIdenticalCurrentVersion = YES; } - NSString *crashUUID = @""; + } else { + report = [[BITPLCrashReport alloc] initWithData:crashData error:&error]; + } + + if (report == nil && crashLogString == nil) { + BITHockeyLog(@"WARNING: Could not parse crash report"); + // we cannot do anything with this report, so delete it + [self cleanCrashReportWithFilename:filename]; + // we don't continue with the next report here, even if there are to prevent calling sendCrashReports from itself again + // the next crash will be automatically send on the next app start/becoming active event + return; + } + + installString = bit_appAnonID() ?: @""; + + if (report) { if (report.uuidRef != NULL) { crashUUID = (NSString *) CFBridgingRelease(CFUUIDCreateString(NULL, report.uuidRef)); } - NSString *installString = bit_appAnonID() ?: @""; - NSString *crashLogString = [BITCrashReportTextFormatter stringValueForCrashReport:report crashReporterKey:installString]; - + metaFilename = [filename stringByAppendingPathExtension:@"meta"]; + crashLogString = [BITCrashReportTextFormatter stringValueForCrashReport:report crashReporterKey:installString]; + appBundleIdentifier = report.applicationInfo.applicationIdentifier; + appBundleVersion = report.applicationInfo.applicationVersion; + osVersion = report.systemInfo.operatingSystemVersion; + deviceModel = [self getDevicePlatform]; + appBinaryUUIDs = [self extractAppUUIDs:report]; if ([report.applicationInfo.applicationVersion compare:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]] == NSOrderedSame) { _crashIdenticalCurrentVersion = YES; } - - if (crashes == nil) { - crashes = [NSMutableString string]; - } - - NSString *username = @""; - NSString *useremail = @""; - NSString *userid = @""; - NSString *applicationLog = @""; - NSString *description = @""; - - NSString *errorString = nil; - NSPropertyListFormat format; - - NSData *plist = [NSData dataWithContentsOfFile:[filename stringByAppendingString:@".meta"]]; - if (plist) { - NSDictionary *metaDict = (NSDictionary *)[NSPropertyListSerialization - propertyListFromData:plist - mutabilityOption:NSPropertyListMutableContainersAndLeaves - format:&format - errorDescription:&errorString]; - - username = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]] ?: @""; - useremail = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]] ?: @""; - userid = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]] ?: @""; - applicationLog = [metaDict objectForKey:kBITCrashMetaApplicationLog] ?: @""; - - BITCrashAttachment *attachment = [self attachmentForCrashReport:filename]; - if (attachment) { - NSDictionary *attachmentDict = @{KBITAttachmentDictIndex: @(i), - KBITAttachmentDictAttachment: attachment}; - [attachments addObject:attachmentDict]; - } - } else { - BITHockeyLog(@"ERROR: Reading crash meta data. %@", error); - } - - if ([applicationLog length] > 0) { - description = [NSString stringWithFormat:@"%@", applicationLog]; - } - - [crashes appendFormat:@"%s%@%@%@%@%@%@%@%@%@%@%@", - [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleExecutable"] UTF8String], - [self extractAppUUIDs:report], - report.applicationInfo.applicationIdentifier, - report.systemInfo.operatingSystemVersion, - [self getDevicePlatform], - [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"], - report.applicationInfo.applicationVersion, - crashUUID, - [crashLogString stringByReplacingOccurrencesOfString:@"]]>" withString:@"]]" @"]]>" options:NSLiteralSearch range:NSMakeRange(0,crashLogString.length)], - userid, - username, - useremail, - installString, - [description stringByReplacingOccurrencesOfString:@"]]>" withString:@"]]" @"]]>" options:NSLiteralSearch range:NSMakeRange(0,description.length)]]; - - - // store this crash report as user approved, so if it fails it will retry automatically - [_approvedCrashReports setObject:[NSNumber numberWithBool:YES] forKey:filename]; - } else { - // we cannot do anything with this report, so delete it - [_fileManager removeItemAtPath:filename error:&error]; - [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.data", filename] error:&error]; - [_fileManager removeItemAtPath:[NSString stringWithFormat:@"%@.meta", filename] error:&error]; - - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]]; - [self removeKeyFromKeychain:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]]; } + + if ([report.applicationInfo.applicationVersion compare:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]] == NSOrderedSame) { + _crashIdenticalCurrentVersion = YES; + } + + NSString *username = @""; + NSString *useremail = @""; + NSString *userid = @""; + NSString *applicationLog = @""; + NSString *description = @""; + + NSData *plist = [NSData dataWithContentsOfFile:[_crashesDir stringByAppendingPathComponent:metaFilename]]; + if (plist) { + NSDictionary *metaDict = (NSDictionary *)[NSPropertyListSerialization + propertyListFromData:plist + mutabilityOption:NSPropertyListMutableContainersAndLeaves + format:&format + errorDescription:&errorString]; + + username = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserName]] ?: @""; + useremail = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserEmail]] ?: @""; + userid = [self stringValueFromKeychainForKey:[NSString stringWithFormat:@"%@.%@", cacheFilename, kBITCrashMetaUserID]] ?: @""; + applicationLog = [metaDict objectForKey:kBITCrashMetaApplicationLog] ?: @""; + description = [NSString stringWithContentsOfFile:[NSString stringWithFormat:@"%@.desc", [_crashesDir stringByAppendingPathComponent: cacheFilename]] encoding:NSUTF8StringEncoding error:&error]; + attachment = [self attachmentForCrashReport:filename]; + } else { + BITHockeyLog(@"ERROR: Reading crash meta data. %@", error); + } + + if ([applicationLog length] > 0) { + if ([description length] > 0) { + description = [NSString stringWithFormat:@"%@\n\nLog:\n%@", description, applicationLog]; + } else { + description = [NSString stringWithFormat:@"Log:\n%@", applicationLog]; + } + } + + crashXML = [NSString stringWithFormat:@"%s%@%@%@%@%@%@%@%@%@%@%@", + [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleExecutable"] UTF8String], + appBinaryUUIDs, + appBundleIdentifier, + osVersion, + deviceModel, + [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"], + appBundleVersion, + crashUUID, + [crashLogString stringByReplacingOccurrencesOfString:@"]]>" withString:@"]]" @"]]>" options:NSLiteralSearch range:NSMakeRange(0,crashLogString.length)], + userid, + username, + useremail, + installString, + [description stringByReplacingOccurrencesOfString:@"]]>" withString:@"]]" @"]]>" options:NSLiteralSearch range:NSMakeRange(0,description.length)]]; + + // store this crash report as user approved, so if it fails it will retry automatically + [_approvedCrashReports setObject:[NSNumber numberWithBool:YES] forKey:filename]; + + BITHockeyLog(@"INFO: Sending crash reports:\n%@", crashXML); + [self sendCrashReportWithFilename:filename xml:crashXML attachment:attachment]; + } else { + // we cannot do anything with this report, so delete it + [self cleanCrashReportWithFilename:filename]; } - + [self saveSettings]; - - if (crashes != nil) { - BITHockeyLog(@"INFO: Sending crash reports:\n%@", crashes); - [self postXML:[NSString stringWithFormat:@"%@", crashes] attachments:attachments]; - } } @@ -932,37 +1326,72 @@ NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { switch (buttonIndex) { case 0: - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillCancelSendingCrashReport:)]) { - [self.delegate crashManagerWillCancelSendingCrashReport:self]; - } - - _sendingInProgress = NO; - [self cleanCrashReports]; + [self handleUserInput:BITCrashManagerUserInputDontSend withUserProvidedMetaData:nil]; break; case 1: - [self sendCrashReports]; + [self handleUserInput:BITCrashManagerUserInputSend withUserProvidedMetaData:nil]; break; - case 2: { - _crashManagerStatus = BITCrashManagerStatusAutoSend; - [[NSUserDefaults standardUserDefaults] setInteger:_crashManagerStatus forKey:kBITCrashManagerStatus]; - [[NSUserDefaults standardUserDefaults] synchronize]; - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillSendCrashReportsAlways:)]) { - [self.delegate crashManagerWillSendCrashReportsAlways:self]; - } - - [self sendCrashReports]; - break; - } - default: - _sendingInProgress = NO; - [self cleanCrashReports]; + case 2: + [self handleUserInput:BITCrashManagerUserInputAlwaysSend withUserProvidedMetaData:nil]; break; } } + + #pragma mark - Networking +- (NSURLRequest *)requestWithXML:(NSString*)xml attachment:(BITCrashAttachment *)attachment { + NSString *postCrashPath = [NSString stringWithFormat:@"api/2/apps/%@/crashes", self.encodedAppIdentifier]; + + NSMutableURLRequest *request = [self.hockeyAppClient requestWithMethod:@"POST" + path:postCrashPath + parameters:nil]; + + [request setCachePolicy: NSURLRequestReloadIgnoringLocalCacheData]; + [request setValue:@"HockeySDK/iOS" forHTTPHeaderField:@"User-Agent"]; + [request setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"]; + + NSString *boundary = @"----FOO"; + NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary]; + [request setValue:contentType forHTTPHeaderField:@"Content-type"]; + + NSMutableData *postBody = [NSMutableData data]; + + [postBody appendData:[BITHockeyAppClient dataWithPostValue:BITHOCKEY_NAME + forKey:@"sdk" + boundary:boundary]]; + + [postBody appendData:[BITHockeyAppClient dataWithPostValue:BITHOCKEY_VERSION + forKey:@"sdk_version" + boundary:boundary]]; + + [postBody appendData:[BITHockeyAppClient dataWithPostValue:@"no" + forKey:@"feedbackEnabled" + boundary:boundary]]; + + [postBody appendData:[BITHockeyAppClient dataWithPostValue:[xml dataUsingEncoding:NSUTF8StringEncoding] + forKey:@"xml" + contentType:@"text/xml" + boundary:boundary + filename:@"crash.xml"]]; + + if (attachment) { + [postBody appendData:[BITHockeyAppClient dataWithPostValue:attachment.crashAttachmentData + forKey:@"attachment0" + contentType:attachment.contentType + boundary:boundary + filename:attachment.filename]]; + } + + [postBody appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; + + [request setHTTPBody:postBody]; + + return request; +} + /** * Send the XML data to the server * @@ -970,149 +1399,84 @@ NSString *const kBITCrashManagerStatus = @"BITCrashManagerStatus"; * * @param xml The XML data that needs to be send to the server */ -- (void)postXML:(NSString*)xml attachments:(NSArray *)attachments { - NSMutableURLRequest *request = nil; - NSString *boundary = @"----FOO"; +- (void)sendCrashReportWithFilename:(NSString *)filename xml:(NSString*)xml attachment:(BITCrashAttachment *)attachment { - request = [NSMutableURLRequest requestWithURL: - [NSURL URLWithString:[NSString stringWithFormat:@"%@api/2/apps/%@/crashes?sdk=%@&sdk_version=%@&feedbackEnabled=no", - self.serverURL, - [self encodedAppIdentifier], - BITHOCKEY_NAME, - BITHOCKEY_VERSION - ] - ]]; + NSURLRequest* request = [self requestWithXML:xml attachment:attachment]; - [request setCachePolicy: NSURLRequestReloadIgnoringLocalCacheData]; - [request setValue:@"HockeySDK/iOS" forHTTPHeaderField:@"User-Agent"]; - [request setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"]; - [request setTimeoutInterval: 15]; - [request setHTTPMethod:@"POST"]; - NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary]; - [request setValue:contentType forHTTPHeaderField:@"Content-type"]; - - NSMutableData *postBody = [NSMutableData data]; + __weak typeof (self) weakSelf = self; + BITHTTPOperation *operation = [self.hockeyAppClient + operationWithURLRequest:request + completion:^(BITHTTPOperation *operation, NSData* responseData, NSError *error) { + typeof (self) strongSelf = weakSelf; + + _sendingInProgress = NO; + + NSInteger statusCode = [operation.response statusCode]; + + if (nil == error) { + if (nil == responseData || [responseData length] == 0) { + error = [NSError errorWithDomain:kBITCrashErrorDomain + code:BITCrashAPIReceivedEmptyResponse + userInfo:@{ + NSLocalizedDescriptionKey: @"Sending failed with an empty response!" + } + ]; + } else if (statusCode >= 200 && statusCode < 400) { + [strongSelf cleanCrashReportWithFilename:filename]; + + // HockeyApp uses PList XML format + NSMutableDictionary *response = [NSPropertyListSerialization propertyListFromData:responseData + mutabilityOption:NSPropertyListMutableContainersAndLeaves + format:nil + errorDescription:NULL]; + BITHockeyLog(@"INFO: Received API response: %@", response); + + if (strongSelf.delegate != nil && + [strongSelf.delegate respondsToSelector:@selector(crashManagerDidFinishSendingCrashReport:)]) { + [strongSelf.delegate crashManagerDidFinishSendingCrashReport:self]; + } + + // only if sending the crash report went successfully, continue with the next one (if there are more) + [strongSelf sendNextCrashReport]; + } else if (statusCode == 400) { + [strongSelf cleanCrashReportWithFilename:filename]; + + error = [NSError errorWithDomain:kBITCrashErrorDomain + code:BITCrashAPIAppVersionRejected + userInfo:@{ + NSLocalizedDescriptionKey: @"The server rejected receiving crash reports for this app version!" + } + ]; + } else { + error = [NSError errorWithDomain:kBITCrashErrorDomain + code:BITCrashAPIErrorWithStatusCode + userInfo:@{ + NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Sending failed with status code: %li", (long)statusCode] + } + ]; + } + } + + if (error) { + if (strongSelf.delegate != nil && + [strongSelf.delegate respondsToSelector:@selector(crashManager:didFailWithError:)]) { + [strongSelf.delegate crashManager:self didFailWithError:error]; + } + + BITHockeyLog(@"ERROR: %@", [error localizedDescription]); + } + + }]; - [postBody appendData:[BITHockeyAppClient dataWithPostValue:[xml dataUsingEncoding:NSUTF8StringEncoding] - forKey:@"xml" - contentType:@"text/xml" - boundary:boundary - filename:@"crash.xml"]]; - - for (NSDictionary *dict in attachments) { - NSInteger index = [(NSNumber *)dict[KBITAttachmentDictIndex] integerValue]; - NSString *key = [NSString stringWithFormat:@"attachment%ld", (long)index]; - - BITCrashAttachment *attachment = (BITCrashAttachment *)dict[KBITAttachmentDictAttachment]; - - [postBody appendData:[BITHockeyAppClient dataWithPostValue:attachment.crashAttachmentData - forKey:key - contentType:attachment.contentType - boundary:boundary - filename:attachment.filename]]; - } - - [postBody appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; - - [request setHTTPBody:postBody]; - - _statusCode = 200; - - //Release when done in the delegate method - _responseData = [[NSMutableData alloc] init]; - - _urlConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; - - if (!_urlConnection) { - BITHockeyLog(@"INFO: Sending crash reports could not start!"); - _sendingInProgress = NO; - } else { - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillSendCrashReport:)]) { - [self.delegate crashManagerWillSendCrashReport:self]; - } - - BITHockeyLog(@"INFO: Sending crash reports started."); - } -} - -#pragma mark - NSURLConnection Delegate - -- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { - if ([response isKindOfClass:[NSHTTPURLResponse class]]) { - _statusCode = [(NSHTTPURLResponse *)response statusCode]; - } -} - -- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { - [_responseData appendData:data]; -} - -- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManager:didFailWithError:)]) { - [self.delegate crashManager:self didFailWithError:error]; + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerWillSendCrashReport:)]) { + [self.delegate crashManagerWillSendCrashReport:self]; } - BITHockeyLog(@"ERROR: %@", [error localizedDescription]); - - _sendingInProgress = NO; - - _responseData = nil; - _urlConnection = nil; + BITHockeyLog(@"INFO: Sending crash reports started."); + + [self.hockeyAppClient enqeueHTTPOperation:operation]; } -- (void)connectionDidFinishLoading:(NSURLConnection *)connection { - NSError *error = nil; - - if (_statusCode >= 200 && _statusCode < 400 && _responseData != nil && [_responseData length] > 0) { - [self cleanCrashReports]; - - // HockeyApp uses PList XML format - NSMutableDictionary *response = [NSPropertyListSerialization propertyListFromData:_responseData - mutabilityOption:NSPropertyListMutableContainersAndLeaves - format:nil - errorDescription:NULL]; - BITHockeyLog(@"INFO: Received API response: %@", response); - - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManagerDidFinishSendingCrashReport:)]) { - [self.delegate crashManagerDidFinishSendingCrashReport:self]; - } - } else if (_statusCode == 400) { - [self cleanCrashReports]; - - error = [NSError errorWithDomain:kBITCrashErrorDomain - code:BITCrashAPIAppVersionRejected - userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"The server rejected receiving crash reports for this app version!", NSLocalizedDescriptionKey, nil]]; - - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManager:didFailWithError:)]) { - [self.delegate crashManager:self didFailWithError:error]; - } - - BITHockeyLog(@"ERROR: %@", [error localizedDescription]); - } else { - if (_responseData == nil || [_responseData length] == 0) { - error = [NSError errorWithDomain:kBITCrashErrorDomain - code:BITCrashAPIReceivedEmptyResponse - userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Sending failed with an empty response!", NSLocalizedDescriptionKey, nil]]; - } else { - error = [NSError errorWithDomain:kBITCrashErrorDomain - code:BITCrashAPIErrorWithStatusCode - userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"Sending failed with status code: %li", (long)_statusCode], NSLocalizedDescriptionKey, nil]]; - } - - if (self.delegate != nil && [self.delegate respondsToSelector:@selector(crashManager:didFailWithError:)]) { - [self.delegate crashManager:self didFailWithError:error]; - } - - BITHockeyLog(@"ERROR: %@", [error localizedDescription]); - } - - _sendingInProgress = NO; - - _responseData = nil; - _urlConnection = nil; -} - - @end #endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ diff --git a/Classes/BITCrashManagerDelegate.h b/Classes/BITCrashManagerDelegate.h index 2697106ad7..7098e6078c 100644 --- a/Classes/BITCrashManagerDelegate.h +++ b/Classes/BITCrashManagerDelegate.h @@ -158,4 +158,25 @@ */ - (void)crashManagerDidFinishSendingCrashReport:(BITCrashManager *)crashManager; +///----------------------------------------------------------------------------- +/// @name Experimental +///----------------------------------------------------------------------------- + +/** Define if a report should be considered as a crash report + + Due to the risk, that these reports may be false positives, this delegates allows the + developer to influence which reports detected by the heuristic should actually be reported. + + The developer can use the following property to get more information about the crash scenario: + - `[BITCrashManager didReceiveMemoryWarningInLastSession]`: Did the app receive a low memory warning + + This allows only reports to be considered where at least one low memory warning notification was + received by the app to reduce to possibility of having false positives. + + @param crashManager The `BITCrashManager` instance invoking this delegate + @return `YES` if the heuristic based detected report should be reported, otherwise `NO` + @see `[BITCrashManager didReceiveMemoryWarningInLastSession]` + */ +-(BOOL)considerAppNotTerminatedCleanlyReportForCrashManager:(BITCrashManager *)crashManager; + @end diff --git a/Classes/BITCrashManagerPrivate.h b/Classes/BITCrashManagerPrivate.h index f2231342f4..f42062d07b 100644 --- a/Classes/BITCrashManagerPrivate.h +++ b/Classes/BITCrashManagerPrivate.h @@ -31,13 +31,30 @@ #if HOCKEYSDK_FEATURE_CRASH_REPORTER +#import + +@class BITHockeyAppClient; + @interface BITCrashManager () { } +/** + * must be set + */ +@property (nonatomic, strong) BITHockeyAppClient *hockeyAppClient; + @property (nonatomic) NSUncaughtExceptionHandler *exceptionHandler; +@property (nonatomic, strong) NSFileManager *fileManager; + @property (nonatomic, strong) BITPLCrashReporter *plCrashReporter; +@property (nonatomic) NSString *lastCrashFilename; + +@property (nonatomic, copy, setter = setAlertViewHandler:) BITCustomAlertViewHandler alertViewHandler; + +@property (nonatomic, strong) NSString *crashesDir; + #if HOCKEYSDK_FEATURE_AUTHENTICATOR // Only set via BITAuthenticator @@ -61,8 +78,15 @@ - (BOOL)hasPendingCrashReport; - (BOOL)hasNonApprovedCrashReports; +- (void)persistUserProvidedMetaData:(BITCrashMetaData *)userProvidedMetaData; +- (void)persistAttachment:(BITCrashAttachment *)attachment withFilename:(NSString *)filename; + +- (BITCrashAttachment *)attachmentForCrashReport:(NSString *)filename; + - (void)invokeDelayedProcessing; -- (void)sendCrashReports; +- (void)sendNextCrashReport; + +- (void)setLastCrashFilename:(NSString *)lastCrashFilename; @end diff --git a/Classes/BITCrashMetaData.h b/Classes/BITCrashMetaData.h new file mode 100644 index 0000000000..ac6bb2b650 --- /dev/null +++ b/Classes/BITCrashMetaData.h @@ -0,0 +1,54 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + + +@interface BITCrashMetaData : NSObject + +/** + * User provided description that should be attached to the crash report as plain text + */ +@property (nonatomic, copy) NSString *userDescription; + +/** + * User name that should be attached to the crash report + */ +@property (nonatomic, copy) NSString *userName; + +/** + * User email that should be attached to the crash report + */ +@property (nonatomic, copy) NSString *userEmail; + +/** + * User ID that should be attached to the crash report + */ +@property (nonatomic, copy) NSString *userID; + +@end diff --git a/Classes/BITCrashMetaData.m b/Classes/BITCrashMetaData.m new file mode 100644 index 0000000000..824d149b93 --- /dev/null +++ b/Classes/BITCrashMetaData.m @@ -0,0 +1,34 @@ +/* + * Author: Andreas Linde + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "BITCrashMetaData.h" + + +@implementation BITCrashMetaData + +@end diff --git a/Classes/BITCrashReportTextFormatter.h b/Classes/BITCrashReportTextFormatter.h index 13ab19f964..5ddef4a22d 100644 --- a/Classes/BITCrashReportTextFormatter.h +++ b/Classes/BITCrashReportTextFormatter.h @@ -49,5 +49,6 @@ + (NSString *)stringValueForCrashReport:(PLCrashReport *)report crashReporterKey:(NSString *)crashReporterKey; + (NSArray *)arrayOfAppUUIDsForCrashReport:(PLCrashReport *)report; ++ (NSString *)bit_archNameFromCPUType:(uint64_t)cpuType subType:(uint64_t)subType; @end diff --git a/Classes/BITCrashReportTextFormatter.m b/Classes/BITCrashReportTextFormatter.m index 5c6b576c4a..01c9ea6842 100644 --- a/Classes/BITCrashReportTextFormatter.m +++ b/Classes/BITCrashReportTextFormatter.m @@ -655,66 +655,73 @@ static const char *findSEL (const char *imageName, NSString *imageUUID, uint64_t { NSString *archName = @"???"; if (imageInfo.codeType != nil && imageInfo.codeType.typeEncoding == PLCrashReportProcessorTypeEncodingMach) { - switch (imageInfo.codeType.type) { - case CPU_TYPE_ARM: - /* Apple includes subtype for ARM binaries. */ - switch (imageInfo.codeType.subtype) { - case CPU_SUBTYPE_ARM_V6: - archName = @"armv6"; - break; - - case CPU_SUBTYPE_ARM_V7: - archName = @"armv7"; - break; - - case CPU_SUBTYPE_ARM_V7S: - archName = @"armv7s"; - break; - - default: - archName = @"arm-unknown"; - break; - } - break; - - case CPU_TYPE_ARM64: - /* Apple includes subtype for ARM64 binaries. */ - switch (imageInfo.codeType.subtype) { - case CPU_SUBTYPE_ARM_ALL: - archName = @"arm64"; - break; - - case CPU_SUBTYPE_ARM_V8: - archName = @"arm64"; - break; - - default: - archName = @"arm64-unknown"; - break; - } - break; - - case CPU_TYPE_X86: - archName = @"i386"; - break; - - case CPU_TYPE_X86_64: - archName = @"x86_64"; - break; - - case CPU_TYPE_POWERPC: - archName = @"powerpc"; - break; - - default: - // Use the default archName value (initialized above). - break; - } + archName = [BITCrashReportTextFormatter bit_archNameFromCPUType:imageInfo.codeType.type subType:imageInfo.codeType.subtype]; } return archName; } ++ (NSString *)bit_archNameFromCPUType:(uint64_t)cpuType subType:(uint64_t)subType { + NSString *archName = @"???"; + switch (cpuType) { + case CPU_TYPE_ARM: + /* Apple includes subtype for ARM binaries. */ + switch (subType) { + case CPU_SUBTYPE_ARM_V6: + archName = @"armv6"; + break; + + case CPU_SUBTYPE_ARM_V7: + archName = @"armv7"; + break; + + case CPU_SUBTYPE_ARM_V7S: + archName = @"armv7s"; + break; + + default: + archName = @"arm-unknown"; + break; + } + break; + + case CPU_TYPE_ARM64: + /* Apple includes subtype for ARM64 binaries. */ + switch (subType) { + case CPU_SUBTYPE_ARM_ALL: + archName = @"arm64"; + break; + + case CPU_SUBTYPE_ARM_V8: + archName = @"arm64"; + break; + + default: + archName = @"arm64-unknown"; + break; + } + break; + + case CPU_TYPE_X86: + archName = @"i386"; + break; + + case CPU_TYPE_X86_64: + archName = @"x86_64"; + break; + + case CPU_TYPE_POWERPC: + archName = @"powerpc"; + break; + + default: + // Use the default archName value (initialized above). + break; + } + + return archName; +} + /** * Format a stack frame for display in a thread backtrace. diff --git a/Classes/BITHockeyManager.m b/Classes/BITHockeyManager.m index 34c8292c9b..fb74376d56 100644 --- a/Classes/BITHockeyManager.m +++ b/Classes/BITHockeyManager.m @@ -610,6 +610,7 @@ bitstadium_info_t bitstadium_library_info __attribute__((section("__TEXT,__bit_h #if HOCKEYSDK_FEATURE_CRASH_REPORTER BITHockeyLog(@"INFO: Setup CrashManager"); _crashManager = [[BITCrashManager alloc] initWithAppIdentifier:_appIdentifier isAppStoreEnvironment:_appStoreEnvironment]; + _crashManager.hockeyAppClient = [self hockeyAppClient]; _crashManager.delegate = _delegate; #endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ diff --git a/Classes/HockeySDK.h b/Classes/HockeySDK.h index c364d4eace..709333dc9f 100644 --- a/Classes/HockeySDK.h +++ b/Classes/HockeySDK.h @@ -39,6 +39,8 @@ #import "BITCrashManager.h" #import "BITCrashAttachment.h" #import "BITCrashManagerDelegate.h" +#import "BITCrashDetails.h" +#import "BITCrashMetaData.h" #endif /* HOCKEYSDK_FEATURE_CRASH_REPORTER */ #if HOCKEYSDK_FEATURE_UPDATES || HOCKEYSDK_FEATURE_JIRA_MOBILE_CONNECT diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index efd12187f7..73f560e05f 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -111,6 +111,8 @@ 1E7A45FC16F54FB5005B08F1 /* OCHamcrestIOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E7A45FA16F54FB5005B08F1 /* OCHamcrestIOS.framework */; }; 1E7A45FD16F54FB5005B08F1 /* OCMockitoIOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E7A45FB16F54FB5005B08F1 /* OCMockitoIOS.framework */; }; 1E84DB3417E099BA00AC83FD /* HockeySDKFeatureConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E84DB3317E0977C00AC83FD /* HockeySDKFeatureConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1E90FD7318EDB86400CF0417 /* BITCrashDetails.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E90FD7118EDB86400CF0417 /* BITCrashDetails.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1E90FD7418EDB86400CF0417 /* BITCrashDetails.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E90FD7218EDB86400CF0417 /* BITCrashDetails.m */; }; 1E94F9E116E91330006570AD /* BITStoreUpdateManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E94F9DF16E91330006570AD /* BITStoreUpdateManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1E94F9E216E91330006570AD /* BITStoreUpdateManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E94F9E016E91330006570AD /* BITStoreUpdateManager.m */; }; 1E94F9E416E9136B006570AD /* BITStoreUpdateManagerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E94F9E316E9136B006570AD /* BITStoreUpdateManagerPrivate.h */; }; @@ -127,6 +129,9 @@ 1EAF20AA162DC0F600957B1D /* feedbackActiviy.png in Resources */ = {isa = PBXBuildFile; fileRef = 1EAF20A6162DC0F600957B1D /* feedbackActiviy.png */; }; 1EAF20AB162DC0F600957B1D /* feedbackActiviy@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1EAF20A7162DC0F600957B1D /* feedbackActiviy@2x.png */; }; 1EB52FD5167B766100C801D5 /* HockeySDK.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1E59555F15B6F80E00A03429 /* HockeySDK.strings */; }; + 1ECA8F4D192B5BD8006B9416 /* BITCrashDetailsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ECA8F4B192B5BD8006B9416 /* BITCrashDetailsPrivate.h */; }; + 1ECA8F51192B6954006B9416 /* BITCrashMetaData.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ECA8F4F192B6954006B9416 /* BITCrashMetaData.h */; }; + 1ECA8F52192B6954006B9416 /* BITCrashMetaData.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ECA8F50192B6954006B9416 /* BITCrashMetaData.m */; }; 1ED570C718BF878C00AB3350 /* BITCrashAttachment.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ED570C518BF878C00AB3350 /* BITCrashAttachment.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1ED570C818BF878C00AB3350 /* BITCrashAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ED570C618BF878C00AB3350 /* BITCrashAttachment.m */; }; 1EF95CA6162CB037000AE3AD /* BITFeedbackActivity.h in Headers */ = {isa = PBXBuildFile; fileRef = 1EF95CA4162CB036000AE3AD /* BITFeedbackActivity.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -270,6 +275,8 @@ 1E7A45FA16F54FB5005B08F1 /* OCHamcrestIOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OCHamcrestIOS.framework; sourceTree = ""; }; 1E7A45FB16F54FB5005B08F1 /* OCMockitoIOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OCMockitoIOS.framework; sourceTree = ""; }; 1E84DB3317E0977C00AC83FD /* HockeySDKFeatureConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HockeySDKFeatureConfig.h; sourceTree = ""; }; + 1E90FD7118EDB86400CF0417 /* BITCrashDetails.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITCrashDetails.h; sourceTree = ""; }; + 1E90FD7218EDB86400CF0417 /* BITCrashDetails.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITCrashDetails.m; sourceTree = ""; }; 1E94F9DF16E91330006570AD /* BITStoreUpdateManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITStoreUpdateManager.h; sourceTree = ""; }; 1E94F9E016E91330006570AD /* BITStoreUpdateManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITStoreUpdateManager.m; sourceTree = ""; }; 1E94F9E316E9136B006570AD /* BITStoreUpdateManagerPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITStoreUpdateManagerPrivate.h; sourceTree = ""; }; @@ -285,6 +292,9 @@ 1EAF20A6162DC0F600957B1D /* feedbackActiviy.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = feedbackActiviy.png; sourceTree = ""; }; 1EAF20A7162DC0F600957B1D /* feedbackActiviy@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "feedbackActiviy@2x.png"; sourceTree = ""; }; 1EB52FC3167B73D400C801D5 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/HockeySDK.strings; sourceTree = ""; }; + 1ECA8F4B192B5BD8006B9416 /* BITCrashDetailsPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITCrashDetailsPrivate.h; sourceTree = ""; }; + 1ECA8F4F192B6954006B9416 /* BITCrashMetaData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITCrashMetaData.h; sourceTree = ""; }; + 1ECA8F50192B6954006B9416 /* BITCrashMetaData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITCrashMetaData.m; sourceTree = ""; }; 1ED570C518BF878C00AB3350 /* BITCrashAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITCrashAttachment.h; sourceTree = ""; }; 1ED570C618BF878C00AB3350 /* BITCrashAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITCrashAttachment.m; sourceTree = ""; }; 1EDA60CF15C2C1450032D10B /* HockeySDK-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "HockeySDK-Info.plist"; sourceTree = ""; }; @@ -484,6 +494,11 @@ 1E754E571621FBB70070AB92 /* BITCrashManager.m */, 1EFF03D717F20F8300A5F13C /* BITCrashManagerPrivate.h */, 1E754E581621FBB70070AB92 /* BITCrashManagerDelegate.h */, + 1E90FD7118EDB86400CF0417 /* BITCrashDetails.h */, + 1E90FD7218EDB86400CF0417 /* BITCrashDetails.m */, + 1ECA8F4B192B5BD8006B9416 /* BITCrashDetailsPrivate.h */, + 1ECA8F4F192B6954006B9416 /* BITCrashMetaData.h */, + 1ECA8F50192B6954006B9416 /* BITCrashMetaData.m */, 1ED570C518BF878C00AB3350 /* BITCrashAttachment.h */, 1ED570C618BF878C00AB3350 /* BITCrashAttachment.m */, 1E754E5A1621FBB70070AB92 /* BITCrashReportTextFormatter.h */, @@ -640,11 +655,13 @@ 1E5955FD15B7877B00A03429 /* BITHockeyManagerDelegate.h in Headers */, 1E754E5C1621FBB70070AB92 /* BITCrashManager.h in Headers */, 1E754E5E1621FBB70070AB92 /* BITCrashManagerDelegate.h in Headers */, + 1E90FD7318EDB86400CF0417 /* BITCrashDetails.h in Headers */, 1E49A4731612226D00463151 /* BITUpdateManager.h in Headers */, 1E49A4791612226D00463151 /* BITUpdateManagerDelegate.h in Headers */, 1E49A44E1612223B00463151 /* BITFeedbackManager.h in Headers */, E4B4DB7D17B435550099C67F /* BITAuthenticationViewController.h in Headers */, 1E49A4481612223B00463151 /* BITFeedbackListViewController.h in Headers */, + 1ECA8F4D192B5BD8006B9416 /* BITCrashDetailsPrivate.h in Headers */, 1E49A47F1612226D00463151 /* BITUpdateViewController.h in Headers */, 1E49A43C1612223B00463151 /* BITFeedbackComposeViewController.h in Headers */, E40E0B0C17DA1AFF005E38C1 /* BITHockeyAppClient.h in Headers */, @@ -662,6 +679,7 @@ 1E49A46D1612226D00463151 /* BITAppVersionMetaInfo.h in Headers */, 1E49A47C1612226D00463151 /* BITUpdateManagerPrivate.h in Headers */, 1E49A4851612226D00463151 /* BITUpdateViewControllerPrivate.h in Headers */, + 1ECA8F51192B6954006B9416 /* BITCrashMetaData.h in Headers */, 1E49A4B5161222B900463151 /* BITHockeyBaseManagerPrivate.h in Headers */, E4933E8017B66CDA00B11ACC /* BITHTTPOperation.h in Headers */, 1E49A4BE161222B900463151 /* BITHockeyHelper.h in Headers */, @@ -839,7 +857,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Sets the target folders and the final framework product.\nFMK_NAME=HockeySDK\nFMK_VERSION=A\nFMK_RESOURCE_BUNDLE=HockeySDKResources\n\n# Documentation\nHOCKEYSDK_DOCSET_VERSION_NAME=\"de.bitstadium.${HOCKEYSDK_DOCSET_NAME}-${VERSION_STRING}\"\n\n# Install dir will be the final output to the framework.\n# The following line create it in the root folder of the current project.\nPRODUCTS_DIR=${SRCROOT}/../Products\nPLCR_DIR=${SRCROOT}/../Vendor/CrashReporter.framework\nZIP_FOLDER=HockeySDK-iOS\nTEMP_DIR=${PRODUCTS_DIR}/${ZIP_FOLDER}\nINSTALL_DIR=${TEMP_DIR}/${FMK_NAME}.framework\n\n# Working dir will be deleted after the framework creation.\nWRK_DIR=build\nDEVICE_DIR=${WRK_DIR}/Release-iphoneos\nSIMULATOR_DIR=${WRK_DIR}/Release-iphonesimulator\nHEADERS_DIR=${WRK_DIR}/Release-iphoneos/usr/local/include\n\n# Building both architectures.\nxcodebuild -project \"HockeySDK.xcodeproj\" -configuration \"Release\" -target \"${FMK_NAME}\" -sdk iphoneos\nxcodebuild -project \"HockeySDK.xcodeproj\" -configuration \"Release\" -target \"${FMK_NAME}\" -sdk iphonesimulator\n\n# Cleaning the oldest.\nif [ -d \"${TEMP_DIR}\" ]\nthen\nrm -rf \"${TEMP_DIR}\"\nfi\n\n# Creates and renews the final product folder.\nmkdir -p \"${INSTALL_DIR}\"\nmkdir -p \"${INSTALL_DIR}/Versions\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers\"\n\n# Creates the internal links.\n# It MUST uses relative path, otherwise will not work when the folder is copied/moved.\nln -s \"${FMK_VERSION}\" \"${INSTALL_DIR}/Versions/Current\"\nln -s \"Versions/Current/Headers\" \"${INSTALL_DIR}/Headers\"\nln -s \"Versions/Current/Resources\" \"${INSTALL_DIR}/Resources\"\nln -s \"Versions/Current/${FMK_NAME}\" \"${INSTALL_DIR}/${FMK_NAME}\"\n\n# Copies the headers and resources files to the final product folder.\ncp -R \"${SRCROOT}/build/Release-iphoneos/include/HockeySDK/\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers/\"\ncp -R \"${PLCR_DIR}/Versions/A/Headers/\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers/\"\ncp -R \"${DEVICE_DIR}/${FMK_RESOURCE_BUNDLE}.bundle\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/\"\ncp -f \"${SRCROOT}/${FMK_NAME}.xcconfig\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/\"\n\n# Uses the Lipo Tool to merge both binary files (i386 + armv6/armv7) into one Universal final product.\nlipo -create \"${DEVICE_DIR}/lib${FMK_NAME}.a\" \"${SIMULATOR_DIR}/lib${FMK_NAME}.a\" -output \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\"\n\n# Combine the CrashReporter static library into a new Hockey static library file if they are not already present and copy the public headers too\nif [ -z $(otool -L \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" | grep 'libCrashReporter') ]\nthen\nlibtool -static -o \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" \"${SRCROOT}/../Vendor/CrashReporter.framework/Versions/A/CrashReporter\"\nfi\n\nrm -r \"${WRK_DIR}\"\n\n# build embeddedframework folder and move framework into it\nmkdir \"${INSTALL_DIR}/../${FMK_NAME}.embeddedframework\"\nmv \"${INSTALL_DIR}\" \"${INSTALL_DIR}/../${FMK_NAME}.embeddedframework/${FMK_NAME}.framework\"\n\n# link Resources\nNEW_INSTALL_DIR=${TEMP_DIR}/${FMK_NAME}.embeddedframework\nmkdir \"${NEW_INSTALL_DIR}/Resources\"\nln -s \"../${FMK_NAME}.framework/Resources/${FMK_RESOURCE_BUNDLE}.bundle\" \"${NEW_INSTALL_DIR}/Resources/${FMK_RESOURCE_BUNDLE}.bundle\"\nln -s \"../${FMK_NAME}.framework/Resources/${FMK_NAME}.xcconfig\" \"${NEW_INSTALL_DIR}/Resources/${FMK_NAME}.xcconfig\"\n\n# copy license, changelog, documentation, integration json\ncp -f \"${SRCROOT}/../Docs/Changelog-template.md\" \"${TEMP_DIR}/CHANGELOG\"\ncp -f \"${SRCROOT}/../Docs/Guide-Installation-Setup-template.md\" \"${TEMP_DIR}/README.md\"\ncp -f \"${SRCROOT}/../LICENSE\" \"${TEMP_DIR}\"\nmkdir \"${TEMP_DIR}/${HOCKEYSDK_DOCSET_VERSION_NAME}.docset\"\ncp -R \"${SRCROOT}/../documentation/docset/Contents\" \"${TEMP_DIR}/${HOCKEYSDK_DOCSET_VERSION_NAME}.docset\"\n\n# build zip\ncd \"${PRODUCTS_DIR}\"\nrm -f \"${FMK_NAME}-iOS-${VERSION_STRING}.zip\"\nzip -yr \"${FMK_NAME}-iOS-${VERSION_STRING}.zip\" \"${ZIP_FOLDER}\" -x \\*/.*\n"; + shellScript = "# Sets the target folders and the final framework product.\nFMK_NAME=HockeySDK\nFMK_VERSION=A\nFMK_RESOURCE_BUNDLE=HockeySDKResources\n\n# Documentation\nHOCKEYSDK_DOCSET_VERSION_NAME=\"de.bitstadium.${HOCKEYSDK_DOCSET_NAME}-${VERSION_STRING}\"\n\n# Install dir will be the final output to the framework.\n# The following line create it in the root folder of the current project.\nPRODUCTS_DIR=${SRCROOT}/../Products\nZIP_FOLDER=HockeySDK-iOS\nTEMP_DIR=${PRODUCTS_DIR}/${ZIP_FOLDER}\nINSTALL_DIR=${TEMP_DIR}/${FMK_NAME}.framework\n\n# Working dir will be deleted after the framework creation.\nWRK_DIR=build\nDEVICE_DIR=${WRK_DIR}/Release-iphoneos\nSIMULATOR_DIR=${WRK_DIR}/Release-iphonesimulator\nHEADERS_DIR=${WRK_DIR}/Release-iphoneos/usr/local/include\n\n# Building both architectures.\nxcodebuild -project \"HockeySDK.xcodeproj\" -configuration \"Release\" -target \"${FMK_NAME}\" -sdk iphoneos\nxcodebuild -project \"HockeySDK.xcodeproj\" -configuration \"Release\" -target \"${FMK_NAME}\" -sdk iphonesimulator\n\n# Cleaning the oldest.\nif [ -d \"${TEMP_DIR}\" ]\nthen\nrm -rf \"${TEMP_DIR}\"\nfi\n\n# Creates and renews the final product folder.\nmkdir -p \"${INSTALL_DIR}\"\nmkdir -p \"${INSTALL_DIR}/Versions\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources\"\nmkdir -p \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers\"\n\n# Creates the internal links.\n# It MUST uses relative path, otherwise will not work when the folder is copied/moved.\nln -s \"${FMK_VERSION}\" \"${INSTALL_DIR}/Versions/Current\"\nln -s \"Versions/Current/Headers\" \"${INSTALL_DIR}/Headers\"\nln -s \"Versions/Current/Resources\" \"${INSTALL_DIR}/Resources\"\nln -s \"Versions/Current/${FMK_NAME}\" \"${INSTALL_DIR}/${FMK_NAME}\"\n\n# Copies the headers and resources files to the final product folder.\ncp -R \"${SRCROOT}/build/Release-iphoneos/include/HockeySDK/\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers/\"\ncp -R \"${DEVICE_DIR}/${FMK_RESOURCE_BUNDLE}.bundle\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/\"\ncp -f \"${SRCROOT}/${FMK_NAME}.xcconfig\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/\"\n\n# Uses the Lipo Tool to merge both binary files (i386 + armv6/armv7) into one Universal final product.\nlipo -create \"${DEVICE_DIR}/lib${FMK_NAME}.a\" \"${SIMULATOR_DIR}/lib${FMK_NAME}.a\" -output \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\"\n\n# Combine the CrashReporter static library into a new Hockey static library file if they are not already present and copy the public headers too\nif [ -z $(otool -L \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" | grep 'libCrashReporter') ]\nthen\nlibtool -static -o \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" \"${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}\" \"${SRCROOT}/../Vendor/CrashReporter.framework/Versions/A/CrashReporter\"\nfi\n\nrm -r \"${WRK_DIR}\"\n\n# build embeddedframework folder and move framework into it\nmkdir \"${INSTALL_DIR}/../${FMK_NAME}.embeddedframework\"\nmv \"${INSTALL_DIR}\" \"${INSTALL_DIR}/../${FMK_NAME}.embeddedframework/${FMK_NAME}.framework\"\n\n# link Resources\nNEW_INSTALL_DIR=${TEMP_DIR}/${FMK_NAME}.embeddedframework\nmkdir \"${NEW_INSTALL_DIR}/Resources\"\nln -s \"../${FMK_NAME}.framework/Resources/${FMK_RESOURCE_BUNDLE}.bundle\" \"${NEW_INSTALL_DIR}/Resources/${FMK_RESOURCE_BUNDLE}.bundle\"\nln -s \"../${FMK_NAME}.framework/Resources/${FMK_NAME}.xcconfig\" \"${NEW_INSTALL_DIR}/Resources/${FMK_NAME}.xcconfig\"\n\n# copy license, changelog, documentation, integration json\ncp -f \"${SRCROOT}/../Docs/Changelog-template.md\" \"${TEMP_DIR}/CHANGELOG\"\ncp -f \"${SRCROOT}/../Docs/Guide-Installation-Setup-template.md\" \"${TEMP_DIR}/README.md\"\ncp -f \"${SRCROOT}/../LICENSE\" \"${TEMP_DIR}\"\nmkdir \"${TEMP_DIR}/${HOCKEYSDK_DOCSET_VERSION_NAME}.docset\"\ncp -R \"${SRCROOT}/../documentation/docset/Contents\" \"${TEMP_DIR}/${HOCKEYSDK_DOCSET_VERSION_NAME}.docset\"\n\n# build zip\ncd \"${PRODUCTS_DIR}\"\nrm -f \"${FMK_NAME}-iOS-${VERSION_STRING}.zip\"\nzip -yr \"${FMK_NAME}-iOS-${VERSION_STRING}.zip\" \"${ZIP_FOLDER}\" -x \\*/.*\n"; }; 1E8E66B215BC3D8200632A2E /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -880,6 +898,7 @@ 1E49A4451612223B00463151 /* BITFeedbackListViewCell.m in Sources */, 1E49A44B1612223B00463151 /* BITFeedbackListViewController.m in Sources */, 1E49A4511612223B00463151 /* BITFeedbackManager.m in Sources */, + 1ECA8F52192B6954006B9416 /* BITCrashMetaData.m in Sources */, E4933E8117B66CDA00B11ACC /* BITHTTPOperation.m in Sources */, 1E49A45A1612223B00463151 /* BITFeedbackMessage.m in Sources */, 1ED570C818BF878C00AB3350 /* BITCrashAttachment.m in Sources */, @@ -893,6 +912,7 @@ 1E49A4C1161222B900463151 /* BITHockeyHelper.m in Sources */, 1E49A4C7161222B900463151 /* BITAppStoreHeader.m in Sources */, 1E49A4CD161222B900463151 /* BITStoreButton.m in Sources */, + 1E90FD7418EDB86400CF0417 /* BITCrashDetails.m in Sources */, 1E49A4D3161222B900463151 /* BITWebTableViewCell.m in Sources */, E48A3DED17B3ED1C00924C3D /* BITAuthenticator.m in Sources */, 1E49A4DB161222D400463151 /* HockeySDKPrivate.m in Sources */, diff --git a/Support/HockeySDKTests/BITCrashManagerTests.m b/Support/HockeySDKTests/BITCrashManagerTests.m index 099b608e1d..e276813017 100644 --- a/Support/HockeySDKTests/BITCrashManagerTests.m +++ b/Support/HockeySDKTests/BITCrashManagerTests.m @@ -23,6 +23,7 @@ #import "BITTestHelper.h" +#define kBITCrashMetaAttachment @"BITCrashMetaAttachment" @interface BITCrashManagerTests : SenTestCase @@ -71,7 +72,6 @@ [self startManager]; } - #pragma mark - Setup Tests - (void)testThatItInstantiates { @@ -81,6 +81,35 @@ #pragma mark - Persistence tests +- (void)testPersistUserProvidedMetaData { + NSString *tempCrashName = @"tempCrash"; + [_sut setLastCrashFilename:tempCrashName]; + + BITCrashMetaData *metaData = [BITCrashMetaData new]; + [metaData setUserDescription:@"Test string"]; + [_sut persistUserProvidedMetaData:metaData]; + + NSError *error; + NSString *description = [NSString stringWithContentsOfFile:[NSString stringWithFormat:@"%@.desc", [[_sut crashesDir] stringByAppendingPathComponent: tempCrashName]] encoding:NSUTF8StringEncoding error:&error]; + assertThat(description, equalTo(@"Test string")); +} + +- (void)testPersistAttachment { + NSString *filename = @"TestAttachment"; + NSData *data = [[NSData alloc] initWithBase64Encoding:@"TestData"]; + NSString* type = @"text/plain"; + + BITCrashAttachment *originalAttachment = [[BITCrashAttachment alloc] initWithFilename:filename crashAttachmentData:data contentType:type]; + NSString *attachmentFilename = [[_sut crashesDir] stringByAppendingPathComponent:@"testAttachment"]; + + [_sut persistAttachment:originalAttachment withFilename:attachmentFilename]; + + BITCrashAttachment *decodedAttachment = [_sut attachmentForCrashReport:attachmentFilename]; + + assertThat(decodedAttachment.filename, equalTo(filename)); + assertThat(decodedAttachment.crashAttachmentData, equalTo(data)); + assertThat(decodedAttachment.contentType, equalTo(type)); +} #pragma mark - Helper @@ -123,6 +152,44 @@ [verifyCount(delegateMock, times(1)) userEmailForHockeyManager:hm componentManager:_sut]; } +#pragma mark - Handle User Input + +- (void)testHandleUserInputDontSend { + id delegateMock = mockProtocol(@protocol(BITCrashManagerDelegate)); + _sut.delegate = delegateMock; + + assertThatBool([_sut handleUserInput:BITCrashManagerUserInputDontSend withUserProvidedMetaData:nil], equalToBool(YES)); + + [verify(delegateMock) crashManagerWillCancelSendingCrashReport:_sut]; + +} + +- (void)testHandleUserInputSend { + assertThatBool([_sut handleUserInput:BITCrashManagerUserInputSend withUserProvidedMetaData:nil], equalToBool(YES)); +} + +- (void)testHandleUserInputAlwaysSend { + id delegateMock = mockProtocol(@protocol(BITCrashManagerDelegate)); + _sut.delegate = delegateMock; + NSUserDefaults *mockUserDefaults = mock([NSUserDefaults class]); + + //Test if CrashManagerStatus is unset + [given([mockUserDefaults integerForKey:@"BITCrashManagerStatus"]) willReturn:nil]; + + //Test if method runs through + assertThatBool([_sut handleUserInput:BITCrashManagerUserInputAlwaysSend withUserProvidedMetaData:nil], equalToBool(YES)); + + //Test if correct CrashManagerStatus is now set + [given([mockUserDefaults integerForKey:@"BITCrashManagerStauts"]) willReturnInt:BITCrashManagerStatusAutoSend]; + + //Verify that delegate method has been called + [verify(delegateMock) crashManagerWillSendCrashReportsAlways:_sut]; + +} + +- (void)testHandleUserInputWithInvalidInput { + assertThatBool([_sut handleUserInput:3 withUserProvidedMetaData:nil], equalToBool(NO)); +} #pragma mark - Debugger @@ -208,7 +275,7 @@ assertThatBool([_sut hasNonApprovedCrashReports], equalToBool(YES)); // this is currently sending blindly, needs refactoring to test properly - [_sut sendCrashReports]; + [_sut sendNextCrashReport]; [verifyCount(delegateMock, times(1)) crashManagerWillSendCrashReport:_sut]; [_sut cleanCrashReports];