From ee4952a8548d2659374ddd5ecae7417a7c0e8b1b Mon Sep 17 00:00:00 2001 From: Konstantin R <111510241+konst-sauce@users.noreply.github.com> Date: Wed, 28 Sep 2022 12:55:49 +0200 Subject: [PATCH 01/17] Added BacktraceCrashLoopDetector --- Backtrace.xcodeproj/project.pbxproj | 24 +++-- Sources/Features/Error/BacktraceError.swift | 4 + .../Public/BacktraceCrashLoopDetector.swift | 95 +++++++++++++++++++ 3 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 Sources/Public/BacktraceCrashLoopDetector.swift diff --git a/Backtrace.xcodeproj/project.pbxproj b/Backtrace.xcodeproj/project.pbxproj index d4407ad1..8fe18b35 100644 --- a/Backtrace.xcodeproj/project.pbxproj +++ b/Backtrace.xcodeproj/project.pbxproj @@ -179,6 +179,9 @@ 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 */; }; + 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 +452,7 @@ 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 = ""; }; + 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; }; @@ -932,6 +936,7 @@ F2AFB59622274E1400AAA1D7 /* Public */ = { isa = PBXGroup; children = ( + B5E58C6828E1A843001F9650 /* BacktraceCrashLoopDetector.swift */, 6E45A3A6273095E500DB0BAC /* BacktraceMetricsSettings.swift */, F29CD79321FDD5E900216C59 /* BacktraceClientDelegate.swift */, F2AFB59922274E5400AAA1D7 /* BacktraceClientCustomizing.swift */, @@ -1917,6 +1922,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 */, @@ -2021,6 +2027,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 */, @@ -2150,6 +2157,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 */, @@ -2381,7 +2389,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 +2468,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 +2700,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 +2782,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; @@ -3185,7 +3193,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 +3277,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 +3491,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 +3516,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -3562,6 +3572,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 +3591,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/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/BacktraceCrashLoopDetector.swift b/Sources/Public/BacktraceCrashLoopDetector.swift new file mode 100644 index 00000000..70aae54c --- /dev/null +++ b/Sources/Public/BacktraceCrashLoopDetector.swift @@ -0,0 +1,95 @@ +// +// BacktraceCrashLoopDetector.swift +// Backtrace +// + +import Foundation +import Backtrace_PLCrashReporter + +@objc public class BacktraceCrashLoopDetector: NSObject { + + fileprivate struct StartUpEvent: Codable { + var timestamp: Double + var isSuccessful: Bool + } + + @objc private static let plistKey = "CrashLoopDB" + @objc private static let eventsForCrashLoopCount = 5 + + @objc public static let shared = BacktraceCrashLoopDetector() + + fileprivate var startupEvents: [StartUpEvent] = [] + + @objc override private init() { + super.init() + } + + @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 allows to avoid a headache with reading/writing to/from the custom file. + + - But we should consider shared computers as well: + 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) + } + + @objc private func saveEvents() { + let data = try? PropertyListEncoder().encode(startupEvents) + UserDefaults.standard.set(data, forKey: BacktraceCrashLoopDetector.plistKey) + } + + @objc private func addCurrentEvent() { + var event = StartUpEvent(timestamp: Double(Date.timeIntervalSinceReferenceDate), isSuccessful: true) + + let plReporter = PLCrashReporter(configuration: PLCrashReporterConfig.defaultConfiguration()) + + if plReporter?.hasPendingCrashReport() ?? false { + event.isSuccessful = false + } +// event.isSuccessful = false + startupEvents.append(event) + } + + @objc private func badEventsCount() -> Int { + + var badEventsCount = 0 + for event in startupEvents { + badEventsCount += (event.isSuccessful ? 0 : 1) + } + + if startupEvents.count >= BacktraceCrashLoopDetector.eventsForCrashLoopCount { + startupEvents.remove(at: 0) + } + + return badEventsCount + } + + @objc func detectCrashloop() -> Bool { + + loadEvents() + addCurrentEvent() + + let count = badEventsCount() + saveEvents() + + // true -> crash loop detected -> set safe mode + // false -> crash loop NOT detected -> set normal mode + return count >= BacktraceCrashLoopDetector.eventsForCrashLoopCount + } +} From 861aa76fa9bd60f77133545cd882d129e6b41d42 Mon Sep 17 00:00:00 2001 From: Konstantin R <111510241+konst-sauce@users.noreply.github.com> Date: Wed, 28 Sep 2022 13:20:13 +0200 Subject: [PATCH 02/17] Added public methods to configure safe mode and access its state. Removed shared instance of BCLD --- Sources/Public/BacktraceClient.swift | 25 +++++++++++++++++++ .../Public/BacktraceCrashLoopDetector.swift | 15 ++++++----- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/Sources/Public/BacktraceClient.swift b/Sources/Public/BacktraceClient.swift index 12b2fcbf..353bc4d2 100644 --- a/Sources/Public/BacktraceClient.swift +++ b/Sources/Public/BacktraceClient.swift @@ -17,6 +17,8 @@ import Foundation @objc private let breadcrumbsInstance: BacktraceBreadcrumbs = BacktraceBreadcrumbs() #endif + private var isSafeMode = false + private let reporter: BacktraceReporter private let dispatcher: Dispatching private let reportingPolicy: ReportingPolicy @@ -85,10 +87,33 @@ import Foundation self.metricsInstance = BacktraceMetrics(api: api) super.init() + + let crashLoopDetector = BacktraceCrashLoopDetector() + isSafeMode = crashLoopDetector.detectCrashloop() + try startCrashReporter() } } +// MARK: - BacktraceClient Safe Mode public API +extension BacktraceClient { + @objc public func enableSafeMode() { + isSafeMode = true + + // Do any additional setup here - f.e. turn off reporting etc + } + + @objc public func disableSafeMode() { + isSafeMode = false + + // Do any additional setup here - f.e. turn on reporting etc + } + + @objc public func isInSafeMode() -> Bool { + return isSafeMode + } +} + // MARK: - BacktraceClientProviding extension BacktraceClient: BacktraceClientCustomizing { diff --git a/Sources/Public/BacktraceCrashLoopDetector.swift b/Sources/Public/BacktraceCrashLoopDetector.swift index 70aae54c..48f03517 100644 --- a/Sources/Public/BacktraceCrashLoopDetector.swift +++ b/Sources/Public/BacktraceCrashLoopDetector.swift @@ -8,6 +8,11 @@ import Backtrace_PLCrashReporter @objc public class BacktraceCrashLoopDetector: NSObject { + enum SafetyMode { + case normal + case safe + } + fileprivate struct StartUpEvent: Codable { var timestamp: Double var isSuccessful: Bool @@ -16,14 +21,8 @@ import Backtrace_PLCrashReporter @objc private static let plistKey = "CrashLoopDB" @objc private static let eventsForCrashLoopCount = 5 - @objc public static let shared = BacktraceCrashLoopDetector() - fileprivate var startupEvents: [StartUpEvent] = [] - @objc override private init() { - super.init() - } - @objc private func loadEvents() { // Cleanup old events - f.e, for multiple usages of detector @@ -31,9 +30,9 @@ import Backtrace_PLCrashReporter /* - Since detector's DB is relatively small, UserDefaults are a good option here, - plus allows to avoid a headache with reading/writing to/from the custom file. + plus they allow to avoid a headache with reading/writing to/from the custom file. - - But we should consider shared computers as well: + - 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. From b8d96795cf6cdfdf07a31b0f6ef3398fdbedd9f4 Mon Sep 17 00:00:00 2001 From: Konstantin R <111510241+konst-sauce@users.noreply.github.com> Date: Wed, 28 Sep 2022 13:26:16 +0200 Subject: [PATCH 03/17] Added detection to iOS example. Moved detection to isSafeToLaunch --- Examples/Example-iOS/AppDelegate.swift | 10 ++++++++++ Sources/Public/BacktraceClient.swift | 12 ++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Examples/Example-iOS/AppDelegate.swift b/Examples/Example-iOS/AppDelegate.swift index 5290237f..5172cc78 100644 --- a/Examples/Example-iOS/AppDelegate.swift +++ b/Examples/Example-iOS/AppDelegate.swift @@ -18,6 +18,16 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + let isSafeToLaunch = BacktraceClient.isSafeToLaunch() + + if !isSafeToLaunch { + // Perform custom checks if necessary and decide if Backtrace should be launched + } + else { + + } + let backtraceCredentials = BacktraceCredentials(endpoint: URL(string: Keys.backtraceUrl as String)!, token: Keys.backtraceToken as String) diff --git a/Sources/Public/BacktraceClient.swift b/Sources/Public/BacktraceClient.swift index 353bc4d2..716b1eea 100644 --- a/Sources/Public/BacktraceClient.swift +++ b/Sources/Public/BacktraceClient.swift @@ -87,16 +87,20 @@ import Foundation self.metricsInstance = BacktraceMetrics(api: api) super.init() - - let crashLoopDetector = BacktraceCrashLoopDetector() - isSafeMode = crashLoopDetector.detectCrashloop() - + try startCrashReporter() } } // MARK: - BacktraceClient Safe Mode public API extension BacktraceClient { + + @objc public static func isSafeToLaunch() -> Bool { + let crashLoopDetector = BacktraceCrashLoopDetector() + let isInCrashLoop = crashLoopDetector.detectCrashloop() + return !isInCrashLoop + } + @objc public func enableSafeMode() { isSafeMode = true From 3d18e2f16211e68bc3b215d34e53c8a9d62d481a Mon Sep 17 00:00:00 2001 From: Konstantin R <111510241+konst-sauce@users.noreply.github.com> Date: Wed, 28 Sep 2022 15:11:59 +0200 Subject: [PATCH 04/17] Added Unit Tests, added logs, polished CL detector --- Backtrace.xcodeproj/project.pbxproj | 8 +++ .../Public/BacktraceCrashLoopDetector.swift | 30 +++++++---- Tests/BacktraceCrashLoopDetectorTests.swift | 52 +++++++++++++++++++ 3 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 Tests/BacktraceCrashLoopDetectorTests.swift diff --git a/Backtrace.xcodeproj/project.pbxproj b/Backtrace.xcodeproj/project.pbxproj index 8fe18b35..32a849aa 100644 --- a/Backtrace.xcodeproj/project.pbxproj +++ b/Backtrace.xcodeproj/project.pbxproj @@ -179,6 +179,9 @@ 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 */; }; @@ -452,6 +455,7 @@ 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; }; @@ -817,6 +821,7 @@ A24A4B4928B595D8004F5052 /* BacktraceWatcherTests.swift */, A24A4B5528B595D8004F5052 /* CrashReporterTests.swift */, A24A4B5228B595D8004F5052 /* DispatcherTests.swift */, + B50C5A7F28E4740A004BB1DA /* BacktraceCrashLoopDetectorTests.swift */, F21DD3AF2255E99E00404CC3 /* Resources */, F2AB6370224647F000939BC9 /* Helpers */, F2AB636F224647DE00939BC9 /* Mocks */, @@ -1987,6 +1992,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 */, @@ -2082,6 +2088,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 */, @@ -2193,6 +2200,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 */, diff --git a/Sources/Public/BacktraceCrashLoopDetector.swift b/Sources/Public/BacktraceCrashLoopDetector.swift index 48f03517..9918e60c 100644 --- a/Sources/Public/BacktraceCrashLoopDetector.swift +++ b/Sources/Public/BacktraceCrashLoopDetector.swift @@ -13,15 +13,15 @@ import Backtrace_PLCrashReporter case safe } - fileprivate struct StartUpEvent: Codable { + internal struct StartUpEvent: Codable { var timestamp: Double var isSuccessful: Bool } @objc private static let plistKey = "CrashLoopDB" - @objc private static let eventsForCrashLoopCount = 5 + @objc internal static let eventsForCrashLoopCount = 5 - fileprivate var startupEvents: [StartUpEvent] = [] + internal var startupEvents: [StartUpEvent] = [] @objc private func loadEvents() { @@ -46,11 +46,13 @@ import Backtrace_PLCrashReporter else { return } startupEvents.append(contentsOf: array) + print("Events Loaded: \(startupEvents.count)") } - @objc private func saveEvents() { + @objc internal func saveEvents() { let data = try? PropertyListEncoder().encode(startupEvents) UserDefaults.standard.set(data, forKey: BacktraceCrashLoopDetector.plistKey) + print("Events Saved: \(startupEvents.count)") } @objc private func addCurrentEvent() { @@ -62,7 +64,15 @@ import Backtrace_PLCrashReporter event.isSuccessful = false } // event.isSuccessful = false + print("New Event: {timestamp:\(event.timestamp)--successful:\(event.isSuccessful)}") + startupEvents.append(event) + + if startupEvents.count > BacktraceCrashLoopDetector.eventsForCrashLoopCount { + startupEvents.remove(at: 0) + } + + print("Event Added: \(startupEvents.count)") } @objc private func badEventsCount() -> Int { @@ -71,17 +81,17 @@ import Backtrace_PLCrashReporter for event in startupEvents { badEventsCount += (event.isSuccessful ? 0 : 1) } - - if startupEvents.count >= BacktraceCrashLoopDetector.eventsForCrashLoopCount { - startupEvents.remove(at: 0) - } + print("Bad Events Count: \(badEventsCount)") return badEventsCount } @objc func detectCrashloop() -> Bool { + print("Starting Crash Loop Detection") + loadEvents() + addCurrentEvent() let count = badEventsCount() @@ -89,6 +99,8 @@ import Backtrace_PLCrashReporter // true -> crash loop detected -> set safe mode // false -> crash loop NOT detected -> set normal mode - return count >= BacktraceCrashLoopDetector.eventsForCrashLoopCount + let result = count >= BacktraceCrashLoopDetector.eventsForCrashLoopCount + print("Finishing Crash Loop Detection: \(result)") + return result } } diff --git a/Tests/BacktraceCrashLoopDetectorTests.swift b/Tests/BacktraceCrashLoopDetectorTests.swift new file mode 100644 index 00000000..390672cf --- /dev/null +++ b/Tests/BacktraceCrashLoopDetectorTests.swift @@ -0,0 +1,52 @@ +import XCTest + +import Nimble +import Quick +@testable import Backtrace + +final class BacktraceCrashLoopDetectorTests: QuickSpec { + + override func spec() { + describe("Crash Loop Detector") { + + let crashLoopDetector = BacktraceCrashLoopDetector() + let eventsCount = BacktraceCrashLoopDetector.eventsForCrashLoopCount + let timeIntervalStep = 200 + var isCrashLoop = false + + context("No Crash Loop Case") { + + let expectedResult = false + + 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() + isCrashLoop = crashLoopDetector.detectCrashloop() + + it("checks if no crash loop detected") { + expect { isCrashLoop == expectedResult }.to(beTrue()) + } + } + + context("Crash Loop Case") { + + let expectedResult = true + + 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 == expectedResult }.to(beTrue()) + } + } + } + } +} From 8c07770d03dfd64194bc610d232a0b71fc86e11f Mon Sep 17 00:00:00 2001 From: Konstantin R <111510241+konst-sauce@users.noreply.github.com> Date: Thu, 29 Sep 2022 16:47:35 +0200 Subject: [PATCH 05/17] Removed PLCR dependency from BTCL detector, added UI, polishes --- Examples/Example-iOS/AppDelegate.swift | 4 +-- Examples/Example-iOS/ViewController.swift | 2 ++ Sources/Public/BacktraceClient.swift | 9 +++--- .../Public/BacktraceCrashLoopDetector.swift | 31 +++++++++++++------ 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/Examples/Example-iOS/AppDelegate.swift b/Examples/Example-iOS/AppDelegate.swift index 5172cc78..7cca54ab 100644 --- a/Examples/Example-iOS/AppDelegate.swift +++ b/Examples/Example-iOS/AppDelegate.swift @@ -23,9 +23,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { if !isSafeToLaunch { // Perform custom checks if necessary and decide if Backtrace should be launched - } - else { - + return true } let backtraceCredentials = BacktraceCredentials(endpoint: URL(string: Keys.backtraceUrl as String)!, diff --git a/Examples/Example-iOS/ViewController.swift b/Examples/Example-iOS/ViewController.swift index 2b045ac4..4f00dea9 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: " + BacktraceCrashLoopDetector.badEventsCount.description + + "\nIs Safe to Launch: " + (BacktraceClient.isInSafeMode() ? "FALSE" : "TRUE") } @IBAction func outOfMemoryReportAction(_ sender: Any) { diff --git a/Sources/Public/BacktraceClient.swift b/Sources/Public/BacktraceClient.swift index 716b1eea..face95e8 100644 --- a/Sources/Public/BacktraceClient.swift +++ b/Sources/Public/BacktraceClient.swift @@ -17,7 +17,7 @@ import Foundation @objc private let breadcrumbsInstance: BacktraceBreadcrumbs = BacktraceBreadcrumbs() #endif - private var isSafeMode = false + private static var isSafeMode = false private let reporter: BacktraceReporter private let dispatcher: Dispatching @@ -98,22 +98,23 @@ extension BacktraceClient { @objc public static func isSafeToLaunch() -> Bool { let crashLoopDetector = BacktraceCrashLoopDetector() let isInCrashLoop = crashLoopDetector.detectCrashloop() + isSafeMode = isInCrashLoop return !isInCrashLoop } - @objc public func enableSafeMode() { + @objc public static func enableSafeMode() { isSafeMode = true // Do any additional setup here - f.e. turn off reporting etc } - @objc public func disableSafeMode() { + @objc public static func disableSafeMode() { isSafeMode = false // Do any additional setup here - f.e. turn on reporting etc } - @objc public func isInSafeMode() -> Bool { + @objc public static func isInSafeMode() -> Bool { return isSafeMode } } diff --git a/Sources/Public/BacktraceCrashLoopDetector.swift b/Sources/Public/BacktraceCrashLoopDetector.swift index 9918e60c..a42b8f16 100644 --- a/Sources/Public/BacktraceCrashLoopDetector.swift +++ b/Sources/Public/BacktraceCrashLoopDetector.swift @@ -4,7 +4,6 @@ // import Foundation -import Backtrace_PLCrashReporter @objc public class BacktraceCrashLoopDetector: NSObject { @@ -20,6 +19,7 @@ import Backtrace_PLCrashReporter @objc private static let plistKey = "CrashLoopDB" @objc internal static let eventsForCrashLoopCount = 5 + @objc public static var badEventsCount = 0 internal var startupEvents: [StartUpEvent] = [] @@ -55,15 +55,28 @@ import Backtrace_PLCrashReporter print("Events Saved: \(startupEvents.count)") } - @objc private func addCurrentEvent() { - var event = StartUpEvent(timestamp: Double(Date.timeIntervalSinceReferenceDate), isSuccessful: true) + @objc private func hasCrashReport() -> Bool { - let plReporter = PLCrashReporter(configuration: PLCrashReporterConfig.defaultConfiguration()) + let bundleIDBT = Bundle.main.bundleIdentifier ?? "" + let appIDPath = bundleIDBT.replacingOccurrences(of: "/", with: "_") - if plReporter?.hasPendingCrashReport() ?? false { - event.isSuccessful = false - } -// event.isSuccessful = false + let paths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true) + let cacheDir = URL(fileURLWithPath: paths.count > 0 ? 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: "") + print("reportFullPath: \(reportFullPath)") + let exists = FileManager.default.fileExists(atPath: reportFullPath) + return exists + } + + @objc private func addCurrentEvent() { + + var event = StartUpEvent(timestamp: Double(Date.timeIntervalSinceReferenceDate), isSuccessful: true) + event.isSuccessful = !hasCrashReport() print("New Event: {timestamp:\(event.timestamp)--successful:\(event.isSuccessful)}") startupEvents.append(event) @@ -81,7 +94,7 @@ import Backtrace_PLCrashReporter for event in startupEvents { badEventsCount += (event.isSuccessful ? 0 : 1) } - + BacktraceCrashLoopDetector.badEventsCount = badEventsCount print("Bad Events Count: \(badEventsCount)") return badEventsCount } From c2906603b952ebdb93ad5d23a93e4ef10f5e9117 Mon Sep 17 00:00:00 2001 From: Konstantin R <111510241+konst-sauce@users.noreply.github.com> Date: Thu, 29 Sep 2022 18:46:12 +0200 Subject: [PATCH 06/17] Polished naming, added reset of CL detector when safe mode is necessary --- Examples/Example-iOS/AppDelegate.swift | 9 +++- Examples/Example-iOS/ViewController.swift | 2 +- Sources/Public/BacktraceClient.swift | 45 ++++++++++++++----- .../Public/BacktraceCrashLoopDetector.swift | 44 ++++++++++++------ Tests/BacktraceCrashLoopDetectorTests.swift | 2 +- 5 files changed, 74 insertions(+), 28 deletions(-) diff --git a/Examples/Example-iOS/AppDelegate.swift b/Examples/Example-iOS/AppDelegate.swift index 7cca54ab..162b49c5 100644 --- a/Examples/Example-iOS/AppDelegate.swift +++ b/Examples/Example-iOS/AppDelegate.swift @@ -19,9 +19,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - let isSafeToLaunch = BacktraceClient.isSafeToLaunch() + // Enable crash loop detector, it crashes count threshold is not specified - default will be used + BacktraceClient.enableCrashLoopDetection() + defer { BacktraceClient.resetCrashLoopDetection() } + + let isSafeModeRequired = BacktraceClient.isSafeModeRequired() - if !isSafeToLaunch { + if isSafeModeRequired { + // Remove current crash file for not to be trapped in a loop of crash detections // Perform custom checks if necessary and decide if Backtrace should be launched return true } diff --git a/Examples/Example-iOS/ViewController.swift b/Examples/Example-iOS/ViewController.swift index 4f00dea9..f309099e 100644 --- a/Examples/Example-iOS/ViewController.swift +++ b/Examples/Example-iOS/ViewController.swift @@ -10,7 +10,7 @@ class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - textView.text = "BadEvents: " + BacktraceCrashLoopDetector.badEventsCount.description + textView.text = "BadEvents: " + BacktraceClient.consecutiveCrashesCount().description + "\nIs Safe to Launch: " + (BacktraceClient.isInSafeMode() ? "FALSE" : "TRUE") } diff --git a/Sources/Public/BacktraceClient.swift b/Sources/Public/BacktraceClient.swift index face95e8..05334922 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,12 +22,14 @@ import Foundation @objc private let breadcrumbsInstance: BacktraceBreadcrumbs = BacktraceBreadcrumbs() #endif - private static var isSafeMode = false + private static var workingMode = WorkingMode.normal private let reporter: BacktraceReporter private let dispatcher: Dispatching private let reportingPolicy: ReportingPolicy + private static var crashLoopDetector: BacktraceCrashLoopDetector? + /// Initialize `BacktraceClient` with credentials. To learn more about credentials, see /// https://help.backtrace.io/troubleshooting/what-is-a-submission-url /// and https://help.backtrace.io/troubleshooting/what-is-a-submission-token . @@ -95,27 +102,43 @@ import Foundation // MARK: - BacktraceClient Safe Mode public API extension BacktraceClient { - @objc public static func isSafeToLaunch() -> Bool { - let crashLoopDetector = BacktraceCrashLoopDetector() - let isInCrashLoop = crashLoopDetector.detectCrashloop() - isSafeMode = isInCrashLoop - return !isInCrashLoop - } - @objc public static func enableSafeMode() { - isSafeMode = true + workingMode = .safe // Do any additional setup here - f.e. turn off reporting etc } @objc public static func disableSafeMode() { - isSafeMode = false + workingMode = .normal // Do any additional setup here - f.e. turn on reporting etc } @objc public static func isInSafeMode() -> Bool { - return isSafeMode + return workingMode == .safe + } + + @objc public static func enableCrashLoopDetection(_ threshold: Int = 0) { + crashLoopDetector = BacktraceCrashLoopDetector() + crashLoopDetector?.updateThreshold(threshold) + } + + @objc public static func disableCrashLoopDetection() { + crashLoopDetector = nil + } + + @objc public static func resetCrashLoopDetection() { + crashLoopDetector?.deleteCrashReport() + } + + @objc public static func isSafeModeRequired() -> Bool { + let isInCrashLoop = crashLoopDetector?.detectCrashloop() ?? false + if isInCrashLoop { enableSafeMode() } + return isInCrashLoop + } + + @objc public static func consecutiveCrashesCount() -> Int { + return crashLoopDetector?.consecutiveCrashesCount ?? 0 } } diff --git a/Sources/Public/BacktraceCrashLoopDetector.swift b/Sources/Public/BacktraceCrashLoopDetector.swift index a42b8f16..29d2383a 100644 --- a/Sources/Public/BacktraceCrashLoopDetector.swift +++ b/Sources/Public/BacktraceCrashLoopDetector.swift @@ -7,22 +7,23 @@ import Foundation @objc public class BacktraceCrashLoopDetector: NSObject { - enum SafetyMode { - case normal - case safe - } - internal struct StartUpEvent: Codable { var timestamp: Double var isSuccessful: Bool } @objc private static let plistKey = "CrashLoopDB" - @objc internal static let eventsForCrashLoopCount = 5 - @objc public static var badEventsCount = 0 + @objc internal static let consecutiveCrashesThreshold = 5 + @objc internal var consecutiveCrashesCount = 0 + @objc private var threshold = consecutiveCrashesThreshold + internal var startupEvents: [StartUpEvent] = [] + @objc internal func updateThreshold(_ threshold: Int) { + self.threshold = threshold == 0 ? BacktraceCrashLoopDetector.consecutiveCrashesThreshold : threshold + } + @objc private func loadEvents() { // Cleanup old events - f.e, for multiple usages of detector @@ -54,9 +55,8 @@ import Foundation UserDefaults.standard.set(data, forKey: BacktraceCrashLoopDetector.plistKey) print("Events Saved: \(startupEvents.count)") } - - @objc private func hasCrashReport() -> Bool { + @objc private func reportFilePath() -> String { let bundleIDBT = Bundle.main.bundleIdentifier ?? "" let appIDPath = bundleIDBT.replacingOccurrences(of: "/", with: "_") @@ -69,7 +69,11 @@ import Foundation let reportName = "live_report.plcrash" let reportFullPath = crashReportDir.appendingPathComponent(reportName).absoluteString.replacingOccurrences(of: "file://", with: "") print("reportFullPath: \(reportFullPath)") - let exists = FileManager.default.fileExists(atPath: reportFullPath) + return reportFullPath + } + + @objc private func hasCrashReport() -> Bool { + let exists = FileManager.default.fileExists(atPath: reportFilePath()) return exists } @@ -81,7 +85,7 @@ import Foundation startupEvents.append(event) - if startupEvents.count > BacktraceCrashLoopDetector.eventsForCrashLoopCount { + if startupEvents.count > BacktraceCrashLoopDetector.consecutiveCrashesThreshold { startupEvents.remove(at: 0) } @@ -94,7 +98,7 @@ import Foundation for event in startupEvents { badEventsCount += (event.isSuccessful ? 0 : 1) } - BacktraceCrashLoopDetector.badEventsCount = badEventsCount + self.consecutiveCrashesCount = badEventsCount print("Bad Events Count: \(badEventsCount)") return badEventsCount } @@ -112,8 +116,22 @@ import Foundation // true -> crash loop detected -> set safe mode // false -> crash loop NOT detected -> set normal mode - let result = count >= BacktraceCrashLoopDetector.eventsForCrashLoopCount + let result = count >= BacktraceCrashLoopDetector.consecutiveCrashesThreshold print("Finishing Crash Loop Detection: \(result)") return result } + + @objc func deleteCrashReport() { + let path = reportFilePath() + let fileURL = URL(fileURLWithPath: path) + try? FileManager.default.removeItem(at: fileURL) + + startupEvents.removeAll() + saveEvents() + } + +// +// @objc func consecutiveCrashesCount() -> Int { +// return self.consecutiveCrashesCount +// } } diff --git a/Tests/BacktraceCrashLoopDetectorTests.swift b/Tests/BacktraceCrashLoopDetectorTests.swift index 390672cf..9f7fe000 100644 --- a/Tests/BacktraceCrashLoopDetectorTests.swift +++ b/Tests/BacktraceCrashLoopDetectorTests.swift @@ -10,7 +10,7 @@ final class BacktraceCrashLoopDetectorTests: QuickSpec { describe("Crash Loop Detector") { let crashLoopDetector = BacktraceCrashLoopDetector() - let eventsCount = BacktraceCrashLoopDetector.eventsForCrashLoopCount + let eventsCount = BacktraceCrashLoopDetector.consecutiveCrashesThreshold let timeIntervalStep = 200 var isCrashLoop = false From f0f9d56003030468579dc30b64b712e33e3aed5c Mon Sep 17 00:00:00 2001 From: Konstantin R <111510241+konst-sauce@users.noreply.github.com> Date: Fri, 30 Sep 2022 16:39:57 +0200 Subject: [PATCH 07/17] Polished crash loop detection Added removing of crash report when safe mode detected to prevent entering into crash loop detection loop. Made bad events calculation reverse - from newest event back to closest successful. --- Examples/Example-iOS/AppDelegate.swift | 2 +- Sources/Public/BacktraceCrashLoopDetector.swift | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Examples/Example-iOS/AppDelegate.swift b/Examples/Example-iOS/AppDelegate.swift index 162b49c5..4ce2ee73 100644 --- a/Examples/Example-iOS/AppDelegate.swift +++ b/Examples/Example-iOS/AppDelegate.swift @@ -21,12 +21,12 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { // Enable crash loop detector, it crashes count threshold is not specified - default will be used BacktraceClient.enableCrashLoopDetection() - defer { BacktraceClient.resetCrashLoopDetection() } let isSafeModeRequired = BacktraceClient.isSafeModeRequired() if isSafeModeRequired { // Remove current crash file for not to be trapped in a loop of crash detections + BacktraceClient.resetCrashLoopDetection() // Perform custom checks if necessary and decide if Backtrace should be launched return true } diff --git a/Sources/Public/BacktraceCrashLoopDetector.swift b/Sources/Public/BacktraceCrashLoopDetector.swift index 29d2383a..07f40e4e 100644 --- a/Sources/Public/BacktraceCrashLoopDetector.swift +++ b/Sources/Public/BacktraceCrashLoopDetector.swift @@ -95,8 +95,11 @@ import Foundation @objc private func badEventsCount() -> Int { var badEventsCount = 0 - for event in startupEvents { - badEventsCount += (event.isSuccessful ? 0 : 1) + for event in startupEvents.reversed() { + if event.isSuccessful { + break + } + badEventsCount += 1 } self.consecutiveCrashesCount = badEventsCount print("Bad Events Count: \(badEventsCount)") @@ -125,8 +128,6 @@ import Foundation let path = reportFilePath() let fileURL = URL(fileURLWithPath: path) try? FileManager.default.removeItem(at: fileURL) - - startupEvents.removeAll() saveEvents() } From dc9fba383396e9a6a500e673624d3edc7013226d Mon Sep 17 00:00:00 2001 From: Konstantin R <111510241+konst-sauce@users.noreply.github.com> Date: Fri, 30 Sep 2022 19:25:13 +0200 Subject: [PATCH 08/17] Updated tvOS and macOS examples --- Backtrace.xcodeproj/project.pbxproj | 4 ++ Examples/Example-macOS-ObjC/AppDelegate.m | 8 ++++ .../Base.lproj/Main.storyboard | 15 ++++-- Examples/Example-tvOS/AppDelegate.swift | 13 +++++ .../Example-tvOS/Base.lproj/Main.storyboard | 47 ++++++++++++++----- Examples/Example-tvOS/ViewController.swift | 6 ++- 6 files changed, 75 insertions(+), 18 deletions(-) diff --git a/Backtrace.xcodeproj/project.pbxproj b/Backtrace.xcodeproj/project.pbxproj index 32a849aa..762b28fc 100644 --- a/Backtrace.xcodeproj/project.pbxproj +++ b/Backtrace.xcodeproj/project.pbxproj @@ -2995,6 +2995,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; @@ -3018,6 +3019,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -3074,6 +3076,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; @@ -3091,6 +3094,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; diff --git a/Examples/Example-macOS-ObjC/AppDelegate.m b/Examples/Example-macOS-ObjC/AppDelegate.m index 4e968297..d8f7987a 100644 --- a/Examples/Example-macOS-ObjC/AppDelegate.m +++ b/Examples/Example-macOS-ObjC/AppDelegate.m @@ -10,6 +10,14 @@ @implementation AppDelegate - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + [BacktraceClient enableCrashLoopDetection: 0]; + if([BacktraceClient isSafeModeRequired]) { + // Remove current crash file for not to be trapped in a loop of crash detections + [BacktraceClient resetCrashLoopDetection]; + // Perform 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..d025cd2c 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,7 +711,7 @@ @@ -739,6 +740,10 @@ + diff --git a/Examples/Example-tvOS/AppDelegate.swift b/Examples/Example-tvOS/AppDelegate.swift index ca063273..149ebadf 100644 --- a/Examples/Example-tvOS/AppDelegate.swift +++ b/Examples/Example-tvOS/AppDelegate.swift @@ -16,6 +16,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + // Enable crash loop detector, it crashes count threshold is not specified - default will be used + BacktraceClient.enableCrashLoopDetection() + + let isSafeModeRequired = BacktraceClient.isSafeModeRequired() + + if isSafeModeRequired { + // Remove current crash file for not to be trapped in a loop of crash detections + BacktraceClient.resetCrashLoopDetection() + // Perform 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) { From 8497a458dae8c72e158771dd18d8feb091985d2a Mon Sep 17 00:00:00 2001 From: Konstantin R <111510241+konst-sauce@users.noreply.github.com> Date: Wed, 5 Oct 2022 15:56:59 +0200 Subject: [PATCH 09/17] Updated with comments from the pull request review --- .../Public/BacktraceCrashLoopDetector.swift | 55 ++++++++++--------- Tests/BacktraceCrashLoopDetectorTests.swift | 29 +++++----- 2 files changed, 43 insertions(+), 41 deletions(-) diff --git a/Sources/Public/BacktraceCrashLoopDetector.swift b/Sources/Public/BacktraceCrashLoopDetector.swift index 07f40e4e..ba2fdfff 100644 --- a/Sources/Public/BacktraceCrashLoopDetector.swift +++ b/Sources/Public/BacktraceCrashLoopDetector.swift @@ -6,15 +6,15 @@ import Foundation @objc public class BacktraceCrashLoopDetector: NSObject { - + internal struct StartUpEvent: Codable { var timestamp: Double var isSuccessful: Bool } - - @objc private static let plistKey = "CrashLoopDB" + + @objc private static let plistKey = "CrashLoopDetectorData" @objc internal static let consecutiveCrashesThreshold = 5 - @objc internal var consecutiveCrashesCount = 0 + @objc private(set) var consecutiveCrashesCount = 0 @objc private var threshold = consecutiveCrashesThreshold @@ -23,7 +23,7 @@ import Foundation @objc internal func updateThreshold(_ threshold: Int) { self.threshold = threshold == 0 ? BacktraceCrashLoopDetector.consecutiveCrashesThreshold : threshold } - + @objc private func loadEvents() { // Cleanup old events - f.e, for multiple usages of detector @@ -47,41 +47,49 @@ import Foundation else { return } startupEvents.append(contentsOf: array) - print("Events Loaded: \(startupEvents.count)") + BacktraceLogger.debug("Events Loaded: \(startupEvents.count)") } - + @objc internal func saveEvents() { let data = try? PropertyListEncoder().encode(startupEvents) UserDefaults.standard.set(data, forKey: BacktraceCrashLoopDetector.plistKey) - print("Events Saved: \(startupEvents.count)") + BacktraceLogger.debug("Events Saved: \(startupEvents.count)") } @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.count > 0 ? paths[0] : "") + let cacheDir = URL(fileURLWithPath: paths.isEmpty ? "" : paths[0]) let bundleIDPLCR = "com.plausiblelabs.crashreporter.data" - let crashReportDir = cacheDir.appendingPathComponent(bundleIDPLCR).appendingPathComponent(appIDPath) + let crashReportDir = cacheDir.appendingPathComponent(bundleIDPLCR) + .appendingPathComponent(appIDPath) let reportName = "live_report.plcrash" - let reportFullPath = crashReportDir.appendingPathComponent(reportName).absoluteString.replacingOccurrences(of: "file://", with: "") - print("reportFullPath: \(reportFullPath)") + let reportFullPath = crashReportDir.appendingPathComponent(reportName) + .absoluteString + .replacingOccurrences(of: "file://", with: "") + BacktraceLogger.debug("reportFullPath: \(reportFullPath)") return reportFullPath } - + @objc private func hasCrashReport() -> Bool { let exists = FileManager.default.fileExists(atPath: reportFilePath()) return exists } - + @objc private func addCurrentEvent() { var event = StartUpEvent(timestamp: Double(Date.timeIntervalSinceReferenceDate), isSuccessful: true) event.isSuccessful = !hasCrashReport() - print("New Event: {timestamp:\(event.timestamp)--successful:\(event.isSuccessful)}") + BacktraceLogger.debug("New Event: {timestamp:\(event.timestamp)--successful:\(event.isSuccessful)}") startupEvents.append(event) @@ -89,7 +97,7 @@ import Foundation startupEvents.remove(at: 0) } - print("Event Added: \(startupEvents.count)") + BacktraceLogger.debug("Event Added: \(startupEvents.count)") } @objc private func badEventsCount() -> Int { @@ -102,13 +110,13 @@ import Foundation badEventsCount += 1 } self.consecutiveCrashesCount = badEventsCount - print("Bad Events Count: \(badEventsCount)") + BacktraceLogger.debug("Bad Events Count: \(badEventsCount)") return badEventsCount } - + @objc func detectCrashloop() -> Bool { - print("Starting Crash Loop Detection") + BacktraceLogger.debug("Starting Crash Loop Detection") loadEvents() @@ -120,19 +128,14 @@ import Foundation // true -> crash loop detected -> set safe mode // false -> crash loop NOT detected -> set normal mode let result = count >= BacktraceCrashLoopDetector.consecutiveCrashesThreshold - print("Finishing Crash Loop Detection: \(result)") + BacktraceLogger.debug("Finishing Crash Loop Detection: \(result)") return result } - + @objc func deleteCrashReport() { let path = reportFilePath() let fileURL = URL(fileURLWithPath: path) try? FileManager.default.removeItem(at: fileURL) saveEvents() } - -// -// @objc func consecutiveCrashesCount() -> Int { -// return self.consecutiveCrashesCount -// } } diff --git a/Tests/BacktraceCrashLoopDetectorTests.swift b/Tests/BacktraceCrashLoopDetectorTests.swift index 9f7fe000..b99584ba 100644 --- a/Tests/BacktraceCrashLoopDetectorTests.swift +++ b/Tests/BacktraceCrashLoopDetectorTests.swift @@ -8,43 +8,42 @@ final class BacktraceCrashLoopDetectorTests: QuickSpec { override func spec() { describe("Crash Loop Detector") { - - let crashLoopDetector = BacktraceCrashLoopDetector() - let eventsCount = BacktraceCrashLoopDetector.consecutiveCrashesThreshold - let timeIntervalStep = 200 - var isCrashLoop = false context("No Crash Loop Case") { - - let expectedResult = false - + + 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() - isCrashLoop = crashLoopDetector.detectCrashloop() + let isCrashLoop = crashLoopDetector.detectCrashloop() it("checks if no crash loop detected") { - expect { isCrashLoop == expectedResult }.to(beTrue()) + expect { isCrashLoop }.to(beFalse()) } } context("Crash Loop Case") { - - let expectedResult = true - + + 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() + let isCrashLoop = crashLoopDetector.detectCrashloop() it("checks if crash loop detected") { - expect { isCrashLoop == expectedResult }.to(beTrue()) + expect { isCrashLoop }.to(beTrue()) } } } From 3fc8bf6565788fa44d200c7f1c09f2c76bb26c01 Mon Sep 17 00:00:00 2001 From: Konstantin R <111510241+konst-sauce@users.noreply.github.com> Date: Fri, 14 Oct 2022 18:10:44 +0200 Subject: [PATCH 10/17] Added Crash Loop counter --- Backtrace.xcodeproj/project.pbxproj | 8 ++ .../Public/BacktraceCrashLoopCounter.swift | 77 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 Sources/Public/BacktraceCrashLoopCounter.swift diff --git a/Backtrace.xcodeproj/project.pbxproj b/Backtrace.xcodeproj/project.pbxproj index 762b28fc..3e990801 100644 --- a/Backtrace.xcodeproj/project.pbxproj +++ b/Backtrace.xcodeproj/project.pbxproj @@ -182,6 +182,9 @@ 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 */; }; + B51F3E2C28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F3E2B28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift */; }; + B51F3E2D28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F3E2B28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift */; }; + B51F3E2E28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F3E2B28F6FA8F0096E21A /* BacktraceCrashLoopCounter.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 */; }; @@ -456,6 +459,7 @@ 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 = ""; }; + B51F3E2B28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceCrashLoopCounter.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; }; @@ -942,6 +946,7 @@ isa = PBXGroup; children = ( B5E58C6828E1A843001F9650 /* BacktraceCrashLoopDetector.swift */, + B51F3E2B28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift */, 6E45A3A6273095E500DB0BAC /* BacktraceMetricsSettings.swift */, F29CD79321FDD5E900216C59 /* BacktraceClientDelegate.swift */, F2AFB59922274E5400AAA1D7 /* BacktraceClientCustomizing.swift */, @@ -1949,6 +1954,7 @@ 28F95BEE225260D5003936E0 /* NetworkReachability.swift in Sources */, 6EB713FA276294160075D1C1 /* MetricsRequest.swift in Sources */, 6EB713F627617ED00075D1C1 /* BacktraceMetricsContainer.swift in Sources */, + B51F3E2E28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift in Sources */, 28F95BC822526023003936E0 /* URLSession+Sync.swift in Sources */, 28F95BED225260D3003936E0 /* AttributesProvider.swift in Sources */, 28F95BDB22526088003936E0 /* SignalContext.swift in Sources */, @@ -2081,6 +2087,7 @@ F28F165921E2A0DA008E4B96 /* URLSession+Sync.swift in Sources */, F2AB636E22442B5100939BC9 /* DebuggerChecker.swift in Sources */, F286353B2283685100F45412 /* Map+KeyPath.swift in Sources */, + B51F3E2D28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2158,6 +2165,7 @@ 28966EFA2214BBD200E6E891 /* AttributesStorage.swift in Sources */, 6EB713F8276294160075D1C1 /* MetricsRequest.swift in Sources */, 6EB713F427617ED00075D1C1 /* BacktraceMetricsContainer.swift in Sources */, + B51F3E2C28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift in Sources */, F259E4D62229A40C00F282C7 /* Result.swift in Sources */, 2846E1FE223070CB0035F98C /* Attachment.swift in Sources */, F2D7122421F10E78002D2A26 /* BacktraceCredentials.swift in Sources */, diff --git a/Sources/Public/BacktraceCrashLoopCounter.swift b/Sources/Public/BacktraceCrashLoopCounter.swift new file mode 100644 index 00000000..5eaf4224 --- /dev/null +++ b/Sources/Public/BacktraceCrashLoopCounter.swift @@ -0,0 +1,77 @@ +// +// BacktraceCrashLoopCounter.swift +// Backtrace +// + +import Foundation + +class BacktraceCrashLoopCounter { + + static private var counter = 0 + + static internal func start() { + checkFileExists() + BacktraceLogger.debug("Cache Dir: \(cacheDir())") + BacktraceLogger.debug("Crash Loop Counter File Path: \(filePath())") + load() + } + + static internal func load() { + guard let contents = try? String(contentsOfFile: filePath()) + else { + counter = 0 + return + } + counter = Int(contents) ?? 0 + } + + static internal func crashesCount() -> Int { + return counter + } + + static private func checkFileExists() { + let path = filePath() + // We can use just 'createFile' to check if file exists, but, as per docs: + // "If a file already exists at path, this method overwrites the contents of that file + // if the current process has the appropriate privileges to do so." + // https://developer.apple.com/documentation/foundation/filemanager/1410695-createfile + if !FileManager.default.fileExists(atPath: path) { + FileManager.default.createFile(atPath: path, contents: nil) + // Write current counter + reset() + } + } + + static internal func increment() { + counter += 1 + save() + } + + static internal func reset() { + counter = 0 + save() + } + + static private func save() { + let contents = String(counter) + try? contents.write(toFile: filePath(), atomically: true, encoding: .utf8) + } + + static private func fileURL() -> URL { + let cacheDir = cacheDir() + let filePath = cacheDir.appendingPathComponent("BacktraceCrashLoopCounter.txt") + return filePath + } + + static private func filePath() -> String { + let filePath = fileURL().absoluteString + .replacingOccurrences(of: "file://", with: "") + return filePath + } + + static private func cacheDir() -> URL { + let paths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true) + let cacheDir = URL(fileURLWithPath: paths.isEmpty ? "" : paths[0]) + return cacheDir + } +} From 03d6cd0b58ae2e29e4446a831b81b6c89ffd0888 Mon Sep 17 00:00:00 2001 From: Konstantin R <111510241+konst-sauce@users.noreply.github.com> Date: Thu, 20 Oct 2022 14:29:06 +0200 Subject: [PATCH 11/17] Polished Crash Loop detection Added Crash Loop Counter - which counts crash loop events directly in the PLCrashReporter's signals handler. Updated Crash Loop Detector to work with Crash Loop Counter. Updated files structure. Polished comments and code. --- Backtrace.xcodeproj/project.pbxproj | 22 ++- Examples/Example-iOS/AppDelegate.swift | 4 +- Examples/Example-macOS-ObjC/AppDelegate.m | 4 +- Examples/Example-tvOS/AppDelegate.swift | 4 +- Sources/Public/BacktraceClient.swift | 6 +- Sources/Public/BacktraceCrashReporter.swift | 2 + .../BacktraceCrashLoop.swift | 23 +++ .../BacktraceCrashLoopCounter.swift | 40 +++--- .../BacktraceCrashLoopDetector.swift | 136 +++++++++++------- 9 files changed, 154 insertions(+), 87 deletions(-) create mode 100644 Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoop.swift rename Sources/Public/{ => Internal/CrashLoopDetector}/BacktraceCrashLoopCounter.swift (68%) rename Sources/Public/{ => Internal/CrashLoopDetector}/BacktraceCrashLoopDetector.swift (60%) diff --git a/Backtrace.xcodeproj/project.pbxproj b/Backtrace.xcodeproj/project.pbxproj index 3e990801..4246266c 100644 --- a/Backtrace.xcodeproj/project.pbxproj +++ b/Backtrace.xcodeproj/project.pbxproj @@ -185,6 +185,9 @@ B51F3E2C28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F3E2B28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift */; }; B51F3E2D28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F3E2B28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift */; }; B51F3E2E28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F3E2B28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift */; }; + B51F3E30290172430096E21A /* BacktraceCrashLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F3E2F290172430096E21A /* BacktraceCrashLoop.swift */; }; + B51F3E31290172430096E21A /* BacktraceCrashLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F3E2F290172430096E21A /* BacktraceCrashLoop.swift */; }; + B51F3E32290172430096E21A /* BacktraceCrashLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F3E2F290172430096E21A /* BacktraceCrashLoop.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 */; }; @@ -460,6 +463,7 @@ AFCCCEC126260BC400B83A28 /* AttachmentBookmarkHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentBookmarkHandler.swift; sourceTree = ""; }; B50C5A7F28E4740A004BB1DA /* BacktraceCrashLoopDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceCrashLoopDetectorTests.swift; sourceTree = ""; }; B51F3E2B28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceCrashLoopCounter.swift; sourceTree = ""; }; + B51F3E2F290172430096E21A /* BacktraceCrashLoop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceCrashLoop.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; }; @@ -730,6 +734,16 @@ path = Model; sourceTree = ""; }; + B51F3E33290173650096E21A /* CrashLoopDetector */ = { + isa = PBXGroup; + children = ( + B51F3E2F290172430096E21A /* BacktraceCrashLoop.swift */, + B5E58C6828E1A843001F9650 /* BacktraceCrashLoopDetector.swift */, + B51F3E2B28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift */, + ); + path = CrashLoopDetector; + sourceTree = ""; + }; E1CB76ADFD3A1D9326B4E46D /* Pods */ = { isa = PBXGroup; children = ( @@ -945,13 +959,11 @@ F2AFB59622274E1400AAA1D7 /* Public */ = { isa = PBXGroup; children = ( - B5E58C6828E1A843001F9650 /* BacktraceCrashLoopDetector.swift */, - B51F3E2B28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift */, + 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 */, @@ -984,6 +996,7 @@ F2AFB5A022274F1000AAA1D7 /* Internal */ = { isa = PBXGroup; children = ( + B51F3E33290173650096E21A /* CrashLoopDetector */, F2A81B4C23EF1730007C63E4 /* BacktraceApiProtocol.swift */, F21D302A224A18D50013B5D7 /* Store.swift */, F2AB636C22442B5100939BC9 /* DebuggerChecker.swift */, @@ -1975,6 +1988,7 @@ 28F95BE2225260A2003936E0 /* BacktraceApi.swift in Sources */, 28F95BDC2252608B003936E0 /* Result.swift in Sources */, F2A81B4F23EF1730007C63E4 /* BacktraceApiProtocol.swift in Sources */, + B51F3E32290172430096E21A /* BacktraceCrashLoop.swift in Sources */, F29959AD22553B340085B5C3 /* Model.xcdatamodeld in Sources */, 28F95BE3225260A5003936E0 /* HttpMethod.swift in Sources */, 28F95BD22252606B003936E0 /* BacktraceDatabaseSettings.swift in Sources */, @@ -2047,6 +2061,7 @@ F2AFB5922225E5D000AAA1D7 /* Foundation+Extensions.swift in Sources */, F26EBF3D23F21BCB00A64218 /* BacktraceNetworkClient.swift in Sources */, 28AC7740220A2A3300FED661 /* MultipartRequest.swift in Sources */, + B51F3E31290172430096E21A /* BacktraceCrashLoop.swift in Sources */, 6E896E99272767080005CDF2 /* Payload.swift in Sources */, F2AB63822246E16400939BC9 /* ReportingPolicy.swift in Sources */, F25F9E9B21EE84EA00236E04 /* BacktraceResult.swift in Sources */, @@ -2169,6 +2184,7 @@ F259E4D62229A40C00F282C7 /* Result.swift in Sources */, 2846E1FE223070CB0035F98C /* Attachment.swift in Sources */, F2D7122421F10E78002D2A26 /* BacktraceCredentials.swift in Sources */, + B51F3E30290172430096E21A /* BacktraceCrashLoop.swift in Sources */, 6E896E912727627C0005CDF2 /* BacktraceMetrics.swift in Sources */, 6EB713EC275ED4EF0075D1C1 /* SummedEventsPayload.swift in Sources */, 28A652F2285C6C1500306631 /* BacktraceBreadcrumbsLogManager.swift in Sources */, diff --git a/Examples/Example-iOS/AppDelegate.swift b/Examples/Example-iOS/AppDelegate.swift index 4ce2ee73..8bc2df65 100644 --- a/Examples/Example-iOS/AppDelegate.swift +++ b/Examples/Example-iOS/AppDelegate.swift @@ -25,9 +25,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { let isSafeModeRequired = BacktraceClient.isSafeModeRequired() if isSafeModeRequired { - // Remove current crash file for not to be trapped in a loop of crash detections - BacktraceClient.resetCrashLoopDetection() - // Perform custom checks if necessary and decide if Backtrace should be launched + // TODO: Perform any custom checks if necessary and decide if Backtrace should be launched return true } diff --git a/Examples/Example-macOS-ObjC/AppDelegate.m b/Examples/Example-macOS-ObjC/AppDelegate.m index d8f7987a..cd25d714 100644 --- a/Examples/Example-macOS-ObjC/AppDelegate.m +++ b/Examples/Example-macOS-ObjC/AppDelegate.m @@ -12,9 +12,7 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { [BacktraceClient enableCrashLoopDetection: 0]; if([BacktraceClient isSafeModeRequired]) { - // Remove current crash file for not to be trapped in a loop of crash detections - [BacktraceClient resetCrashLoopDetection]; - // Perform custom checks if necessary and decide if Backtrace should be launched + // TODO: Perform any custom checks if necessary and decide if Backtrace should be launched return; } diff --git a/Examples/Example-tvOS/AppDelegate.swift b/Examples/Example-tvOS/AppDelegate.swift index 149ebadf..1c019899 100644 --- a/Examples/Example-tvOS/AppDelegate.swift +++ b/Examples/Example-tvOS/AppDelegate.swift @@ -23,9 +23,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let isSafeModeRequired = BacktraceClient.isSafeModeRequired() if isSafeModeRequired { - // Remove current crash file for not to be trapped in a loop of crash detections - BacktraceClient.resetCrashLoopDetection() - // Perform custom checks if necessary and decide if Backtrace should be launched + // TODO: Perform any custom checks if necessary and decide if Backtrace should be launched return true } diff --git a/Sources/Public/BacktraceClient.swift b/Sources/Public/BacktraceClient.swift index 05334922..3f08fe8d 100644 --- a/Sources/Public/BacktraceClient.swift +++ b/Sources/Public/BacktraceClient.swift @@ -119,6 +119,8 @@ extension BacktraceClient { } @objc public static func enableCrashLoopDetection(_ threshold: Int = 0) { + BacktraceCrashLoopCounter.start() + crashLoopDetector = BacktraceCrashLoopDetector() crashLoopDetector?.updateThreshold(threshold) } @@ -127,7 +129,9 @@ extension BacktraceClient { crashLoopDetector = nil } - @objc public static func resetCrashLoopDetection() { + @available(*, deprecated, message: "Temporarily not needed") + @objc private static func resetCrashLoopDetection() { + BacktraceCrashLoopCounter.reset() crashLoopDetector?.deleteCrashReport() } diff --git a/Sources/Public/BacktraceCrashReporter.swift b/Sources/Public/BacktraceCrashReporter.swift index 5e04d504..91a03efd 100644 --- a/Sources/Public/BacktraceCrashReporter.swift +++ b/Sources/Public/BacktraceCrashReporter.swift @@ -28,6 +28,8 @@ extension BacktraceCrashReporter: CrashReporting { _ uContext: UnsafeMutablePointer?, _ context: UnsafeMutableRawPointer?) -> Void = { signalInfoPointer, _, context in BacktraceOomWatcher.clean() + BacktraceCrashLoopCounter.increment() + guard let attributesProvider = context?.assumingMemoryBound(to: SignalContext.self).pointee, let signalInfo = signalInfoPointer?.pointee else { return diff --git a/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoop.swift b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoop.swift new file mode 100644 index 00000000..f88ad39c --- /dev/null +++ b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoop.swift @@ -0,0 +1,23 @@ +// +// BacktraceCrashLoop.swift +// Backtrace +// + +import Foundation + +@objc internal class BacktraceCrashLoop: NSObject { + + @objc internal static func LogDebug(_ message: String = "") { + + /* Routed logging here to add prefix for more convenient filtering of + BTCLD logs in Xcode's outputs + */ + let prefix = "BT CL: " + + /* 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/Sources/Public/BacktraceCrashLoopCounter.swift b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopCounter.swift similarity index 68% rename from Sources/Public/BacktraceCrashLoopCounter.swift rename to Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopCounter.swift index 5eaf4224..b022073e 100644 --- a/Sources/Public/BacktraceCrashLoopCounter.swift +++ b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopCounter.swift @@ -5,18 +5,18 @@ import Foundation -class BacktraceCrashLoopCounter { +@objc internal class BacktraceCrashLoopCounter: BacktraceCrashLoop { static private var counter = 0 - + static internal func start() { checkFileExists() - BacktraceLogger.debug("Cache Dir: \(cacheDir())") - BacktraceLogger.debug("Crash Loop Counter File Path: \(filePath())") - load() + BacktraceCrashLoop.LogDebug("Cache Dir: \(cacheDir())") + BacktraceCrashLoop.LogDebug("Crash Loop Counter File Path: \(filePath())") + loadCounter() } - - static internal func load() { + + static internal func loadCounter() { guard let contents = try? String(contentsOfFile: filePath()) else { counter = 0 @@ -28,35 +28,37 @@ class BacktraceCrashLoopCounter { static internal func crashesCount() -> Int { return counter } - + static private func checkFileExists() { let path = filePath() - // We can use just 'createFile' to check if file exists, but, as per docs: - // "If a file already exists at path, this method overwrites the contents of that file - // if the current process has the appropriate privileges to do so." - // https://developer.apple.com/documentation/foundation/filemanager/1410695-createfile + /* + We can use just 'createFile' to check if file exists, but, as per docs: + "If a file already exists at path, this method overwrites the contents of that file + if the current process has the appropriate privileges to do so." + https://developer.apple.com/documentation/foundation/filemanager/1410695-createfile + */ if !FileManager.default.fileExists(atPath: path) { FileManager.default.createFile(atPath: path, contents: nil) // Write current counter reset() } } - + static internal func increment() { counter += 1 - save() + saveCounter() } - + static internal func reset() { counter = 0 - save() + saveCounter() } - - static private func save() { + + static private func saveCounter() { let contents = String(counter) try? contents.write(toFile: filePath(), atomically: true, encoding: .utf8) } - + static private func fileURL() -> URL { let cacheDir = cacheDir() let filePath = cacheDir.appendingPathComponent("BacktraceCrashLoopCounter.txt") diff --git a/Sources/Public/BacktraceCrashLoopDetector.swift b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift similarity index 60% rename from Sources/Public/BacktraceCrashLoopDetector.swift rename to Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift index ba2fdfff..1fa1b51e 100644 --- a/Sources/Public/BacktraceCrashLoopDetector.swift +++ b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift @@ -5,11 +5,22 @@ import Foundation -@objc public class BacktraceCrashLoopDetector: NSObject { +@objc internal class BacktraceCrashLoopDetector: BacktraceCrashLoop { internal struct StartUpEvent: Codable { + var uuid: String var timestamp: Double - var isSuccessful: Bool + var crashesCount: Int + + func description() -> String { + let string = """ + New Crash Loop Event:\n + UUID: \(uuid)\n + Timestamp: \(timestamp)\n + Crashes Count: \(crashesCount) + """ + return string + } } @objc private static let plistKey = "CrashLoopDetectorData" @@ -19,11 +30,31 @@ import Foundation @objc private var threshold = consecutiveCrashesThreshold internal var startupEvents: [StartUpEvent] = [] - + + @objc internal func updateThreshold(_ threshold: Int) { self.threshold = threshold == 0 ? BacktraceCrashLoopDetector.consecutiveCrashesThreshold : threshold } + @objc internal func detectCrashloop() -> Bool { + + BacktraceCrashLoop.LogDebug("Starting Crash Loop Detection") + + loadEvents() + addCurrentEvent() + saveEvents() + + let count = badEventsCount() + + /* + true -> crash loop detected -> set safe mode + false -> crash loop NOT detected -> set normal mode + */ + let result = count >= BacktraceCrashLoopDetector.consecutiveCrashesThreshold + BacktraceCrashLoop.LogDebug("Finishing Crash Loop Detection: \(result)") + return result + } + @objc private func loadEvents() { // Cleanup old events - f.e, for multiple usages of detector @@ -47,92 +78,87 @@ import Foundation else { return } startupEvents.append(contentsOf: array) - BacktraceLogger.debug("Events Loaded: \(startupEvents.count)") + BacktraceCrashLoopDetector.LogDebug("Events Loaded: \(startupEvents.count)") } @objc internal func saveEvents() { let data = try? PropertyListEncoder().encode(startupEvents) UserDefaults.standard.set(data, forKey: BacktraceCrashLoopDetector.plistKey) - BacktraceLogger.debug("Events Saved: \(startupEvents.count)") + BacktraceCrashLoopDetector.LogDebug("Events Saved: \(startupEvents.count)") } - @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 + @objc internal func addCurrentEvent() { - 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: "") - BacktraceLogger.debug("reportFullPath: \(reportFullPath)") - return reportFullPath - } - - @objc private func hasCrashReport() -> Bool { - let exists = FileManager.default.fileExists(atPath: reportFilePath()) - return exists - } - - @objc private func addCurrentEvent() { + let crashesCount = BacktraceCrashLoopCounter.crashesCount() + let event = StartUpEvent(uuid: UUID().uuidString, + timestamp: Double(Date.timeIntervalSinceReferenceDate), + crashesCount: crashesCount) - var event = StartUpEvent(timestamp: Double(Date.timeIntervalSinceReferenceDate), isSuccessful: true) - event.isSuccessful = !hasCrashReport() - BacktraceLogger.debug("New Event: {timestamp:\(event.timestamp)--successful:\(event.isSuccessful)}") + BacktraceCrashLoop.LogDebug(event.description()) startupEvents.append(event) if startupEvents.count > BacktraceCrashLoopDetector.consecutiveCrashesThreshold { - startupEvents.remove(at: 0) + startupEvents.removeFirst() } - BacktraceLogger.debug("Event Added: \(startupEvents.count)") + BacktraceCrashLoop.LogDebug("Event Added: \(startupEvents.count)") } - @objc private func badEventsCount() -> Int { + @objc internal func badEventsCount() -> Int { var badEventsCount = 0 for event in startupEvents.reversed() { - if event.isSuccessful { + if event.crashesCount > 0 { break } badEventsCount += 1 } + self.consecutiveCrashesCount = badEventsCount - BacktraceLogger.debug("Bad Events Count: \(badEventsCount)") + BacktraceCrashLoop.LogDebug("Bad Events Count: \(badEventsCount)") return badEventsCount } +} - @objc func detectCrashloop() -> Bool { +extension BacktraceCrashLoopDetector { - BacktraceLogger.debug("Starting Crash Loop Detection") - - loadEvents() + // MARK: Deprecated methods - addCurrentEvent() + @available(*, deprecated, message: "Temporarily not needed") + @objc internal func reportFilePath() -> String { - let count = badEventsCount() - saveEvents() + /* 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: "_") - // true -> crash loop detected -> set safe mode - // false -> crash loop NOT detected -> set normal mode - let result = count >= BacktraceCrashLoopDetector.consecutiveCrashesThreshold - BacktraceLogger.debug("Finishing Crash Loop Detection: \(result)") - return result + 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: "") + BacktraceCrashLoop.LogDebug("reportFullPath: \(reportFullPath)") + return reportFullPath + } + + @available(*, deprecated, message: "Temporarily not needed") + @objc internal func hasCrashReport() -> Bool { + let exists = FileManager.default.fileExists(atPath: reportFilePath()) + return exists } - @objc func deleteCrashReport() { + @available(*, deprecated, message: "Temporarily not needed") + @objc internal func deleteCrashReport() { let path = reportFilePath() let fileURL = URL(fileURLWithPath: path) try? FileManager.default.removeItem(at: fileURL) From 0c3e4863a9b372ed0d204b65b5254e3d8fbf9eaf Mon Sep 17 00:00:00 2001 From: Konstantin R <111510241+konst-sauce@users.noreply.github.com> Date: Thu, 20 Oct 2022 16:36:26 +0200 Subject: [PATCH 12/17] Updated crash loop detector algorithm, polished names, added comments. --- Examples/Example-iOS/AppDelegate.swift | 5 ++- Examples/Example-macOS-ObjC/AppDelegate.m | 2 + Examples/Example-tvOS/AppDelegate.swift | 5 ++- Sources/Public/BacktraceClient.swift | 7 ++- .../BacktraceCrashLoopDetector.swift | 43 +++++++++++-------- 5 files changed, 41 insertions(+), 21 deletions(-) diff --git a/Examples/Example-iOS/AppDelegate.swift b/Examples/Example-iOS/AppDelegate.swift index 8bc2df65..86e9e74b 100644 --- a/Examples/Example-iOS/AppDelegate.swift +++ b/Examples/Example-iOS/AppDelegate.swift @@ -19,7 +19,10 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Enable crash loop detector, it crashes count threshold is not specified - default will be used + /* 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() let isSafeModeRequired = BacktraceClient.isSafeModeRequired() diff --git a/Examples/Example-macOS-ObjC/AppDelegate.m b/Examples/Example-macOS-ObjC/AppDelegate.m index cd25d714..6e18df0f 100644 --- a/Examples/Example-macOS-ObjC/AppDelegate.m +++ b/Examples/Example-macOS-ObjC/AppDelegate.m @@ -10,7 +10,9 @@ @implementation AppDelegate - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + // Enable crash loop detector, pass 0 to use default threshold value '5' [BacktraceClient enableCrashLoopDetection: 0]; + if([BacktraceClient isSafeModeRequired]) { // TODO: Perform any custom checks if necessary and decide if Backtrace should be launched return; diff --git a/Examples/Example-tvOS/AppDelegate.swift b/Examples/Example-tvOS/AppDelegate.swift index 1c019899..f910e0b4 100644 --- a/Examples/Example-tvOS/AppDelegate.swift +++ b/Examples/Example-tvOS/AppDelegate.swift @@ -17,7 +17,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Enable crash loop detector, it crashes count threshold is not specified - default will be used + /* 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() let isSafeModeRequired = BacktraceClient.isSafeModeRequired() diff --git a/Sources/Public/BacktraceClient.swift b/Sources/Public/BacktraceClient.swift index 3f08fe8d..cb7f4ffd 100644 --- a/Sources/Public/BacktraceClient.swift +++ b/Sources/Public/BacktraceClient.swift @@ -99,7 +99,7 @@ import Foundation } } -// MARK: - BacktraceClient Safe Mode public API +// MARK: - BacktraceClient Safe Mode public API (crash loop detection) extension BacktraceClient { @objc public static func enableSafeMode() { @@ -144,6 +144,11 @@ extension BacktraceClient { @objc public static func consecutiveCrashesCount() -> Int { return crashLoopDetector?.consecutiveCrashesCount ?? 0 } + + // Added for testing without debugging purposes + @objc public static func crashLoopEventsDatabase() -> String { + return crashLoopDetector?.databaseDescription() ?? "Not enabled" + } } // MARK: - BacktraceClientProviding diff --git a/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift index 1fa1b51e..aa3f4853 100644 --- a/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift +++ b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift @@ -41,23 +41,23 @@ import Foundation BacktraceCrashLoop.LogDebug("Starting Crash Loop Detection") loadEvents() - addCurrentEvent() + addStartupEvent() saveEvents() - let count = badEventsCount() + consecutiveCrashesCount = consecutiveEventsCount() /* true -> crash loop detected -> set safe mode false -> crash loop NOT detected -> set normal mode */ - let result = count >= BacktraceCrashLoopDetector.consecutiveCrashesThreshold + let result = consecutiveCrashesCount >= BacktraceCrashLoopDetector.consecutiveCrashesThreshold BacktraceCrashLoop.LogDebug("Finishing Crash Loop Detection: \(result)") return result } @objc private func loadEvents() { - // Cleanup old events - f.e, for multiple usages of detector + // Cleanup old events - f.e. for multiple usages of detector startupEvents.removeAll() /* @@ -87,7 +87,7 @@ import Foundation BacktraceCrashLoopDetector.LogDebug("Events Saved: \(startupEvents.count)") } - @objc internal func addCurrentEvent() { + @objc internal func addStartupEvent() { let crashesCount = BacktraceCrashLoopCounter.crashesCount() let event = StartUpEvent(uuid: UUID().uuidString, @@ -102,29 +102,36 @@ import Foundation startupEvents.removeFirst() } - BacktraceCrashLoop.LogDebug("Event Added: \(startupEvents.count)") + BacktraceCrashLoop.LogDebug("Startup Event Added: \(startupEvents.count)") } - @objc internal func badEventsCount() -> Int { + @objc internal func consecutiveEventsCount() -> Int { - var badEventsCount = 0 - for event in startupEvents.reversed() { - if event.crashesCount > 0 { - break + var count = 0 + var previousValue = 0 + for event in startupEvents { + if event.crashesCount > previousValue { + count += 1 } - badEventsCount += 1 + previousValue = event.crashesCount } - - self.consecutiveCrashesCount = badEventsCount - BacktraceCrashLoop.LogDebug("Bad Events Count: \(badEventsCount)") - return badEventsCount + + BacktraceCrashLoop.LogDebug("Consecutive Events Count: \(count)") + return count + } + + @objc internal func databaseDescription() -> String { + var string = "" + for event in startupEvents { + string += event.description() + "\n" + } + return string } } +// MARK: Deprecated methods extension BacktraceCrashLoopDetector { - // MARK: Deprecated methods - @available(*, deprecated, message: "Temporarily not needed") @objc internal func reportFilePath() -> String { From d0d63d6fa9a9c9ad4345e13604ce648e26d5ad8d Mon Sep 17 00:00:00 2001 From: Konstantin R <111510241+konst-sauce@users.noreply.github.com> Date: Wed, 18 Jan 2023 20:09:25 +0100 Subject: [PATCH 13/17] Final polishes - updated CLD algorithm, updated all examples to utilise CLD functionality --- Examples/Example-iOS-ObjC/AppDelegate.m | 27 ++++++++++-------- Examples/Example-iOS-ObjC/Info.plist | 4 +-- Examples/Example-iOS-ObjC/ViewController.m | 5 ++++ Examples/Example-iOS/AppDelegate.swift | 6 ++-- Examples/Example-macOS-ObjC/AppDelegate.m | 2 ++ .../Base.lproj/Main.storyboard | 2 +- Examples/Example-macOS-ObjC/ViewController.m | 28 +++++++++++++------ Examples/Example-tvOS/AppDelegate.swift | 6 ++-- Sources/Public/BacktraceClient.swift | 5 ++-- .../BacktraceCrashLoopDetector.swift | 8 +++++- 10 files changed, 59 insertions(+), 34 deletions(-) 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/Info.plist b/Examples/Example-iOS-ObjC/Info.plist index 6c7276ff..ba316170 100644 --- a/Examples/Example-iOS-ObjC/Info.plist +++ b/Examples/Example-iOS-ObjC/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - $(MARKETING_VERSION) + 1.0 CFBundleVersion - $(CURRENT_PROJECT_VERSION) + 1.0 LSRequiresIPhoneOS UILaunchStoryboardName 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 86e9e74b..26d3ac34 100644 --- a/Examples/Example-iOS/AppDelegate.swift +++ b/Examples/Example-iOS/AppDelegate.swift @@ -25,9 +25,9 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { */ BacktraceClient.enableCrashLoopDetection() - let isSafeModeRequired = BacktraceClient.isSafeModeRequired() - - if isSafeModeRequired { + 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 } diff --git a/Examples/Example-macOS-ObjC/AppDelegate.m b/Examples/Example-macOS-ObjC/AppDelegate.m index 6e18df0f..8ed96b61 100644 --- a/Examples/Example-macOS-ObjC/AppDelegate.m +++ b/Examples/Example-macOS-ObjC/AppDelegate.m @@ -14,6 +14,8 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { [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; } diff --git a/Examples/Example-macOS-ObjC/Base.lproj/Main.storyboard b/Examples/Example-macOS-ObjC/Base.lproj/Main.storyboard index d025cd2c..c4cf964e 100644 --- a/Examples/Example-macOS-ObjC/Base.lproj/Main.storyboard +++ b/Examples/Example-macOS-ObjC/Base.lproj/Main.storyboard @@ -715,7 +715,7 @@ - + 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 f910e0b4..216dffe7 100644 --- a/Examples/Example-tvOS/AppDelegate.swift +++ b/Examples/Example-tvOS/AppDelegate.swift @@ -23,9 +23,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { */ BacktraceClient.enableCrashLoopDetection() - let isSafeModeRequired = BacktraceClient.isSafeModeRequired() - - if isSafeModeRequired { + 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 } diff --git a/Sources/Public/BacktraceClient.swift b/Sources/Public/BacktraceClient.swift index cb7f4ffd..1bbdcf4c 100644 --- a/Sources/Public/BacktraceClient.swift +++ b/Sources/Public/BacktraceClient.swift @@ -129,10 +129,9 @@ extension BacktraceClient { crashLoopDetector = nil } - @available(*, deprecated, message: "Temporarily not needed") - @objc private static func resetCrashLoopDetection() { + @objc public static func resetCrashLoopDetection() { BacktraceCrashLoopCounter.reset() - crashLoopDetector?.deleteCrashReport() + crashLoopDetector?.clearStartupEvents() } @objc public static func isSafeModeRequired() -> Bool { diff --git a/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift index aa3f4853..05ebdfec 100644 --- a/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift +++ b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift @@ -76,7 +76,7 @@ import Foundation guard let array = try? PropertyListDecoder().decode([StartUpEvent].self, from: data) else { return } - + startupEvents.append(contentsOf: array) BacktraceCrashLoopDetector.LogDebug("Events Loaded: \(startupEvents.count)") } @@ -105,6 +105,12 @@ import Foundation BacktraceCrashLoop.LogDebug("Startup Event Added: \(startupEvents.count)") } + @objc internal func clearStartupEvents() { + startupEvents.removeAll() + saveEvents() + BacktraceCrashLoop.LogDebug("Startup Events Cleared: \(startupEvents.count)") + } + @objc internal func consecutiveEventsCount() -> Int { var count = 0 From c08a9719417547abe0183e1879be98335068ad6a Mon Sep 17 00:00:00 2001 From: Konstantin R <111510241+konst-sauce@users.noreply.github.com> Date: Mon, 30 Jan 2023 15:45:46 +0100 Subject: [PATCH 14/17] Polished for comments in PR --- Examples/Example-iOS-ObjC/AppDelegate.m | 3 +++ Examples/Example-iOS-ObjC/Info.plist | 4 ++-- Examples/Example-iOS/AppDelegate.swift | 3 +++ Examples/Example-macOS-ObjC/AppDelegate.m | 5 ++++- Examples/Example-tvOS/AppDelegate.swift | 3 +++ Sources/Public/BacktraceClient.swift | 4 +--- .../Internal/CrashLoopDetector/BacktraceCrashLoop.swift | 2 +- .../CrashLoopDetector/BacktraceCrashLoopCounter.swift | 1 - .../CrashLoopDetector/BacktraceCrashLoopDetector.swift | 9 ++++----- 9 files changed, 21 insertions(+), 13 deletions(-) diff --git a/Examples/Example-iOS-ObjC/AppDelegate.m b/Examples/Example-iOS-ObjC/AppDelegate.m index 763d68bf..08977af2 100644 --- a/Examples/Example-iOS-ObjC/AppDelegate.m +++ b/Examples/Example-iOS-ObjC/AppDelegate.m @@ -24,6 +24,9 @@ You can pass crashes count threshold (maximum amount of launching events to eval // TODO: Perform any custom checks if necessary and decide if Backtrace should be launched return NO; } + else { + [BacktraceClient disableCrashLoopDetection]; + } NSArray *paths = @[[[NSBundle mainBundle] pathForResource: @"test" ofType: @"txt"]]; NSString *fileName = @"myCustomFile.txt"; diff --git a/Examples/Example-iOS-ObjC/Info.plist b/Examples/Example-iOS-ObjC/Info.plist index ba316170..6c7276ff 100644 --- a/Examples/Example-iOS-ObjC/Info.plist +++ b/Examples/Example-iOS-ObjC/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + $(MARKETING_VERSION) CFBundleVersion - 1.0 + $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/Examples/Example-iOS/AppDelegate.swift b/Examples/Example-iOS/AppDelegate.swift index 26d3ac34..d5b4bedf 100644 --- a/Examples/Example-iOS/AppDelegate.swift +++ b/Examples/Example-iOS/AppDelegate.swift @@ -31,6 +31,9 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { // TODO: Perform any custom checks if necessary and decide if Backtrace should be launched return true } + else { + BacktraceClient.disableCrashLoopDetection() + } let backtraceCredentials = BacktraceCredentials(endpoint: URL(string: Keys.backtraceUrl as String)!, token: Keys.backtraceToken as String) diff --git a/Examples/Example-macOS-ObjC/AppDelegate.m b/Examples/Example-macOS-ObjC/AppDelegate.m index 8ed96b61..bedd6ad2 100644 --- a/Examples/Example-macOS-ObjC/AppDelegate.m +++ b/Examples/Example-macOS-ObjC/AppDelegate.m @@ -19,7 +19,10 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // TODO: Perform any custom checks if necessary and decide if Backtrace should be launched return; } - + else { + [BacktraceClient disableCrashLoopDetection]; + } + BacktraceCredentials *credentials = [[BacktraceCredentials alloc] initWithSubmissionUrl: [NSURL URLWithString: Keys.backtraceSubmissionUrl]]; BacktraceDatabaseSettings *backtraceDatabaseSettings = [[BacktraceDatabaseSettings alloc] init]; diff --git a/Examples/Example-tvOS/AppDelegate.swift b/Examples/Example-tvOS/AppDelegate.swift index 216dffe7..8dd1e9a3 100644 --- a/Examples/Example-tvOS/AppDelegate.swift +++ b/Examples/Example-tvOS/AppDelegate.swift @@ -29,6 +29,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // TODO: Perform any custom checks if necessary and decide if Backtrace should be launched return true } + else { + BacktraceClient.disableCrashLoopDetection() + } let backtraceCredentials = BacktraceCredentials(endpoint: URL(string: Keys.backtraceUrl as String)!, token: Keys.backtraceToken as String) diff --git a/Sources/Public/BacktraceClient.swift b/Sources/Public/BacktraceClient.swift index 1bbdcf4c..05e4d8ef 100644 --- a/Sources/Public/BacktraceClient.swift +++ b/Sources/Public/BacktraceClient.swift @@ -120,9 +120,7 @@ extension BacktraceClient { @objc public static func enableCrashLoopDetection(_ threshold: Int = 0) { BacktraceCrashLoopCounter.start() - - crashLoopDetector = BacktraceCrashLoopDetector() - crashLoopDetector?.updateThreshold(threshold) + crashLoopDetector = BacktraceCrashLoopDetector(threshold) } @objc public static func disableCrashLoopDetection() { diff --git a/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoop.swift b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoop.swift index f88ad39c..18f3dcce 100644 --- a/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoop.swift +++ b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoop.swift @@ -12,7 +12,7 @@ import Foundation /* Routed logging here to add prefix for more convenient filtering of BTCLD logs in Xcode's outputs */ - let prefix = "BT CL: " + 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 diff --git a/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopCounter.swift b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopCounter.swift index b022073e..4213b674 100644 --- a/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopCounter.swift +++ b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopCounter.swift @@ -11,7 +11,6 @@ import Foundation static internal func start() { checkFileExists() - BacktraceCrashLoop.LogDebug("Cache Dir: \(cacheDir())") BacktraceCrashLoop.LogDebug("Crash Loop Counter File Path: \(filePath())") loadCounter() } diff --git a/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift index 05ebdfec..467bfbc3 100644 --- a/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift +++ b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift @@ -27,15 +27,14 @@ import Foundation @objc internal static let consecutiveCrashesThreshold = 5 @objc private(set) var consecutiveCrashesCount = 0 - @objc private var threshold = consecutiveCrashesThreshold + @objc private var threshold = 0 internal var startupEvents: [StartUpEvent] = [] - - @objc internal func updateThreshold(_ threshold: Int) { + init(_ threshold: Int) { self.threshold = threshold == 0 ? BacktraceCrashLoopDetector.consecutiveCrashesThreshold : threshold } - + @objc internal func detectCrashloop() -> Bool { BacktraceCrashLoop.LogDebug("Starting Crash Loop Detection") @@ -51,7 +50,7 @@ import Foundation false -> crash loop NOT detected -> set normal mode */ let result = consecutiveCrashesCount >= BacktraceCrashLoopDetector.consecutiveCrashesThreshold - BacktraceCrashLoop.LogDebug("Finishing Crash Loop Detection: \(result)") + BacktraceCrashLoop.LogDebug("Finishing Crash Loop Detection: Is in the crash loop - \(result)") return result } From 35258e3b5f9ac49c672204502ed4ee78414fc478 Mon Sep 17 00:00:00 2001 From: Konstantin R <111510241+konst-sauce@users.noreply.github.com> Date: Mon, 6 Feb 2023 16:25:37 +0100 Subject: [PATCH 15/17] backtrace.version attribute fix --- Sources/Features/Attributes/DefaultAttributes.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 742e02180378d0424408fcfa273add6e67786409 Mon Sep 17 00:00:00 2001 From: Konstantin R <111510241+konst-sauce@users.noreply.github.com> Date: Mon, 13 Feb 2023 17:40:57 +0100 Subject: [PATCH 16/17] CLD algorithm polished. Some PR comments utilised. --- Backtrace.xcodeproj/project.pbxproj | 16 --- Examples/Example-iOS/AppDelegate.swift | 5 +- Sources/Public/BacktraceClient.swift | 39 ++++--- Sources/Public/BacktraceCrashReporter.swift | 1 - .../BacktraceCrashLoop.swift | 23 ---- .../BacktraceCrashLoopCounter.swift | 78 ------------- .../BacktraceCrashLoopDetector.swift | 107 +++++++++++------- 7 files changed, 89 insertions(+), 180 deletions(-) delete mode 100644 Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoop.swift delete mode 100644 Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopCounter.swift diff --git a/Backtrace.xcodeproj/project.pbxproj b/Backtrace.xcodeproj/project.pbxproj index 4246266c..56013da6 100644 --- a/Backtrace.xcodeproj/project.pbxproj +++ b/Backtrace.xcodeproj/project.pbxproj @@ -182,12 +182,6 @@ 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 */; }; - B51F3E2C28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F3E2B28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift */; }; - B51F3E2D28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F3E2B28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift */; }; - B51F3E2E28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F3E2B28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift */; }; - B51F3E30290172430096E21A /* BacktraceCrashLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F3E2F290172430096E21A /* BacktraceCrashLoop.swift */; }; - B51F3E31290172430096E21A /* BacktraceCrashLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F3E2F290172430096E21A /* BacktraceCrashLoop.swift */; }; - B51F3E32290172430096E21A /* BacktraceCrashLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F3E2F290172430096E21A /* BacktraceCrashLoop.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 */; }; @@ -462,8 +456,6 @@ 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 = ""; }; - B51F3E2B28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceCrashLoopCounter.swift; sourceTree = ""; }; - B51F3E2F290172430096E21A /* BacktraceCrashLoop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceCrashLoop.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; }; @@ -737,9 +729,7 @@ B51F3E33290173650096E21A /* CrashLoopDetector */ = { isa = PBXGroup; children = ( - B51F3E2F290172430096E21A /* BacktraceCrashLoop.swift */, B5E58C6828E1A843001F9650 /* BacktraceCrashLoopDetector.swift */, - B51F3E2B28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift */, ); path = CrashLoopDetector; sourceTree = ""; @@ -1967,7 +1957,6 @@ 28F95BEE225260D5003936E0 /* NetworkReachability.swift in Sources */, 6EB713FA276294160075D1C1 /* MetricsRequest.swift in Sources */, 6EB713F627617ED00075D1C1 /* BacktraceMetricsContainer.swift in Sources */, - B51F3E2E28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift in Sources */, 28F95BC822526023003936E0 /* URLSession+Sync.swift in Sources */, 28F95BED225260D3003936E0 /* AttributesProvider.swift in Sources */, 28F95BDB22526088003936E0 /* SignalContext.swift in Sources */, @@ -1988,7 +1977,6 @@ 28F95BE2225260A2003936E0 /* BacktraceApi.swift in Sources */, 28F95BDC2252608B003936E0 /* Result.swift in Sources */, F2A81B4F23EF1730007C63E4 /* BacktraceApiProtocol.swift in Sources */, - B51F3E32290172430096E21A /* BacktraceCrashLoop.swift in Sources */, F29959AD22553B340085B5C3 /* Model.xcdatamodeld in Sources */, 28F95BE3225260A5003936E0 /* HttpMethod.swift in Sources */, 28F95BD22252606B003936E0 /* BacktraceDatabaseSettings.swift in Sources */, @@ -2061,7 +2049,6 @@ F2AFB5922225E5D000AAA1D7 /* Foundation+Extensions.swift in Sources */, F26EBF3D23F21BCB00A64218 /* BacktraceNetworkClient.swift in Sources */, 28AC7740220A2A3300FED661 /* MultipartRequest.swift in Sources */, - B51F3E31290172430096E21A /* BacktraceCrashLoop.swift in Sources */, 6E896E99272767080005CDF2 /* Payload.swift in Sources */, F2AB63822246E16400939BC9 /* ReportingPolicy.swift in Sources */, F25F9E9B21EE84EA00236E04 /* BacktraceResult.swift in Sources */, @@ -2102,7 +2089,6 @@ F28F165921E2A0DA008E4B96 /* URLSession+Sync.swift in Sources */, F2AB636E22442B5100939BC9 /* DebuggerChecker.swift in Sources */, F286353B2283685100F45412 /* Map+KeyPath.swift in Sources */, - B51F3E2D28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2180,11 +2166,9 @@ 28966EFA2214BBD200E6E891 /* AttributesStorage.swift in Sources */, 6EB713F8276294160075D1C1 /* MetricsRequest.swift in Sources */, 6EB713F427617ED00075D1C1 /* BacktraceMetricsContainer.swift in Sources */, - B51F3E2C28F6FA8F0096E21A /* BacktraceCrashLoopCounter.swift in Sources */, F259E4D62229A40C00F282C7 /* Result.swift in Sources */, 2846E1FE223070CB0035F98C /* Attachment.swift in Sources */, F2D7122421F10E78002D2A26 /* BacktraceCredentials.swift in Sources */, - B51F3E30290172430096E21A /* BacktraceCrashLoop.swift in Sources */, 6E896E912727627C0005CDF2 /* BacktraceMetrics.swift in Sources */, 6EB713EC275ED4EF0075D1C1 /* SummedEventsPayload.swift in Sources */, 28A652F2285C6C1500306631 /* BacktraceBreadcrumbsLogManager.swift in Sources */, diff --git a/Examples/Example-iOS/AppDelegate.swift b/Examples/Example-iOS/AppDelegate.swift index d5b4bedf..d1591a8d 100644 --- a/Examples/Example-iOS/AppDelegate.swift +++ b/Examples/Example-iOS/AppDelegate.swift @@ -28,12 +28,9 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { 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 + // Perform any custom checks if necessary and decide if Backtrace should be launched return true } - else { - BacktraceClient.disableCrashLoopDetection() - } let backtraceCredentials = BacktraceCredentials(endpoint: URL(string: Keys.backtraceUrl as String)!, token: Keys.backtraceToken as String) diff --git a/Sources/Public/BacktraceClient.swift b/Sources/Public/BacktraceClient.swift index 05e4d8ef..46a53e9d 100644 --- a/Sources/Public/BacktraceClient.swift +++ b/Sources/Public/BacktraceClient.swift @@ -28,8 +28,6 @@ import Foundation private let dispatcher: Dispatching private let reportingPolicy: ReportingPolicy - private static var crashLoopDetector: BacktraceCrashLoopDetector? - /// Initialize `BacktraceClient` with credentials. To learn more about credentials, see /// https://help.backtrace.io/troubleshooting/what-is-a-submission-url /// and https://help.backtrace.io/troubleshooting/what-is-a-submission-token . @@ -117,34 +115,35 @@ extension BacktraceClient { @objc public static func isInSafeMode() -> Bool { return workingMode == .safe } - + @objc public static func enableCrashLoopDetection(_ threshold: Int = 0) { - BacktraceCrashLoopCounter.start() - crashLoopDetector = BacktraceCrashLoopDetector(threshold) - } - - @objc public static func disableCrashLoopDetection() { - crashLoopDetector = nil + BacktraceCrashLoopDetector.instance.updateThreshold(threshold) + + let isInCrashLoop = BacktraceCrashLoopDetector.instance.detectCrashloop() + + if isInCrashLoop { + enableSafeMode() + } + else { + disableSafeMode() + } } - + @objc public static func resetCrashLoopDetection() { - BacktraceCrashLoopCounter.reset() - crashLoopDetector?.clearStartupEvents() + BacktraceCrashLoopDetector.instance.clearStartupEvents() } - + @objc public static func isSafeModeRequired() -> Bool { - let isInCrashLoop = crashLoopDetector?.detectCrashloop() ?? false - if isInCrashLoop { enableSafeMode() } - return isInCrashLoop + return workingMode == .safe } - + @objc public static func consecutiveCrashesCount() -> Int { - return crashLoopDetector?.consecutiveCrashesCount ?? 0 + return BacktraceCrashLoopDetector.instance.consecutiveCrashesCount } - + // Added for testing without debugging purposes @objc public static func crashLoopEventsDatabase() -> String { - return crashLoopDetector?.databaseDescription() ?? "Not enabled" + return BacktraceCrashLoopDetector.instance.databaseDescription() } } diff --git a/Sources/Public/BacktraceCrashReporter.swift b/Sources/Public/BacktraceCrashReporter.swift index 91a03efd..6e30a201 100644 --- a/Sources/Public/BacktraceCrashReporter.swift +++ b/Sources/Public/BacktraceCrashReporter.swift @@ -28,7 +28,6 @@ extension BacktraceCrashReporter: CrashReporting { _ uContext: UnsafeMutablePointer?, _ context: UnsafeMutableRawPointer?) -> Void = { signalInfoPointer, _, context in BacktraceOomWatcher.clean() - BacktraceCrashLoopCounter.increment() guard let attributesProvider = context?.assumingMemoryBound(to: SignalContext.self).pointee, let signalInfo = signalInfoPointer?.pointee else { diff --git a/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoop.swift b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoop.swift deleted file mode 100644 index 18f3dcce..00000000 --- a/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoop.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// BacktraceCrashLoop.swift -// Backtrace -// - -import Foundation - -@objc internal class BacktraceCrashLoop: NSObject { - - @objc internal static func LogDebug(_ 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/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopCounter.swift b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopCounter.swift deleted file mode 100644 index 4213b674..00000000 --- a/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopCounter.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// BacktraceCrashLoopCounter.swift -// Backtrace -// - -import Foundation - -@objc internal class BacktraceCrashLoopCounter: BacktraceCrashLoop { - - static private var counter = 0 - - static internal func start() { - checkFileExists() - BacktraceCrashLoop.LogDebug("Crash Loop Counter File Path: \(filePath())") - loadCounter() - } - - static internal func loadCounter() { - guard let contents = try? String(contentsOfFile: filePath()) - else { - counter = 0 - return - } - counter = Int(contents) ?? 0 - } - - static internal func crashesCount() -> Int { - return counter - } - - static private func checkFileExists() { - let path = filePath() - /* - We can use just 'createFile' to check if file exists, but, as per docs: - "If a file already exists at path, this method overwrites the contents of that file - if the current process has the appropriate privileges to do so." - https://developer.apple.com/documentation/foundation/filemanager/1410695-createfile - */ - if !FileManager.default.fileExists(atPath: path) { - FileManager.default.createFile(atPath: path, contents: nil) - // Write current counter - reset() - } - } - - static internal func increment() { - counter += 1 - saveCounter() - } - - static internal func reset() { - counter = 0 - saveCounter() - } - - static private func saveCounter() { - let contents = String(counter) - try? contents.write(toFile: filePath(), atomically: true, encoding: .utf8) - } - - static private func fileURL() -> URL { - let cacheDir = cacheDir() - let filePath = cacheDir.appendingPathComponent("BacktraceCrashLoopCounter.txt") - return filePath - } - - static private func filePath() -> String { - let filePath = fileURL().absoluteString - .replacingOccurrences(of: "file://", with: "") - return filePath - } - - static private func cacheDir() -> URL { - let paths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true) - let cacheDir = URL(fileURLWithPath: paths.isEmpty ? "" : paths[0]) - return cacheDir - } -} diff --git a/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift index 467bfbc3..bdbc2b51 100644 --- a/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift +++ b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift @@ -5,24 +5,26 @@ import Foundation -@objc internal class BacktraceCrashLoopDetector: BacktraceCrashLoop { - +@objc internal class BacktraceCrashLoopDetector: NSObject { + internal struct StartUpEvent: Codable { var uuid: String - var timestamp: Double - var crashesCount: Int + var eventTimestamp: Double + var reportCreationTimestamp: Double func description() -> String { let string = """ - New Crash Loop Event:\n - UUID: \(uuid)\n - Timestamp: \(timestamp)\n - Crashes Count: \(crashesCount) + 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 @@ -31,26 +33,24 @@ import Foundation internal var startupEvents: [StartUpEvent] = [] - init(_ threshold: Int) { + override private init() { + } + + @objc internal func updateThreshold(_ threshold: Int) { self.threshold = threshold == 0 ? BacktraceCrashLoopDetector.consecutiveCrashesThreshold : threshold } @objc internal func detectCrashloop() -> Bool { - BacktraceCrashLoop.LogDebug("Starting Crash Loop Detection") + CLDLogDebug("Starting Crash Loop Detection") loadEvents() - addStartupEvent() - saveEvents() + addEvent() consecutiveCrashesCount = consecutiveEventsCount() - /* - true -> crash loop detected -> set safe mode - false -> crash loop NOT detected -> set normal mode - */ let result = consecutiveCrashesCount >= BacktraceCrashLoopDetector.consecutiveCrashesThreshold - BacktraceCrashLoop.LogDebug("Finishing Crash Loop Detection: Is in the crash loop - \(result)") + CLDLogDebug("Finishing Crash Loop Detection: Is in the crash loop - \(result)") return result } @@ -77,51 +77,58 @@ import Foundation else { return } startupEvents.append(contentsOf: array) - BacktraceCrashLoopDetector.LogDebug("Events Loaded: \(startupEvents.count)") + CLDLogDebug("Events Loaded: \(startupEvents.count)") } - @objc internal func saveEvents() { + @objc private func saveEvents() { let data = try? PropertyListEncoder().encode(startupEvents) UserDefaults.standard.set(data, forKey: BacktraceCrashLoopDetector.plistKey) - BacktraceCrashLoopDetector.LogDebug("Events Saved: \(startupEvents.count)") + CLDLogDebug("Events Saved: \(startupEvents.count)") } - @objc internal func addStartupEvent() { + @objc private func addEvent() { - let crashesCount = BacktraceCrashLoopCounter.crashesCount() + let reportTime = reportFileCreationTime() + let event = StartUpEvent(uuid: UUID().uuidString, - timestamp: Double(Date.timeIntervalSinceReferenceDate), - crashesCount: crashesCount) + eventTimestamp: Double(Date.timeIntervalSinceReferenceDate), + reportCreationTimestamp: reportTime) - BacktraceCrashLoop.LogDebug(event.description()) + CLDLogDebug(event.description()) - startupEvents.append(event) + startupEvents.insert(event, at: 0) - if startupEvents.count > BacktraceCrashLoopDetector.consecutiveCrashesThreshold { + while startupEvents.count > BacktraceCrashLoopDetector.consecutiveCrashesThreshold && !startupEvents.isEmpty { startupEvents.removeFirst() } - BacktraceCrashLoop.LogDebug("Startup Event Added: \(startupEvents.count)") + CLDLogDebug("Startup Event Added, Total Events => \(startupEvents.count)") + + saveEvents() } @objc internal func clearStartupEvents() { startupEvents.removeAll() saveEvents() - BacktraceCrashLoop.LogDebug("Startup Events Cleared: \(startupEvents.count)") + CLDLogDebug("Startup Events Cleared: \(startupEvents.count)") } @objc internal func consecutiveEventsCount() -> Int { var count = 0 - var previousValue = 0 + var previousTime = 0.0 for event in startupEvents { - if event.crashesCount > previousValue { + if event.reportCreationTimestamp == 0 || event.reportCreationTimestamp == previousTime { + break + } + + if previousTime == 0 || event.reportCreationTimestamp < previousTime { count += 1 } - previousValue = event.crashesCount - } - BacktraceCrashLoop.LogDebug("Consecutive Events Count: \(count)") + previousTime = event.reportCreationTimestamp + } + CLDLogDebug("Consecutive Events Count: \(count)") return count } @@ -130,15 +137,14 @@ import Foundation for event in startupEvents { string += event.description() + "\n" } - return string + return string.isEmpty ? "No events" : string } } // MARK: Deprecated methods extension BacktraceCrashLoopDetector { - @available(*, deprecated, message: "Temporarily not needed") - @objc internal func reportFilePath() -> String { + @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, @@ -159,10 +165,20 @@ extension BacktraceCrashLoopDetector { let reportFullPath = crashReportDir.appendingPathComponent(reportName) .absoluteString .replacingOccurrences(of: "file://", with: "") - BacktraceCrashLoop.LogDebug("reportFullPath: \(reportFullPath)") + 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()) @@ -177,3 +193,18 @@ extension BacktraceCrashLoopDetector { 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) +} From 887589e8034a0f28733c22d0f54e122626da89d3 Mon Sep 17 00:00:00 2001 From: Konstantin R <111510241+konst-sauce@users.noreply.github.com> Date: Mon, 13 Feb 2023 18:04:44 +0100 Subject: [PATCH 17/17] Polished other example apps, fixed bug. --- Examples/Example-iOS-ObjC/AppDelegate.m | 3 --- Examples/Example-macOS-ObjC/AppDelegate.m | 3 --- Examples/Example-tvOS/AppDelegate.swift | 3 --- .../CrashLoopDetector/BacktraceCrashLoopDetector.swift | 4 ---- 4 files changed, 13 deletions(-) diff --git a/Examples/Example-iOS-ObjC/AppDelegate.m b/Examples/Example-iOS-ObjC/AppDelegate.m index 08977af2..763d68bf 100644 --- a/Examples/Example-iOS-ObjC/AppDelegate.m +++ b/Examples/Example-iOS-ObjC/AppDelegate.m @@ -24,9 +24,6 @@ You can pass crashes count threshold (maximum amount of launching events to eval // TODO: Perform any custom checks if necessary and decide if Backtrace should be launched return NO; } - else { - [BacktraceClient disableCrashLoopDetection]; - } NSArray *paths = @[[[NSBundle mainBundle] pathForResource: @"test" ofType: @"txt"]]; NSString *fileName = @"myCustomFile.txt"; diff --git a/Examples/Example-macOS-ObjC/AppDelegate.m b/Examples/Example-macOS-ObjC/AppDelegate.m index bedd6ad2..fc9bbdf2 100644 --- a/Examples/Example-macOS-ObjC/AppDelegate.m +++ b/Examples/Example-macOS-ObjC/AppDelegate.m @@ -19,9 +19,6 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // TODO: Perform any custom checks if necessary and decide if Backtrace should be launched return; } - else { - [BacktraceClient disableCrashLoopDetection]; - } BacktraceCredentials *credentials = [[BacktraceCredentials alloc] initWithSubmissionUrl: [NSURL URLWithString: Keys.backtraceSubmissionUrl]]; diff --git a/Examples/Example-tvOS/AppDelegate.swift b/Examples/Example-tvOS/AppDelegate.swift index 8dd1e9a3..216dffe7 100644 --- a/Examples/Example-tvOS/AppDelegate.swift +++ b/Examples/Example-tvOS/AppDelegate.swift @@ -29,9 +29,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // TODO: Perform any custom checks if necessary and decide if Backtrace should be launched return true } - else { - BacktraceClient.disableCrashLoopDetection() - } let backtraceCredentials = BacktraceCredentials(endpoint: URL(string: Keys.backtraceUrl as String)!, token: Keys.backtraceToken as String) diff --git a/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift index bdbc2b51..e8538ce8 100644 --- a/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift +++ b/Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift @@ -97,10 +97,6 @@ import Foundation CLDLogDebug(event.description()) startupEvents.insert(event, at: 0) - - while startupEvents.count > BacktraceCrashLoopDetector.consecutiveCrashesThreshold && !startupEvents.isEmpty { - startupEvents.removeFirst() - } CLDLogDebug("Startup Event Added, Total Events => \(startupEvents.count)")