Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve KSCrash installation error handling and reporting #531

Merged
merged 11 commits into from
Jul 15, 2024
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? {
GLinnik21 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading