Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/crashloop #100

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 39 additions & 7 deletions Backtrace.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -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 = "<group>"; };
AFCCCE222625392300B83A28 /* ReportMetadataStorageMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportMetadataStorageMock.swift; sourceTree = "<group>"; };
AFCCCEC126260BC400B83A28 /* AttachmentBookmarkHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentBookmarkHandler.swift; sourceTree = "<group>"; };
B50C5A7F28E4740A004BB1DA /* BacktraceCrashLoopDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceCrashLoopDetectorTests.swift; sourceTree = "<group>"; };
B5E58C6828E1A843001F9650 /* BacktraceCrashLoopDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceCrashLoopDetector.swift; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>";
};
B51F3E33290173650096E21A /* CrashLoopDetector */ = {
isa = PBXGroup;
children = (
B5E58C6828E1A843001F9650 /* BacktraceCrashLoopDetector.swift */,
);
path = CrashLoopDetector;
sourceTree = "<group>";
};
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;
27 changes: 15 additions & 12 deletions Examples/Example-iOS-ObjC/AppDelegate.m
Original file line number Diff line number Diff line change
@@ -10,6 +10,21 @@ @interface AppDelegate () <BacktraceClientDelegate>
@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {


/* Enable crash loop detector.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't be better if we describe the story what someone want to use it?

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);
5 changes: 5 additions & 0 deletions Examples/Example-iOS-ObjC/ViewController.m
Original file line number Diff line number Diff line change
@@ -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 {
14 changes: 14 additions & 0 deletions Examples/Example-iOS/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -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)

2 changes: 2 additions & 0 deletions Examples/Example-iOS/ViewController.swift
Original file line number Diff line number Diff line change
@@ -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) {
10 changes: 10 additions & 0 deletions Examples/Example-macOS-ObjC/AppDelegate.m
Original file line number Diff line number Diff line change
@@ -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];
17 changes: 11 additions & 6 deletions Examples/Example-macOS-ObjC/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -619,7 +620,7 @@
<menuItem title="Show Sidebar" keyEquivalent="s" id="kIP-vf-haE">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleSourceList:" target="Ady-hI-5gd" id="iwa-gc-5KM"/>
<action selector="toggleSidebar:" target="Ady-hI-5gd" id="iwa-gc-5KM"/>
</connections>
</menuItem>
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
@@ -710,16 +711,16 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Snf-7p-7db">
<rect key="frame" x="4" y="3" width="472" height="61"/>
<rect key="frame" x="3" y="3" width="474" height="62"/>
<constraints>
<constraint firstAttribute="height" constant="50" id="wS4-BS-4BZ"/>
</constraints>
<buttonCell key="cell" type="push" title="Button" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="vpD-Xg-4cH">
<buttonCell key="cell" type="push" title="Crash Me" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="vpD-Xg-4cH">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="liveReportButtonAction:" target="XfG-lQ-9wD" id="r4X-yE-pnP"/>
<action selector="crashAction:" target="XfG-lQ-9wD" id="OfJ-Ce-bpL"/>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't liveReportButton stay here?

</connections>
</button>
<scrollView borderType="none" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" translatesAutoresizingMaskIntoConstraints="NO" id="FmX-dS-tKd">
@@ -739,6 +740,10 @@
</textView>
</subviews>
</clipView>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="KrE-Pr-E3e">
<rect key="frame" x="-100" y="-100" width="460" height="16"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="7RO-Uj-aNq">
<rect key="frame" x="444" y="0.0" width="16" height="190"/>
<autoresizingMask key="autoresizingMask"/>
28 changes: 19 additions & 9 deletions Examples/Example-macOS-ObjC/ViewController.m
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions Examples/Example-tvOS/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -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()
47 changes: 35 additions & 12 deletions Examples/Example-tvOS/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
<deployment identifier="tvOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -20,45 +21,67 @@
<rect key="frame" x="0.0" y="0.0" width="1920" height="1080"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Hallo Backtrace!" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="veV-HX-OPH">
<rect key="frame" x="823" y="517" width="274" height="46"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="gPz-1q-PTQ">
<rect key="frame" x="868" y="618" width="184" height="86"/>
<rect key="frame" x="868" y="695" width="184" height="86"/>
<constraints>
<constraint firstAttribute="height" constant="86" id="0DY-Rq-FLW"/>
<constraint firstAttribute="width" constant="184" id="Od8-Qg-Ier"/>
</constraints>
<inset key="contentEdgeInsets" minX="40" minY="20" maxX="40" maxY="20"/>
<state key="normal" title="crash!"/>
<connections>
<action selector="crashButtonTapped:" destination="BYZ-38-t0r" eventType="primaryActionTriggered" id="ZM4-I6-LK8"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="h51-nh-gL6">
<rect key="frame" x="868" y="752" width="184" height="86"/>
<rect key="frame" x="868" y="829" width="184" height="86"/>
<constraints>
<constraint firstAttribute="width" constant="184" id="hQ2-j5-ayT"/>
<constraint firstAttribute="height" constant="86" id="oBz-2G-eVQ"/>
</constraints>
<inset key="contentEdgeInsets" minX="40" minY="20" maxX="40" maxY="20"/>
<state key="normal" title="report"/>
<connections>
<action selector="liveReportAction:" destination="BYZ-38-t0r" eventType="primaryActionTriggered" id="uIJ-lS-CdJ"/>
</connections>
</button>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="e7h-zs-C3T">
<rect key="frame" x="722" y="440" width="476" height="200"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" constant="476" id="MaH-Mu-WWH"/>
<constraint firstAttribute="height" constant="200" id="wW3-N9-Ufi"/>
</constraints>
<string key="text">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.</string>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<viewLayoutGuide key="safeArea" id="wu6-TO-1qx"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="veV-HX-OPH" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="17k-zz-Pug"/>
<constraint firstItem="gPz-1q-PTQ" firstAttribute="top" secondItem="e7h-zs-C3T" secondAttribute="bottom" constant="55" id="9E5-wo-PsD"/>
<constraint firstItem="h51-nh-gL6" firstAttribute="leading" secondItem="gPz-1q-PTQ" secondAttribute="leading" id="Afd-hR-gvj"/>
<constraint firstItem="gPz-1q-PTQ" firstAttribute="centerX" secondItem="veV-HX-OPH" secondAttribute="centerX" id="QGb-qr-5bA"/>
<constraint firstItem="e7h-zs-C3T" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="Axg-Ip-JAr"/>
<constraint firstItem="gPz-1q-PTQ" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="BcV-Hc-3Yq"/>
<constraint firstItem="h51-nh-gL6" firstAttribute="trailing" secondItem="gPz-1q-PTQ" secondAttribute="trailing" id="VVl-st-9zp"/>
<constraint firstItem="e7h-zs-C3T" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="WCL-e3-T5V"/>
<constraint firstItem="h51-nh-gL6" firstAttribute="top" secondItem="gPz-1q-PTQ" secondAttribute="bottom" constant="48" id="fe1-xN-kS8"/>
<constraint firstItem="veV-HX-OPH" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="hk3-YU-TgW"/>
<constraint firstItem="gPz-1q-PTQ" firstAttribute="top" secondItem="veV-HX-OPH" secondAttribute="bottom" constant="55" id="m11-Tg-aLY"/>
</constraints>
</view>
<connections>
<outlet property="textView" destination="e7h-zs-C3T" id="wAx-Ba-LQ8"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="69" y="69"/>
</scene>
</scenes>
<resources>
<systemColor name="labelColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
6 changes: 5 additions & 1 deletion Examples/Example-tvOS/ViewController.swift
Original file line number Diff line number Diff line change
@@ -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) {
4 changes: 2 additions & 2 deletions Sources/Features/Attributes/DefaultAttributes.swift
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions Sources/Features/Error/BacktraceError.swift
Original file line number Diff line number Diff line change
@@ -35,6 +35,10 @@ enum FileError: BacktraceError {
case invalidPropertyList
}

enum CrashLoopError: BacktraceError {
case crashLoopDetected
}

enum CodingError: BacktraceError {
case encodingFailed
}
58 changes: 58 additions & 0 deletions Sources/Public/BacktraceClient.swift
Original file line number Diff line number Diff line change
@@ -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 {

1 change: 1 addition & 0 deletions Sources/Public/BacktraceCrashReporter.swift
Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@ extension BacktraceCrashReporter: CrashReporting {
_ uContext: UnsafeMutablePointer<ucontext_t>?,
_ context: UnsafeMutableRawPointer?) -> Void = { signalInfoPointer, _, context in
BacktraceOomWatcher.clean()

guard let attributesProvider = context?.assumingMemoryBound(to: SignalContext.self).pointee,
let signalInfo = signalInfoPointer?.pointee else {
return
Original file line number Diff line number Diff line change
@@ -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)
}
51 changes: 51 additions & 0 deletions Tests/BacktraceCrashLoopDetectorTests.swift
Original file line number Diff line number Diff line change
@@ -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())
}
}
}
}
}