diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e18a31..ed89d3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: jobs: full-build: - runs-on: macOS-latest + runs-on: macOS-12 steps: - name: Checkout uses: actions/checkout@v1 diff --git a/SplunkRumCrashReporting/SplunkRumCrashReporting.xcodeproj/project.pbxproj b/SplunkRumCrashReporting/SplunkRumCrashReporting.xcodeproj/project.pbxproj index 07d9a9b..45fe737 100644 --- a/SplunkRumCrashReporting/SplunkRumCrashReporting.xcodeproj/project.pbxproj +++ b/SplunkRumCrashReporting/SplunkRumCrashReporting.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -14,6 +14,7 @@ 86461EFC269729C0007C6DC0 /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 86461EFB269729C0007C6DC0 /* CrashReporter */; }; 86461F0526972A11007C6DC0 /* sample_v1.plcrash in Resources */ = {isa = PBXBuildFile; fileRef = 86461F0426972A11007C6DC0 /* sample_v1.plcrash */; }; 86D3180A271655B300B43379 /* SplunkOtel in Frameworks */ = {isa = PBXBuildFile; productRef = 86D31809271655B300B43379 /* SplunkOtel */; }; + BA5DB5512D10A99F0090298A /* sample_v3.plcrash in Resources */ = {isa = PBXBuildFile; fileRef = BA5DB5502D10A99F0090298A /* sample_v3.plcrash */; }; D774545D28E38CF40056159F /* DeviceStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = D774545C28E38CF40056159F /* DeviceStats.swift */; }; D7C64D1228E494C50086368D /* DeviceStatsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C64D1128E494C50086368D /* DeviceStatsTests.swift */; }; D7D14290293804A200CAD87E /* sample_v2.plcrash in Resources */ = {isa = PBXBuildFile; fileRef = D7D1428F293804A200CAD87E /* sample_v2.plcrash */; }; @@ -38,6 +39,7 @@ 86461EEA26972906007C6DC0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 86461EF626972964007C6DC0 /* CrashReporting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrashReporting.swift; sourceTree = ""; }; 86461F0426972A11007C6DC0 /* sample_v1.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file.plcrash; path = sample_v1.plcrash; sourceTree = ""; }; + BA5DB5502D10A99F0090298A /* sample_v3.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = sample_v3.plcrash; sourceTree = ""; }; D774545C28E38CF40056159F /* DeviceStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStats.swift; sourceTree = ""; }; D7C64D1128E494C50086368D /* DeviceStatsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatsTests.swift; sourceTree = ""; }; D7D1428F293804A200CAD87E /* sample_v2.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = sample_v2.plcrash; sourceTree = ""; }; @@ -96,6 +98,7 @@ 86461EE726972906007C6DC0 /* SplunkRumCrashReportingTests */ = { isa = PBXGroup; children = ( + BA5DB5502D10A99F0090298A /* sample_v3.plcrash */, D7D1428F293804A200CAD87E /* sample_v2.plcrash */, 86461F0426972A11007C6DC0 /* sample_v1.plcrash */, 86461EE826972906007C6DC0 /* CrashTests.swift */, @@ -213,6 +216,7 @@ buildActionMask = 2147483647; files = ( 86461F0526972A11007C6DC0 /* sample_v1.plcrash in Resources */, + BA5DB5512D10A99F0090298A /* sample_v3.plcrash in Resources */, D7D14290293804A200CAD87E /* sample_v2.plcrash in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/SplunkRumCrashReporting/SplunkRumCrashReporting/CrashReporting.swift b/SplunkRumCrashReporting/SplunkRumCrashReporting/CrashReporting.swift index d2f70fa..854a8d2 100644 --- a/SplunkRumCrashReporting/SplunkRumCrashReporting/CrashReporting.swift +++ b/SplunkRumCrashReporting/SplunkRumCrashReporting/CrashReporting.swift @@ -24,6 +24,7 @@ let CrashReportingVersionString = "0.6.0" var TheCrashReporter: PLCrashReporter? private var customDataDictionary: [String: String] = [String: String]() +private var allUsedImageNames: [String] = [] func initializeCrashReporting() { let startupSpan = buildTracer().spanBuilder(spanName: "SplunkRumCrashReporting").startSpan() @@ -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() @@ -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 { @@ -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) @@ -124,24 +137,44 @@ func loadPendingCrashReport(_ data: Data!) throws { if report.customData != nil { let customData = NSKeyedUnarchiver.unarchiveObject(with: report.customData) as? [String: String] if customData != nil { - span.setAttribute(key: "crash.rumSessionId", value: customData!["sessionId"]!) + span.setAttribute(key: "session.id", value: customData!["sessionId"]!) span.setAttribute(key: "crash.batteryLevel", value: customData!["batteryLevel"]!) 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: "session.id", value: String(bytes: report.customData, encoding: String.Encoding.utf8) ?? "Unknown") } } // "marketing version" here matches up to our use of CFBundleShortVersionString span.setAttribute(key: "crash.app.version", value: report.applicationInfo.applicationMarketingVersion) span.setAttribute(key: "error", value: true) span.addEvent(name: "crash.timestamp", timestamp: report.systemInfo.timestamp) + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss ZZZZZ" + span.setAttribute(key: "crash.observedTimestamp", value: formatter.string(from: now)) 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 + if report.hasProcessInfo { + span.setAttribute(key: "crash.isNative", value: report.processInfo.native) + } + var allThreads: [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.threads", value: threadPayload) + var images: [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) @@ -200,3 +233,128 @@ func formatStackFrame(frame: PLCrashReportStackFrameInfo, frameNum: Int, report: initializeCrashReporting() } } + +// Symbolication Support Code + +// Extracts detail for one thread +func detailedThreadToStackFrames(report: PLCrashReport, thread: PLCrashReportThreadInfo) -> [String: Any] { + + var oneThread: [String: Any] = [:] + var allStackFrames: [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.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: [Any]) -> [Any] { + var outputImages: [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) -> [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 +} diff --git a/SplunkRumCrashReporting/SplunkRumCrashReportingTests/CrashTests.swift b/SplunkRumCrashReporting/SplunkRumCrashReportingTests/CrashTests.swift index 97654d1..2be22b6 100644 --- a/SplunkRumCrashReporting/SplunkRumCrashReportingTests/CrashTests.swift +++ b/SplunkRumCrashReporting/SplunkRumCrashReportingTests/CrashTests.swift @@ -120,4 +120,59 @@ class CrashTests: XCTestCase { XCTAssertEqual(startup!.attributes["component"]?.description, "appstart") } + func testBasics_v3() throws { + let crashPath = Bundle(for: CrashTests.self).url(forResource: "sample_v3", withExtension: "plcrash")! + let crashData = try Data(contentsOf: crashPath) + + SplunkRumBuilder(beaconUrl: "http://127.0.0.1:8989/v1/traces", rumAuth: "FAKE") + .allowInsecureBeacon(enabled: true) + .debug(enabled: true) + .build() + let tracerProvider = TracerProviderBuilder() + .add(spanProcessor: SimpleSpanProcessor(spanExporter: TestSpanExporter())) + .build() + OpenTelemetry.registerTracerProvider(tracerProvider: tracerProvider) + localSpans.removeAll() + + SplunkRumCrashReporting.start() + try loadPendingCrashReport(crashData) + + XCTAssertEqual(localSpans.count, 2) + let crashReport = localSpans.first(where: { (span) -> Bool in + return span.name == "SIGTRAP" + }) + let startup = localSpans.first(where: { (span) -> Bool in + return span.name == "SplunkRumCrashReporting" + }) + + XCTAssertNotNil(crashReport) + XCTAssertNotEqual(crashReport!.attributes["splunk.rumSessionId"], crashReport!.attributes["crash.rumSessionId"]) + XCTAssertEqual(crashReport!.attributes["crash.rumSessionId"]?.description, "a9ef9e0a7683eaf973ec8fa4b31df3f9") + XCTAssertEqual(crashReport!.attributes["crash.address"]?.description, "6786470812") + XCTAssertEqual(crashReport!.attributes["component"]?.description, "crash") + XCTAssertEqual(crashReport!.attributes["error"]?.description, "true") + XCTAssertEqual(crashReport!.attributes["exception.type"]?.description, "SIGTRAP") + XCTAssertTrue(crashReport!.attributes["exception.stacktrace"]?.description.contains("UIKitCore") ?? false) + XCTAssertEqual(crashReport!.attributes["crash.batteryLevel"]?.description, "100.0%") + XCTAssertEqual(crashReport!.attributes["crash.freeDiskSpace"]?.description, "628.03 GB") + XCTAssertEqual(crashReport!.attributes["crash.freeMemory"]?.description, "31.88 GB") + XCTAssertEqual(crashReport!.attributes["crash.app.version"]?.description, "1.0") + XCTAssertNotNil(crashReport!.attributes["exception.threads"]) + XCTAssertTrue(crashReport!.attributes["exception.threads"]?.description.contains("threadNumber") ?? false) + XCTAssertTrue(crashReport!.attributes["exception.threads"]?.description.contains("crashed") ?? false) + XCTAssertTrue(crashReport!.attributes["exception.threads"]?.description.contains("instructionPointer") ?? false) + XCTAssertTrue(crashReport!.attributes["exception.threads"]?.description.contains("baseAddress") ?? false) + XCTAssertTrue(crashReport!.attributes["exception.threads"]?.description.contains("imageName") ?? false) + XCTAssertTrue(crashReport!.attributes["exception.threads"]?.description.contains("offset") ?? false) + XCTAssertNotNil(crashReport!.attributes["exception.images"]) + XCTAssertTrue(crashReport!.attributes["exception.images"]?.description.contains("imageUUID") ?? false) + XCTAssertTrue(crashReport!.attributes["exception.images"]?.description.contains("imageSize") ?? false) + XCTAssertTrue(crashReport!.attributes["exception.images"]?.description.contains("imagePath") ?? false) + XCTAssertTrue(crashReport!.attributes["exception.images"]?.description.contains("codeType") ?? false) + XCTAssertTrue(crashReport!.attributes["exception.images"]?.description.contains("baseAddress") ?? false) + + XCTAssertNotNil(startup) + XCTAssertEqual(startup!.attributes["component"]?.description, "appstart") + + } } diff --git a/SplunkRumCrashReporting/SplunkRumCrashReportingTests/sample_v3.plcrash b/SplunkRumCrashReporting/SplunkRumCrashReportingTests/sample_v3.plcrash new file mode 100644 index 0000000..8dfba9a Binary files /dev/null and b/SplunkRumCrashReporting/SplunkRumCrashReportingTests/sample_v3.plcrash differ