From e680dd95150992489fdcfbf870e6baf8a7158a44 Mon Sep 17 00:00:00 2001 From: Nikolay Volosatov Date: Tue, 3 Sep 2024 21:11:43 +0100 Subject: [PATCH] Extract report store API (#549) * Rename C API report store * Move reporting to a separate class * Fix unit tests * Fix format * Fix linter issues * Update C API for report store * Fix tests * Fix linter issues * Fix wrong "c" * Update docs * Introduce store configuration * Fix config memory leak * Fix sample * Fix default report store installation * Add default reports subfolder to API * Fix linter * Fix delete behavior of installation * Add docs * Fix order of `installation.addConditionalAlert` in README * Address code review --- README.md | 10 +- .../IntegrationTestsHelper/ReportConfig.swift | 7 +- ...le.swift => CrashReportStore+Bridge.swift} | 29 ++- .../Sources/LibraryBridge/InstallBridge.swift | 7 +- .../Common/Sources/SampleUI/SampleView.swift | 2 +- .../SampleUI/Screens/InstallView.swift | 1 - .../Sources/SampleUI/Screens/MainView.swift | 13 +- .../SampleUI/Screens/ReportingView.swift | 7 +- .../KSCrashDemangleFilter/KSDemangle_CPP.h | 2 + .../KSCrashDemangleFilter/KSDemangle_Swift.h | 3 +- .../KSCrashInstallation.m | 31 ++- Sources/KSCrashRecording/KSCrash+Private.h | 18 ++ Sources/KSCrashRecording/KSCrash.m | 171 ++------------ Sources/KSCrashRecording/KSCrashC.c | 117 ++-------- .../KSCrashConfiguration+Private.h | 6 + .../KSCrashRecording/KSCrashConfiguration.m | 66 +++++- Sources/KSCrashRecording/KSCrashReportStore.m | 211 ++++++++++++++++++ .../KSCrashReportStoreC+Private.h | 51 +++++ ...ashReportStore.c => KSCrashReportStoreC.c} | 161 +++++++------ .../Monitors/KSCrashMonitor_Memory.m | 3 +- Sources/KSCrashRecording/include/KSCrash.h | 67 +----- Sources/KSCrashRecording/include/KSCrashC.h | 59 +---- .../include/KSCrashCConfiguration.h | 104 +++++++-- .../include/KSCrashConfiguration.h | 51 +++-- .../include/KSCrashReportStore.h | 140 ++++++++++++ .../KSCrashReportStoreC.h} | 71 +++--- .../KSCrashConfiguration_Tests.m | 42 +--- .../KSCrashMonitor_Memory_Tests.m | 14 +- ...re_Tests.m => KSCrashReportStoreC_Tests.m} | 82 +++---- 29 files changed, 916 insertions(+), 630 deletions(-) rename Samples/Common/Sources/LibraryBridge/{ReportingSample.swift => CrashReportStore+Bridge.swift} (89%) create mode 100644 Sources/KSCrashRecording/KSCrashReportStore.m create mode 100644 Sources/KSCrashRecording/KSCrashReportStoreC+Private.h rename Sources/KSCrashRecording/{KSCrashReportStore.c => KSCrashReportStoreC.c} (57%) create mode 100644 Sources/KSCrashRecording/include/KSCrashReportStore.h rename Sources/KSCrashRecording/{KSCrashReportStore.h => include/KSCrashReportStoreC.h} (50%) rename Tests/KSCrashRecordingTests/{KSCrashReportStore_Tests.m => KSCrashReportStoreC_Tests.m} (69%) diff --git a/README.md b/README.md index 7aaa0924..d3a965a1 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let installation = CrashInstallationStandard.shared installation.url = URL(string: "http://put.your.url.here")! + // Install the crash reporting system + let config = KSCrashConfiguration() + config.monitors = [.machException, .signal] + installation.install(with: config) // set `nil` for default config + // Optional: Add an alert confirmation (recommended for email installation) installation.addConditionalAlert( withTitle: "Crash Detected", @@ -139,11 +144,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { noAnswer: "No thanks" ) - // Install the crash reporting system - let config = KSCrashConfiguration() - config.monitors = [.machException, .signal] - installation.install(with: config) // set `nil` for default config - return true } } diff --git a/Samples/Common/Sources/IntegrationTestsHelper/ReportConfig.swift b/Samples/Common/Sources/IntegrationTestsHelper/ReportConfig.swift index b53ba64e..0cb20044 100644 --- a/Samples/Common/Sources/IntegrationTestsHelper/ReportConfig.swift +++ b/Samples/Common/Sources/IntegrationTestsHelper/ReportConfig.swift @@ -41,11 +41,14 @@ public struct ReportConfig: Codable { extension ReportConfig { func report() { let url = URL(fileURLWithPath: directoryPath) - KSCrash.shared.sink = CrashReportFilterPipeline(filtersArray: [ + guard let store = KSCrash.shared.reportStore else { + return + } + store.sink = CrashReportFilterPipeline(filtersArray: [ CrashReportFilterAppleFmt(), DirectorySink(url), ]) - KSCrash.shared.sendAllReports() + store.sendAllReports() } } diff --git a/Samples/Common/Sources/LibraryBridge/ReportingSample.swift b/Samples/Common/Sources/LibraryBridge/CrashReportStore+Bridge.swift similarity index 89% rename from Samples/Common/Sources/LibraryBridge/ReportingSample.swift rename to Samples/Common/Sources/LibraryBridge/CrashReportStore+Bridge.swift index a21510ca..a2c5ebb6 100644 --- a/Samples/Common/Sources/LibraryBridge/ReportingSample.swift +++ b/Samples/Common/Sources/LibraryBridge/CrashReportStore+Bridge.swift @@ -1,5 +1,5 @@ // -// ReportingSample.swift +// CrashReportStore+Bridge.swift // // Created by Nikolay Volosatov on 2024-06-23. // @@ -31,12 +31,25 @@ import KSCrashSinks import KSCrashDemangleFilter import Logging -public class ReportingSample { +public extension CrashReportStore { private static let logger = Logger(label: "ReportingSample") - public static func logToConsole() { - KSCrash.shared.sink = CrashReportSinkConsole.filter().defaultCrashReportFilterSet() - KSCrash.shared.sendAllReports { reports, isSuccess, error in + func logAll() async throws { + let (reports, isSuccess) = try await sendAllReports() + guard isSuccess else { + return + } + for report in reports { + guard let report = report as? CrashReportDictionary else { + continue + } + Self.logger.info("\(report.value)") + } + } + + func logToConsole() { + sink = CrashReportSinkConsole.filter().defaultCrashReportFilterSet() + sendAllReports { reports, isSuccess, error in if isSuccess, let reports { Self.logger.info("Logged \(reports.count) reports") for (idx, report) in reports.enumerated() { @@ -57,13 +70,13 @@ public class ReportingSample { } } - public static func sampleLogToConsole() { - KSCrash.shared.sink = CrashReportFilterPipeline(filtersArray: [ + func sampleLogToConsole() { + sink = CrashReportFilterPipeline(filtersArray: [ CrashReportFilterDemangle(), SampleFilter(), SampleSink(), ]) - KSCrash.shared.sendAllReports() + sendAllReports() } } diff --git a/Samples/Common/Sources/LibraryBridge/InstallBridge.swift b/Samples/Common/Sources/LibraryBridge/InstallBridge.swift index 9a93ae7e..17725645 100644 --- a/Samples/Common/Sources/LibraryBridge/InstallBridge.swift +++ b/Samples/Common/Sources/LibraryBridge/InstallBridge.swift @@ -70,6 +70,8 @@ public class InstallBridge: ObservableObject { @Published public var reportsOnlySetup: Bool = false @Published public var error: InstallationError? + @Published public var reportStore: CrashReportStore? + public init() { config = .init() @@ -87,6 +89,7 @@ public class InstallBridge: ObservableObject { do { try KSCrash.shared.install(with: config) + reportStore = KSCrash.shared.reportStore installed = true } catch let error as KSCrashInstallError { let message = error.localizedDescription @@ -101,7 +104,9 @@ public class InstallBridge: ObservableObject { public func setupReportsOnly() { do { - try KSCrash.shared.setupReportStore(withPath: config.installPath) + let config = CrashReportStoreConfiguration() + config.reportsPath = self.config.installPath.map { $0 + "/" + CrashReportStore.defaultInstallSubfolder } + reportStore = try CrashReportStore(configuration: config) reportsOnlySetup = true } catch let error as KSCrashInstallError { let message = error.localizedDescription diff --git a/Samples/Common/Sources/SampleUI/SampleView.swift b/Samples/Common/Sources/SampleUI/SampleView.swift index e14ffe5e..102020d8 100644 --- a/Samples/Common/Sources/SampleUI/SampleView.swift +++ b/Samples/Common/Sources/SampleUI/SampleView.swift @@ -38,7 +38,7 @@ public struct SampleView: View { NavigationView { if installBridge.installed || installBridge.reportsOnlySetup { MainView( - reportsOnlySetup: $installBridge.reportsOnlySetup + bridge: installBridge ) .navigationTitle("KSCrash Sample") } else { diff --git a/Samples/Common/Sources/SampleUI/Screens/InstallView.swift b/Samples/Common/Sources/SampleUI/Screens/InstallView.swift index 8078c5d5..23827522 100644 --- a/Samples/Common/Sources/SampleUI/Screens/InstallView.swift +++ b/Samples/Common/Sources/SampleUI/Screens/InstallView.swift @@ -76,7 +76,6 @@ struct InstallView: View { Toggle(isOn: bridge.configBinding(for: \.enableSwapCxaThrow)) { Text("Swap __cxa_throw") } - // TODO: Add deleteBehaviorAfterSendAll } Button("Only set up reports") { diff --git a/Samples/Common/Sources/SampleUI/Screens/MainView.swift b/Samples/Common/Sources/SampleUI/Screens/MainView.swift index 1675bd54..735a65a5 100644 --- a/Samples/Common/Sources/SampleUI/Screens/MainView.swift +++ b/Samples/Common/Sources/SampleUI/Screens/MainView.swift @@ -26,19 +26,20 @@ import Foundation import SwiftUI +import LibraryBridge struct MainView: View { - @Binding var reportsOnlySetup: Bool + @ObservedObject var bridge: InstallBridge var body: some View { List { Section { - if reportsOnlySetup { + if bridge.reportsOnlySetup { Text("It's only reporting that was set up. Crashes won't be caught. You can go back to the install screen.") .foregroundStyle(Color.secondary) Button("Back to Install") { - reportsOnlySetup = false + bridge.reportsOnlySetup = false } } else { Text("KSCrash is installed successfully") @@ -47,7 +48,11 @@ struct MainView: View { } NavigationLink("Crash", destination: CrashView()) - NavigationLink("Report", destination: ReportingView()) + if let store = bridge.reportStore { + NavigationLink("Report", destination: ReportingView(store: store)) + } else { + Text("Reporting is not available") + } } } } diff --git a/Samples/Common/Sources/SampleUI/Screens/ReportingView.swift b/Samples/Common/Sources/SampleUI/Screens/ReportingView.swift index abb99955..0af5bbc9 100644 --- a/Samples/Common/Sources/SampleUI/Screens/ReportingView.swift +++ b/Samples/Common/Sources/SampleUI/Screens/ReportingView.swift @@ -26,15 +26,18 @@ import SwiftUI import LibraryBridge +import KSCrashRecording struct ReportingView: View { + let store: CrashReportStore + var body: some View { List { Button("Log To Console") { - ReportingSample.logToConsole() + store.logToConsole() } Button("Sample Custom Log To Console") { - ReportingSample.sampleLogToConsole() + store.sampleLogToConsole() } } .navigationTitle("Report") diff --git a/Sources/KSCrashDemangleFilter/KSDemangle_CPP.h b/Sources/KSCrashDemangleFilter/KSDemangle_CPP.h index b6116b6c..1015c335 100644 --- a/Sources/KSCrashDemangleFilter/KSDemangle_CPP.h +++ b/Sources/KSCrashDemangleFilter/KSDemangle_CPP.h @@ -32,6 +32,8 @@ extern "C" { #endif /** Demangle a C++ symbol. + * + * @warning MEMORY MANAGEMENT WARNING: User is responsible for calling free() on the returned value. * * @param mangledSymbol The mangled symbol. * diff --git a/Sources/KSCrashDemangleFilter/KSDemangle_Swift.h b/Sources/KSCrashDemangleFilter/KSDemangle_Swift.h index f46cf8ad..5b900275 100644 --- a/Sources/KSCrashDemangleFilter/KSDemangle_Swift.h +++ b/Sources/KSCrashDemangleFilter/KSDemangle_Swift.h @@ -32,11 +32,12 @@ extern "C" { #endif /** Demangle a Swift symbol. + * + * @warning MEMORY MANAGEMENT WARNING: User is responsible for calling free() on the returned value. * * @param mangledSymbol The mangled symbol. * * @return A demangled symbol, or NULL if demangling failed. - * MEMORY MANAGEMENT WARNING: User is responsible for calling free() on the returned value. */ char *ksdm_demangleSwift(const char *mangledSymbol); diff --git a/Sources/KSCrashInstallations/KSCrashInstallation.m b/Sources/KSCrashInstallations/KSCrashInstallation.m index f004fd20..21fd0bb4 100644 --- a/Sources/KSCrashInstallations/KSCrashInstallation.m +++ b/Sources/KSCrashInstallations/KSCrashInstallation.m @@ -322,9 +322,19 @@ - (void)sendAllReportsWithCompletion:(KSCrashReportFilterCompletion)onCompletion } sink = [KSCrashReportFilterPipeline filterWithFiltersArray:sinkFilters]; - KSCrash *handler = [KSCrash sharedInstance]; - handler.sink = sink; - [handler sendAllReportsWithCompletion:onCompletion]; + KSCrashReportStore *store = [KSCrash sharedInstance].reportStore; + if (store == nil) { + onCompletion( + nil, NO, + [KSNSErrorHelper + errorWithDomain:[[self class] description] + code:0 + description:@"Reporting is not allowed before the call of `installWithConfiguration:error:`"]); + return; + } + + store.sink = sink; + [store sendAllReportsWithCompletion:onCompletion]; } - (void)addPreFilter:(id)filter @@ -346,14 +356,13 @@ - (void)addConditionalAlertWithTitle:(NSString *)title message:message yesAnswer:yesAnswer noAnswer:noAnswer]]; - // FIXME: Accessing config - // KSCrash* handler = [KSCrash sharedInstance]; - // if(handler.deleteBehaviorAfterSendAll == KSCDeleteOnSucess) - // { - // // Better to delete always, or else the user will keep getting nagged - // // until he presses "yes"! - // handler.deleteBehaviorAfterSendAll = KSCDeleteAlways; - // } + + KSCrashReportStore *store = [KSCrash sharedInstance].reportStore; + if (store.reportCleanupPolicy == KSCrashReportCleanupPolicyOnSuccess) { + // Better to delete always, or else the user will keep getting nagged + // until he presses "yes"! + store.reportCleanupPolicy = KSCrashReportCleanupPolicyAlways; + } } - (void)addUnconditionalAlertWithTitle:(NSString *)title diff --git a/Sources/KSCrashRecording/KSCrash+Private.h b/Sources/KSCrashRecording/KSCrash+Private.h index 555b771c..3ca229e0 100644 --- a/Sources/KSCrashRecording/KSCrash+Private.h +++ b/Sources/KSCrashRecording/KSCrash+Private.h @@ -28,12 +28,30 @@ #define KSCrash_Private_h #import "KSCrash.h" +#import "KSCrashError.h" + +NS_ASSUME_NONNULL_BEGIN + +#ifdef __cplusplus +extern "C" { +#endif + +NSString *kscrash_getBundleName(void); +NSString *kscrash_getDefaultInstallPath(void); + +#ifdef __cplusplus +} +#endif @interface KSCrash () @property(nonatomic, readwrite, assign) NSUncaughtExceptionHandler *uncaughtExceptionHandler; @property(nonatomic, readwrite, assign) NSUncaughtExceptionHandler *currentSnapshotUserReportedExceptionHandler; ++ (NSError *)errorForInstallErrorCode:(KSCrashInstallErrorCode)errorCode; + @end +NS_ASSUME_NONNULL_END + #endif /* KSCrash_Private_h */ diff --git a/Sources/KSCrashRecording/KSCrash.m b/Sources/KSCrashRecording/KSCrash.m index b4c417e9..ea37384f 100644 --- a/Sources/KSCrashRecording/KSCrash.m +++ b/Sources/KSCrashRecording/KSCrash.m @@ -61,7 +61,7 @@ @interface KSCrash () static BOOL gIsSharedInstanceCreated = NO; -static NSString *getBundleName(void) +NSString *kscrash_getBundleName(void) { NSString *bundleName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"]; if (bundleName == nil) { @@ -70,7 +70,7 @@ @interface KSCrash () return bundleName; } -static NSString *getDefaultInstallPath(void) +NSString *kscrash_getDefaultInstallPath(void) { NSArray *directories = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); if ([directories count] == 0) { @@ -82,7 +82,7 @@ @interface KSCrash () KSLOG_ERROR(@"Could not locate cache directory path."); return nil; } - NSString *pathEnd = [@"KSCrash" stringByAppendingPathComponent:getBundleName()]; + NSString *pathEnd = [@"KSCrash" stringByAppendingPathComponent:kscrash_getBundleName()]; return [cachePath stringByAppendingPathComponent:pathEnd]; } @@ -119,7 +119,7 @@ + (instancetype)sharedInstance - (instancetype)init { if ((self = [super init])) { - _bundleName = getBundleName(); + _bundleName = kscrash_getBundleName(); } return self; } @@ -221,65 +221,36 @@ - (NSDictionary *)systemInfo - (BOOL)installWithConfiguration:(KSCrashConfiguration *)configuration error:(NSError **)error { self.configuration = [configuration copy] ?: [KSCrashConfiguration new]; - NSString *installPath = configuration.installPath ?: getDefaultInstallPath(); - KSCrashInstallErrorCode result = - kscrash_install(self.bundleName.UTF8String, installPath.UTF8String, [self.configuration toCConfiguration]); + self.configuration.installPath = configuration.installPath ?: kscrash_getDefaultInstallPath(); - if (result != KSCrashInstallErrorNone) { - if (error != NULL) { - *error = [self errorForInstallErrorCode:result]; - } + if (self.configuration.reportStoreConfiguration.appName == nil) { + self.configuration.reportStoreConfiguration.appName = self.bundleName; + } + if (self.configuration.reportStoreConfiguration.reportsPath == nil) { + self.configuration.reportStoreConfiguration.reportsPath = [self.configuration.installPath + stringByAppendingPathComponent:[KSCrashReportStore defaultInstallSubfolder]]; + } + KSCrashReportStore *reportStore = + [KSCrashReportStore storeWithConfiguration:self.configuration.reportStoreConfiguration error:error]; + if (reportStore == nil) { return NO; } - return YES; -} - -- (BOOL)setupReportStoreWithPath:(NSString *)installPath error:(NSError **)error -{ + KSCrashCConfiguration config = [self.configuration toCConfiguration]; KSCrashInstallErrorCode result = - kscrash_setupReportsStore(self.bundleName.UTF8String, (installPath ?: getDefaultInstallPath()).UTF8String); - + kscrash_install(self.bundleName.UTF8String, self.configuration.installPath.UTF8String, &config); + KSCrashCConfiguration_Release(&config); if (result != KSCrashInstallErrorNone) { if (error != NULL) { - *error = [self errorForInstallErrorCode:result]; + *error = [KSCrash errorForInstallErrorCode:result]; } return NO; } + _reportStore = reportStore; return YES; } -- (void)sendAllReportsWithCompletion:(KSCrashReportFilterCompletion)onCompletion -{ - NSArray *reports = [self allReports]; - - KSLOG_INFO(@"Sending %d crash reports", [reports count]); - - [self sendReports:reports - onCompletion:^(NSArray *filteredReports, BOOL completed, NSError *error) { - KSLOG_DEBUG(@"Process finished with completion: %d", completed); - if (error != nil) { - KSLOG_ERROR(@"Failed to send reports: %@", error); - } - if ((self.configuration.deleteBehaviorAfterSendAll == KSCDeleteOnSucess && completed) || - self.configuration.deleteBehaviorAfterSendAll == KSCDeleteAlways) { - kscrash_deleteAllReports(); - } - kscrash_callCompletion(onCompletion, filteredReports, completed, error); - }]; -} - -- (void)deleteAllReports -{ - kscrash_deleteAllReports(); -} - -- (void)deleteReportWithID:(int64_t)reportID -{ - kscrash_deleteReportWithID(reportID); -} - - (void)reportUserException:(NSString *)name reason:(NSString *)reason language:(NSString *)language @@ -324,106 +295,6 @@ -(TYPE)NAME { return kscrashstate_currentState()->NAME; } SYNTHESIZE_CRASH_STATE_PROPERTY(NSInteger, sessionsSinceLaunch) SYNTHESIZE_CRASH_STATE_PROPERTY(BOOL, crashedLastLaunch) -- (NSInteger)reportCount -{ - return kscrash_getReportCount(); -} - -- (void)sendReports:(NSArray> *)reports onCompletion:(KSCrashReportFilterCompletion)onCompletion -{ - if ([reports count] == 0) { - kscrash_callCompletion(onCompletion, reports, YES, nil); - return; - } - - if (self.sink == nil) { - kscrash_callCompletion(onCompletion, reports, NO, - [KSNSErrorHelper errorWithDomain:[[self class] description] - code:0 - description:@"No sink set. Crash reports not sent."]); - return; - } - - [self.sink filterReports:reports - onCompletion:^(NSArray *filteredReports, BOOL completed, NSError *error) { - kscrash_callCompletion(onCompletion, filteredReports, completed, error); - }]; -} - -- (NSData *)loadCrashReportJSONWithID:(int64_t)reportID -{ - char *report = kscrash_readReport(reportID); - if (report != NULL) { - return [NSData dataWithBytesNoCopy:report length:strlen(report) freeWhenDone:YES]; - } - return nil; -} - -- (void)doctorReport:(NSMutableDictionary *)report -{ - NSMutableDictionary *crashReport = report[KSCrashField_Crash]; - if (crashReport != nil) { - crashReport[KSCrashField_Diagnosis] = [[KSCrashDoctor doctor] diagnoseCrash:report]; - } - crashReport = report[KSCrashField_RecrashReport][KSCrashField_Crash]; - if (crashReport != nil) { - crashReport[KSCrashField_Diagnosis] = [[KSCrashDoctor doctor] diagnoseCrash:report]; - } -} - -- (NSArray *)reportIDs -{ - int reportCount = kscrash_getReportCount(); - int64_t reportIDsC[reportCount]; - reportCount = kscrash_getReportIDs(reportIDsC, reportCount); - NSMutableArray *reportIDs = [NSMutableArray arrayWithCapacity:(NSUInteger)reportCount]; - for (int i = 0; i < reportCount; i++) { - [reportIDs addObject:[NSNumber numberWithLongLong:reportIDsC[i]]]; - } - return [reportIDs copy]; -} - -- (KSCrashReportDictionary *)reportForID:(int64_t)reportID -{ - NSData *jsonData = [self loadCrashReportJSONWithID:reportID]; - if (jsonData == nil) { - return nil; - } - - NSError *error = nil; - NSMutableDictionary *crashReport = - [KSJSONCodec decode:jsonData - options:KSJSONDecodeOptionIgnoreNullInArray | KSJSONDecodeOptionIgnoreNullInObject | - KSJSONDecodeOptionKeepPartialObject - error:&error]; - if (error != nil) { - KSLOG_ERROR(@"Encountered error loading crash report %" PRIx64 ": %@", reportID, error); - } - if (crashReport == nil) { - KSLOG_ERROR(@"Could not load crash report"); - return nil; - } - [self doctorReport:crashReport]; - - return [KSCrashReportDictionary reportWithValue:crashReport]; -} - -- (NSArray *)allReports -{ - int reportCount = kscrash_getReportCount(); - int64_t reportIDs[reportCount]; - reportCount = kscrash_getReportIDs(reportIDs, reportCount); - NSMutableArray *reports = [NSMutableArray arrayWithCapacity:(NSUInteger)reportCount]; - for (int i = 0; i < reportCount; i++) { - KSCrashReportDictionary *report = [self reportForID:reportIDs[i]]; - if (report != nil) { - [reports addObject:report]; - } - } - - return reports; -} - // ============================================================================ #pragma mark - Utility - // ============================================================================ @@ -438,7 +309,7 @@ - (NSMutableData *)nullTerminated:(NSData *)data return mutable; } -- (NSError *)errorForInstallErrorCode:(KSCrashInstallErrorCode)errorCode ++ (NSError *)errorForInstallErrorCode:(KSCrashInstallErrorCode)errorCode { NSString *errorDescription; switch (errorCode) { diff --git a/Sources/KSCrashRecording/KSCrashC.c b/Sources/KSCrashRecording/KSCrashC.c index ac7af424..5b2d18fb 100644 --- a/Sources/KSCrashRecording/KSCrashC.c +++ b/Sources/KSCrashRecording/KSCrashC.c @@ -41,7 +41,7 @@ #include "KSCrashMonitor_Zombie.h" #include "KSCrashReportC.h" #include "KSCrashReportFixer.h" -#include "KSCrashReportStore.h" +#include "KSCrashReportStoreC+Private.h" #include "KSFileUtils.h" #include "KSObjC.h" #include "KSString.h" @@ -55,6 +55,8 @@ #include "KSLogger.h" +#define KSC_MAX_APP_NAME_LENGTH 100 + typedef enum { KSApplicationStateNone, KSApplicationStateDidBecomeActive, @@ -92,6 +94,7 @@ static bool g_shouldPrintPreviousLog = false; static char g_consoleLogPath[KSFU_MAX_PATH_LENGTH]; static KSCrashMonitorType g_monitoring = KSCrashMonitorTypeProductionSafeMinimal; static char g_lastCrashReportFilePath[KSFU_MAX_PATH_LENGTH]; +static KSCrashReportStoreCConfiguration g_reportStoreConfig; static KSReportWrittenCallback g_reportWrittenCallback; static KSApplicationState g_lastApplicationState = KSApplicationStateNone; @@ -153,7 +156,7 @@ static void onCrash(struct KSCrash_MonitorContext *monitorContext) kscrashreport_writeStandardReport(monitorContext, monitorContext->reportPath); } else { char crashReportFilePath[KSFU_MAX_PATH_LENGTH]; - int64_t reportID = kscrs_getNextCrashReport(crashReportFilePath); + int64_t reportID = kscrs_getNextCrashReport(crashReportFilePath, &g_reportStoreConfig); strncpy(g_lastCrashReportFilePath, crashReportFilePath, sizeof(g_lastCrashReportFilePath)); kscrashreport_writeStandardReport(monitorContext, crashReportFilePath); @@ -181,6 +184,8 @@ static void setMonitors(KSCrashMonitorType monitorTypes) void handleConfiguration(KSCrashCConfiguration *configuration) { + g_reportStoreConfig = KSCrashReportStoreCConfiguration_Copy(&configuration->reportStoreConfiguration); + if (configuration->userInfoJSON != NULL) { kscrashreport_setUserInfoJSON(configuration->userInfoJSON); } @@ -199,7 +204,6 @@ void handleConfiguration(KSCrashCConfiguration *configuration) g_reportWrittenCallback = configuration->reportWrittenCallback; g_shouldAddConsoleLogToReport = configuration->addConsoleLogToReport; g_shouldPrintPreviousLog = configuration->printPreviousLogOnStartup; - kscrs_setMaxReportCount(configuration->maxReportCount); if (configuration->enableSwapCxaThrow) { kscm_enableSwapCxaThrow(); @@ -209,23 +213,8 @@ void handleConfiguration(KSCrashCConfiguration *configuration) #pragma mark - API - // ============================================================================ -static KSCrashInstallErrorCode setupReportsStore(const char *appName, const char *const installPath) -{ - char path[KSFU_MAX_PATH_LENGTH]; - if (snprintf(path, sizeof(path), "%s/Reports", installPath) >= (int)sizeof(path)) { - KSLOG_ERROR("Reports path is too long."); - return KSCrashInstallErrorPathTooLong; - } - if (ksfu_makePath(path) == false) { - KSLOG_ERROR("Could not create path: %s", path); - return KSCrashInstallErrorCouldNotCreatePath; - } - kscrs_initialize(appName, path); - return KSCrashInstallErrorNone; -} - KSCrashInstallErrorCode kscrash_install(const char *appName, const char *const installPath, - KSCrashCConfiguration configuration) + KSCrashCConfiguration *configuration) { KSLOG_DEBUG("Installing crash reporter."); @@ -239,14 +228,23 @@ KSCrashInstallErrorCode kscrash_install(const char *appName, const char *const i return KSCrashInstallErrorInvalidParameter; } - handleConfiguration(&configuration); + handleConfiguration(configuration); - KSCrashInstallErrorCode result = setupReportsStore(appName, installPath); - if (result != KSCrashInstallErrorNone) { - return result; + if (g_reportStoreConfig.appName == NULL) { + g_reportStoreConfig.appName = strdup(appName); } char path[KSFU_MAX_PATH_LENGTH]; + if (g_reportStoreConfig.reportsPath == NULL) { + if (snprintf(path, sizeof(path), "%s/" KSCRS_DEFAULT_REPORTS_FOLDER, installPath) >= (int)sizeof(path)) { + KSLOG_ERROR("Reports path is too long."); + return KSCrashInstallErrorPathTooLong; + } + g_reportStoreConfig.reportsPath = strdup(path); + } + + kscrs_initialize(&g_reportStoreConfig); + if (snprintf(path, sizeof(path), "%s/Data", installPath) >= (int)sizeof(path)) { KSLOG_ERROR("Data path is too long."); return KSCrashInstallErrorPathTooLong; @@ -276,7 +274,7 @@ KSCrashInstallErrorCode kscrash_install(const char *appName, const char *const i ksccd_init(60); kscm_setEventCallback(onCrash); - setMonitors(configuration.monitors); + setMonitors(configuration->monitors); if (kscm_activateMonitors() == false) { KSLOG_ERROR("No crash monitors are active"); return KSCrashInstallErrorNoActiveMonitors; @@ -289,28 +287,6 @@ KSCrashInstallErrorCode kscrash_install(const char *appName, const char *const i return KSCrashInstallErrorNone; } -KSCrashInstallErrorCode kscrash_setupReportsStore(const char *appName, const char *const installPath) -{ - KSLOG_DEBUG("Installing reports store."); - - if (g_installed) { - KSLOG_DEBUG("Crash reporter is already installed and it's not allowed to set up reports store."); - return KSCrashInstallErrorAlreadyInstalled; - } - - if (appName == NULL || installPath == NULL) { - KSLOG_ERROR("Invalid parameters: appName or installPath is NULL."); - return KSCrashInstallErrorInvalidParameter; - } - - KSCrashInstallErrorCode result = setupReportsStore(appName, installPath); - if (result != KSCrashInstallErrorNone) { - return result; - } - - return KSCrashInstallErrorNone; -} - void kscrash_setUserInfoJSON(const char *const userInfoJSON) { kscrashreport_setUserInfoJSON(userInfoJSON); } const char *kscrash_getUserInfoJSON(void) { return kscrashreport_getUserInfoJSON(); } @@ -353,54 +329,7 @@ void kscrash_notifyAppTerminate(void) void kscrash_notifyAppCrash(void) { kscrashstate_notifyAppCrash(); } -int kscrash_getReportCount(void) { return kscrs_getReportCount(); } - -int kscrash_getReportIDs(int64_t *reportIDs, int count) { return kscrs_getReportIDs(reportIDs, count); } - -char *kscrash_readReportAtPath(const char *path) -{ - if (!path) { - return NULL; - } - - char *rawReport = kscrs_readReportAtPath(path); - if (rawReport == NULL) { - return NULL; - } - - char *fixedReport = kscrf_fixupCrashReport(rawReport); - - free(rawReport); - return fixedReport; -} - -char *kscrash_readReport(int64_t reportID) -{ - if (reportID <= 0) { - KSLOG_ERROR("Report ID was %" PRIx64, reportID); - return NULL; - } - - char *rawReport = kscrs_readReport(reportID); - if (rawReport == NULL) { - KSLOG_ERROR("Failed to load report ID %" PRIx64, reportID); - return NULL; - } - - char *fixedReport = kscrf_fixupCrashReport(rawReport); - if (fixedReport == NULL) { - KSLOG_ERROR("Failed to fixup report ID %" PRIx64, reportID); - } - - free(rawReport); - return fixedReport; -} - int64_t kscrash_addUserReport(const char *report, int reportLength) { - return kscrs_addUserReport(report, reportLength); + return kscrs_addUserReport(report, reportLength, &g_reportStoreConfig); } - -void kscrash_deleteAllReports(void) { kscrs_deleteAllReports(); } - -void kscrash_deleteReportWithID(int64_t reportID) { kscrs_deleteReportWithID(reportID); } diff --git a/Sources/KSCrashRecording/KSCrashConfiguration+Private.h b/Sources/KSCrashRecording/KSCrashConfiguration+Private.h index 05074948..8fa4cd35 100644 --- a/Sources/KSCrashRecording/KSCrashConfiguration+Private.h +++ b/Sources/KSCrashRecording/KSCrashConfiguration+Private.h @@ -36,4 +36,10 @@ @end +@interface KSCrashReportStoreConfiguration () + +- (KSCrashReportStoreCConfiguration)toCConfiguration; + +@end + #endif /* KSCrashConfiguration_Private_h */ diff --git a/Sources/KSCrashRecording/KSCrashConfiguration.m b/Sources/KSCrashRecording/KSCrashConfiguration.m index 04a555d2..99e64944 100644 --- a/Sources/KSCrashRecording/KSCrashConfiguration.m +++ b/Sources/KSCrashRecording/KSCrashConfiguration.m @@ -26,7 +26,9 @@ #import "KSCrashConfiguration.h" #import +#import "KSCrash+Private.h" #import "KSCrashConfiguration+Private.h" +#import "KSCrashReportStore.h" @implementation KSCrashConfiguration @@ -57,10 +59,13 @@ - (instancetype)init _reportWrittenCallback = nil; _addConsoleLogToReport = cConfig.addConsoleLogToReport ? YES : NO; _printPreviousLogOnStartup = cConfig.printPreviousLogOnStartup ? YES : NO; - _maxReportCount = cConfig.maxReportCount; _enableSwapCxaThrow = cConfig.enableSwapCxaThrow ? YES : NO; - _deleteBehaviorAfterSendAll = KSCDeleteAlways; // Used only in Obj-C interface + _reportStoreConfiguration = [KSCrashReportStoreConfiguration new]; + _reportStoreConfiguration.appName = nil; + _reportStoreConfiguration.maxReportCount = cConfig.reportStoreConfiguration.maxReportCount; + + KSCrashCConfiguration_Release(&cConfig); } return self; } @@ -69,6 +74,7 @@ - (KSCrashCConfiguration)toCConfiguration { KSCrashCConfiguration config = KSCrashCConfiguration_Default(); + config.reportStoreConfiguration = [self.reportStoreConfiguration toCConfiguration]; config.monitors = self.monitors; config.userInfoJSON = self.userInfoJSON ? [self jsonStringFromDictionary:self.userInfoJSON] : NULL; config.deadlockWatchdogInterval = self.deadlockWatchdogInterval; @@ -84,7 +90,6 @@ - (KSCrashCConfiguration)toCConfiguration } config.addConsoleLogToReport = self.addConsoleLogToReport; config.printPreviousLogOnStartup = self.printPreviousLogOnStartup; - config.maxReportCount = self.maxReportCount; config.enableSwapCxaThrow = self.enableSwapCxaThrow; return config; @@ -118,6 +123,12 @@ - (const char *)jsonStringFromDictionary:(NSDictionary *)dictionary - (nonnull id)copyWithZone:(nullable NSZone *)zone { KSCrashConfiguration *copy = [[KSCrashConfiguration allocWithZone:zone] init]; + if (copy == nil) { + return nil; + } + copy->_reportStoreConfiguration = [self.reportStoreConfiguration copyWithZone:zone]; + + copy.installPath = [self.installPath copyWithZone:zone]; copy.monitors = self.monitors; copy.userInfoJSON = [self.userInfoJSON copyWithZone:zone]; copy.deadlockWatchdogInterval = self.deadlockWatchdogInterval; @@ -131,9 +142,54 @@ - (nonnull id)copyWithZone:(nullable NSZone *)zone copy.reportWrittenCallback = [self.reportWrittenCallback copy]; copy.addConsoleLogToReport = self.addConsoleLogToReport; copy.printPreviousLogOnStartup = self.printPreviousLogOnStartup; - copy.maxReportCount = self.maxReportCount; copy.enableSwapCxaThrow = self.enableSwapCxaThrow; - copy.deleteBehaviorAfterSendAll = self.deleteBehaviorAfterSendAll; + return copy; +} + +@end + +@implementation KSCrashReportStoreConfiguration + +- (instancetype)init +{ + self = [super init]; + if (self != nil) { + _appName = nil; + _reportsPath = nil; + + KSCrashReportStoreCConfiguration cConfig = KSCrashReportStoreCConfiguration_Default(); + _maxReportCount = (NSInteger)cConfig.maxReportCount; + } + return self; +} + +- (KSCrashReportStoreCConfiguration)toCConfiguration +{ + NSString *resolvedAppName = self.appName ?: kscrash_getBundleName(); + NSString *resolvedReportsPath = self.reportsPath; + if (resolvedReportsPath == nil) { + // If reports path is not provided we use a default subfolder of a default install path. + resolvedReportsPath = kscrash_getDefaultInstallPath(); + resolvedReportsPath = + [resolvedReportsPath stringByAppendingPathComponent:[KSCrashReportStore defaultInstallSubfolder]]; + } + + KSCrashReportStoreCConfiguration config = KSCrashReportStoreCConfiguration_Default(); + config.appName = resolvedAppName != nil ? strdup(resolvedAppName.UTF8String) : NULL; + config.reportsPath = resolvedReportsPath != nil ? strdup(resolvedReportsPath.UTF8String) : NULL; + config.maxReportCount = (int)self.maxReportCount; + + return config; +} + +#pragma mark - NSCopying + +- (nonnull id)copyWithZone:(nullable NSZone *)zone +{ + KSCrashReportStoreConfiguration *copy = [[KSCrashReportStoreConfiguration allocWithZone:zone] init]; + copy.reportsPath = [self.reportsPath copyWithZone:zone]; + copy.appName = [self.appName copyWithZone:zone]; + copy.maxReportCount = self.maxReportCount; return copy; } diff --git a/Sources/KSCrashRecording/KSCrashReportStore.m b/Sources/KSCrashRecording/KSCrashReportStore.m new file mode 100644 index 00000000..9f878e16 --- /dev/null +++ b/Sources/KSCrashRecording/KSCrashReportStore.m @@ -0,0 +1,211 @@ +// +// KSCrashReportStore.m +// +// Created by Nikolay Volosatov on 2024-08-28. +// +// Copyright (c) 2012 Karl Stenerud. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall remain in place +// in this source code. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#import "KSCrashReportStore.h" + +#import "KSCrash+Private.h" +#import "KSCrashConfiguration+Private.h" +#import "KSCrashDoctor.h" +#import "KSCrashReport.h" +#import "KSCrashReportFields.h" +#import "KSCrashReportFilter.h" +#import "KSCrashReportStoreC.h" +#import "KSJSONCodecObjC.h" +#import "KSNSErrorHelper.h" + +// #define KSLogger_LocalLevel TRACE +#import "KSLogger.h" + +@implementation KSCrashReportStore { + KSCrashReportStoreCConfiguration _cConfig; +} + ++ (NSString *)defaultInstallSubfolder +{ + return @KSCRS_DEFAULT_REPORTS_FOLDER; +} + ++ (instancetype)defaultStoreWithError:(NSError **)error +{ + return [KSCrashReportStore storeWithConfiguration:nil error:error]; +} + ++ (instancetype)storeWithConfiguration:(KSCrashReportStoreConfiguration *)configuration error:(NSError **)error +{ + return [[KSCrashReportStore alloc] initWithConfiguration:configuration error:error]; +} + +- (nullable instancetype)initWithConfiguration:(KSCrashReportStoreConfiguration *)configuration error:(NSError **)error +{ + self = [super init]; + if (self != nil) { + _cConfig = [(configuration ?: [KSCrashReportStoreConfiguration new]) toCConfiguration]; + _reportCleanupPolicy = KSCrashReportCleanupPolicyAlways; + + kscrs_initialize(&_cConfig); + } + return self; +} + +- (void)dealloc +{ + KSCrashReportStoreCConfiguration_Release(&_cConfig); +} + +- (void)sendAllReportsWithCompletion:(KSCrashReportFilterCompletion)onCompletion +{ + NSArray *reports = [self allReports]; + + KSLOG_INFO(@"Sending %d crash reports", [reports count]); + + __weak __typeof(self) weakSelf = self; + [self sendReports:reports + onCompletion:^(NSArray *filteredReports, BOOL completed, NSError *error) { + KSLOG_DEBUG(@"Process finished with completion: %d", completed); + if (error != nil) { + KSLOG_ERROR(@"Failed to send reports: %@", error); + } + if ((self.reportCleanupPolicy == KSCrashReportCleanupPolicyOnSuccess && completed) || + self.reportCleanupPolicy == KSCrashReportCleanupPolicyAlways) { + [weakSelf deleteAllReports]; + } + kscrash_callCompletion(onCompletion, filteredReports, completed, error); + }]; +} + +- (void)deleteAllReports +{ + kscrs_deleteAllReports(&_cConfig); +} + +- (void)deleteReportWithID:(int64_t)reportID +{ + kscrs_deleteReportWithID(reportID, &_cConfig); +} + +#pragma mark - Private API + +- (NSInteger)reportCount +{ + return kscrs_getReportCount(&_cConfig); +} + +- (void)sendReports:(NSArray> *)reports onCompletion:(KSCrashReportFilterCompletion)onCompletion +{ + if ([reports count] == 0) { + kscrash_callCompletion(onCompletion, reports, YES, nil); + return; + } + + if (self.sink == nil) { + kscrash_callCompletion(onCompletion, reports, NO, + [KSNSErrorHelper errorWithDomain:[[self class] description] + code:0 + description:@"No sink set. Crash reports not sent."]); + return; + } + + [self.sink filterReports:reports + onCompletion:^(NSArray *filteredReports, BOOL completed, NSError *error) { + kscrash_callCompletion(onCompletion, filteredReports, completed, error); + }]; +} + +- (NSData *)loadCrashReportJSONWithID:(int64_t)reportID +{ + char *report = kscrs_readReport(reportID, &_cConfig); + if (report != NULL) { + return [NSData dataWithBytesNoCopy:report length:strlen(report) freeWhenDone:YES]; + } + return nil; +} + +- (void)doctorReport:(NSMutableDictionary *)report +{ + NSMutableDictionary *crashReport = report[KSCrashField_Crash]; + if (crashReport != nil) { + crashReport[KSCrashField_Diagnosis] = [[KSCrashDoctor doctor] diagnoseCrash:report]; + } + crashReport = report[KSCrashField_RecrashReport][KSCrashField_Crash]; + if (crashReport != nil) { + crashReport[KSCrashField_Diagnosis] = [[KSCrashDoctor doctor] diagnoseCrash:report]; + } +} + +- (NSArray *)reportIDs +{ + int reportCount = kscrs_getReportCount(&_cConfig); + int64_t reportIDsC[reportCount]; + reportCount = kscrs_getReportIDs(reportIDsC, reportCount, &_cConfig); + NSMutableArray *reportIDs = [NSMutableArray arrayWithCapacity:(NSUInteger)reportCount]; + for (int i = 0; i < reportCount; i++) { + [reportIDs addObject:[NSNumber numberWithLongLong:reportIDsC[i]]]; + } + return [reportIDs copy]; +} + +- (KSCrashReportDictionary *)reportForID:(int64_t)reportID +{ + NSData *jsonData = [self loadCrashReportJSONWithID:reportID]; + if (jsonData == nil) { + return nil; + } + + NSError *error = nil; + NSMutableDictionary *crashReport = + [KSJSONCodec decode:jsonData + options:KSJSONDecodeOptionIgnoreNullInArray | KSJSONDecodeOptionIgnoreNullInObject | + KSJSONDecodeOptionKeepPartialObject + error:&error]; + if (error != nil) { + KSLOG_ERROR(@"Encountered error loading crash report %" PRIx64 ": %@", reportID, error); + } + if (crashReport == nil) { + KSLOG_ERROR(@"Could not load crash report"); + return nil; + } + [self doctorReport:crashReport]; + + return [KSCrashReportDictionary reportWithValue:crashReport]; +} + +- (NSArray *)allReports +{ + int reportCount = kscrs_getReportCount(&_cConfig); + int64_t reportIDs[reportCount]; + reportCount = kscrs_getReportIDs(reportIDs, reportCount, &_cConfig); + NSMutableArray *reports = [NSMutableArray arrayWithCapacity:(NSUInteger)reportCount]; + for (int i = 0; i < reportCount; i++) { + KSCrashReportDictionary *report = [self reportForID:reportIDs[i]]; + if (report != nil) { + [reports addObject:report]; + } + } + + return reports; +} + +@end diff --git a/Sources/KSCrashRecording/KSCrashReportStoreC+Private.h b/Sources/KSCrashRecording/KSCrashReportStoreC+Private.h new file mode 100644 index 00000000..605a3fb7 --- /dev/null +++ b/Sources/KSCrashRecording/KSCrashReportStoreC+Private.h @@ -0,0 +1,51 @@ +// +// KSCrashReportStoreC+Private.h +// +// Created by Nikolay Volosatov on 2024-08-30. +// +// Copyright (c) 2012 Karl Stenerud. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall remain in place +// in this source code. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#ifndef KSCrashReportStoreC_Private_h +#define KSCrashReportStoreC_Private_h + +#include "KSCrashReportStoreC.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Get the next crash report to be generated. + * Max length for paths is KSCRS_MAX_PATH_LENGTH + * + * @param crashReportPathBuffer Buffer to store the crash report path. + * @param configuration The store configuretion (e.g. reports path, app name etc). + * + * @return The report ID of the next report. + */ +int64_t kscrs_getNextCrashReport(char *crashReportPathBuffer, + const KSCrashReportStoreCConfiguration *const configuration); + +#ifdef __cplusplus +} +#endif + +#endif // KSCrashReportStoreC_Private_h diff --git a/Sources/KSCrashRecording/KSCrashReportStore.c b/Sources/KSCrashRecording/KSCrashReportStoreC.c similarity index 57% rename from Sources/KSCrashRecording/KSCrashReportStore.c rename to Sources/KSCrashRecording/KSCrashReportStoreC.c index 7fbc81a3..3c4b9984 100644 --- a/Sources/KSCrashRecording/KSCrashReportStore.c +++ b/Sources/KSCrashRecording/KSCrashReportStoreC.c @@ -1,5 +1,5 @@ // -// KSCrashReportStore.c +// KSCrashReportStoreC.c // // Created by Karl Stenerud on 2012-02-05. // @@ -24,8 +24,6 @@ // THE SOFTWARE. // -#include "KSCrashReportStore.h" - #include #include #include @@ -37,15 +35,14 @@ #include #include +#include "KSCrashReportFixer.h" +#include "KSCrashReportStoreC+Private.h" #include "KSFileUtils.h" #include "KSLogger.h" -static int g_maxReportCount = 5; // Have to use max 32-bit atomics because of MIPS. static _Atomic(uint32_t) g_nextUniqueIDLow; static int64_t g_nextUniqueIDHigh; -static const char *g_appName; -static const char *g_reportsPath; static pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; static int compareInt64(const void *a, const void *b) @@ -61,38 +58,32 @@ static int compareInt64(const void *a, const void *b) static inline int64_t getNextUniqueID(void) { return g_nextUniqueIDHigh + g_nextUniqueIDLow++; } -static void getCrashReportPathByID(int64_t id, char *pathBuffer) +static void getCrashReportPathByID(int64_t id, char *pathBuffer, const KSCrashReportStoreCConfiguration *const config) { - assert(g_reportsPath != NULL); - snprintf(pathBuffer, KSCRS_MAX_PATH_LENGTH, "%s/%s-report-%016llx.json", g_reportsPath, g_appName, id); + snprintf(pathBuffer, KSCRS_MAX_PATH_LENGTH, "%s/%s-report-%016llx.json", config->reportsPath, config->appName, id); } -static int64_t getReportIDFromFilename(const char *filename) +static int64_t getReportIDFromFilename(const char *filename, const KSCrashReportStoreCConfiguration *const config) { char scanFormat[100]; - sprintf(scanFormat, "%s-report-%%" PRIx64 ".json", g_appName); + sprintf(scanFormat, "%s-report-%%" PRIx64 ".json", config->appName); int64_t reportID = 0; sscanf(filename, scanFormat, &reportID); return reportID; } -static int getReportCount(void) +static int getReportCount(const KSCrashReportStoreCConfiguration *const config) { - if (g_reportsPath == NULL) { - KSLOG_ERROR("Reports store is not set up"); - return 0; - } - int count = 0; - DIR *dir = opendir(g_reportsPath); + DIR *dir = opendir(config->reportsPath); if (dir == NULL) { - KSLOG_ERROR("Could not open directory %s", g_reportsPath); + KSLOG_ERROR("Could not open directory %s", config->reportsPath); goto done; } struct dirent *ent; while ((ent = readdir(dir)) != NULL) { - if (getReportIDFromFilename(ent->d_name) > 0) { + if (getReportIDFromFilename(ent->d_name, config) > 0) { count++; } } @@ -104,23 +95,18 @@ static int getReportCount(void) return count; } -static int getReportIDs(int64_t *reportIDs, int count) +static int getReportIDs(int64_t *reportIDs, int count, const KSCrashReportStoreCConfiguration *const config) { - if (g_reportsPath == NULL) { - KSLOG_ERROR("Reports store is not set up"); - return 0; - } - int index = 0; - DIR *dir = opendir(g_reportsPath); + DIR *dir = opendir(config->reportsPath); if (dir == NULL) { - KSLOG_ERROR("Could not open directory %s", g_reportsPath); + KSLOG_ERROR("Could not open directory %s", config->reportsPath); goto done; } struct dirent *ent; while ((ent = readdir(dir)) != NULL && index < count) { - int64_t reportID = getReportIDFromFilename(ent->d_name); + int64_t reportID = getReportIDFromFilename(ent->d_name, config); if (reportID > 0) { reportIDs[index++] = reportID; } @@ -135,15 +121,25 @@ static int getReportIDs(int64_t *reportIDs, int count) return index; } -static void pruneReports(void) +static void deleteReportWithID(int64_t reportID, const KSCrashReportStoreCConfiguration *const config) +{ + char path[KSCRS_MAX_PATH_LENGTH]; + getCrashReportPathByID(reportID, path, config); + ksfu_removeFile(path, true); +} + +static void pruneReports(const KSCrashReportStoreCConfiguration *const config) { - int reportCount = getReportCount(); - if (reportCount > g_maxReportCount) { + if (config->maxReportCount <= 0) { + return; + } + int reportCount = getReportCount(config); + if (reportCount > config->maxReportCount) { int64_t reportIDs[reportCount]; - reportCount = getReportIDs(reportIDs, reportCount); + reportCount = getReportIDs(reportIDs, reportCount, config); - for (int i = 0; i < reportCount - g_maxReportCount; i++) { - kscrs_deleteReportWithID(reportIDs[i]); + for (int i = 0; i < reportCount - config->maxReportCount; i++) { + deleteReportWithID(reportIDs[i], config); } } } @@ -168,82 +164,91 @@ static void initializeIDs(void) // Public API -void kscrs_initialize(const char *appName, const char *reportsPath) +KSCrashInstallErrorCode kscrs_initialize(const KSCrashReportStoreCConfiguration *const configuration) { - const char *previousAppName = NULL; - const char *previousReportsPath = NULL; - + KSCrashInstallErrorCode result = KSCrashInstallErrorNone; pthread_mutex_lock(&g_mutex); - previousAppName = g_appName; - previousReportsPath = g_reportsPath; - g_appName = strdup(appName); - g_reportsPath = strdup(reportsPath); - ksfu_makePath(reportsPath); - pruneReports(); - initializeIDs(); - pthread_mutex_unlock(&g_mutex); - - if (previousAppName) { - KSLOG_WARN("Reports app name is changed from '%s' to '%s'", previousAppName, appName); - free((void *)previousAppName); - } - if (previousReportsPath) { - KSLOG_WARN("Reports path is changed from '%s' to '%s'", previousReportsPath, reportsPath); - free((void *)previousReportsPath); + if (ksfu_makePath(configuration->reportsPath) == false) { + KSLOG_ERROR("Could not create path: %s", configuration->reportsPath); + result = KSCrashInstallErrorCouldNotCreatePath; + } else { + pruneReports(configuration); + initializeIDs(); } + pthread_mutex_unlock(&g_mutex); + return KSCrashInstallErrorNone; } -int64_t kscrs_getNextCrashReport(char *crashReportPathBuffer) +int64_t kscrs_getNextCrashReport(char *crashReportPathBuffer, + const KSCrashReportStoreCConfiguration *const configuration) { int64_t nextID = getNextUniqueID(); if (crashReportPathBuffer) { - getCrashReportPathByID(nextID, crashReportPathBuffer); + getCrashReportPathByID(nextID, crashReportPathBuffer, configuration); } return nextID; } -int kscrs_getReportCount(void) +int kscrs_getReportCount(const KSCrashReportStoreCConfiguration *const configuration) { pthread_mutex_lock(&g_mutex); - int count = getReportCount(); + int count = getReportCount(configuration); pthread_mutex_unlock(&g_mutex); return count; } -int kscrs_getReportIDs(int64_t *reportIDs, int count) +int kscrs_getReportIDs(int64_t *reportIDs, int count, const KSCrashReportStoreCConfiguration *const configuration) { pthread_mutex_lock(&g_mutex); - count = getReportIDs(reportIDs, count); + count = getReportIDs(reportIDs, count, configuration); pthread_mutex_unlock(&g_mutex); return count; } +static char *readReportAtPath(const char *path) +{ + char *rawReport; + ksfu_readEntireFile(path, &rawReport, NULL, 2000000); + if (rawReport == NULL) { + KSLOG_ERROR("Failed to load report at path: %s", path); + return NULL; + } + + char *result = kscrf_fixupCrashReport(rawReport); + free(rawReport); + if (result == NULL) { + KSLOG_ERROR("Failed to fixup report at path: %s", path); + return NULL; + } + + return result; +} + char *kscrs_readReportAtPath(const char *path) { pthread_mutex_lock(&g_mutex); - char *result; - ksfu_readEntireFile(path, &result, NULL, 2000000); + char *result = readReportAtPath(path); pthread_mutex_unlock(&g_mutex); return result; } -char *kscrs_readReport(int64_t reportID) +char *kscrs_readReport(int64_t reportID, const KSCrashReportStoreCConfiguration *const configuration) { pthread_mutex_lock(&g_mutex); char path[KSCRS_MAX_PATH_LENGTH]; - getCrashReportPathByID(reportID, path); - char *result; - ksfu_readEntireFile(path, &result, NULL, 2000000); + getCrashReportPathByID(reportID, path, configuration); + char *result = readReportAtPath(path); pthread_mutex_unlock(&g_mutex); return result; } -int64_t kscrs_addUserReport(const char *report, int reportLength) +int64_t kscrs_addUserReport(const char *report, int reportLength, + const KSCrashReportStoreCConfiguration *const configuration) { pthread_mutex_lock(&g_mutex); int64_t currentID = getNextUniqueID(); char crashReportPath[KSCRS_MAX_PATH_LENGTH]; - getCrashReportPathByID(currentID, crashReportPath); + getCrashReportPathByID(currentID, crashReportPath, configuration); int fd = open(crashReportPath, O_WRONLY | O_CREAT, 0644); if (fd < 0) { @@ -269,22 +274,16 @@ int64_t kscrs_addUserReport(const char *report, int reportLength) return currentID; } -void kscrs_deleteAllReports(void) +void kscrs_deleteAllReports(const KSCrashReportStoreCConfiguration *const configuration) { pthread_mutex_lock(&g_mutex); - if (g_reportsPath != NULL) { - ksfu_deleteContentsOfPath(g_reportsPath); - } else { - KSLOG_WARN("Reports store is not set up"); - } + ksfu_deleteContentsOfPath(configuration->reportsPath); pthread_mutex_unlock(&g_mutex); } -void kscrs_deleteReportWithID(int64_t reportID) +void kscrs_deleteReportWithID(int64_t reportID, const KSCrashReportStoreCConfiguration *const configuration) { - char path[KSCRS_MAX_PATH_LENGTH]; - getCrashReportPathByID(reportID, path); - ksfu_removeFile(path, true); + pthread_mutex_lock(&g_mutex); + deleteReportWithID(reportID, configuration); + pthread_mutex_unlock(&g_mutex); } - -void kscrs_setMaxReportCount(int maxReportCount) { g_maxReportCount = maxReportCount; } diff --git a/Sources/KSCrashRecording/Monitors/KSCrashMonitor_Memory.m b/Sources/KSCrashRecording/Monitors/KSCrashMonitor_Memory.m index 3f0841df..6420cc92 100644 --- a/Sources/KSCrashRecording/Monitors/KSCrashMonitor_Memory.m +++ b/Sources/KSCrashRecording/Monitors/KSCrashMonitor_Memory.m @@ -33,6 +33,7 @@ #import "KSCrashMonitorContext.h" #import "KSCrashMonitorContextHelper.h" #import "KSCrashReportFields.h" +#import "KSCrashReportStoreC.h" #import "KSDate.h" #import "KSFileUtils.h" #import "KSID.h" @@ -320,7 +321,7 @@ static void kscm_memory_check_for_oom_in_previous_session(void) // Ignore this check if we want to report all OOM, foreground and background. if (userPerceivedOOM) { NSURL *url = kscm_memory_oom_breadcrumb_URL(); - const char *reportContents = kscrash_readReportAtPath(url.path.UTF8String); + const char *reportContents = kscrs_readReportAtPath(url.path.UTF8String); if (reportContents) { NSData *data = [NSData dataWithBytes:reportContents length:strlen(reportContents)]; NSMutableDictionary *json = diff --git a/Sources/KSCrashRecording/include/KSCrash.h b/Sources/KSCrashRecording/include/KSCrash.h index 151a2c36..229c5aaf 100644 --- a/Sources/KSCrashRecording/include/KSCrash.h +++ b/Sources/KSCrashRecording/include/KSCrash.h @@ -28,6 +28,7 @@ #import "KSCrashMonitorType.h" #import "KSCrashReportFilter.h" +#import "KSCrashReportStore.h" #import "KSCrashReportWriter.h" NS_ASSUME_NONNULL_BEGIN @@ -52,15 +53,6 @@ NS_ASSUME_NONNULL_BEGIN */ @property(atomic, readwrite, strong, nullable) NSDictionary *userInfo; -/** The report sink where reports get sent. - * This MUST be set or else the reporter will not send reports (although it will - * still record them). - * - * Note: If you use an installation, it will automatically set this property. - * Do not modify it in such a case. - */ -@property(nonatomic, readwrite, strong, nullable) id sink; - #pragma mark - Information - /** Exposes the uncaughtExceptionHandler if set from KSCrash. Is nil if debugger is running. */ @@ -127,61 +119,12 @@ NS_ASSUME_NONNULL_BEGIN */ - (BOOL)installWithConfiguration:(KSCrashConfiguration *)configuration error:(NSError **)error; -/** Sets up the crash repors store. - * A call of this method is required before working with crash reports. - * The `installWithConfiguration:error:` method sets up the crash report store. - * You only need to call this method if you are not using the `installWithConfiguration:error:` method - * or want to read crash reports from a custom location. - * - * @note This method can be called multiple times, but only before `installWithConfiguration:error:` is called. - * - * @param installPath The path to the directory where the crash reports are stored. If `nil`, the default path is used. - * @param error A pointer to an NSError object. If an error occurs, this pointer is set to an actual error object. - * @return YES if the crash report store was successfully set up, NO otherwise. - */ -- (BOOL)setupReportStoreWithPath:(nullable NSString *)installPath error:(NSError **)error; - -/** Send all outstanding crash reports to the current sink. - * It will only attempt to send the most recent 5 reports. All others will be - * deleted. Once the reports are successfully sent to the server, they may be - * deleted locally, depending on the property "deleteAfterSendAll". - * - * @note A call of `setupReportStoreWithPath:error:` or `installWithConfiguration:error:` is required - * before working with crash reports. - * @note Property "sink" MUST be set or else this method will call `onCompletion` with an error. - * - * @param onCompletion Called when sending is complete (nil = ignore). - */ -- (void)sendAllReportsWithCompletion:(nullable KSCrashReportFilterCompletion)onCompletion; - -/** Get all unsent report IDs. */ -@property(nonatomic, readonly, strong) NSArray *reportIDs; - -/** Get report. - * - * @note A call of `setupReportStoreWithPath:error:` or `installWithConfiguration:error:` is required - * before working with crash reports. - * - * @param reportID An ID of report. - * - * @return A crash report with a dictionary value. The dectionary fields are described in KSCrashReportFields.h. - */ -- (nullable KSCrashReportDictionary *)reportForID:(int64_t)reportID NS_SWIFT_NAME(report(for:)); - -/** Delete all unsent reports. - * @note A call of `setupReportStoreWithPath:error:` or `installWithConfiguration:error:` is required - * before working with crash reports. - */ -- (void)deleteAllReports; - -/** Delete report. - * - * @note A call of `setupReportStoreWithPath:error:` or `installWithConfiguration:error:` is required - * before working with crash reports. +/** The installed report store. + * This is the store that is used to save and load crash reports. * - * @param reportID An ID of report to delete. + * @note If the crash reporter is not installed, this will be `nil`. */ -- (void)deleteReportWithID:(int64_t)reportID NS_SWIFT_NAME(deleteReport(with:)); +@property(nonatomic, strong, readonly, nullable) KSCrashReportStore *reportStore; /** Report a custom, user defined exception. * This can be useful when dealing with scripting languages. diff --git a/Sources/KSCrashRecording/include/KSCrashC.h b/Sources/KSCrashRecording/include/KSCrashC.h index 42d586d8..b9d23a00 100644 --- a/Sources/KSCrashRecording/include/KSCrashC.h +++ b/Sources/KSCrashRecording/include/KSCrashC.h @@ -67,7 +67,7 @@ extern "C" { * * Example usage: * ``` - * KSCrashCConfiguration config = KSCrashCConfiguration_Default; + * KSCrashCConfiguration config = KSCrashCConfiguration_Default(); * config.monitors = KSCrashMonitorTypeAll; * config.userInfoJSON = "{ \"user\": \"example\" }"; * KSCrashInstallErrorCode result = kscrash_install("MyApp", "/path/to/install", config); @@ -83,21 +83,7 @@ extern "C" { * without restarting the application. */ KSCrashInstallErrorCode kscrash_install(const char *appName, const char *const installPath, - KSCrashCConfiguration configuration); - -/** Sets up the crash repors store. - * This function is used to initialize the storage for crash reports. - * The `kscrash_install` function sets up the reports store internally. - * You only need to call this function if you are not using the `kscrash_install` function - * or want to read crash reports from a custom location. - * - * @note this function can be called multiple times, but only before `kscrash_install` is called. - * - * @param appName The name of the application. Usually it's bundle name. - * @param installPath The directory where the crash reports and related data will be stored. - * @return KSCrashInstallErrorCode indicating the result of the setup. - */ -KSCrashInstallErrorCode kscrash_setupReportsStore(const char *appName, const char *const installPath); + KSCrashCConfiguration *configuration); /** Set the user-supplied data in JSON format. * @@ -168,37 +154,6 @@ void kscrash_notifyAppCrash(void); #pragma mark-- Reporting -- -/** Get the number of reports on disk. - */ -int kscrash_getReportCount(void); - -/** Get a list of IDs for all reports on disk. - * - * @param reportIDs An array big enough to hold all report IDs. - * @param count How many reports the array can hold. - * - * @return The number of report IDs that were placed in the array. - */ -int kscrash_getReportIDs(int64_t *reportIDs, int count); - -/** Read a report. - * - * @param reportID The report's ID. - * - * @return The NULL terminated report, or NULL if not found. - * MEMORY MANAGEMENT WARNING: User is responsible for calling free() on the returned value. - */ -char *kscrash_readReport(int64_t reportID); - -/** Read a report at a specified path. - * - * @param path The full path to the report. - * - * @return The NULL terminated report, or NULL if not found. - * MEMORY MANAGEMENT WARNING: User is responsible for calling free() on the returned value. - */ -char *kscrash_readReportAtPath(const char *path); - /** Add a custom report to the store. * * @param report The report's contents (must be JSON encoded). @@ -208,16 +163,6 @@ char *kscrash_readReportAtPath(const char *path); */ int64_t kscrash_addUserReport(const char *report, int reportLength); -/** Delete all reports on disk. - */ -void kscrash_deleteAllReports(void); - -/** Delete report. - * - * @param reportID An ID of report to delete. - */ -void kscrash_deleteReportWithID(int64_t reportID); - #ifdef __cplusplus } #endif diff --git a/Sources/KSCrashRecording/include/KSCrashCConfiguration.h b/Sources/KSCrashRecording/include/KSCrashCConfiguration.h index f7a3e9f0..a4b5c0bd 100644 --- a/Sources/KSCrashRecording/include/KSCrashCConfiguration.h +++ b/Sources/KSCrashRecording/include/KSCrashCConfiguration.h @@ -31,6 +31,7 @@ #include "KSCrashMonitorType.h" #include "KSCrashReportWriter.h" +#include "string.h" #ifdef __cplusplus extern "C" { @@ -42,9 +43,67 @@ extern "C" { */ typedef void (*KSReportWrittenCallback)(int64_t reportID); +/** Configuration for managing crash reports through the report store API. + */ +typedef struct { + /** The name of the application. + * This identifier is used to distinguish the application in crash reports. + * It is crucial for correlating crash data with the specific application version. + * + * @note This field must be set prior to using this configuration with any `kscrs_` functions. + */ + const char *appName; + + /** The directory path for storing crash reports. + * The specified directory must have write permissions. If it doesn't exist, + * the system will attempt to create it automatically. + * + * @note This field must be set prior to using this configuration with any `kscrs_` functions. + */ + const char *reportsPath; + + /** The maximum number of crash reports to retain on disk. + * + * Defines the upper limit of crash reports to keep in storage. When this threshold + * is reached, the system will remove the oldest reports to accommodate new ones. + * + * **Default**: 5 + */ + int maxReportCount; +} KSCrashReportStoreCConfiguration; + +static inline KSCrashReportStoreCConfiguration KSCrashReportStoreCConfiguration_Default(void) +{ + return (KSCrashReportStoreCConfiguration) { + .appName = NULL, + .reportsPath = NULL, + .maxReportCount = 5, + }; +} + +static inline KSCrashReportStoreCConfiguration KSCrashReportStoreCConfiguration_Copy( + KSCrashReportStoreCConfiguration *configuration) +{ + return (KSCrashReportStoreCConfiguration) { + .appName = configuration->appName ? strdup(configuration->appName) : NULL, + .reportsPath = configuration->reportsPath ? strdup(configuration->reportsPath) : NULL, + .maxReportCount = configuration->maxReportCount, + }; +} + +static inline void KSCrashReportStoreCConfiguration_Release(KSCrashReportStoreCConfiguration *configuration) +{ + free((void *)configuration->appName); + free((void *)configuration->reportsPath); +} + /** Configuration for KSCrash settings. */ typedef struct { + /** The report store configuration to be used for the corresponding installation. + */ + KSCrashReportStoreCConfiguration reportStoreConfiguration; + /** The crash types that will be handled. * Some crash types may not be enabled depending on circumstances (e.g., running in a debugger). */ @@ -140,15 +199,6 @@ typedef struct { */ bool printPreviousLogOnStartup; - /** The maximum number of crash reports allowed on disk before old ones get deleted. - * - * Specifies the maximum number of crash reports to keep on disk. When this limit - * is reached, the oldest reports will be deleted to make room for new ones. - * - * **Default**: 5 - */ - int maxReportCount; - /** If true, enable C++ exceptions catching with `__cxa_throw` swap. * * This experimental feature works similarly to `LD_PRELOAD` and supports catching @@ -163,18 +213,30 @@ typedef struct { static inline KSCrashCConfiguration KSCrashCConfiguration_Default(void) { - return (KSCrashCConfiguration) { .monitors = KSCrashMonitorTypeProductionSafeMinimal, - .userInfoJSON = NULL, - .deadlockWatchdogInterval = 0.0, - .enableQueueNameSearch = false, - .enableMemoryIntrospection = false, - .doNotIntrospectClasses = { .strings = NULL, .length = 0 }, - .crashNotifyCallback = NULL, - .reportWrittenCallback = NULL, - .addConsoleLogToReport = false, - .printPreviousLogOnStartup = false, - .maxReportCount = 5, - .enableSwapCxaThrow = true }; + return (KSCrashCConfiguration) { + .reportStoreConfiguration = KSCrashReportStoreCConfiguration_Default(), + .monitors = KSCrashMonitorTypeProductionSafeMinimal, + .userInfoJSON = NULL, + .deadlockWatchdogInterval = 0.0, + .enableQueueNameSearch = false, + .enableMemoryIntrospection = false, + .doNotIntrospectClasses = { .strings = NULL, .length = 0 }, + .crashNotifyCallback = NULL, + .reportWrittenCallback = NULL, + .addConsoleLogToReport = false, + .printPreviousLogOnStartup = false, + .enableSwapCxaThrow = true, + }; +} + +static inline void KSCrashCConfiguration_Release(KSCrashCConfiguration *configuration) +{ + KSCrashReportStoreCConfiguration_Release(&configuration->reportStoreConfiguration); + free((void *)configuration->userInfoJSON); + for (int idx = 0; idx < configuration->doNotIntrospectClasses.length; ++idx) { + free((void *)(configuration->doNotIntrospectClasses.strings[idx])); + } + free(configuration->doNotIntrospectClasses.strings); } #ifdef __cplusplus diff --git a/Sources/KSCrashRecording/include/KSCrashConfiguration.h b/Sources/KSCrashRecording/include/KSCrashConfiguration.h index b280b21a..05d8bb65 100644 --- a/Sources/KSCrashRecording/include/KSCrashConfiguration.h +++ b/Sources/KSCrashRecording/include/KSCrashConfiguration.h @@ -30,11 +30,7 @@ NS_ASSUME_NONNULL_BEGIN -typedef NS_ENUM(NSUInteger, KSCDeleteBehavior) { - KSCDeleteNever, - KSCDeleteOnSucess, - KSCDeleteAlways -} NS_SWIFT_NAME(DeleteBehavior); +@class KSCrashReportStoreConfiguration; @interface KSCrashConfiguration : NSObject @@ -46,6 +42,11 @@ typedef NS_ENUM(NSUInteger, KSCDeleteBehavior) { */ @property(nonatomic, copy, nullable) NSString *installPath; +/** The configuration for report store. + * @note See `KSCrashStoreConfiguration` for more details. + */ +@property(nonatomic, strong, readonly) KSCrashReportStoreConfiguration *reportStoreConfiguration; + /** The crash types that will be handled. * Some crash types may not be enabled depending on circumstances (e.g., running in a debugger). * @@ -140,15 +141,6 @@ typedef NS_ENUM(NSUInteger, KSCDeleteBehavior) { */ @property(nonatomic, assign) BOOL printPreviousLogOnStartup; -/** The maximum number of crash reports allowed on disk before old ones get deleted. - * - * Specifies the maximum number of crash reports to keep on disk. When this limit - * is reached, the oldest reports will be deleted to make room for new ones. - * - * **Default**: 5 - */ -@property(nonatomic, assign) int maxReportCount; - /** If true, enable C++ exceptions catching with `__cxa_throw` swap. * * This experimental feature works similarly to `LD_PRELOAD` and supports catching @@ -160,16 +152,33 @@ typedef NS_ENUM(NSUInteger, KSCDeleteBehavior) { */ @property(nonatomic, assign) BOOL enableSwapCxaThrow; -/** What to do after sending reports via sendAllReportsWithCompletion: +@end + +NS_SWIFT_NAME(CrashReportStoreConfiguration) +@interface KSCrashReportStoreConfiguration : NSObject + +/** Specifies a custom directory path for reports store. + * If `nil` the default directory is used: `Reports` within the installation directory. * - * - Use KSCDeleteNever if you will manually manage the reports. - * - Use KSCDeleteAlways if you will be using an alert confirmation (otherwise it - * will nag the user incessantly until he selects "yes"). - * - Use KSCDeleteOnSuccess for all other situations. + * **Default**: `nil` + */ +@property(nonatomic, copy, nullable) NSString *reportsPath; + +/** Specifies a custom app name to be used in report file name. + * If `nil` the default value is used: `CFBundleName` from Info.plist. + * + * **Default**: `nil` + */ +@property(nonatomic, copy, nullable) NSString *appName; + +/** The maximum number of crash reports allowed on disk before old ones get deleted. + * + * Specifies the maximum number of crash reports to keep on disk. When this limit + * is reached, the oldest reports will be deleted to make room for new ones. * - * Default: KSCDeleteAlways + * **Default**: 5 */ -@property(nonatomic, assign) KSCDeleteBehavior deleteBehaviorAfterSendAll; +@property(nonatomic, assign) NSInteger maxReportCount; @end diff --git a/Sources/KSCrashRecording/include/KSCrashReportStore.h b/Sources/KSCrashRecording/include/KSCrashReportStore.h new file mode 100644 index 00000000..21a2bc35 --- /dev/null +++ b/Sources/KSCrashRecording/include/KSCrashReportStore.h @@ -0,0 +1,140 @@ +// +// KSCrashReportStore.h +// +// Created by Nikolay Volosatov on 2024-08-28. +// +// Copyright (c) 2012 Karl Stenerud. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall remain in place +// in this source code. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#import + +#import "KSCrashReportFilter.h" + +NS_ASSUME_NONNULL_BEGIN + +@class KSCrashReportDictionary; +@class KSCrashReportStoreConfiguration; + +typedef NS_ENUM(NSUInteger, KSCrashReportCleanupPolicy) { + KSCrashReportCleanupPolicyNever, + KSCrashReportCleanupPolicyOnSuccess, + KSCrashReportCleanupPolicyAlways, +} NS_SWIFT_NAME(CrashReportCleanupPolicy); + +NS_SWIFT_NAME(CrashReportStore) +@interface KSCrashReportStore : NSObject + +/** The default folder name inside the KSCrash install path that is used for report store. + */ +@property(nonatomic, class, copy, readonly) NSString *defaultInstallSubfolder; + +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +/** The report store with the default configuration. + * + * @param error If an error occurs, upon return contains an NSError object that + * describes the problem. + * + * @return The default report store or `nil` if an error occurred. + */ ++ (nullable instancetype)defaultStoreWithError:(NSError **)error; + +/** The report store with the given configuration. + * If the configuration is nil, the default configuration will be used. + * + * @param configuration The configuration to use. + * @param error If an error occurs, upon return contains an NSError object that + * + * @return The report store or `nil` if an error occurred. + */ ++ (nullable instancetype)storeWithConfiguration:(nullable KSCrashReportStoreConfiguration *)configuration + error:(NSError **)error; + +#pragma mark - Configuration + +/** The report sink where reports get sent. + * This MUST be set or else the reporter will not send reports (although it will + * still record them). + * + * Note: If you use an installation, it will automatically set this property. + * Do not modify it in such a case. + */ +@property(nonatomic, readwrite, strong, nullable) id sink; + +/** What to do after sending reports via sendAllReportsWithCompletion: + * + * - Use KSCrashReportCleanupPolicyNever if you manually manage the reports. + * - Use KSCrashReportCleanupPolicyAlways if you are using an alert confirmation + * (otherwise it will nag the user incessantly until he selects "yes"). + * - Use KSCrashReportCleanupPolicyOnSucess for all other situations. + * + * Default: KSCrashReportCleanupPolicyAlways + */ +@property(nonatomic, assign) KSCrashReportCleanupPolicy reportCleanupPolicy; + +#pragma mark - Reports API + +/** Get all unsent report IDs. */ +@property(nonatomic, readonly, strong) NSArray *reportIDs; + +/** Send all outstanding crash reports to the current sink. + * It will only attempt to send the most recent 5 reports. All others will be + * deleted. Once the reports are successfully sent to the server, they may be + * deleted locally, depending on the property "deleteAfterSendAll". + * + * @note A call of `setupReportStoreWithPath:error:` or `installWithConfiguration:error:` is required + * before working with crash reports. + * @note Property "sink" MUST be set or else this method will call `onCompletion` with an error. + * + * @param onCompletion Called when sending is complete (nil = ignore). + */ +- (void)sendAllReportsWithCompletion:(nullable KSCrashReportFilterCompletion)onCompletion; + +/** Get report. + * + * @note A call of `setupReportStoreWithPath:error:` or `installWithConfiguration:error:` is required + * before working with crash reports. + * + * @param reportID An ID of report. + * + * @return A crash report with a dictionary value. The dectionary fields are described in KSCrashReportFields.h. + */ +- (nullable KSCrashReportDictionary *)reportForID:(int64_t)reportID NS_SWIFT_NAME(report(for:)); + +/** Delete all unsent reports. + * @note A call of `setupReportStoreWithPath:error:` or `installWithConfiguration:error:` is required + * before working with crash reports. + */ +- (void)deleteAllReports; + +/** Delete report. + * + * @note A call of `setupReportStoreWithPath:error:` or `installWithConfiguration:error:` is required + * before working with crash reports. + * + * @param reportID An ID of report to delete. + */ +- (void)deleteReportWithID:(int64_t)reportID NS_SWIFT_NAME(deleteReport(with:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/KSCrashRecording/KSCrashReportStore.h b/Sources/KSCrashRecording/include/KSCrashReportStoreC.h similarity index 50% rename from Sources/KSCrashRecording/KSCrashReportStore.h rename to Sources/KSCrashRecording/include/KSCrashReportStoreC.h index 6489946d..6b4995a7 100644 --- a/Sources/KSCrashRecording/KSCrashReportStore.h +++ b/Sources/KSCrashRecording/include/KSCrashReportStoreC.h @@ -1,5 +1,5 @@ // -// KSCrashReportStore.h +// KSCrashReportStoreC.h // // Created by Karl Stenerud on 2012-02-05. // @@ -24,83 +24,96 @@ // THE SOFTWARE. // -#ifndef HDR_KSCrashReportStore_h -#define HDR_KSCrashReportStore_h +#ifndef HDR_KSCrashReportStoreC_h +#define HDR_KSCrashReportStoreC_h #include +#include "KSCrashCConfiguration.h" +#include "KSCrashError.h" + #ifdef __cplusplus extern "C" { #endif #define KSCRS_MAX_PATH_LENGTH 500 -/** Initialize the report store. - * - * @param appName The application's name. - * @param reportsPath Full path to directory where the reports are to be stored (path will be created if needed). +/** The default name of a folder (inside the KSCrash install path) that is used for report store. */ -void kscrs_initialize(const char *appName, const char *reportsPath); +#define KSCRS_DEFAULT_REPORTS_FOLDER "Reports" -/** Get the next crash report to be generated. - * Max length for paths is KSCRS_MAX_PATH_LENGTH - * - * @param crashReportPathBuffer Buffer to store the crash report path. +/** Initialize the report store. * - * @return the report ID of the next report. + * @param configuration The store configuretion (e.g. reports path, app name etc). */ -int64_t kscrs_getNextCrashReport(char *crashReportPathBuffer); +KSCrashInstallErrorCode kscrs_initialize(const KSCrashReportStoreCConfiguration *const configuration); /** Get the number of reports on disk. + * + * @param configuration The store configuretion (e.g. reports path, app name etc). + * + * @return The number of reports on disk. */ -int kscrs_getReportCount(void); +int kscrs_getReportCount(const KSCrashReportStoreCConfiguration *const configuration); /** Get a list of IDs for all reports on disk. * * @param reportIDs An array big enough to hold all report IDs. * @param count How many reports the array can hold. + * @param configuration The store configuretion (e.g. reports path, app name etc). * * @return The number of report IDs that were placed in the array. */ -int kscrs_getReportIDs(int64_t *reportIDs, int count); +int kscrs_getReportIDs(int64_t *reportIDs, int count, const KSCrashReportStoreCConfiguration *const configuration); /** Read a report. + * + * @warning MEMORY MANAGEMENT WARNING: User is responsible for calling free() on the returned value. * * @param reportID The report's ID. + * @param configuration The store configuretion (e.g. reports path, app name etc). + * + * @return The NULL terminated report, or NULL if not found. + */ +char *kscrs_readReport(int64_t reportID, const KSCrashReportStoreCConfiguration *const configuration); + +/** Read a report at a given path. + * This is a convenience method for reading reports that are not in the standard reports directory. + * + * @warning MEMORY MANAGEMENT WARNING: User is responsible for calling free() on the returned value. + * + * @param path The full path to the report. * * @return The NULL terminated report, or NULL if not found. - * MEMORY MANAGEMENT WARNING: User is responsible for calling free() on the returned value. */ -char *kscrs_readReport(int64_t reportID); char *kscrs_readReportAtPath(const char *path); /** Add a custom report to the store. * * @param report The report's contents (must be JSON encoded). * @param reportLength The length of the report in bytes. + * @param configuration The store configuretion (e.g. reports path, app name etc). * - * @return the new report's ID. + * @return The new report's ID. */ -int64_t kscrs_addUserReport(const char *report, int reportLength); +int64_t kscrs_addUserReport(const char *report, int reportLength, + const KSCrashReportStoreCConfiguration *const configuration); /** Delete all reports on disk. + * + * @param configuration The store configuretion (e.g. reports path, app name etc). */ -void kscrs_deleteAllReports(void); +void kscrs_deleteAllReports(const KSCrashReportStoreCConfiguration *const configuration); /** Delete report. * * @param reportID An ID of report to delete. + * @param configuration The store configuretion (e.g. reports path, app name etc). */ -void kscrs_deleteReportWithID(int64_t reportID); - -/** Set the maximum number of reports allowed on disk before old ones get deleted. - * - * @param maxReportCount The maximum number of reports. - */ -void kscrs_setMaxReportCount(int maxReportCount); +void kscrs_deleteReportWithID(int64_t reportID, const KSCrashReportStoreCConfiguration *const configuration); #ifdef __cplusplus } #endif -#endif // HDR_KSCrashReportStore_h +#endif // HDR_KSCrashReportStoreC_h diff --git a/Tests/KSCrashRecordingTests/KSCrashConfiguration_Tests.m b/Tests/KSCrashRecordingTests/KSCrashConfiguration_Tests.m index bcbf2620..e183a0c3 100644 --- a/Tests/KSCrashRecordingTests/KSCrashConfiguration_Tests.m +++ b/Tests/KSCrashRecordingTests/KSCrashConfiguration_Tests.m @@ -47,9 +47,8 @@ - (void)testInitializationDefaults XCTAssertNil(config.reportWrittenCallback); XCTAssertFalse(config.addConsoleLogToReport); XCTAssertFalse(config.printPreviousLogOnStartup); - XCTAssertEqual(config.maxReportCount, 5); + XCTAssertEqual(config.reportStoreConfiguration.maxReportCount, 5); XCTAssertTrue(config.enableSwapCxaThrow); - XCTAssertEqual(config.deleteBehaviorAfterSendAll, KSCDeleteAlways); } - (void)testToCConfiguration @@ -63,7 +62,7 @@ - (void)testToCConfiguration config.doNotIntrospectClasses = @[ @"ClassA", @"ClassB" ]; config.addConsoleLogToReport = YES; config.printPreviousLogOnStartup = YES; - config.maxReportCount = 10; + config.reportStoreConfiguration.maxReportCount = 10; config.enableSwapCxaThrow = NO; KSCrashCConfiguration cConfig = [config toCConfiguration]; @@ -79,15 +78,11 @@ - (void)testToCConfiguration XCTAssertEqual(strcmp(cConfig.doNotIntrospectClasses.strings[1], "ClassB"), 0); XCTAssertTrue(cConfig.addConsoleLogToReport); XCTAssertTrue(cConfig.printPreviousLogOnStartup); - XCTAssertEqual(cConfig.maxReportCount, 10); + XCTAssertEqual(cConfig.reportStoreConfiguration.maxReportCount, 10); XCTAssertFalse(cConfig.enableSwapCxaThrow); // Free memory allocated for C string array - for (int i = 0; i < cConfig.doNotIntrospectClasses.length; i++) { - free((void *)cConfig.doNotIntrospectClasses.strings[i]); - } - free(cConfig.doNotIntrospectClasses.strings); - free((void *)cConfig.userInfoJSON); + KSCrashCConfiguration_Release(&cConfig); } - (void)testCopyWithZone @@ -101,7 +96,7 @@ - (void)testCopyWithZone config.doNotIntrospectClasses = @[ @"ClassA", @"ClassB" ]; config.addConsoleLogToReport = YES; config.printPreviousLogOnStartup = YES; - config.maxReportCount = 10; + config.reportStoreConfiguration.maxReportCount = 10; config.enableSwapCxaThrow = NO; KSCrashConfiguration *copy = [config copy]; @@ -114,9 +109,8 @@ - (void)testCopyWithZone XCTAssertEqualObjects(copy.doNotIntrospectClasses, (@[ @"ClassA", @"ClassB" ])); XCTAssertTrue(copy.addConsoleLogToReport); XCTAssertTrue(copy.printPreviousLogOnStartup); - XCTAssertEqual(copy.maxReportCount, 10); + XCTAssertEqual(copy.reportStoreConfiguration.maxReportCount, 10); XCTAssertFalse(copy.enableSwapCxaThrow); - XCTAssertEqual(copy.deleteBehaviorAfterSendAll, KSCDeleteAlways); } - (void)testEmptyDictionaryForJSONConversion @@ -128,7 +122,7 @@ - (void)testEmptyDictionaryForJSONConversion XCTAssertTrue(cConfig.userInfoJSON != NULL); XCTAssertEqual(strcmp(cConfig.userInfoJSON, "{}"), 0); - free((void *)cConfig.userInfoJSON); + KSCrashCConfiguration_Release(&cConfig); } - (void)testLargeDataForJSONConversion @@ -148,7 +142,7 @@ - (void)testLargeDataForJSONConversion XCTAssertTrue([jsonString containsString:@"key999"]); XCTAssertTrue([jsonString containsString:@"value999"]); - free((void *)cConfig.userInfoJSON); + KSCrashCConfiguration_Release(&cConfig); } - (void)testSpecialCharactersInStrings @@ -160,7 +154,7 @@ - (void)testSpecialCharactersInStrings XCTAssertTrue(cConfig.userInfoJSON != NULL); XCTAssertTrue(strstr(cConfig.userInfoJSON, "special characters: @#$%^&*()") != NULL); - free((void *)cConfig.userInfoJSON); + KSCrashCConfiguration_Release(&cConfig); } - (void)testNilAndEmptyArraysForCStringConversion @@ -178,7 +172,8 @@ - (void)testNilAndEmptyArraysForCStringConversion XCTAssertTrue(cConfig2.doNotIntrospectClasses.strings != NULL); XCTAssertEqual(cConfig2.doNotIntrospectClasses.length, 0); - free(cConfig2.doNotIntrospectClasses.strings); + KSCrashCConfiguration_Release(&cConfig1); + KSCrashCConfiguration_Release(&cConfig2); } - (void)testCopyingWithNilProperties @@ -192,19 +187,4 @@ - (void)testCopyingWithNilProperties XCTAssertNil(copy.doNotIntrospectClasses); } -- (void)testBoundaryValuesForMaxReportCount -{ - KSCrashConfiguration *config = [[KSCrashConfiguration alloc] init]; - - // Test with 0 - config.maxReportCount = 0; - KSCrashCConfiguration cConfig = [config toCConfiguration]; - XCTAssertEqual(cConfig.maxReportCount, 0); - - // Test with a large number - config.maxReportCount = INT_MAX; - cConfig = [config toCConfiguration]; - XCTAssertEqual(cConfig.maxReportCount, INT_MAX); -} - @end diff --git a/Tests/KSCrashRecordingTests/KSCrashMonitor_Memory_Tests.m b/Tests/KSCrashRecordingTests/KSCrashMonitor_Memory_Tests.m index b8e79eeb..3beca27d 100644 --- a/Tests/KSCrashRecordingTests/KSCrashMonitor_Memory_Tests.m +++ b/Tests/KSCrashRecordingTests/KSCrashMonitor_Memory_Tests.m @@ -27,7 +27,7 @@ #import "KSCrashAppStateTracker.h" #import "KSCrashMonitorContext.h" #import "KSCrashMonitor_Memory.h" -#import "KSCrashReportStore.h" +#import "KSCrashReportStoreC.h" #import "KSSystemCapabilities.h" @interface KSCrashMonitor_Memory_Tests : XCTestCase @@ -97,9 +97,11 @@ - (void)testInstallation [mngr removeItemAtURL:breadcrumbURL error:nil]; // init + const char *appName = "test"; KSCrashCConfiguration config = KSCrashCConfiguration_Default(); config.monitors = KSCrashMonitorTypeMemoryTermination; - kscrash_install("test", installURL.path.UTF8String, config); + kscrash_install(appName, installURL.path.UTF8String, &config); + KSCrashCConfiguration_Release(&config); // init memory API KSCrashMonitorAPI *api = kscm_memory_getAPI(); @@ -146,12 +148,16 @@ - (void)testInstallation // check the last report, it should be the OOM report NSMutableArray *reports = [NSMutableArray array]; + KSCrashReportStoreCConfiguration storeConfig = { + .appName = appName, + .reportsPath = reportsPath.path.UTF8String, + }; int64_t reportIDs[10] = { 0 }; - kscrash_getReportIDs(reportIDs, 10); + kscrs_getReportIDs(reportIDs, 10, &storeConfig); for (int index = 0; index < 10; index++) { int64_t reportID = reportIDs[index]; if (reportID) { - char *report = kscrash_readReport(reportID); + char *report = kscrs_readReport(reportID, &storeConfig); if (report) { NSData *data = [[NSData alloc] initWithBytes:report length:strlen(report)]; NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; diff --git a/Tests/KSCrashRecordingTests/KSCrashReportStore_Tests.m b/Tests/KSCrashRecordingTests/KSCrashReportStoreC_Tests.m similarity index 69% rename from Tests/KSCrashRecordingTests/KSCrashReportStore_Tests.m rename to Tests/KSCrashRecordingTests/KSCrashReportStoreC_Tests.m index e60ddcb9..15f1e80f 100644 --- a/Tests/KSCrashRecordingTests/KSCrashReportStore_Tests.m +++ b/Tests/KSCrashRecordingTests/KSCrashReportStoreC_Tests.m @@ -1,5 +1,5 @@ // -// KSCrashReportStore_Tests.m +// KSCrashReportStoreC_Tests.m // // Created by Karl Stenerud on 2012-02-05. // @@ -26,13 +26,14 @@ #import "FileBasedTestCase.h" -#import "KSCrashReportStore.h" +#import "KSCrashReportStoreC+Private.h" #include #define REPORT_PREFIX @"CrashReport-KSCrashTest" +#define REPORT_CONTENTS(NUM) @"{\n \"a\": \"" #NUM "\"\n}" -@interface KSCrashReportStore_Tests : FileBasedTestCase +@interface KSCrashReportStoreC_Tests : FileBasedTestCase @property(nonatomic, readwrite, copy) NSString *appName; @property(nonatomic, readwrite, copy) NSString *reportStorePath; @@ -40,7 +41,9 @@ @interface KSCrashReportStore_Tests : FileBasedTestCase @end -@implementation KSCrashReportStore_Tests +@implementation KSCrashReportStoreC_Tests { + KSCrashReportStoreCConfiguration _storeConfig; +} - (int64_t)getReportIDFromPath:(NSString *)path { @@ -60,16 +63,24 @@ - (void)setUp } - (void)prepareReportStoreWithPathEnd:(NSString *)pathEnd +{ + [self prepareReportStoreWithPathEnd:pathEnd maxReportCount:5]; +} + +- (void)prepareReportStoreWithPathEnd:(NSString *)pathEnd maxReportCount:(int)maxReportCount { self.reportStorePath = [self.tempPath stringByAppendingPathComponent:pathEnd]; - kscrs_initialize(self.appName.UTF8String, self.reportStorePath.UTF8String); + _storeConfig.appName = self.appName.UTF8String; + _storeConfig.reportsPath = self.reportStorePath.UTF8String; + _storeConfig.maxReportCount = maxReportCount; + kscrs_initialize(&_storeConfig); } - (NSArray *)getReportIDs { - int reportCount = kscrs_getReportCount(); + int reportCount = kscrs_getReportCount(&_storeConfig); int64_t rawReportIDs[reportCount]; - reportCount = kscrs_getReportIDs(rawReportIDs, reportCount); + reportCount = kscrs_getReportIDs(rawReportIDs, reportCount, &_storeConfig); NSMutableArray *reportIDs = [NSMutableArray new]; for (int i = 0; i < reportCount; i++) { [reportIDs addObject:@(rawReportIDs[i])]; @@ -81,7 +92,7 @@ - (int64_t)writeCrashReportWithStringContents:(NSString *)contents { NSData *crashData = [contents dataUsingEncoding:NSUTF8StringEncoding]; char crashReportPath[KSCRS_MAX_PATH_LENGTH]; - kscrs_getNextCrashReport(crashReportPath); + kscrs_getNextCrashReport(crashReportPath, &_storeConfig); [crashData writeToFile:[NSString stringWithUTF8String:crashReportPath] atomically:YES]; return [self getReportIDFromPath:[NSString stringWithUTF8String:crashReportPath]]; } @@ -89,12 +100,12 @@ - (int64_t)writeCrashReportWithStringContents:(NSString *)contents - (int64_t)writeUserReportWithStringContents:(NSString *)contents { NSData *data = [contents dataUsingEncoding:NSUTF8StringEncoding]; - return kscrs_addUserReport(data.bytes, (int)data.length); + return kscrs_addUserReport(data.bytes, (int)data.length, &_storeConfig); } - (void)loadReportID:(int64_t)reportID reportString:(NSString *__autoreleasing *)reportString { - char *reportBytes = kscrs_readReport(reportID); + char *reportBytes = kscrs_readReport(reportID, &_storeConfig); if (reportBytes == NULL) { reportString = nil; @@ -107,7 +118,7 @@ - (void)loadReportID:(int64_t)reportID reportString:(NSString *__autoreleasing * - (void)expectHasReportCount:(int)reportCount { - XCTAssertEqual(kscrs_getReportCount(), reportCount); + XCTAssertEqual(kscrs_getReportCount(&_storeConfig), reportCount); } - (void)expectReports:(NSArray *)reportIDs areStrings:(NSArray *)reportStrings @@ -130,32 +141,29 @@ - (void)testReportStorePathExists - (void)testCrashReportCount1 { [self prepareReportStoreWithPathEnd:@"testCrashReportCount1"]; - NSString *reportContents = @"Testing"; - [self writeCrashReportWithStringContents:reportContents]; + [self writeCrashReportWithStringContents:REPORT_CONTENTS(0)]; [self expectHasReportCount:1]; } - (void)testStoresLoadsOneCrashReport { [self prepareReportStoreWithPathEnd:@"testStoresLoadsOneCrashReport"]; - NSString *reportContents = @"Testing"; - int64_t reportID = [self writeCrashReportWithStringContents:reportContents]; - [self expectReports:@[ @(reportID) ] areStrings:@[ reportContents ]]; + int64_t reportID = [self writeCrashReportWithStringContents:REPORT_CONTENTS(0)]; + [self expectReports:@[ @(reportID) ] areStrings:@[ REPORT_CONTENTS(0) ]]; } - (void)testStoresLoadsOneUserReport { [self prepareReportStoreWithPathEnd:@"testStoresLoadsOneUserReport"]; - NSString *reportContents = @"Testing"; - int64_t reportID = [self writeUserReportWithStringContents:reportContents]; - [self expectReports:@[ @(reportID) ] areStrings:@[ reportContents ]]; + int64_t reportID = [self writeUserReportWithStringContents:REPORT_CONTENTS(0)]; + [self expectReports:@[ @(reportID) ] areStrings:@[ REPORT_CONTENTS(0) ]]; } - (void)testStoresLoadsMultipleReports { [self prepareReportStoreWithPathEnd:@"testStoresLoadsMultipleReports"]; NSMutableArray *reportIDs = [NSMutableArray new]; - NSArray *reportContents = @[ @"report1", @"report2", @"report3", @"report4" ]; + NSArray *reportContents = @[ REPORT_CONTENTS(1), REPORT_CONTENTS(2), REPORT_CONTENTS(3), REPORT_CONTENTS(4) ]; [reportIDs addObject:@([self writeCrashReportWithStringContents:reportContents[0]])]; [reportIDs addObject:@([self writeUserReportWithStringContents:reportContents[1]])]; [reportIDs addObject:@([self writeUserReportWithStringContents:reportContents[2]])]; @@ -167,31 +175,30 @@ - (void)testStoresLoadsMultipleReports - (void)testDeleteAllReports { [self prepareReportStoreWithPathEnd:@"testDeleteAllReports"]; - [self writeCrashReportWithStringContents:@"1"]; - [self writeUserReportWithStringContents:@"2"]; - [self writeUserReportWithStringContents:@"3"]; - [self writeCrashReportWithStringContents:@"4"]; + [self writeCrashReportWithStringContents:REPORT_CONTENTS(1)]; + [self writeUserReportWithStringContents:REPORT_CONTENTS(2)]; + [self writeUserReportWithStringContents:REPORT_CONTENTS(3)]; + [self writeCrashReportWithStringContents:REPORT_CONTENTS(4)]; [self expectHasReportCount:4]; - kscrs_deleteAllReports(); + kscrs_deleteAllReports(&_storeConfig); [self expectHasReportCount:0]; } - (void)testPruneReports { int reportStorePrunesTo = 7; - kscrs_setMaxReportCount(reportStorePrunesTo); - [self prepareReportStoreWithPathEnd:@"testDeleteAllReports"]; + [self prepareReportStoreWithPathEnd:@"testDeleteAllReports" maxReportCount:reportStorePrunesTo]; int64_t prunedReportID = [self writeUserReportWithStringContents:@"u1"]; - [self writeCrashReportWithStringContents:@"c1"]; - [self writeUserReportWithStringContents:@"u2"]; - [self writeCrashReportWithStringContents:@"c2"]; - [self writeCrashReportWithStringContents:@"c3"]; - [self writeUserReportWithStringContents:@"u3"]; - [self writeCrashReportWithStringContents:@"c4"]; - [self writeCrashReportWithStringContents:@"c5"]; + [self writeCrashReportWithStringContents:REPORT_CONTENTS(c1)]; + [self writeUserReportWithStringContents:REPORT_CONTENTS(u2)]; + [self writeCrashReportWithStringContents:REPORT_CONTENTS(c2)]; + [self writeCrashReportWithStringContents:REPORT_CONTENTS(c3)]; + [self writeUserReportWithStringContents:REPORT_CONTENTS(u3)]; + [self writeCrashReportWithStringContents:REPORT_CONTENTS(c4)]; + [self writeCrashReportWithStringContents:REPORT_CONTENTS(c5)]; [self expectHasReportCount:8]; // Calls kscrs_initialize() again, which prunes the reports. - [self prepareReportStoreWithPathEnd:@"testDeleteAllReports"]; + [self prepareReportStoreWithPathEnd:@"testDeleteAllReports" maxReportCount:reportStorePrunesTo]; [self expectHasReportCount:reportStorePrunesTo]; NSArray *reportIDs = [self getReportIDs]; XCTAssertFalse([reportIDs containsObject:@(prunedReportID)]); @@ -201,9 +208,8 @@ - (void)testStoresLoadsWithUnicodeAppName { self.appName = @"ЙогуртЙод"; [self prepareReportStoreWithPathEnd:@"testStoresLoadsWithUnicodeAppName"]; - NSString *reportContents = @"Testing"; - int64_t reportID = [self writeCrashReportWithStringContents:reportContents]; - [self expectReports:@[ @(reportID) ] areStrings:@[ reportContents ]]; + int64_t reportID = [self writeCrashReportWithStringContents:REPORT_CONTENTS(0)]; + [self expectReports:@[ @(reportID) ] areStrings:@[ REPORT_CONTENTS(0) ]]; } @end