Skip to content

Commit

Permalink
Update Crash Reporting code
Browse files Browse the repository at this point in the history
- Add additional data to crash spans
- Add debugger detection
  • Loading branch information
SMickelsn committed Dec 16, 2024
1 parent 63b7ece commit b3ad9d4
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:

jobs:
full-build:
runs-on: macOS-latest
runs-on: macOS-13
steps:
- name: Checkout
uses: actions/checkout@v1
Expand Down
173 changes: 163 additions & 10 deletions SplunkRumCrashReporting/SplunkRumCrashReporting/CrashReporting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ let CrashReportingVersionString = "0.6.0"

var TheCrashReporter: PLCrashReporter?
private var customDataDictionary: [String: String] = [String: String]()
private var allUsedImageNames: Array <String> = []

func initializeCrashReporting() {
let startupSpan = buildTracer().spanBuilder(spanName: "SplunkRumCrashReporting").startSpan()
Expand All @@ -39,11 +40,20 @@ func initializeCrashReporting() {
return
}
let crashReporter = crashReporter_!
let success = crashReporter.enable()
SplunkRum.debugLog("PLCrashReporter enabled: "+success.description)
if !success {
startupSpan.setAttribute(key: "error.message", value: "Cannot enable PLCrashReporter")
return

// Stop enable if debugger attached
var inDebugger = false
if isDebuggerAttached() {
startupSpan.setAttribute(key: "error.message", value: "Debugger present. Will not construct PLCrashReporter")
SplunkRum.debugLog("Debugger present. Will not enable PLCrashReporter")
inDebugger = true;
}
if inDebugger == false {
let success = crashReporter.enable()
SplunkRum.debugLog("PLCrashReporter enabled: "+success.description)
if !success {
startupSpan.setAttribute(key: "error.message", value: "Cannot enable PLCrashReporter")
}
}
TheCrashReporter = crashReporter
updateCrashReportSessionId()
Expand All @@ -58,6 +68,9 @@ func initializeCrashReporting() {
}
SplunkRum.debugLog("Had a pending crash report")
do {
allUsedImageNames.removeAll()
let path = crashReporter.crashReportPath()
print(path as Any)
let data = crashReporter.loadPendingCrashReportData()
try loadPendingCrashReport(data)
} catch {
Expand All @@ -66,8 +79,8 @@ func initializeCrashReporting() {
// yes, fall through to purge
}
crashReporter.purgePendingCrashReport()

}

private func buildTracer() -> Tracer {
return OpenTelemetry.instance.tracerProvider.get(instrumentationName: "splunk-ios-crashreporting", instrumentationVersion: CrashReportingVersionString)

Expand Down Expand Up @@ -129,7 +142,7 @@ func loadPendingCrashReport(_ data: Data!) throws {
span.setAttribute(key: "crash.freeDiskSpace", value: customData!["freeDiskSpace"]!)
span.setAttribute(key: "crash.freeMemory", value: customData!["freeMemory"]!)
} else {
span.setAttribute(key: "crash.rumSessionId", value: String(decoding: report.customData, as: UTF8.self))
span.setAttribute(key: "crash.rumSessionId", value: String(bytes: report.customData, encoding: String.Encoding.utf8) ?? "Unknown")
}
}
// "marketing version" here matches up to our use of CFBundleShortVersionString
Expand All @@ -138,10 +151,25 @@ func loadPendingCrashReport(_ data: Data!) throws {
span.addEvent(name: "crash.timestamp", timestamp: report.systemInfo.timestamp)
span.setAttribute(key: "exception.type", value: exceptionType ?? "unknown")
span.setAttribute(key: "crash.address", value: report.signalInfo.address.description)
for case let thread as PLCrashReportThreadInfo in report.threads where thread.crashed {
span.setAttribute(key: "exception.stacktrace", value: crashedThreadToStack(report: report, thread: thread))
break

var allThreads: Array <Any> = []
for case let thread as PLCrashReportThreadInfo in report.threads {

// Original crashed thread handler
if (thread.crashed) {
span.setAttribute(key: "exception.stacktrace", value: crashedThreadToStack(report: report, thread: thread))
}

// Detailed thread handler
allThreads.append(detailedThreadToStackFrames(report: report, thread: thread))
}
let threadPayload = convertArrayToJSONString(allThreads) ?? "Unable to create stack frames"
span.setAttribute(key: "exception.stackFrames", value: threadPayload)
var images: Array <Any> = []
images = imageList(images: report.images)
let imagesPayload = convertArrayToJSONString(images) ?? "Unable to create images"
span.setAttribute(key: "exception.images", value: imagesPayload)

if report.hasExceptionInfo {
span.setAttribute(key: "exception.type", value: report.exceptionInfo.exceptionName)
span.setAttribute(key: "exception.message", value: report.exceptionInfo.exceptionReason)
Expand Down Expand Up @@ -200,3 +228,128 @@ func formatStackFrame(frame: PLCrashReportStackFrameInfo, frameNum: Int, report:
initializeCrashReporting()
}
}

// Symbolication Support Code

// Extracts detail for one thread
func detailedThreadToStackFrames(report: PLCrashReport, thread: PLCrashReportThreadInfo) -> Dictionary<String, Any> {

var oneThread: [String:Any] = [:]
var allStackFrames: Array <Any> = []

let threadNum = thread.threadNumber as NSNumber
oneThread["threadNumber"] = threadNum.stringValue
oneThread["crashed"] = thread.crashed

var frameNum = 0
while frameNum < thread.stackFrames.count {
var oneFrame: [String:Any] = [:]

let frame = thread.stackFrames[frameNum] as! PLCrashReportStackFrameInfo
let instructionPointer = frame.instructionPointer
oneFrame["instructionPointer"] = instructionPointer

var baseAddress: UInt64 = 0
var offset: UInt64 = 0
var imageName = "???"

let imageInfo = report.image(forAddress: instructionPointer)
if imageInfo != nil {
imageName = imageInfo?.imageName ?? "???"
baseAddress = imageInfo!.imageBaseAddress
offset = instructionPointer - baseAddress
}
oneFrame["imageName"] = imageName
allUsedImageNames.append(imageName)

if frame.symbolInfo != nil {
let symbolName = frame.symbolInfo.symbolName
let symOffset = instructionPointer - frame.symbolInfo.startAddress
oneFrame["symbolName"] = symbolName
oneFrame["offset"] = symOffset
} else {
oneFrame["baseAddress"] = baseAddress
oneFrame["offset"] = offset
}
allStackFrames.append(oneFrame)
frameNum += 1
}
oneThread["stackFrames"] = allStackFrames
return oneThread
}

// Returns true if debugger is attached
private func isDebuggerAttached() -> Bool {
var debuggerIsAttached = false

var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
var info = kinfo_proc()
var infoSize = MemoryLayout<kinfo_proc>.size

_ = name.withUnsafeMutableBytes { (nameBytePtr: UnsafeMutableRawBufferPointer) -> Bool in
guard let nameBytesBlindMemory = nameBytePtr.bindMemory(to: Int32.self).baseAddress else {
return false
}
return sysctl(nameBytesBlindMemory, 4, &info, &infoSize, nil, 0) != -1
}
if !debuggerIsAttached && (info.kp_proc.p_flag & P_TRACED) != 0 {
debuggerIsAttached = true
}
return debuggerIsAttached
}

// Returns array of code images used by app
func imageList(images: Array<Any>) -> Array<Any> {
var outputImages: Array<Any> = []
for image in images {
var imageDictionary: [String:Any] = [:]
guard let image = image as? PLCrashReportBinaryImageInfo else {
continue
}

// Only add the image to the list if it was noted in the stack traces
if(allUsedImageNames.contains(image.imageName)) {
imageDictionary["codeType"] = cpuTypeDictionary(cpuType: image.codeType)
imageDictionary["baseAddress"] = image.imageBaseAddress
imageDictionary["imageSize"] = image.imageSize
imageDictionary["imagePath"] = image.imageName
imageDictionary["imageUUID"] = image.imageUUID

outputImages.append(imageDictionary)
}
}
return outputImages
}

// Returns formatted cpu data
func cpuTypeDictionary(cpuType: PLCrashReportProcessorInfo) -> Dictionary<String, String> {
var dictionary: [String:String] = [:]
dictionary.updateValue(String(cpuType.type), forKey: "cType")
dictionary.updateValue(String(cpuType.subtype), forKey: "cSubType")
return dictionary
}

// JSON support code
func convertDictionaryToJSONString(_ dictionary: [String: Any]) -> String? {
guard let jsonData = try? JSONSerialization.data(withJSONObject: dictionary, options: .prettyPrinted) else {

return nil
}
guard let jsonString = String(data: jsonData, encoding: .utf8) else {

return nil
}
return jsonString
}

func convertArrayToJSONString(_ array: [Any]) -> String? {
guard let jsonData = try? JSONSerialization.data(withJSONObject: array, options: .prettyPrinted) else {

return nil
}
guard let jsonString = String(data: jsonData, encoding: .utf8) else {

return nil
}
return jsonString
}

0 comments on commit b3ad9d4

Please sign in to comment.