From 5f4d2c3907ec0939f3e276404d423e10088c4a25 Mon Sep 17 00:00:00 2001 From: melekr Date: Wed, 9 Apr 2025 18:59:39 -0400 Subject: [PATCH] Allow for custom crash directory and FileProtectionType --- Sources/Public/BacktraceClient.swift | 15 ++++- .../Public/BacktraceClientConfiguration.swift | 41 +++++++++++- Sources/Public/BacktraceCrashReporter.swift | 42 ++++++++++++ Tests/BacktraceClientSpec.swift | 64 +++++++++++++++++++ Tests/BacktraceCrashReporterSpec.swift | 56 ++++++++++++++++ 5 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 Tests/BacktraceClientSpec.swift create mode 100644 Tests/BacktraceCrashReporterSpec.swift diff --git a/Sources/Public/BacktraceClient.swift b/Sources/Public/BacktraceClient.swift index c3adb720..15ddbf98 100644 --- a/Sources/Public/BacktraceClient.swift +++ b/Sources/Public/BacktraceClient.swift @@ -52,7 +52,20 @@ import Foundation @objc public convenience init(configuration: BacktraceClientConfiguration) throws { let api = BacktraceApi(credentials: configuration.credentials, reportsPerMin: configuration.reportsPerMin) - let reporter = try BacktraceReporter(reporter: BacktraceCrashReporter(), api: api, dbSettings: configuration.dbSettings, + + let crashReporter: BacktraceCrashReporter + if let customDir = configuration.crashDirectory { + crashReporter = BacktraceCrashReporter( + crashDirectory: customDir, + fileProtection: configuration.fileProtection, + signalHandlerType: .BSD, + symbolicationStrategy: .all + ) + } else { + crashReporter = BacktraceCrashReporter() + } + + let reporter = try BacktraceReporter(reporter: crashReporter, api: api, dbSettings: configuration.dbSettings, credentials: configuration.credentials) try self.init(configuration: configuration, debugger: DebuggerChecker.self, reporter: reporter, dispatcher: Dispatcher(), api: api) diff --git a/Sources/Public/BacktraceClientConfiguration.swift b/Sources/Public/BacktraceClientConfiguration.swift index 9b64b62b..2485fca2 100644 --- a/Sources/Public/BacktraceClientConfiguration.swift +++ b/Sources/Public/BacktraceClientConfiguration.swift @@ -27,6 +27,17 @@ import Foundation /// Flag responsible for detecting and sending possible OOM cashes @objc public var detectOom: Bool = false + + /// Custom directory for storing `.plcrash` files. Defaults to `nil`, + /// Defaults to PLCrashReporter standard directory. + @objc public var crashDirectory: URL? + + /// File protection for the custom directory. Defaults to `.none`. + /// + /// - Important: Using `.none` ensures the app can write crash reports + /// even if the device is locked. More secure options can cause missed crash logs. + @objc public var fileProtection: FileProtectionType = .none + /// Produces Backtrace client configuration settings. /// /// - Parameters: @@ -41,8 +52,7 @@ import Foundation /// - credentials: Backtrace server API credentials. /// - dbSettings: Backtrace database settings. /// - reportsPerMin: Maximum number of records sent to Backtrace services in 1 minute. Default: `30`. - /// - allowsAttachingDebugger: if set to `true` BacktraceClient will report reports even when the debugger - /// is attached. Default: `false`. + /// - allowsAttachingDebugger: if set to `true` BacktraceClient will report reports even when the debugger is attached. Default: `false`. /// - detectOOM: if set to `true` BacktraceClient will detect when the app is out of memory. Default: `false`. @objc public init(credentials: BacktraceCredentials, dbSettings: BacktraceDatabaseSettings = BacktraceDatabaseSettings(), @@ -55,4 +65,31 @@ import Foundation self.allowsAttachingDebugger = allowsAttachingDebugger self.detectOom = detectOOM } + + /// Produces Backtrace client configuration settings, custom crash log directory and file protection level. + /// + /// - Parameters: + /// - credentials: Backtrace server API credentials. + /// - dbSettings: Backtrace database settings. + /// - reportsPerMin: Maximum number of records sent to Backtrace services in 1 minute. Default: `30`. + /// - allowsAttachingDebugger: if set to `true` BacktraceClient will report reports even when the debugger is attached. Default: `false`. + /// - detectOOM: if set to `true` BacktraceClient will detect when the app is out of memory. Default: `false`. + /// - crashDirectory: Custom directory for storing `.plcrash` files. Defaults to `nil`PLCrashReporter standard directory. + /// - fileProtection: OS file protection level. Default to`.none`to ensure the crash reports writes even if the device is locked. More secure options can cause missed crash logs. + @objc public init(credentials: BacktraceCredentials, + dbSettings: BacktraceDatabaseSettings = BacktraceDatabaseSettings(), + reportsPerMin: Int = 30, + allowsAttachingDebugger: Bool = false, + detectOOM: Bool = false, + crashDirectory: URL? = nil, + fileProtection: FileProtectionType = .none) { + + self.credentials = credentials + self.dbSettings = dbSettings + self.reportsPerMin = reportsPerMin + self.allowsAttachingDebugger = allowsAttachingDebugger + self.detectOom = detectOOM + self.crashDirectory = crashDirectory + self.fileProtection = fileProtection + } } diff --git a/Sources/Public/BacktraceCrashReporter.swift b/Sources/Public/BacktraceCrashReporter.swift index aa481f35..b8e87bad 100644 --- a/Sources/Public/BacktraceCrashReporter.swift +++ b/Sources/Public/BacktraceCrashReporter.swift @@ -14,6 +14,48 @@ import Darwin @objc public convenience init(config: PLCrashReporterConfig = PLCrashReporterConfig(signalHandlerType: .BSD, symbolicationStrategy: .all)) { self.init(reporter: PLCrashReporter(configuration: config)) } + + /** + Convenience initializer to create an instance of a crash reporter, allow storing crash logs in a custom directory. + + - Parameters: + - crashDirectory: Directory for `.plcrash` logs. + - fileProtection: File protection level. Default is `.none`. + - signalHandlerType: Type of crash signal handling. Defaults to `.BSD`. + - symbolicationStrategy: Strategy for local symbolication. Defaults to `.all`. + + The directory is created if it doesn't exist, and the file protection attribute is applied. + */ + @objc public convenience init(crashDirectory: URL, + fileProtection: FileProtectionType = .none, + signalHandlerType: PLCrashReporterSignalHandlerType = .BSD, + symbolicationStrategy: PLCrashReporterSymbolicationStrategy = .all) { + do { + let fm = FileManager.default + var attributes = [FileAttributeKey: Any]() + attributes[.protectionKey] = fileProtection + try fm.createDirectory( + at: crashDirectory, + withIntermediateDirectories: true, + attributes: attributes + ) + } catch { + BacktraceLogger.error("Could not create custom crash directory: \(error)") + } + + let basePathConfig = PLCrashReporterConfig( + signalHandlerType: signalHandlerType, + symbolicationStrategy: symbolicationStrategy, + basePath: crashDirectory.path + ) + + let defaultConfig = PLCrashReporterConfig( + signalHandlerType: .BSD, + symbolicationStrategy: .all + ) + + self.init(reporter: PLCrashReporter(configuration: basePathConfig) ?? PLCrashReporter(configuration: defaultConfig)) + } /// Creates an instance of a crash reporter. /// - Parameter reporter: An instance of `PLCrashReporter` to use. diff --git a/Tests/BacktraceClientSpec.swift b/Tests/BacktraceClientSpec.swift new file mode 100644 index 00000000..e1b4a265 --- /dev/null +++ b/Tests/BacktraceClientSpec.swift @@ -0,0 +1,64 @@ +import XCTest +import Nimble +import Quick +@testable import Backtrace + +final class BacktraceClientSpec: QuickSpec { + override func spec() { + describe("BacktraceClient") { + + context("when crashDirectory is set") { + it("creates the directory and uses it for crash logs") { + let fileManager = FileManager.default + let customDirectory = fileManager.temporaryDirectory.appendingPathComponent("bt-client-spec-\(UUID().uuidString)") + try? fileManager.removeItem(at: customDirectory) + + let creds = BacktraceCredentials( + endpoint: URL(string: "https://yourteam.backtrace.io")!, + token: "test-token" + ) + let config = BacktraceClientConfiguration( + credentials: creds, + dbSettings: BacktraceDatabaseSettings(), + reportsPerMin: 30, + allowsAttachingDebugger: true, + detectOOM: true, + crashDirectory: customDirectory, + fileProtection: .none + ) + + expect { + try BacktraceClient(configuration: config) + }.toNot(throwError()) + + var isDir: ObjCBool = false + let exists = fileManager.fileExists(atPath: customDirectory.path, isDirectory: &isDir) + expect(exists).to(beTrue()) + expect(isDir.boolValue).to(beTrue()) + + let attributes = try? fileManager.attributesOfItem(atPath: customDirectory.path) + let protection = attributes?[.protectionKey] as? FileProtectionType +#if !targetEnvironment(simulator) + expect(protection).to(equal(FileProtectionType.none)) +#endif + } + } + + context("when crashDirectory is nil") { + it("should not create the custom directory (logic only)") { + let fileManager = FileManager.default + let customDirectory = fileManager.temporaryDirectory.appendingPathComponent("bt-client-spec-\(UUID().uuidString)") + try? fileManager.removeItem(at: customDirectory) + + let crashDirectory: URL? = nil + + expect(crashDirectory).to(beNil()) + + let exists = fileManager.fileExists(atPath: customDirectory.path) + expect(exists).to(beFalse(), description: "Directory should not exist when crashDirectory is nil") + } + } + } + } +} + diff --git a/Tests/BacktraceCrashReporterSpec.swift b/Tests/BacktraceCrashReporterSpec.swift new file mode 100644 index 00000000..eb79d670 --- /dev/null +++ b/Tests/BacktraceCrashReporterSpec.swift @@ -0,0 +1,56 @@ +import XCTest +import Nimble +import Quick +@testable import Backtrace + +final class BacktraceCrashReporterSpec: QuickSpec { + override func spec() { + describe("BacktraceCrashReporter") { + + context("when using the new convenience initializer") { + let fileManager = FileManager.default + var customDirectory: URL! + + beforeEach { + customDirectory = fileManager + .temporaryDirectory + .appendingPathComponent("bt-test-crash-reporter-\(UUID().uuidString)") + try? fileManager.removeItem(at: customDirectory) + } + + it("creates the custom directory with the specified file protection") { + let reporter = BacktraceCrashReporter( + crashDirectory: customDirectory, + fileProtection: .none, + signalHandlerType: .BSD, + symbolicationStrategy: .all + ) + + var isDir: ObjCBool = false + let exists = fileManager.fileExists(atPath: customDirectory.path, isDirectory: &isDir) + expect(exists).to(beTrue(), description: "Expected directory to exist.") + expect(isDir.boolValue).to(beTrue(), description: "Expected path to be a directory.") + +#if !targetEnvironment(simulator) + let attributes = try? fileManager.attributesOfItem(atPath: customDirectory.path) + let protection = attributes?[.protectionKey] as? FileProtectionType + expect(protection).to(equal(FileProtectionType.none), description: "Expected file protection to match input (.none).") +#endif + + expect { try reporter.generateLiveReport(attributes: [:]) }.toNot(throwError()) + } + + it("falls back to default config if custom config fails") { + let invalidPath = URL(fileURLWithPath: "/dev/null/invalid") + + let reporter = BacktraceCrashReporter( + crashDirectory: invalidPath, + fileProtection: .complete + ) + + expect { try reporter.generateLiveReport(attributes: [:]) }.toNot(throwError()) + } + } + } + } +}