From 33feb0a1c3e05c2618077fd6571fc4408d0756af Mon Sep 17 00:00:00 2001 From: Nikolay Volosatov Date: Wed, 28 Aug 2024 12:21:35 +0100 Subject: [PATCH] Add reports-only set up API (#548) * Add reports-only install * Change API and allow repeating reports setup * Fix format * Fix pod lint * Prevent reports store setup after install * Add docs * Fix format --- .../Sources/LibraryBridge/InstallBridge.swift | 16 ++++++ .../Common/Sources/SampleUI/SampleView.swift | 9 +-- .../SampleUI/Screens/InstallView.swift | 6 +- .../Sources/SampleUI/Screens/MainView.swift | 8 ++- Sources/KSCrashRecording/KSCrash.m | 15 +++++ Sources/KSCrashRecording/KSCrashC.c | 56 +++++++++++++++---- Sources/KSCrashRecording/KSCrashReportStore.c | 37 ++++++++++-- Sources/KSCrashRecording/KSCrashReportStore.h | 3 +- Sources/KSCrashRecording/include/KSCrash.h | 27 ++++++++- Sources/KSCrashRecording/include/KSCrashC.h | 14 +++++ .../KSCrashReportStore_Tests.m | 2 +- 11 files changed, 158 insertions(+), 35 deletions(-) diff --git a/Samples/Common/Sources/LibraryBridge/InstallBridge.swift b/Samples/Common/Sources/LibraryBridge/InstallBridge.swift index 06defa08..9a93ae7e 100644 --- a/Samples/Common/Sources/LibraryBridge/InstallBridge.swift +++ b/Samples/Common/Sources/LibraryBridge/InstallBridge.swift @@ -67,6 +67,7 @@ public class InstallBridge: ObservableObject { @Published public var basePath: BasePath = .default @Published public var installed: Bool = false + @Published public var reportsOnlySetup: Bool = false @Published public var error: InstallationError? public init() { @@ -97,6 +98,21 @@ public class InstallBridge: ObservableObject { self.error = .unexpectedError(message) } } + + public func setupReportsOnly() { + do { + try KSCrash.shared.setupReportStore(withPath: config.installPath) + reportsOnlySetup = true + } catch let error as KSCrashInstallError { + let message = error.localizedDescription + Self.logger.error("Failed to install KSCrash: \(message)") + self.error = .kscrashError(message) + } catch { + let message = error.localizedDescription + Self.logger.error("Unexpected error during KSCrash installation: \(message)") + self.error = .unexpectedError(message) + } + } } // An utility method to simplify binding of config fields diff --git a/Samples/Common/Sources/SampleUI/SampleView.swift b/Samples/Common/Sources/SampleUI/SampleView.swift index 771b08bb..e14ffe5e 100644 --- a/Samples/Common/Sources/SampleUI/SampleView.swift +++ b/Samples/Common/Sources/SampleUI/SampleView.swift @@ -34,19 +34,16 @@ public struct SampleView: View { @ObservedObject var installBridge = InstallBridge() - @State private var installSkipped = false - public var body: some View { NavigationView { - if installBridge.installed || installSkipped { + if installBridge.installed || installBridge.reportsOnlySetup { MainView( - installSkipped: $installSkipped + reportsOnlySetup: $installBridge.reportsOnlySetup ) .navigationTitle("KSCrash Sample") } else { InstallView( - bridge: installBridge, - installSkipped: $installSkipped + bridge: installBridge ) .navigationTitle("Install KSCrash") } diff --git a/Samples/Common/Sources/SampleUI/Screens/InstallView.swift b/Samples/Common/Sources/SampleUI/Screens/InstallView.swift index 6fae0245..8078c5d5 100644 --- a/Samples/Common/Sources/SampleUI/Screens/InstallView.swift +++ b/Samples/Common/Sources/SampleUI/Screens/InstallView.swift @@ -31,8 +31,6 @@ import LibraryBridge struct InstallView: View { @ObservedObject var bridge: InstallBridge - @Binding var installSkipped: Bool - @State private var showingInstallAlert = false var body: some View { @@ -81,8 +79,8 @@ struct InstallView: View { // TODO: Add deleteBehaviorAfterSendAll } - Button("Skip install") { - installSkipped = true + Button("Only set up reports") { + bridge.setupReportsOnly() } .foregroundStyle(Color.red) } diff --git a/Samples/Common/Sources/SampleUI/Screens/MainView.swift b/Samples/Common/Sources/SampleUI/Screens/MainView.swift index 1ee86f14..1675bd54 100644 --- a/Samples/Common/Sources/SampleUI/Screens/MainView.swift +++ b/Samples/Common/Sources/SampleUI/Screens/MainView.swift @@ -29,14 +29,16 @@ import SwiftUI struct MainView: View { - @Binding var installSkipped: Bool + @Binding var reportsOnlySetup: Bool var body: some View { List { Section { - if installSkipped { + if 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") { - installSkipped = false + reportsOnlySetup = false } } else { Text("KSCrash is installed successfully") diff --git a/Sources/KSCrashRecording/KSCrash.m b/Sources/KSCrashRecording/KSCrash.m index 9420c9dc..b4c417e9 100644 --- a/Sources/KSCrashRecording/KSCrash.m +++ b/Sources/KSCrashRecording/KSCrash.m @@ -235,6 +235,21 @@ - (BOOL)installWithConfiguration:(KSCrashConfiguration *)configuration error:(NS return YES; } +- (BOOL)setupReportStoreWithPath:(NSString *)installPath error:(NSError **)error +{ + KSCrashInstallErrorCode result = + kscrash_setupReportsStore(self.bundleName.UTF8String, (installPath ?: getDefaultInstallPath()).UTF8String); + + if (result != KSCrashInstallErrorNone) { + if (error != NULL) { + *error = [self errorForInstallErrorCode:result]; + } + return NO; + } + + return YES; +} + - (void)sendAllReportsWithCompletion:(KSCrashReportFilterCompletion)onCompletion { NSArray *reports = [self allReports]; diff --git a/Sources/KSCrashRecording/KSCrashC.c b/Sources/KSCrashRecording/KSCrashC.c index efa1b580..ac7af424 100644 --- a/Sources/KSCrashRecording/KSCrashC.c +++ b/Sources/KSCrashRecording/KSCrashC.c @@ -209,6 +209,21 @@ 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) { @@ -226,19 +241,14 @@ KSCrashInstallErrorCode kscrash_install(const char *appName, const char *const i handleConfiguration(&configuration); - char path[KSFU_MAX_PATH_LENGTH]; - if (snprintf(path, sizeof(path), "%s/Reports", installPath) >= (int)sizeof(path)) { - KSLOG_ERROR("Path too long."); - return KSCrashInstallErrorPathTooLong; - } - if (ksfu_makePath(path) == false) { - KSLOG_ERROR("Could not create path: %s", path); - return KSCrashInstallErrorCouldNotCreatePath; + KSCrashInstallErrorCode result = setupReportsStore(appName, installPath); + if (result != KSCrashInstallErrorNone) { + return result; } - kscrs_initialize(appName, installPath, path); + char path[KSFU_MAX_PATH_LENGTH]; if (snprintf(path, sizeof(path), "%s/Data", installPath) >= (int)sizeof(path)) { - KSLOG_ERROR("Path too long."); + KSLOG_ERROR("Data path is too long."); return KSCrashInstallErrorPathTooLong; } if (ksfu_makePath(path) == false) { @@ -248,14 +258,14 @@ KSCrashInstallErrorCode kscrash_install(const char *appName, const char *const i ksmemory_initialize(path); if (snprintf(path, sizeof(path), "%s/Data/CrashState.json", installPath) >= (int)sizeof(path)) { - KSLOG_ERROR("Path too long."); + KSLOG_ERROR("Crash state path is too long."); return KSCrashInstallErrorPathTooLong; } kscrashstate_initialize(path); if (snprintf(g_consoleLogPath, sizeof(g_consoleLogPath), "%s/Data/ConsoleLog.txt", installPath) >= (int)sizeof(g_consoleLogPath)) { - KSLOG_ERROR("Console log path too long."); + KSLOG_ERROR("Console log path is too long."); return KSCrashInstallErrorPathTooLong; } if (g_shouldPrintPreviousLog) { @@ -279,6 +289,28 @@ 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(); } diff --git a/Sources/KSCrashRecording/KSCrashReportStore.c b/Sources/KSCrashRecording/KSCrashReportStore.c index 901d085c..7fbc81a3 100644 --- a/Sources/KSCrashRecording/KSCrashReportStore.c +++ b/Sources/KSCrashRecording/KSCrashReportStore.c @@ -26,6 +26,7 @@ #include "KSCrashReportStore.h" +#include #include #include #include @@ -45,7 +46,6 @@ static _Atomic(uint32_t) g_nextUniqueIDLow; static int64_t g_nextUniqueIDHigh; static const char *g_appName; static const char *g_reportsPath; -static const char *g_installPath; static pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; static int compareInt64(const void *a, const void *b) @@ -63,6 +63,7 @@ static inline int64_t getNextUniqueID(void) { return g_nextUniqueIDHigh + g_next static void getCrashReportPathByID(int64_t id, char *pathBuffer) { + assert(g_reportsPath != NULL); snprintf(pathBuffer, KSCRS_MAX_PATH_LENGTH, "%s/%s-report-%016llx.json", g_reportsPath, g_appName, id); } @@ -78,6 +79,11 @@ static int64_t getReportIDFromFilename(const char *filename) static int getReportCount(void) { + if (g_reportsPath == NULL) { + KSLOG_ERROR("Reports store is not set up"); + return 0; + } + int count = 0; DIR *dir = opendir(g_reportsPath); if (dir == NULL) { @@ -100,6 +106,11 @@ static int getReportCount(void) static int getReportIDs(int64_t *reportIDs, int count) { + if (g_reportsPath == NULL) { + KSLOG_ERROR("Reports store is not set up"); + return 0; + } + int index = 0; DIR *dir = opendir(g_reportsPath); if (dir == NULL) { @@ -157,17 +168,29 @@ static void initializeIDs(void) // Public API -void kscrs_initialize(const char *appName, const char *installPath, const char *reportsPath) +void kscrs_initialize(const char *appName, const char *reportsPath) { + const char *previousAppName = NULL; + const char *previousReportsPath = NULL; + pthread_mutex_lock(&g_mutex); + previousAppName = g_appName; + previousReportsPath = g_reportsPath; g_appName = strdup(appName); - g_installPath = strdup(installPath); - ksfu_makePath(installPath); 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); + } } int64_t kscrs_getNextCrashReport(char *crashReportPathBuffer) @@ -249,7 +272,11 @@ int64_t kscrs_addUserReport(const char *report, int reportLength) void kscrs_deleteAllReports(void) { pthread_mutex_lock(&g_mutex); - ksfu_deleteContentsOfPath(g_reportsPath); + if (g_reportsPath != NULL) { + ksfu_deleteContentsOfPath(g_reportsPath); + } else { + KSLOG_WARN("Reports store is not set up"); + } pthread_mutex_unlock(&g_mutex); } diff --git a/Sources/KSCrashRecording/KSCrashReportStore.h b/Sources/KSCrashRecording/KSCrashReportStore.h index a99abab1..6489946d 100644 --- a/Sources/KSCrashRecording/KSCrashReportStore.h +++ b/Sources/KSCrashRecording/KSCrashReportStore.h @@ -38,10 +38,9 @@ extern "C" { /** Initialize the report store. * * @param appName The application's name. - * @param installPath Full path to directory where the crash system writes files. * @param reportsPath Full path to directory where the reports are to be stored (path will be created if needed). */ -void kscrs_initialize(const char *appName, const char *installPath, const char *reportsPath); +void kscrs_initialize(const char *appName, const char *reportsPath); /** Get the next crash report to be generated. * Max length for paths is KSCRS_MAX_PATH_LENGTH diff --git a/Sources/KSCrashRecording/include/KSCrash.h b/Sources/KSCrashRecording/include/KSCrash.h index ca625fb2..151a2c36 100644 --- a/Sources/KSCrashRecording/include/KSCrash.h +++ b/Sources/KSCrashRecording/include/KSCrash.h @@ -127,13 +127,28 @@ 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: property "sink" MUST be set or else this method will call onCompletion - * with an error. + * @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). */ @@ -143,6 +158,9 @@ NS_ASSUME_NONNULL_BEGIN @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. * @@ -151,10 +169,15 @@ NS_ASSUME_NONNULL_BEGIN - (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. */ diff --git a/Sources/KSCrashRecording/include/KSCrashC.h b/Sources/KSCrashRecording/include/KSCrashC.h index 5c109015..42d586d8 100644 --- a/Sources/KSCrashRecording/include/KSCrashC.h +++ b/Sources/KSCrashRecording/include/KSCrashC.h @@ -85,6 +85,20 @@ extern "C" { 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); + /** Set the user-supplied data in JSON format. * * @param userInfoJSON Pre-baked JSON containing user-supplied information. diff --git a/Tests/KSCrashRecordingTests/KSCrashReportStore_Tests.m b/Tests/KSCrashRecordingTests/KSCrashReportStore_Tests.m index 90aa6215..e60ddcb9 100644 --- a/Tests/KSCrashRecordingTests/KSCrashReportStore_Tests.m +++ b/Tests/KSCrashRecordingTests/KSCrashReportStore_Tests.m @@ -62,7 +62,7 @@ - (void)setUp - (void)prepareReportStoreWithPathEnd:(NSString *)pathEnd { self.reportStorePath = [self.tempPath stringByAppendingPathComponent:pathEnd]; - kscrs_initialize(self.appName.UTF8String, self.tempPath.UTF8String, self.reportStorePath.UTF8String); + kscrs_initialize(self.appName.UTF8String, self.reportStorePath.UTF8String); } - (NSArray *)getReportIDs