diff --git a/Backtrace.xcodeproj/project.pbxproj b/Backtrace.xcodeproj/project.pbxproj index 751511ba..4479f6aa 100644 --- a/Backtrace.xcodeproj/project.pbxproj +++ b/Backtrace.xcodeproj/project.pbxproj @@ -25,6 +25,9 @@ 2062D9C92C457B4500E4CE3C /* Crash+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2062D9C52C457B4500E4CE3C /* Crash+CoreDataProperties.swift */; }; 2062D9CA2C457B4500E4CE3C /* Crash+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2062D9C42C457B4500E4CE3C /* Crash+CoreDataClass.swift */; }; 2062D9CB2C457B4500E4CE3C /* Crash+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2062D9C52C457B4500E4CE3C /* Crash+CoreDataProperties.swift */; }; + 20B01A542DEE4DCA00FED98F /* BacktraceReporterThreadSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B01A532DEE4DC600FED98F /* BacktraceReporterThreadSpec.swift */; }; + 20B01A552DEE4DCA00FED98F /* BacktraceReporterThreadSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B01A532DEE4DC600FED98F /* BacktraceReporterThreadSpec.swift */; }; + 20B01A562DEE4DCA00FED98F /* BacktraceReporterThreadSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B01A532DEE4DC600FED98F /* BacktraceReporterThreadSpec.swift */; }; 20B01A4B2DB87D1200FED98F /* CustomDirectoryBacktraceClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B01A4A2DB87D1200FED98F /* CustomDirectoryBacktraceClientTests.swift */; }; 20B01A4C2DB87D1200FED98F /* CustomDirectoryBacktraceClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B01A4A2DB87D1200FED98F /* CustomDirectoryBacktraceClientTests.swift */; }; 20B01A4D2DB87D1200FED98F /* CustomDirectoryBacktraceClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B01A4A2DB87D1200FED98F /* CustomDirectoryBacktraceClientTests.swift */; }; @@ -428,6 +431,7 @@ 2050DBBE2C66D98500C6CCA9 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 2062D9C42C457B4500E4CE3C /* Crash+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crash+CoreDataClass.swift"; sourceTree = ""; }; 2062D9C52C457B4500E4CE3C /* Crash+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crash+CoreDataProperties.swift"; sourceTree = ""; }; + 20B01A532DEE4DC600FED98F /* BacktraceReporterThreadSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceReporterThreadSpec.swift; sourceTree = ""; }; 20B01A4A2DB87D1200FED98F /* CustomDirectoryBacktraceClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDirectoryBacktraceClientTests.swift; sourceTree = ""; }; 20D4E5F32CB46A41000C92BF /* BacktraceResources-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "BacktraceResources-Info.plist"; sourceTree = ""; }; 20DE4B332D4830D00076B3F6 /* NSManagedObjectContext+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Extensions.swift"; sourceTree = ""; }; @@ -888,6 +892,7 @@ F266B85321C77D5D00D14417 /* Tests */ = { isa = PBXGroup; children = ( + 20B01A532DEE4DC600FED98F /* BacktraceReporterThreadSpec.swift */, 20B01A4A2DB87D1200FED98F /* CustomDirectoryBacktraceClientTests.swift */, 20DE4B372D48615C0076B3F6 /* NSManagedObjectContextExtensionTests.swift */, A24A4B5428B595D8004F5052 /* AttachmentStorageTests.swift */, @@ -2121,6 +2126,7 @@ A24A4B6228B595D9004F5052 /* BacktraceFileManagerTests.swift in Sources */, AFCCCE252625392300B83A28 /* ReportMetadataStorageMock.swift in Sources */, A24A4B7128B595D9004F5052 /* BacktraceCredentialsTests.swift in Sources */, + 20B01A562DEE4DCA00FED98F /* BacktraceReporterThreadSpec.swift in Sources */, A24A4B5F28B595D9004F5052 /* BacktraceDatabaseTests.swift in Sources */, A24A4B9328B59653004F5052 /* BacktraceNotificationObserverMock.swift in Sources */, A24A4B6B28B595D9004F5052 /* BacktraceRateLimiterTests.swift in Sources */, @@ -2215,6 +2221,7 @@ A24A4B8228B595D9004F5052 /* BacktraceBreadcrumbTests.swift in Sources */, A24A4B7628B595D9004F5052 /* DispatcherTests.swift in Sources */, A24A4B6728B595D9004F5052 /* BacktraceOomWatcherTests.swift in Sources */, + 20B01A542DEE4DCA00FED98F /* BacktraceReporterThreadSpec.swift in Sources */, F2AB636B2244243500939BC9 /* Quick+Throws.swift in Sources */, A24A4B5B28B595D9004F5052 /* BacktraceWatcherTests.swift in Sources */, A24A4B7928B595D9004F5052 /* AttributesTests.swift in Sources */, @@ -2331,6 +2338,7 @@ A24A4B8128B595D9004F5052 /* BacktraceBreadcrumbTests.swift in Sources */, A24A4B7528B595D9004F5052 /* DispatcherTests.swift in Sources */, A24A4B6628B595D9004F5052 /* BacktraceOomWatcherTests.swift in Sources */, + 20B01A552DEE4DCA00FED98F /* BacktraceReporterThreadSpec.swift in Sources */, F2AB636A2244243500939BC9 /* Quick+Throws.swift in Sources */, A24A4B5A28B595D9004F5052 /* BacktraceWatcherTests.swift in Sources */, A24A4B7828B595D9004F5052 /* AttributesTests.swift in Sources */, diff --git a/Sources/Public/BacktraceCrashReporter.swift b/Sources/Public/BacktraceCrashReporter.swift index aa481f35..04881e89 100644 --- a/Sources/Public/BacktraceCrashReporter.swift +++ b/Sources/Public/BacktraceCrashReporter.swift @@ -51,7 +51,20 @@ extension BacktraceCrashReporter: CrashReporting { attributes: Attributes, attachmentPaths: [String] = []) throws -> BacktraceReport { - let reportData = try reporter.generateLiveReport(with: exception) + return try generateLiveReport(exception: exception, + thread: mach_thread_self(), + attributes: attributes, + attachmentPaths: attachmentPaths) + } + + func generateLiveReport(exception: NSException? = nil, + thread: mach_port_t, + attributes: Attributes, + attachmentPaths: [String] = []) throws -> BacktraceReport { + + defer { mach_port_deallocate(mach_task_self_, thread) } + let reportData = try reporter.generateLiveReport(withThread: thread, exception: exception) + return try BacktraceReport(report: reportData, attributes: attributes, attachmentPaths: attachmentPaths) } diff --git a/Sources/Public/Internal/CrashReporting.swift b/Sources/Public/Internal/CrashReporting.swift index f314b25e..60745ff6 100644 --- a/Sources/Public/Internal/CrashReporting.swift +++ b/Sources/Public/Internal/CrashReporting.swift @@ -1,7 +1,8 @@ import Foundation protocol CrashReporting { - func generateLiveReport(exception: NSException?, attributes: Attributes, + func generateLiveReport(exception: NSException?, + attributes: Attributes, attachmentPaths: [String]) throws -> BacktraceReport func pendingCrashReport() throws -> BacktraceReport func purgePendingCrashReport() throws diff --git a/Tests/BacktraceReporterThreadSpec.swift b/Tests/BacktraceReporterThreadSpec.swift new file mode 100644 index 00000000..f9692c4e --- /dev/null +++ b/Tests/BacktraceReporterThreadSpec.swift @@ -0,0 +1,87 @@ +import Quick +import Nimble +import CrashReporter +@testable import Backtrace + +final class BacktraceReporterThreadSpec: QuickSpec { + + override func spec() { + + describe("Thread-aware live-report capture") { + + var backtraceReporter: BacktraceReporter! + var mockPLCR: PLCrashReporterMock! + var urlSession: URLSessionMock! + var credentials: BacktraceCredentials! + + beforeEach { + credentials = BacktraceCredentials( + endpoint: URL(string: "https://example.backtrace.io")!, + token: "" + ) + urlSession = URLSessionMock() + mockPLCR = PLCrashReporterMock() + + let crashReporter = BacktraceCrashReporter(reporter: mockPLCR) + + let api = BacktraceApi( + credentials: credentials, + session: urlSession, + reportsPerMin: 30 + ) + + backtraceReporter = try! BacktraceReporter( + reporter: crashReporter, + api: api, + dbSettings: BacktraceDatabaseSettings(), + credentials: credentials, + urlSession: urlSession + ) + } + + it("passes the calling thread to PLCrashReporter") { + let callThread = mach_thread_self() + _ = try? backtraceReporter.generate() + expect(mockPLCR.lastThread).to(equal(callThread)) + mach_port_deallocate(mach_task_self_, callThread) + } + + it("does not leak Mach SEND rights (> 2 refs to cover noise from Quick/Nimble or XCTest)") { + let before = sendRefCount() + _ = try? backtraceReporter.generate() + let after = sendRefCount() + expect(after).to(beLessThanOrEqualTo(before + 2)) + } + } + } +} + +/// Mock PLCrashReporter stubs that records the thread parameter +private final class PLCrashReporterMock: PLCrashReporter { + + private(set) var lastThread: thread_t = mach_thread_self() + + override func generateLiveReport( + withThread thread: thread_t, + exception: NSException? + ) throws -> Data { + lastThread = thread + return Data([0xCA, 0xFE]) + } + + override func generateLiveReport(with exception: NSException?) throws -> Data { + return Data([0xBE, 0xEF]) + } +} + +/// Current ref-count for thread +private func sendRefCount() -> mach_port_urefs_t { + var refs: mach_port_urefs_t = 0 + mach_port_get_refs( + mach_task_self_, + mach_thread_self(), + MACH_PORT_RIGHT_SEND, + &refs + ) + return refs +}