diff --git a/Backtrace.xcodeproj/project.pbxproj b/Backtrace.xcodeproj/project.pbxproj index d4407ad1..56013da6 100644 --- a/Backtrace.xcodeproj/project.pbxproj +++ b/Backtrace.xcodeproj/project.pbxproj @@ -179,6 +179,12 @@ AFCCCE232625392300B83A28 /* ReportMetadataStorageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCCCE222625392300B83A28 /* ReportMetadataStorageMock.swift */; }; AFCCCE242625392300B83A28 /* ReportMetadataStorageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCCCE222625392300B83A28 /* ReportMetadataStorageMock.swift */; }; AFCCCE252625392300B83A28 /* ReportMetadataStorageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCCCE222625392300B83A28 /* ReportMetadataStorageMock.swift */; }; + B50C5A8028E4740A004BB1DA /* BacktraceCrashLoopDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C5A7F28E4740A004BB1DA /* BacktraceCrashLoopDetectorTests.swift */; }; + B50C5A8128E4740A004BB1DA /* BacktraceCrashLoopDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C5A7F28E4740A004BB1DA /* BacktraceCrashLoopDetectorTests.swift */; }; + B50C5A8228E4740A004BB1DA /* BacktraceCrashLoopDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50C5A7F28E4740A004BB1DA /* BacktraceCrashLoopDetectorTests.swift */; }; + B5E58C6928E1A843001F9650 /* BacktraceCrashLoopDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E58C6828E1A843001F9650 /* BacktraceCrashLoopDetector.swift */; }; + B5E58C6A28E1A843001F9650 /* BacktraceCrashLoopDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E58C6828E1A843001F9650 /* BacktraceCrashLoopDetector.swift */; }; + B5E58C6B28E1A843001F9650 /* BacktraceCrashLoopDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E58C6828E1A843001F9650 /* BacktraceCrashLoopDetector.swift */; }; DAF627C0CA0FE995B581C33B /* Pods_Backtrace_tvOSTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD097A22120C3DCE08382BA5 /* Pods_Backtrace_tvOSTests.framework */; }; F21211A5222348AC000B3692 /* BacktraceCrashReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F21211A4222348AC000B3692 /* BacktraceCrashReporter.swift */; }; F21211A6222348AC000B3692 /* BacktraceCrashReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F21211A4222348AC000B3692 /* BacktraceCrashReporter.swift */; }; @@ -449,6 +455,8 @@ AF7833BA2613D1B400530A10 /* AttachmentsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsStorage.swift; sourceTree = ""; }; AFCCCE222625392300B83A28 /* ReportMetadataStorageMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportMetadataStorageMock.swift; sourceTree = ""; }; AFCCCEC126260BC400B83A28 /* AttachmentBookmarkHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentBookmarkHandler.swift; sourceTree = ""; }; + B50C5A7F28E4740A004BB1DA /* BacktraceCrashLoopDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceCrashLoopDetectorTests.swift; sourceTree = ""; }; + B5E58C6828E1A843001F9650 /* BacktraceCrashLoopDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceCrashLoopDetector.swift; sourceTree = ""; }; B7B445FAC6841A65683F35E9 /* Pods-Backtrace-tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Backtrace-tvOS.debug.xcconfig"; path = "Target Support Files/Pods-Backtrace-tvOS/Pods-Backtrace-tvOS.debug.xcconfig"; sourceTree = ""; }; BECDC44D2F82A1F1FD5CD9D1 /* Pods_Backtrace_macOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Backtrace_macOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BFAF826CD2E1314532AD4FF6 /* Pods_Example_iOS_ObjC.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Example_iOS_ObjC.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -718,6 +726,14 @@ path = Model; sourceTree = ""; }; + B51F3E33290173650096E21A /* CrashLoopDetector */ = { + isa = PBXGroup; + children = ( + B5E58C6828E1A843001F9650 /* BacktraceCrashLoopDetector.swift */, + ); + path = CrashLoopDetector; + sourceTree = ""; + }; E1CB76ADFD3A1D9326B4E46D /* Pods */ = { isa = PBXGroup; children = ( @@ -813,6 +829,7 @@ A24A4B4928B595D8004F5052 /* BacktraceWatcherTests.swift */, A24A4B5528B595D8004F5052 /* CrashReporterTests.swift */, A24A4B5228B595D8004F5052 /* DispatcherTests.swift */, + B50C5A7F28E4740A004BB1DA /* BacktraceCrashLoopDetectorTests.swift */, F21DD3AF2255E99E00404CC3 /* Resources */, F2AB6370224647F000939BC9 /* Helpers */, F2AB636F224647DE00939BC9 /* Mocks */, @@ -932,11 +949,11 @@ F2AFB59622274E1400AAA1D7 /* Public */ = { isa = PBXGroup; children = ( + F22EB87621BBD36800DEE94E /* BacktraceClient.swift */, 6E45A3A6273095E500DB0BAC /* BacktraceMetricsSettings.swift */, F29CD79321FDD5E900216C59 /* BacktraceClientDelegate.swift */, F2AFB59922274E5400AAA1D7 /* BacktraceClientCustomizing.swift */, F240532021C578AA00FC9394 /* BacktraceLogger.swift */, - F22EB87621BBD36800DEE94E /* BacktraceClient.swift */, F25F9E9921EE84EA00236E04 /* BacktraceResult.swift */, 28AC773B21FA5A8400FED661 /* BacktraceDatabaseSettings.swift */, F2D7122021F10C45002D2A26 /* BacktraceClientConfiguration.swift */, @@ -969,6 +986,7 @@ F2AFB5A022274F1000AAA1D7 /* Internal */ = { isa = PBXGroup; children = ( + B51F3E33290173650096E21A /* CrashLoopDetector */, F2A81B4C23EF1730007C63E4 /* BacktraceApiProtocol.swift */, F21D302A224A18D50013B5D7 /* Store.swift */, F2AB636C22442B5100939BC9 /* DebuggerChecker.swift */, @@ -1917,6 +1935,7 @@ AF5AB0BB262622730003698C /* AttachmentBookmarkHandler.swift in Sources */, 28F95BD022526064003936E0 /* BacktraceClient.swift in Sources */, 28F95BCD2252605A003936E0 /* BacktraceClientDelegate.swift in Sources */, + B5E58C6B28E1A843001F9650 /* BacktraceCrashLoopDetector.swift in Sources */, 28F95BC92252602C003936E0 /* Foundation+Extensions.swift in Sources */, 28F95BD622526078003936E0 /* DebuggerChecker.swift in Sources */, 28A65308285D1BF700306631 /* Date+Extensions.swift in Sources */, @@ -1981,6 +2000,7 @@ A24A4B7728B595D9004F5052 /* DispatcherTests.swift in Sources */, A24A4B6828B595D9004F5052 /* BacktraceOomWatcherTests.swift in Sources */, F21DD39F2255666F00404CC3 /* WatcherRepositoryMock.swift in Sources */, + B50C5A8228E4740A004BB1DA /* BacktraceCrashLoopDetectorTests.swift in Sources */, A24A4B5C28B595D9004F5052 /* BacktraceWatcherTests.swift in Sources */, A24A4B7A28B595D9004F5052 /* AttributesTests.swift in Sources */, A24A4B6E28B595D9004F5052 /* AttachmentTests.swift in Sources */, @@ -2021,6 +2041,7 @@ F2AFB59E22274EDA00AAA1D7 /* Dispatching.swift in Sources */, 2846E1F9222F1DE60035F98C /* NetworkReachability.swift in Sources */, F21211A9222348C2000B3692 /* SignalContext.swift in Sources */, + B5E58C6A28E1A843001F9650 /* BacktraceCrashLoopDetector.swift in Sources */, F2AB639D22479A3600939BC9 /* Model.xcdatamodeld in Sources */, F259E4E3222AD9F100F282C7 /* AttributesProvider.swift in Sources */, F266B83321C77B9600D14417 /* BacktraceClient.swift in Sources */, @@ -2075,6 +2096,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B50C5A8128E4740A004BB1DA /* BacktraceCrashLoopDetectorTests.swift in Sources */, A24A4B7C28B595D9004F5052 /* AttachmentStorageTests.swift in Sources */, F2AB637F22464FD500939BC9 /* DebuggerCheckerMock.swift in Sources */, A24A4B7328B595D9004F5052 /* BacktraceApiTests.swift in Sources */, @@ -2150,6 +2172,7 @@ 6E896E912727627C0005CDF2 /* BacktraceMetrics.swift in Sources */, 6EB713EC275ED4EF0075D1C1 /* SummedEventsPayload.swift in Sources */, 28A652F2285C6C1500306631 /* BacktraceBreadcrumbsLogManager.swift in Sources */, + B5E58C6928E1A843001F9650 /* BacktraceCrashLoopDetector.swift in Sources */, F28F164621E28441008E4B96 /* BacktraceReporter.swift in Sources */, F21211A5222348AC000B3692 /* BacktraceCrashReporter.swift in Sources */, 0B6B4CFD25CD8331002DA15C /* BacktraceOomWatcher.swift in Sources */, @@ -2185,6 +2208,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B50C5A8028E4740A004BB1DA /* BacktraceCrashLoopDetectorTests.swift in Sources */, A24A4B7B28B595D9004F5052 /* AttachmentStorageTests.swift in Sources */, F2AB637E22464FD500939BC9 /* DebuggerCheckerMock.swift in Sources */, A24A4B7228B595D9004F5052 /* BacktraceApiTests.swift in Sources */, @@ -2381,7 +2405,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.7.4-beta2; + MARKETING_VERSION = "1.7.4-beta2"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -2460,7 +2484,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.7.4-beta2; + MARKETING_VERSION = "1.7.4-beta2"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = Backtrace.io.Backtrace; @@ -2692,7 +2716,7 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.7.4-beta2; + MARKETING_VERSION = "1.7.4-beta2"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -2774,7 +2798,7 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.7.4-beta2; + MARKETING_VERSION = "1.7.4-beta2"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = Backtrace.io.Backtrace; @@ -2979,6 +3003,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -3002,6 +3027,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -3058,6 +3084,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; ENABLE_NS_ASSERTIONS = NO; @@ -3075,6 +3102,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 1; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = apptailors.co.backtrace.swift.tvos.example; @@ -3185,7 +3213,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.7.4-beta2; + MARKETING_VERSION = "1.7.4-beta2"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -3269,7 +3297,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.7.4-beta2; + MARKETING_VERSION = "1.7.4-beta2"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = Backtrace.io.Backtrace; @@ -3483,6 +3511,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = LZGFT5UUA9; ENABLE_BITCODE = NO; @@ -3507,6 +3536,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -3562,6 +3592,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = LZGFT5UUA9; ENABLE_BITCODE = NO; @@ -3580,6 +3611,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 1; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = apptailors.co.backtrace.swift.ios.example; diff --git a/Examples/Example-iOS-ObjC/AppDelegate.m b/Examples/Example-iOS-ObjC/AppDelegate.m index 476ac91c..763d68bf 100644 --- a/Examples/Example-iOS-ObjC/AppDelegate.m +++ b/Examples/Example-iOS-ObjC/AppDelegate.m @@ -10,6 +10,21 @@ @interface AppDelegate () @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + + + /* Enable crash loop detector. + You can pass crashes count threshold (maximum amount of launching events to evaluate) here. + If threshold is not specified or you pass 0 - default value '5' will be used. + */ + [BacktraceClient enableCrashLoopDetection: 0]; + + if([BacktraceClient isSafeModeRequired]) { + // When crash loop is detected we need to reset crash loop counter to restart crash loop detection from scratch + [BacktraceClient resetCrashLoopDetection]; + // TODO: Perform any custom checks if necessary and decide if Backtrace should be launched + return NO; + } + NSArray *paths = @[[[NSBundle mainBundle] pathForResource: @"test" ofType: @"txt"]]; NSString *fileName = @"myCustomFile.txt"; NSURL *libraryUrl = [[[NSFileManager defaultManager] URLsForDirectory:NSLibraryDirectory @@ -32,18 +47,6 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( BacktraceClient.shared.attributes = @{@"foo": @"bar", @"testing": @YES}; BacktraceClient.shared.attachments = [NSArray arrayWithObjects:fileUrl, nil]; - // sending NSException - @try { - NSArray *array = @[]; - array[1]; // will throw exception - } @catch (NSException *exception) { - [[BacktraceClient shared] sendWithAttachmentPaths: [NSArray init] completion: ^(BacktraceResult * _Nonnull result) { - NSLog(@"%@", result); - }]; - } @finally { - - } - //sending NSError [[BacktraceClient shared] sendWithAttachmentPaths: paths completion: ^(BacktraceResult * _Nonnull result) { NSLog(@"%@", result); diff --git a/Examples/Example-iOS-ObjC/ViewController.m b/Examples/Example-iOS-ObjC/ViewController.m index 798c7961..616a1403 100644 --- a/Examples/Example-iOS-ObjC/ViewController.m +++ b/Examples/Example-iOS-ObjC/ViewController.m @@ -13,6 +13,11 @@ @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; wastedMemory = [[NSMutableData alloc] init]; + + NSString * text = [NSString stringWithFormat: @"BadEvents: %ld\nIs Safe to Launch: %s", + [BacktraceClient consecutiveCrashesCount], + [BacktraceClient isInSafeMode] ? "FALSE" : "TRUE" ]; + [_textView setText: text]; } - (IBAction) outOfMemoryReportAction: (id) sender { diff --git a/Examples/Example-iOS/AppDelegate.swift b/Examples/Example-iOS/AppDelegate.swift index 5290237f..d1591a8d 100644 --- a/Examples/Example-iOS/AppDelegate.swift +++ b/Examples/Example-iOS/AppDelegate.swift @@ -18,6 +18,20 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + /* Enable crash loop detector. + You can pass crashes count threshold (maximum amount of launching events to evaluate) here. + If threshold is not specified or you pass 0 - default value '5' will be used. + */ + BacktraceClient.enableCrashLoopDetection() + + if BacktraceClient.isSafeModeRequired() { + // When crash loop is detected we need to reset crash loop counter to restart crash loop detection from scratch + BacktraceClient.resetCrashLoopDetection() + // Perform any custom checks if necessary and decide if Backtrace should be launched + return true + } + let backtraceCredentials = BacktraceCredentials(endpoint: URL(string: Keys.backtraceUrl as String)!, token: Keys.backtraceToken as String) diff --git a/Examples/Example-iOS/ViewController.swift b/Examples/Example-iOS/ViewController.swift index 2b045ac4..f309099e 100644 --- a/Examples/Example-iOS/ViewController.swift +++ b/Examples/Example-iOS/ViewController.swift @@ -10,6 +10,8 @@ class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + textView.text = "BadEvents: " + BacktraceClient.consecutiveCrashesCount().description + + "\nIs Safe to Launch: " + (BacktraceClient.isInSafeMode() ? "FALSE" : "TRUE") } @IBAction func outOfMemoryReportAction(_ sender: Any) { diff --git a/Examples/Example-macOS-ObjC/AppDelegate.m b/Examples/Example-macOS-ObjC/AppDelegate.m index 4e968297..fc9bbdf2 100644 --- a/Examples/Example-macOS-ObjC/AppDelegate.m +++ b/Examples/Example-macOS-ObjC/AppDelegate.m @@ -10,6 +10,16 @@ @implementation AppDelegate - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + // Enable crash loop detector, pass 0 to use default threshold value '5' + [BacktraceClient enableCrashLoopDetection: 0]; + + if([BacktraceClient isSafeModeRequired]) { + // When crash loop is detected we need to reset crash loop counter to restart crash loop detection from scratch + [BacktraceClient resetCrashLoopDetection]; + // TODO: Perform any custom checks if necessary and decide if Backtrace should be launched + return; + } + BacktraceCredentials *credentials = [[BacktraceCredentials alloc] initWithSubmissionUrl: [NSURL URLWithString: Keys.backtraceSubmissionUrl]]; BacktraceDatabaseSettings *backtraceDatabaseSettings = [[BacktraceDatabaseSettings alloc] init]; diff --git a/Examples/Example-macOS-ObjC/Base.lproj/Main.storyboard b/Examples/Example-macOS-ObjC/Base.lproj/Main.storyboard index bb29672b..c4cf964e 100644 --- a/Examples/Example-macOS-ObjC/Base.lproj/Main.storyboard +++ b/Examples/Example-macOS-ObjC/Base.lproj/Main.storyboard @@ -1,7 +1,8 @@ - + - + + @@ -619,7 +620,7 @@ - + @@ -710,16 +711,16 @@ @@ -739,6 +740,10 @@ + diff --git a/Examples/Example-macOS-ObjC/ViewController.m b/Examples/Example-macOS-ObjC/ViewController.m index 465062a1..c585a5ef 100644 --- a/Examples/Example-macOS-ObjC/ViewController.m +++ b/Examples/Example-macOS-ObjC/ViewController.m @@ -9,30 +9,40 @@ @interface ViewController() @implementation ViewController -- (void)viewDidLoad { +- (void) viewDidLoad { [super viewDidLoad]; + SEL selector = NSSelectorFromString(@"updateUI"); + [self performSelector: selector withObject: nil afterDelay: 0.5]; + // Do any additional setup after loading the view. } -- (IBAction)crashAction:(id)sender { - NSArray *array = @[]; - (void)array[1]; + +- (void) updateUI { + NSString * text = [NSString stringWithFormat: @"BadEvents: %ld\nIs Safe to Launch: %@", + [BacktraceClient consecutiveCrashesCount], + [BacktraceClient isInSafeMode] ? @"FALSE" : @"TRUE" ]; + NSLog(@"updateUI: text = %@", text); + [_textView setString: text]; } -- (IBAction)liveReportAction:(id)sender { +- (IBAction) crashAction:(id)sender { + // NOTE: crashing with array out of bounds case doesn't terminate app on some OS versions, so using runtime crash to be sure signal is received. + NSString * string = [NSString stringWithFormat: @"%@", 12]; +} + +- (IBAction) liveReportAction:(id)sender { } -- (IBAction)liveReportButtonAction:(id)sender { +- (IBAction) liveReportButtonAction:(id)sender { NSArray *array = @[]; (void)array[1]; } -- (void)setRepresentedObject:(id)representedObject { +- (void) setRepresentedObject:(id)representedObject { [super setRepresentedObject:representedObject]; - // Update the view, if already loaded. } - @end diff --git a/Examples/Example-tvOS/AppDelegate.swift b/Examples/Example-tvOS/AppDelegate.swift index ca063273..216dffe7 100644 --- a/Examples/Example-tvOS/AppDelegate.swift +++ b/Examples/Example-tvOS/AppDelegate.swift @@ -16,6 +16,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + /* Enable crash loop detector. + You can pass crashes count threshold (maximum amount of launching events to evaluate) here. + If threshold is not specified or you pass 0 - default value '5' will be used. + */ + BacktraceClient.enableCrashLoopDetection() + + if BacktraceClient.isSafeModeRequired() { + // When crash loop is detected we need to reset crash loop counter to restart crash loop detection from scratch + BacktraceClient.resetCrashLoopDetection() + // TODO: Perform any custom checks if necessary and decide if Backtrace should be launched + return true + } + let backtraceCredentials = BacktraceCredentials(endpoint: URL(string: Keys.backtraceUrl as String)!, token: Keys.backtraceToken as String) let backtraceDatabaseSettings = BacktraceDatabaseSettings() diff --git a/Examples/Example-tvOS/Base.lproj/Main.storyboard b/Examples/Example-tvOS/Base.lproj/Main.storyboard index d553a2ce..4fe0f405 100644 --- a/Examples/Example-tvOS/Base.lproj/Main.storyboard +++ b/Examples/Example-tvOS/Base.lproj/Main.storyboard @@ -5,6 +5,7 @@ + @@ -20,14 +21,12 @@ - + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + - + - + + + - - + + + + + + + + diff --git a/Examples/Example-tvOS/ViewController.swift b/Examples/Example-tvOS/ViewController.swift index f8ee3d7f..fa28f687 100644 --- a/Examples/Example-tvOS/ViewController.swift +++ b/Examples/Example-tvOS/ViewController.swift @@ -3,9 +3,13 @@ import Backtrace class ViewController: UIViewController { + @IBOutlet weak var textView: UITextView! + override func viewDidLoad() { super.viewDidLoad() - // Do any additional setup after loading the view. + + textView.text = "BadEvents: " + BacktraceClient.consecutiveCrashesCount().description + + "\nIs Safe to Launch: " + (BacktraceClient.isInSafeMode() ? "FALSE" : "TRUE") } @IBAction func liveReportAction(_ sender: Any) { diff --git a/Sources/Features/Attributes/DefaultAttributes.swift b/Sources/Features/Attributes/DefaultAttributes.swift index e7fcffae..7175d947 100644 --- a/Sources/Features/Attributes/DefaultAttributes.swift +++ b/Sources/Features/Attributes/DefaultAttributes.swift @@ -201,8 +201,8 @@ struct LibInfo: AttributesSource { private static let applicationLangName = "backtrace-cocoa" var backtraceVersion: String? { - if let bundle = Bundle(identifier: "Backtrace.io.Backtrace"), - let build = bundle.infoDictionary?["CFBundleShortVersionString"] { + let bundle = Bundle(for: BacktraceClient.self) + if let build = bundle.infoDictionary?["CFBundleShortVersionString"] { return build as? String } return nil diff --git a/Sources/Features/Error/BacktraceError.swift b/Sources/Features/Error/BacktraceError.swift index 74368361..9e801af2 100644 --- a/Sources/Features/Error/BacktraceError.swift +++ b/Sources/Features/Error/BacktraceError.swift @@ -35,6 +35,10 @@ enum FileError: BacktraceError { case invalidPropertyList } +enum CrashLoopError: BacktraceError { + case crashLoopDetected +} + enum CodingError: BacktraceError { case encodingFailed } diff --git a/Sources/Public/BacktraceClient.swift b/Sources/Public/BacktraceClient.swift index 12b2fcbf..46a53e9d 100644 --- a/Sources/Public/BacktraceClient.swift +++ b/Sources/Public/BacktraceClient.swift @@ -3,6 +3,11 @@ import Foundation /// Provides the default implementation of `BacktraceClientProtocol` protocol. @objc open class BacktraceClient: NSObject { + enum WorkingMode { + case normal + case safe + } + /// Shared instance of BacktraceClient class. Should be created before sending any reports. @objc public static var shared: BacktraceClientProtocol? @@ -17,6 +22,8 @@ import Foundation @objc private let breadcrumbsInstance: BacktraceBreadcrumbs = BacktraceBreadcrumbs() #endif + private static var workingMode = WorkingMode.normal + private let reporter: BacktraceReporter private let dispatcher: Dispatching private let reportingPolicy: ReportingPolicy @@ -85,10 +92,61 @@ import Foundation self.metricsInstance = BacktraceMetrics(api: api) super.init() + try startCrashReporter() } } +// MARK: - BacktraceClient Safe Mode public API (crash loop detection) +extension BacktraceClient { + + @objc public static func enableSafeMode() { + workingMode = .safe + + // Do any additional setup here - f.e. turn off reporting etc + } + + @objc public static func disableSafeMode() { + workingMode = .normal + + // Do any additional setup here - f.e. turn on reporting etc + } + + @objc public static func isInSafeMode() -> Bool { + return workingMode == .safe + } + + @objc public static func enableCrashLoopDetection(_ threshold: Int = 0) { + BacktraceCrashLoopDetector.instance.updateThreshold(threshold) + + let isInCrashLoop = BacktraceCrashLoopDetector.instance.detectCrashloop() + + if isInCrashLoop { + enableSafeMode() + } + else { + disableSafeMode() + } + } + + @objc public static func resetCrashLoopDetection() { + BacktraceCrashLoopDetector.instance.clearStartupEvents() + } + + @objc public static func isSafeModeRequired() -> Bool { + return workingMode == .safe + } + + @objc public static func consecutiveCrashesCount() -> Int { + return BacktraceCrashLoopDetector.instance.consecutiveCrashesCount + } + + // Added for testing without debugging purposes + @objc public static func crashLoopEventsDatabase() -> String { + return BacktraceCrashLoopDetector.instance.databaseDescription() + } +} + // MARK: - BacktraceClientProviding extension BacktraceClient: BacktraceClientCustomizing { diff --git a/Sources/Public/BacktraceCrashReporter.swift b/Sources/Public/BacktraceCrashReporter.swift index 5e04d504..6e30a201 100644 --- a/Sources/Public/BacktraceCrashReporter.swift +++ b/Sources/Public/BacktraceCrashReporter.swift @@ -28,6 +28,7 @@ extension BacktraceCrashReporter: CrashReporting { _ uContext: UnsafeMutablePointer?, _ context: UnsafeMutableRawPointer?) -> Void = { signalInfoPointer, _, context in BacktraceOomWatcher.clean() + guard let attributesProvider = context?.assumingMemoryBound(to: SignalContext.self).pointee, let signalInfo = signalInfoPointer?.pointee else { return diff --git a/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift new file mode 100644 index 00000000..e8538ce8 --- /dev/null +++ b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift @@ -0,0 +1,206 @@ +// +// BacktraceCrashLoopDetector.swift +// Backtrace +// + +import Foundation + +@objc internal class BacktraceCrashLoopDetector: NSObject { + + internal struct StartUpEvent: Codable { + var uuid: String + var eventTimestamp: Double + var reportCreationTimestamp: Double + + func description() -> String { + let string = """ + New Crash Loop Event: + UUID: \(uuid) + Event Timestamp: \(eventTimestamp) + Report Creation Timestamp: \(reportCreationTimestamp)\n + """ + return string + } + } + + internal static let instance = BacktraceCrashLoopDetector() + + @objc private static let plistKey = "CrashLoopDetectorData" + @objc internal static let consecutiveCrashesThreshold = 5 + @objc private(set) var consecutiveCrashesCount = 0 + + @objc private var threshold = 0 + + internal var startupEvents: [StartUpEvent] = [] + + override private init() { + } + + @objc internal func updateThreshold(_ threshold: Int) { + self.threshold = threshold == 0 ? BacktraceCrashLoopDetector.consecutiveCrashesThreshold : threshold + } + + @objc internal func detectCrashloop() -> Bool { + + CLDLogDebug("Starting Crash Loop Detection") + + loadEvents() + addEvent() + + consecutiveCrashesCount = consecutiveEventsCount() + + let result = consecutiveCrashesCount >= BacktraceCrashLoopDetector.consecutiveCrashesThreshold + CLDLogDebug("Finishing Crash Loop Detection: Is in the crash loop - \(result)") + return result + } + + @objc private func loadEvents() { + + // Cleanup old events - f.e. for multiple usages of detector + startupEvents.removeAll() + + /* + - Since detector's DB is relatively small, UserDefaults are a good option here, + plus they allow to avoid a headache with reading/writing to/from the custom file. + + - But we should consider shared computers as well - comment from UserDefaults docs: + With the exception of managed devices in educational institutions, + a user’s defaults are stored locally on a single device, + and persisted for backup and restore. + To synchronize preferences and other data across a user’s connected devices, + use NSUbiquitousKeyValueStore instead. + */ + guard let data = UserDefaults.standard.object(forKey: BacktraceCrashLoopDetector.plistKey) as? Data + else { return } + + guard let array = try? PropertyListDecoder().decode([StartUpEvent].self, from: data) + else { return } + + startupEvents.append(contentsOf: array) + CLDLogDebug("Events Loaded: \(startupEvents.count)") + } + + @objc private func saveEvents() { + let data = try? PropertyListEncoder().encode(startupEvents) + UserDefaults.standard.set(data, forKey: BacktraceCrashLoopDetector.plistKey) + CLDLogDebug("Events Saved: \(startupEvents.count)") + } + + @objc private func addEvent() { + + let reportTime = reportFileCreationTime() + + let event = StartUpEvent(uuid: UUID().uuidString, + eventTimestamp: Double(Date.timeIntervalSinceReferenceDate), + reportCreationTimestamp: reportTime) + + CLDLogDebug(event.description()) + + startupEvents.insert(event, at: 0) + + CLDLogDebug("Startup Event Added, Total Events => \(startupEvents.count)") + + saveEvents() + } + + @objc internal func clearStartupEvents() { + startupEvents.removeAll() + saveEvents() + CLDLogDebug("Startup Events Cleared: \(startupEvents.count)") + } + + @objc internal func consecutiveEventsCount() -> Int { + + var count = 0 + var previousTime = 0.0 + for event in startupEvents { + if event.reportCreationTimestamp == 0 || event.reportCreationTimestamp == previousTime { + break + } + + if previousTime == 0 || event.reportCreationTimestamp < previousTime { + count += 1 + } + + previousTime = event.reportCreationTimestamp + } + CLDLogDebug("Consecutive Events Count: \(count)") + return count + } + + @objc internal func databaseDescription() -> String { + var string = "" + for event in startupEvents { + string += event.description() + "\n" + } + return string.isEmpty ? "No events" : string + } +} + +// MARK: Deprecated methods +extension BacktraceCrashLoopDetector { + + @objc private func reportFilePath() -> String { + + /* Crash Loop Detector considers all other Backtrace modules as potentially dangerous. + Thats why it formats path to PLCrashReporter's report file itself, + for not to use PLCrashReporter's APIs at all + */ + + let bundleIDBT = Bundle.main.bundleIdentifier ?? "" + let appIDPath = bundleIDBT.replacingOccurrences(of: "/", with: "_") + + let paths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true) + let cacheDir = URL(fileURLWithPath: paths.isEmpty ? "" : paths[0]) + + let bundleIDPLCR = "com.plausiblelabs.crashreporter.data" + let crashReportDir = cacheDir.appendingPathComponent(bundleIDPLCR) + .appendingPathComponent(appIDPath) + + let reportName = "live_report.plcrash" + let reportFullPath = crashReportDir.appendingPathComponent(reportName) + .absoluteString + .replacingOccurrences(of: "file://", with: "") + CLDLogDebug("reportFullPath: \(reportFullPath)") + + return reportFullPath + } + + @objc private func reportFileCreationTime() -> Double { + let attributes = try? FileManager.default.attributesOfItem(atPath: reportFilePath()) + let date = attributes?[.creationDate] as? Date + CLDLogDebug("Report creation date: \(String(describing: date))") + let timeInterval = date?.timeIntervalSinceReferenceDate ?? 0 + CLDLogDebug("Time Interval \(timeInterval)") + return timeInterval + } + + @available(*, deprecated, message: "Temporarily not needed") + @objc internal func hasCrashReport() -> Bool { + let exists = FileManager.default.fileExists(atPath: reportFilePath()) + return exists + } + + @available(*, deprecated, message: "Temporarily not needed") + @objc internal func deleteCrashReport() { + let path = reportFilePath() + let fileURL = URL(fileURLWithPath: path) + try? FileManager.default.removeItem(at: fileURL) + saveEvents() + } +} + + +internal func CLDLogDebug(_ message: String = "") { + + /* Routed logging here to add prefix for more convenient filtering of + BTCLD logs in Xcode's outputs + */ + let prefix = "BT CLD: " + + /* Since Backtrace is not enabled during Crash Loop detection, + BacktraceLogger is also not set up, so it doesn't log messages + => using native 'print' here + */ + print(prefix + message) +} diff --git a/Tests/BacktraceCrashLoopDetectorTests.swift b/Tests/BacktraceCrashLoopDetectorTests.swift new file mode 100644 index 00000000..b99584ba --- /dev/null +++ b/Tests/BacktraceCrashLoopDetectorTests.swift @@ -0,0 +1,51 @@ +import XCTest + +import Nimble +import Quick +@testable import Backtrace + +final class BacktraceCrashLoopDetectorTests: QuickSpec { + + override func spec() { + describe("Crash Loop Detector") { + + context("No Crash Loop Case") { + + let crashLoopDetector = BacktraceCrashLoopDetector() + let eventsCount = BacktraceCrashLoopDetector.consecutiveCrashesThreshold + let timeIntervalStep = 200 + + for index in 0 ..< eventsCount { + let timestamp = Date.timeIntervalSinceReferenceDate - Double(timeIntervalStep * (eventsCount - index)) + let mockEvent = BacktraceCrashLoopDetector.StartUpEvent(timestamp: timestamp, isSuccessful: .random()) + crashLoopDetector.startupEvents.append(mockEvent) + } + crashLoopDetector.saveEvents() + + let isCrashLoop = crashLoopDetector.detectCrashloop() + it("checks if no crash loop detected") { + expect { isCrashLoop }.to(beFalse()) + } + } + + context("Crash Loop Case") { + + let crashLoopDetector = BacktraceCrashLoopDetector() + let eventsCount = BacktraceCrashLoopDetector.consecutiveCrashesThreshold + let timeIntervalStep = 200 + + for index in 0 ..< eventsCount { + let timestamp = Date.timeIntervalSinceReferenceDate - Double(timeIntervalStep * (eventsCount - index)) + let mockEvent = BacktraceCrashLoopDetector.StartUpEvent(timestamp: timestamp, isSuccessful: false) + crashLoopDetector.startupEvents.append(mockEvent) + } + crashLoopDetector.saveEvents() + + let isCrashLoop = crashLoopDetector.detectCrashloop() + it("checks if crash loop detected") { + expect { isCrashLoop }.to(beTrue()) + } + } + } + } +}