Skip to content

Commit

Permalink
Improve KSCrash installation error handling and reporting (#531)
Browse files Browse the repository at this point in the history
* Basic implementation

* Remove nullability from API

* Pass NULL to error in installations

as we want to keep it simple

* Fix installation

* add tests to KSCrashMonitor_Tests

* Update sample to print install error

* Add `none` error code

* Add errors reporting to Installations

* Fix tests

* Return `nil` in case of `none` error
  • Loading branch information
GLinnik21 authored Jul 15, 2024
1 parent 5df9128 commit 3e3d393
Show file tree
Hide file tree
Showing 14 changed files with 294 additions and 50 deletions.
32 changes: 28 additions & 4 deletions Samples/Common/Sources/LibraryBridge/RecordingSample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,33 @@
import Foundation
import KSCrashRecording

public class RecordingSample {
public static func simpleInstall() {
let config = KSCrashConfiguration()
KSCrash.shared.install(with: config)
public struct RecordingSample {
public enum InstallationError: Error, LocalizedError {
case kscrashError(String)
case unexpectedError(String)

public var errorDescription: String? {
switch self {
case .kscrashError(let message), .unexpectedError(let message):
return message
}
}
}

public static func install() -> Result<Void, InstallationError> {
do {
let config = KSCrashConfiguration()
try KSCrash.shared.install(with: config)
print("KSCrash installed successfully")
return .success(())
} catch let error as KSCrashInstallError {
let message = error.localizedDescription
print("Failed to install KSCrash: \(message)")
return .failure(.kscrashError(message))
} catch {
let message = error.localizedDescription
print("Unexpected error during KSCrash installation: \(message)")
return .failure(.unexpectedError(message))
}
}
}
21 changes: 18 additions & 3 deletions Samples/Common/Sources/SampleUI/SampleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ import LibraryBridge
import CrashTriggers

public struct SampleView: View {
@State private var showingInstallAlert = false
@State private var installErrorMessage: String = ""

public init() { }

public var body: some View {
NavigationView {
List {
Expand All @@ -50,8 +53,20 @@ public struct SampleView: View {
}
.navigationTitle("KSCrash Sample")
}
.onAppear {
RecordingSample.simpleInstall()
.onAppear(perform: installKSCrash)
.alert(isPresented: $showingInstallAlert) {
Alert(
title: Text("Installation Failed"),
message: Text(installErrorMessage),
dismissButton: .default(Text("OK"))
)
}
}

private func installKSCrash() {
if case let .failure(error) = RecordingSample.install() {
installErrorMessage = error.localizedDescription
showingInstallAlert = true
}
}
}
14 changes: 9 additions & 5 deletions Sources/KSCrashInstallations/KSCrashInstallation.m
Original file line number Diff line number Diff line change
Expand Up @@ -259,15 +259,12 @@ - (void)setOnCrash:(KSReportWriteCallback)onCrash
}
}

- (void)installWithConfiguration:(KSCrashConfiguration *)configuration
- (BOOL)installWithConfiguration:(KSCrashConfiguration *)configuration error:(NSError **)error
{
KSCrash *handler = [KSCrash sharedInstance];
@synchronized(handler) {
g_crashHandlerData = self.crashHandlerData;

if (configuration == nil) {
configuration = [[KSCrashConfiguration alloc] init];
}
configuration.crashNotifyCallback = ^(const struct KSCrashReportWriter *_Nonnull writer) {
CrashHandlerData *crashHandlerData = g_crashHandlerData;
if (crashHandlerData == NULL) {
Expand All @@ -284,7 +281,14 @@ - (void)installWithConfiguration:(KSCrashConfiguration *)configuration
}
};

[handler installWithConfiguration:configuration];
NSError *installError = nil;
BOOL success = [handler installWithConfiguration:configuration error:&installError];

if (success == NO && error != NULL) {
*error = installError;
}

return success;
}
}

Expand Down
13 changes: 10 additions & 3 deletions Sources/KSCrashInstallations/include/KSCrashInstallation.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,24 @@ NS_SWIFT_NAME(CrashInstallation)
@property(atomic, readwrite, assign, nullable) KSReportWriteCallback onCrash;

/** Install this crash handler with a specific configuration.
* Call this method instead of `-[KSCrash installWithConfiguration:]` to set up the crash handler
* Call this method instead of `-[KSCrash installWithConfiguration:error:]` to set up the crash handler
* tailored for your specific backend requirements.
*
* @param configuration The configuration object containing the settings for the crash handler.
* If nil, a default KSCrashConfiguration will be used.
* @param error On input, a pointer to an error object. If an error occurs, this pointer
* is set to an actual error object containing the error information.
* You may specify nil for this parameter if you do not want the error information.
* See KSCrashError.h for specific error codes that may be returned.
*
* @return YES if the installation was successful, NO otherwise.
*
* @note The `crashNotifyCallback` property of the provided `KSCrashConfiguration` will not take effect
* when using this method. The callback will be internally managed to ensure proper integration
* with the backend.
*
* @see KSCrashError.h for a complete list of possible error codes.
*/
- (void)installWithConfiguration:(nullable KSCrashConfiguration *)configuration;
- (BOOL)installWithConfiguration:(KSCrashConfiguration *)configuration error:(NSError **)error;

/** Convenience method to call -[KSCrash sendAllReportsWithCompletion:].
* This method will set the KSCrash sink and then send all outstanding reports.
Expand Down
56 changes: 53 additions & 3 deletions Sources/KSCrashRecording/KSCrash.m
Original file line number Diff line number Diff line change
Expand Up @@ -240,12 +240,20 @@ - (NSDictionary *)systemInfo
return [dict copy];
}

- (BOOL)installWithConfiguration:(KSCrashConfiguration *)configuration
- (BOOL)installWithConfiguration:(KSCrashConfiguration *)configuration error:(NSError **)error
{
self.configuration = [configuration copy] ?: [KSCrashConfiguration new];
kscrash_install(self.bundleName.UTF8String, self.basePath.UTF8String, [self.configuration toCConfiguration]);
KSCrashInstallErrorCode result =
kscrash_install(self.bundleName.UTF8String, self.basePath.UTF8String, [self.configuration toCConfiguration]);

return true;
if (result != KSCrashInstallErrorNone) {
if (error != NULL) {
*error = [self errorForInstallErrorCode:result];
}
return NO;
}

return YES;
}

- (void)sendAllReportsWithCompletion:(KSCrashReportFilterCompletion)onCompletion
Expand Down Expand Up @@ -436,6 +444,48 @@ - (NSMutableData *)nullTerminated:(NSData *)data
return mutable;
}

- (NSError *)errorForInstallErrorCode:(KSCrashInstallErrorCode)errorCode
{
NSString *errorDescription;
switch (errorCode) {
case KSCrashInstallErrorNone:
return nil;
case KSCrashInstallErrorAlreadyInstalled:
errorDescription = @"KSCrash is already installed";
break;
case KSCrashInstallErrorInvalidParameter:
errorDescription = @"Invalid parameter provided";
break;
case KSCrashInstallErrorPathTooLong:
errorDescription = @"Path is too long";
break;
case KSCrashInstallErrorCouldNotCreatePath:
errorDescription = @"Could not create path";
break;
case KSCrashInstallErrorCouldNotInitializeStore:
errorDescription = @"Could not initialize crash report store";
break;
case KSCrashInstallErrorCouldNotInitializeMemory:
errorDescription = @"Could not initialize memory management";
break;
case KSCrashInstallErrorCouldNotInitializeCrashState:
errorDescription = @"Could not initialize crash state";
break;
case KSCrashInstallErrorCouldNotSetLogFilename:
errorDescription = @"Could not set log filename";
break;
case KSCrashInstallErrorNoActiveMonitors:
errorDescription = @"No crash monitors were activated";
break;
default:
errorDescription = @"Unknown error occurred";
break;
}
return [NSError errorWithDomain:KSCrashErrorDomain
code:errorCode
userInfo:@{ NSLocalizedDescriptionKey : errorDescription }];
}

// ============================================================================
#pragma mark - Notifications -
// ============================================================================
Expand Down
65 changes: 46 additions & 19 deletions Sources/KSCrashRecording/KSCrashC.c
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,13 @@ static void setMonitors(KSCrashMonitorType monitorTypes)
{
g_monitoring = monitorTypes;

if (g_installed) {
for (size_t i = 0; i < g_monitorMappingCount; i++) {
KSCrashMonitorAPI *api = g_monitorMappings[i].getAPI();
if (api != NULL) {
if (monitorTypes & g_monitorMappings[i].type) {
kscm_addMonitor(api);
} else {
kscm_removeMonitor(api);
}
for (size_t i = 0; i < g_monitorMappingCount; i++) {
KSCrashMonitorAPI *api = g_monitorMappings[i].getAPI();
if (api != NULL) {
if (monitorTypes & g_monitorMappings[i].type) {
kscm_addMonitor(api);
} else {
kscm_removeMonitor(api);
}
}
}
Expand Down Expand Up @@ -211,31 +209,55 @@ void handleConfiguration(KSCrashCConfiguration *configuration)
#pragma mark - API -
// ============================================================================

void kscrash_install(const char *appName, const char *const installPath, KSCrashCConfiguration configuration)
KSCrashInstallErrorCode kscrash_install(const char *appName, const char *const installPath,
KSCrashCConfiguration configuration)
{
KSLOG_DEBUG("Installing crash reporter.");

if (g_installed) {
KSLOG_DEBUG("Crash reporter already installed.");
return;
return KSCrashInstallErrorAlreadyInstalled;
}

if (appName == NULL || installPath == NULL) {
KSLOG_ERROR("Invalid parameters: appName or installPath is NULL.");
return KSCrashInstallErrorInvalidParameter;
}
g_installed = 1;

handleConfiguration(&configuration);

char path[KSFU_MAX_PATH_LENGTH];
snprintf(path, sizeof(path), "%s/Reports", installPath);
ksfu_makePath(path);
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;
}
kscrs_initialize(appName, installPath, path);

snprintf(path, sizeof(path), "%s/Data", installPath);
ksfu_makePath(path);
if (snprintf(path, sizeof(path), "%s/Data", 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;
}
ksmemory_initialize(path);

snprintf(path, sizeof(path), "%s/Data/CrashState.json", installPath);
if (snprintf(path, sizeof(path), "%s/Data/CrashState.json", installPath) >= (int)sizeof(path)) {
KSLOG_ERROR("Path too long.");
return KSCrashInstallErrorPathTooLong;
}
kscrashstate_initialize(path);

snprintf(g_consoleLogPath, sizeof(g_consoleLogPath), "%s/Data/ConsoleLog.txt", installPath);
if (snprintf(g_consoleLogPath, sizeof(g_consoleLogPath), "%s/Data/ConsoleLog.txt", installPath) >=
(int)sizeof(g_consoleLogPath)) {
KSLOG_ERROR("Console log path too long.");
return KSCrashInstallErrorPathTooLong;
}
if (g_shouldPrintPreviousLog) {
printPreviousLog(g_consoleLogPath);
}
Expand All @@ -245,11 +267,16 @@ void kscrash_install(const char *appName, const char *const installPath, KSCrash

kscm_setEventCallback(onCrash);
setMonitors(configuration.monitors);
kscm_activateMonitors();
if (kscm_activateMonitors() == false) {
KSLOG_ERROR("No crash monitors are active");
return KSCrashInstallErrorNoActiveMonitors;
}

g_installed = true;
KSLOG_DEBUG("Installation complete.");

notifyOfBeforeInstallationState();
return KSCrashInstallErrorNone;
}

void kscrash_setUserInfoJSON(const char *const userInfoJSON) { kscrashreport_setUserInfoJSON(userInfoJSON); }
Expand Down
14 changes: 10 additions & 4 deletions Sources/KSCrashRecording/include/KSCrash.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,18 @@ NS_ASSUME_NONNULL_BEGIN
+ (void)setBasePath:(nullable NSString *)basePath;

/** Install the crash reporter.
* The reporter will record crashes, but will not send any crash reports unless
* sink is set.
* The reporter will record crashes, but will not send any crash reports unless a sink is set.
*
* @return YES if the reporter successfully installed.
* @param configuration The configuration to use for installation.
* @param error A pointer to an NSError object. If an error occurs, this pointer is set to an actual error object
* containing the error information. You may specify nil for this parameter if you do not want
* the error information.
* @return YES if the reporter successfully installed, NO otherwise.
*
* @note If the installation fails, the error parameter will contain information about the failure reason.
* @note Once installed, the crash reporter cannot be re-installed or modified without restarting the application.
*/
- (BOOL)installWithConfiguration:(KSCrashConfiguration *)configuration;
- (BOOL)installWithConfiguration:(KSCrashConfiguration *)configuration 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
Expand Down
16 changes: 12 additions & 4 deletions Sources/KSCrashRecording/include/KSCrashC.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include <stdbool.h>

#include "KSCrashCConfiguration.h"
#include "KSCrashError.h"
#include "KSCrashMonitorType.h"
#include "KSCrashReportWriter.h"

Expand All @@ -54,19 +55,25 @@ extern "C" {
* The specified directory must be writable, as it will contain log files,
* crash data, and other diagnostic information.
*
* @param configuration A `KSCrashConfiguration` struct containing various settings and options
* @param configuration A `KSCrashCConfiguration` struct containing various settings and options
* for the crash reporter. This struct allows you to specify which types of crashes
* to monitor, user-supplied metadata, memory introspection options,
* and other advanced settings.
* Each field in the configuration struct has default values, which can be overridden
* to tailor the behavior of the crash reporter to your specific requirements.
*
* @return KSCrashInstallErrorCode indicating the result of the installation.
* 0 if installation was successful, other values indicate specific errors.
*
* Example usage:
* ```
* KSCrashConfiguration config = KSCrashConfiguration_Default;
* KSCrashCConfiguration config = KSCrashCConfiguration_Default;
* config.monitors = KSCrashMonitorTypeAll;
* config.userInfoJSON = "{ \"user\": \"example\" }";
* kscrash_install("MyApp", "/path/to/install", config);
* KSCrashInstallErrorCode result = kscrash_install("MyApp", "/path/to/install", config);
* if (result != 0) {
* // Handle installation error
* }
* ```
*
* @note This function must be called before any crashes occur to ensure that
Expand All @@ -75,7 +82,8 @@ extern "C" {
* @note Once installed, the crash reporter cannot be re-installed or modified
* without restarting the application.
*/
void kscrash_install(const char *appName, const char *const installPath, KSCrashCConfiguration configuration);
KSCrashInstallErrorCode kscrash_install(const char *appName, const char *const installPath,
KSCrashCConfiguration configuration);

/** Set the user-supplied data in JSON format.
*
Expand Down
Loading

0 comments on commit 3e3d393

Please sign in to comment.