-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/crashloop #100
Open
konst-sauce
wants to merge
17
commits into
master
Choose a base branch
from
feature/crashloop
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+491
−49
Open
Feature/crashloop #100
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
ee4952a
Added BacktraceCrashLoopDetector
konst-sauce 861aa76
Added public methods to configure safe mode and access its state. Rem…
konst-sauce b8d9679
Added detection to iOS example. Moved detection to isSafeToLaunch
konst-sauce 3d18e2f
Added Unit Tests, added logs, polished CL detector
konst-sauce 8c07770
Removed PLCR dependency from BTCL detector, added UI, polishes
konst-sauce c290660
Polished naming, added reset of CL detector when safe mode is necessary
konst-sauce f0f9d56
Polished crash loop detection
konst-sauce dc9fba3
Updated tvOS and macOS examples
konst-sauce 8497a45
Updated with comments from the pull request review
konst-sauce 3fc8bf6
Added Crash Loop counter
konst-sauce 03d6cd0
Polished Crash Loop detection
konst-sauce 0c3e486
Updated crash loop detector algorithm, polished names, added comments.
konst-sauce d0d63d6
Final polishes - updated CLD algorithm, updated all examples to utili…
konst-sauce c08a971
Polished for comments in PR
konst-sauce 35258e3
backtrace.version attribute fix
konst-sauce 742e021
CLD algorithm polished. Some PR comments utilised.
konst-sauce 887589e
Polished other example apps, fixed bug.
konst-sauce File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,8 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS"> | ||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS"> | ||
<dependencies> | ||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/> | ||
<deployment identifier="macosx"/> | ||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/> | ||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> | ||
</dependencies> | ||
<scenes> | ||
|
@@ -619,7 +620,7 @@ | |
<menuItem title="Show Sidebar" keyEquivalent="s" id="kIP-vf-haE"> | ||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/> | ||
<connections> | ||
<action selector="toggleSourceList:" target="Ady-hI-5gd" id="iwa-gc-5KM"/> | ||
<action selector="toggleSidebar:" target="Ady-hI-5gd" id="iwa-gc-5KM"/> | ||
</connections> | ||
</menuItem> | ||
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa"> | ||
|
@@ -710,16 +711,16 @@ | |
<autoresizingMask key="autoresizingMask"/> | ||
<subviews> | ||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Snf-7p-7db"> | ||
<rect key="frame" x="4" y="3" width="472" height="61"/> | ||
<rect key="frame" x="3" y="3" width="474" height="62"/> | ||
<constraints> | ||
<constraint firstAttribute="height" constant="50" id="wS4-BS-4BZ"/> | ||
</constraints> | ||
<buttonCell key="cell" type="push" title="Button" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="vpD-Xg-4cH"> | ||
<buttonCell key="cell" type="push" title="Crash Me" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="vpD-Xg-4cH"> | ||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> | ||
<font key="font" metaFont="system"/> | ||
</buttonCell> | ||
<connections> | ||
<action selector="liveReportButtonAction:" target="XfG-lQ-9wD" id="r4X-yE-pnP"/> | ||
<action selector="crashAction:" target="XfG-lQ-9wD" id="OfJ-Ce-bpL"/> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't liveReportButton stay here? |
||
</connections> | ||
</button> | ||
<scrollView borderType="none" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" translatesAutoresizingMaskIntoConstraints="NO" id="FmX-dS-tKd"> | ||
|
@@ -739,6 +740,10 @@ | |
</textView> | ||
</subviews> | ||
</clipView> | ||
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="KrE-Pr-E3e"> | ||
<rect key="frame" x="-100" y="-100" width="460" height="16"/> | ||
<autoresizingMask key="autoresizingMask"/> | ||
</scroller> | ||
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="7RO-Uj-aNq"> | ||
<rect key="frame" x="444" y="0.0" width="16" height="190"/> | ||
<autoresizingMask key="autoresizingMask"/> | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
206 changes: 206 additions & 0 deletions
206
Sources/Public/Internal/CrashLoopDetector/BacktraceCrashLoopDetector.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
// | ||
// BacktraceCrashLoopDetector.swift | ||
// Backtrace | ||
// | ||
|
||
import Foundation | ||
|
||
@objc internal class BacktraceCrashLoopDetector: NSObject { | ||
|
||
internal struct StartUpEvent: Codable { | ||
var uuid: String | ||
var eventTimestamp: Double | ||
var reportCreationTimestamp: Double | ||
|
||
func description() -> String { | ||
let string = """ | ||
New Crash Loop Event: | ||
UUID: \(uuid) | ||
Event Timestamp: \(eventTimestamp) | ||
Report Creation Timestamp: \(reportCreationTimestamp)\n | ||
""" | ||
return string | ||
} | ||
} | ||
|
||
internal static let instance = BacktraceCrashLoopDetector() | ||
|
||
@objc private static let plistKey = "CrashLoopDetectorData" | ||
@objc internal static let consecutiveCrashesThreshold = 5 | ||
@objc private(set) var consecutiveCrashesCount = 0 | ||
|
||
@objc private var threshold = 0 | ||
|
||
internal var startupEvents: [StartUpEvent] = [] | ||
|
||
override private init() { | ||
} | ||
|
||
@objc internal func updateThreshold(_ threshold: Int) { | ||
self.threshold = threshold == 0 ? BacktraceCrashLoopDetector.consecutiveCrashesThreshold : threshold | ||
} | ||
|
||
@objc internal func detectCrashloop() -> Bool { | ||
|
||
CLDLogDebug("Starting Crash Loop Detection") | ||
|
||
loadEvents() | ||
addEvent() | ||
|
||
consecutiveCrashesCount = consecutiveEventsCount() | ||
|
||
let result = consecutiveCrashesCount >= BacktraceCrashLoopDetector.consecutiveCrashesThreshold | ||
CLDLogDebug("Finishing Crash Loop Detection: Is in the crash loop - \(result)") | ||
return result | ||
} | ||
|
||
@objc private func loadEvents() { | ||
|
||
// Cleanup old events - f.e. for multiple usages of detector | ||
startupEvents.removeAll() | ||
|
||
/* | ||
- Since detector's DB is relatively small, UserDefaults are a good option here, | ||
plus they allow to avoid a headache with reading/writing to/from the custom file. | ||
|
||
- But we should consider shared computers as well - comment from UserDefaults docs: | ||
With the exception of managed devices in educational institutions, | ||
a user’s defaults are stored locally on a single device, | ||
and persisted for backup and restore. | ||
To synchronize preferences and other data across a user’s connected devices, | ||
use NSUbiquitousKeyValueStore instead. | ||
*/ | ||
guard let data = UserDefaults.standard.object(forKey: BacktraceCrashLoopDetector.plistKey) as? Data | ||
else { return } | ||
|
||
guard let array = try? PropertyListDecoder().decode([StartUpEvent].self, from: data) | ||
else { return } | ||
|
||
startupEvents.append(contentsOf: array) | ||
CLDLogDebug("Events Loaded: \(startupEvents.count)") | ||
} | ||
|
||
@objc private func saveEvents() { | ||
let data = try? PropertyListEncoder().encode(startupEvents) | ||
UserDefaults.standard.set(data, forKey: BacktraceCrashLoopDetector.plistKey) | ||
CLDLogDebug("Events Saved: \(startupEvents.count)") | ||
} | ||
|
||
@objc private func addEvent() { | ||
|
||
let reportTime = reportFileCreationTime() | ||
|
||
let event = StartUpEvent(uuid: UUID().uuidString, | ||
eventTimestamp: Double(Date.timeIntervalSinceReferenceDate), | ||
reportCreationTimestamp: reportTime) | ||
|
||
CLDLogDebug(event.description()) | ||
|
||
startupEvents.insert(event, at: 0) | ||
|
||
CLDLogDebug("Startup Event Added, Total Events => \(startupEvents.count)") | ||
|
||
saveEvents() | ||
} | ||
|
||
@objc internal func clearStartupEvents() { | ||
startupEvents.removeAll() | ||
saveEvents() | ||
CLDLogDebug("Startup Events Cleared: \(startupEvents.count)") | ||
} | ||
|
||
@objc internal func consecutiveEventsCount() -> Int { | ||
|
||
var count = 0 | ||
konst-sauce marked this conversation as resolved.
Show resolved
Hide resolved
|
||
var previousTime = 0.0 | ||
for event in startupEvents { | ||
if event.reportCreationTimestamp == 0 || event.reportCreationTimestamp == previousTime { | ||
break | ||
} | ||
|
||
if previousTime == 0 || event.reportCreationTimestamp < previousTime { | ||
count += 1 | ||
} | ||
|
||
previousTime = event.reportCreationTimestamp | ||
} | ||
CLDLogDebug("Consecutive Events Count: \(count)") | ||
return count | ||
} | ||
|
||
@objc internal func databaseDescription() -> String { | ||
var string = "" | ||
for event in startupEvents { | ||
string += event.description() + "\n" | ||
} | ||
return string.isEmpty ? "No events" : string | ||
} | ||
} | ||
|
||
// MARK: Deprecated methods | ||
extension BacktraceCrashLoopDetector { | ||
|
||
@objc private func reportFilePath() -> String { | ||
|
||
/* Crash Loop Detector considers all other Backtrace modules as potentially dangerous. | ||
Thats why it formats path to PLCrashReporter's report file itself, | ||
for not to use PLCrashReporter's APIs at all | ||
*/ | ||
|
||
let bundleIDBT = Bundle.main.bundleIdentifier ?? "" | ||
konst-sauce marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let appIDPath = bundleIDBT.replacingOccurrences(of: "/", with: "_") | ||
|
||
let paths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true) | ||
let cacheDir = URL(fileURLWithPath: paths.isEmpty ? "" : paths[0]) | ||
|
||
let bundleIDPLCR = "com.plausiblelabs.crashreporter.data" | ||
let crashReportDir = cacheDir.appendingPathComponent(bundleIDPLCR) | ||
.appendingPathComponent(appIDPath) | ||
|
||
let reportName = "live_report.plcrash" | ||
let reportFullPath = crashReportDir.appendingPathComponent(reportName) | ||
.absoluteString | ||
.replacingOccurrences(of: "file://", with: "") | ||
CLDLogDebug("reportFullPath: \(reportFullPath)") | ||
|
||
return reportFullPath | ||
} | ||
|
||
@objc private func reportFileCreationTime() -> Double { | ||
let attributes = try? FileManager.default.attributesOfItem(atPath: reportFilePath()) | ||
let date = attributes?[.creationDate] as? Date | ||
CLDLogDebug("Report creation date: \(String(describing: date))") | ||
let timeInterval = date?.timeIntervalSinceReferenceDate ?? 0 | ||
CLDLogDebug("Time Interval \(timeInterval)") | ||
return timeInterval | ||
} | ||
|
||
@available(*, deprecated, message: "Temporarily not needed") | ||
@objc internal func hasCrashReport() -> Bool { | ||
let exists = FileManager.default.fileExists(atPath: reportFilePath()) | ||
return exists | ||
} | ||
|
||
@available(*, deprecated, message: "Temporarily not needed") | ||
@objc internal func deleteCrashReport() { | ||
let path = reportFilePath() | ||
let fileURL = URL(fileURLWithPath: path) | ||
try? FileManager.default.removeItem(at: fileURL) | ||
saveEvents() | ||
} | ||
} | ||
|
||
|
||
internal func CLDLogDebug(_ message: String = "") { | ||
|
||
/* Routed logging here to add prefix for more convenient filtering of | ||
BTCLD logs in Xcode's outputs | ||
*/ | ||
let prefix = "BT CLD: " | ||
|
||
/* Since Backtrace is not enabled during Crash Loop detection, | ||
BacktraceLogger is also not set up, so it doesn't log messages | ||
=> using native 'print' here | ||
*/ | ||
print(prefix + message) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import XCTest | ||
|
||
import Nimble | ||
import Quick | ||
@testable import Backtrace | ||
|
||
final class BacktraceCrashLoopDetectorTests: QuickSpec { | ||
|
||
override func spec() { | ||
describe("Crash Loop Detector") { | ||
|
||
context("No Crash Loop Case") { | ||
|
||
let crashLoopDetector = BacktraceCrashLoopDetector() | ||
let eventsCount = BacktraceCrashLoopDetector.consecutiveCrashesThreshold | ||
let timeIntervalStep = 200 | ||
|
||
for index in 0 ..< eventsCount { | ||
let timestamp = Date.timeIntervalSinceReferenceDate - Double(timeIntervalStep * (eventsCount - index)) | ||
let mockEvent = BacktraceCrashLoopDetector.StartUpEvent(timestamp: timestamp, isSuccessful: .random()) | ||
crashLoopDetector.startupEvents.append(mockEvent) | ||
} | ||
crashLoopDetector.saveEvents() | ||
|
||
let isCrashLoop = crashLoopDetector.detectCrashloop() | ||
it("checks if no crash loop detected") { | ||
expect { isCrashLoop }.to(beFalse()) | ||
} | ||
} | ||
|
||
context("Crash Loop Case") { | ||
|
||
let crashLoopDetector = BacktraceCrashLoopDetector() | ||
let eventsCount = BacktraceCrashLoopDetector.consecutiveCrashesThreshold | ||
let timeIntervalStep = 200 | ||
|
||
for index in 0 ..< eventsCount { | ||
let timestamp = Date.timeIntervalSinceReferenceDate - Double(timeIntervalStep * (eventsCount - index)) | ||
let mockEvent = BacktraceCrashLoopDetector.StartUpEvent(timestamp: timestamp, isSuccessful: false) | ||
crashLoopDetector.startupEvents.append(mockEvent) | ||
} | ||
crashLoopDetector.saveEvents() | ||
|
||
let isCrashLoop = crashLoopDetector.detectCrashloop() | ||
it("checks if crash loop detected") { | ||
expect { isCrashLoop }.to(beTrue()) | ||
} | ||
} | ||
} | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't be better if we describe the story what someone want to use it?