Skip to content

Commit

Permalink
Add reportNSException method and deprecate currentSnapshotUserReporte…
Browse files Browse the repository at this point in the history
…dExceptionHandler
  • Loading branch information
bamx23 committed Nov 1, 2024
1 parent 24baa5f commit f7a5182
Show file tree
Hide file tree
Showing 10 changed files with 85 additions and 41 deletions.
5 changes: 4 additions & 1 deletion Sources/KSCrashRecording/KSCrash+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,13 @@ NSString *kscrash_getDefaultInstallPath(void);
}
#endif

typedef void KSCrashCustomNSExceptionReporter(NSException *exception, BOOL logAllThreads);

@interface KSCrash ()

@property(nonatomic, readwrite, assign) NSUncaughtExceptionHandler *uncaughtExceptionHandler;
@property(nonatomic, readwrite, assign) NSUncaughtExceptionHandler *currentSnapshotUserReportedExceptionHandler;

@property(nonatomic, assign) KSCrashCustomNSExceptionReporter *customNSExceptionReporter;

+ (NSError *)errorForInstallErrorCode:(KSCrashInstallErrorCode)errorCode;

Expand Down
19 changes: 19 additions & 0 deletions Sources/KSCrashRecording/KSCrash.m
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ @interface KSCrash ()
return [cachePath stringByAppendingPathComponent:pathEnd];
}

static void currentSnapshotUserReportedExceptionHandler(NSException *exception)
{
if (!gIsSharedInstanceCreated) {
KSLOG_ERROR(@"Shared instance must exist before this function is called.");
return;
}
[[KSCrash sharedInstance] reportNSException:exception logAllThreads:YES];
}

@implementation KSCrash

// ============================================================================
Expand Down Expand Up @@ -119,6 +128,7 @@ - (instancetype)init
{
if ((self = [super init])) {
_bundleName = kscrash_getBundleName();
_currentSnapshotUserReportedExceptionHandler = &currentSnapshotUserReportedExceptionHandler;
}
return self;
}
Expand Down Expand Up @@ -278,6 +288,15 @@ - (void)reportUserException:(NSString *)name
kscrash_reportUserException(cName, cReason, cLanguage, cLineOfCode, cStackTrace, logAllThreads, terminateProgram);
}

- (void)reportNSException:(NSException *)exception logAllThreads:(BOOL)logAllThreads
{
if (_customNSExceptionReporter == NULL) {
KSLOG_ERROR(@"NSExcepttion monitor needs to be installed before reporting custom exceptions");
return;
}
_customNSExceptionReporter(exception, logAllThreads);
}

// ============================================================================
#pragma mark - Advanced API -
// ============================================================================
Expand Down
2 changes: 1 addition & 1 deletion Sources/KSCrashRecording/KSCrashC.c
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ static void notifyOfBeforeInstallationState(void)
*/
static void onCrash(struct KSCrash_MonitorContext *monitorContext)
{
if (monitorContext->currentSnapshotUserReported == false) {
if (monitorContext->isUserReported == false) {
KSLOG_DEBUG("Updating application state to note crash.");
kscrashstate_notifyAppCrash();
}
Expand Down
4 changes: 1 addition & 3 deletions Sources/KSCrashRecording/KSCrashReportC.c
Original file line number Diff line number Diff line change
Expand Up @@ -1609,9 +1609,7 @@ void kscrashreport_writeStandardReport(const KSCrash_MonitorContext *const monit
}
if (g_userSectionWriteCallback != NULL) {
ksfu_flushBufferedWriter(&bufferedWriter);
if (monitorContext->currentSnapshotUserReported == false) {
g_userSectionWriteCallback(writer);
}
g_userSectionWriteCallback(writer);
}
writer->endContainer(writer);
ksfu_flushBufferedWriter(&bufferedWriter);
Expand Down
2 changes: 1 addition & 1 deletion Sources/KSCrashRecording/Monitors/KSCrashMonitor_Memory.m
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ static void ksmemory_write_possible_oom(void)
context.eventID = eventID;
context.registersAreValid = false;
context.offendingMachineContext = machineContext;
context.currentSnapshotUserReported = true;
context.isUserReported = true;

// we don't need all the images, we have no stack
context.omitBinaryImages = true;
Expand Down
55 changes: 36 additions & 19 deletions Sources/KSCrashRecording/Monitors/KSCrashMonitor_NSException.m
Original file line number Diff line number Diff line change
Expand Up @@ -59,29 +59,44 @@
* @param exception The exception that was raised.
*/

static void handleException(NSException *exception, BOOL currentSnapshotUserReported)
static void handleException(NSException *exception, BOOL isUserReported, BOOL logAllThreads)
{
KSLOG_DEBUG(@"Trapped exception %@", exception);
if (g_isEnabled) {
thread_act_array_t threads = NULL;
mach_msg_type_number_t numThreads = 0;
ksmc_suspendEnvironment(&threads, &numThreads);
kscm_notifyFatalExceptionCaptured(false);

KSLOG_DEBUG(@"Filling out context.");
NSArray *addresses = [exception callStackReturnAddresses];
NSUInteger numFrames = addresses.count;
uintptr_t *callstack = malloc(numFrames * sizeof(*callstack));
for (NSUInteger i = 0; i < numFrames; i++) {
callstack[i] = (uintptr_t)[addresses[i] unsignedLongLongValue];
if (logAllThreads) {
ksmc_suspendEnvironment(&threads, &numThreads);
}
if (!isUserReported) {
// User-reported exceptions are not considered fatal.
kscm_notifyFatalExceptionCaptured(false);
}

KSLOG_DEBUG(@"Filling out context.");
char eventID[37];
ksid_generate(eventID);
KSMC_NEW_CONTEXT(machineContext);
ksmc_getContextForThread(ksthread_self(), machineContext, true);
KSStackCursor cursor;
kssc_initWithBacktrace(&cursor, callstack, (int)numFrames, 0);

// Use stacktrace from NSException if present,
// otherwise use current thread (can happen for user-reported exceptions).
NSArray *addresses = [exception callStackReturnAddresses];
NSUInteger numFrames = addresses.count;
uintptr_t *callstack = NULL;
if (numFrames != 0) {
callstack = malloc(numFrames * sizeof(*callstack));
for (NSUInteger i = 0; i < numFrames; i++) {
callstack[i] = (uintptr_t)[addresses[i] unsignedLongLongValue];
}
kssc_initWithBacktrace(&cursor, callstack, (int)numFrames, 0);
} else {
kssc_initSelfThread(&cursor, 0);
}

NS_VALID_UNTIL_END_OF_SCOPE NSString *userInfoString =
exception.userInfo != nil ? [NSString stringWithFormat:@"%@", exception.userInfo] : nil;

KSCrash_MonitorContext *crashContext = &g_monitorContext;
memset(crashContext, 0, sizeof(*crashContext));
Expand All @@ -90,29 +105,32 @@ static void handleException(NSException *exception, BOOL currentSnapshotUserRepo
crashContext->offendingMachineContext = machineContext;
crashContext->registersAreValid = false;
crashContext->NSException.name = [[exception name] UTF8String];
crashContext->NSException.userInfo = [[NSString stringWithFormat:@"%@", exception.userInfo] UTF8String];
crashContext->NSException.userInfo = [userInfoString UTF8String];
crashContext->exceptionName = crashContext->NSException.name;
crashContext->crashReason = [[exception reason] UTF8String];
crashContext->stackCursor = &cursor;
crashContext->currentSnapshotUserReported = currentSnapshotUserReported;
crashContext->isUserReported = isUserReported;

KSLOG_DEBUG(@"Calling main crash handler.");
kscm_handleException(crashContext);

free(callstack);
if (currentSnapshotUserReported) {
if (logAllThreads && isUserReported) {
ksmc_resumeEnvironment(threads, numThreads);
}
if (g_previousUncaughtExceptionHandler != NULL) {
if (!isUserReported && g_previousUncaughtExceptionHandler != NULL) {
KSLOG_DEBUG(@"Calling original exception handler.");
g_previousUncaughtExceptionHandler(exception);
}
}
}

static void handleCurrentSnapshotUserReportedException(NSException *exception) { handleException(exception, true); }
static void customNSExceptionReporter(NSException *exception, BOOL logAllThreads)
{
handleException(exception, YES, logAllThreads);
}

static void handleUncaughtException(NSException *exception) { handleException(exception, false); }
static void handleUncaughtException(NSException *exception) { handleException(exception, NO, YES); }

// ============================================================================
#pragma mark - API -
Expand All @@ -129,8 +147,7 @@ static void setEnabled(bool isEnabled)
KSLOG_DEBUG(@"Setting new handler.");
NSSetUncaughtExceptionHandler(&handleUncaughtException);
KSCrash.sharedInstance.uncaughtExceptionHandler = &handleUncaughtException;
KSCrash.sharedInstance.currentSnapshotUserReportedExceptionHandler =
&handleCurrentSnapshotUserReportedException;
KSCrash.sharedInstance.customNSExceptionReporter = &customNSExceptionReporter;
} else {
KSLOG_DEBUG(@"Restoring original handler.");
NSSetUncaughtExceptionHandler(g_previousUncaughtExceptionHandler);
Expand Down
17 changes: 14 additions & 3 deletions Sources/KSCrashRecording/include/KSCrash.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,12 @@ NS_ASSUME_NONNULL_BEGIN
/** Exposes the uncaughtExceptionHandler if set from KSCrash. Is nil if debugger is running. */
@property(nonatomic, readonly, assign) NSUncaughtExceptionHandler *uncaughtExceptionHandler;

/** Exposes the currentSnapshotUserReportedExceptionHandler if set from KSCrash. Is nil if debugger is running. */
@property(nonatomic, readonly, assign) NSUncaughtExceptionHandler *currentSnapshotUserReportedExceptionHandler;
/** Exposes the currentSnapshotUserReportedExceptionHandler if set from KSCrash. Is nil if debugger is running.
*
* @note This property is deprecated in favor of `-reportNSException:logAllThreads:` method.
*/
@property(nonatomic, readonly, assign) NSUncaughtExceptionHandler *currentSnapshotUserReportedExceptionHandler
__attribute__((deprecated("Deprecated in favor of -reportNSException:logAllThreads:terminateProgram:")));

/** Total active time elapsed since the last crash. */
@property(nonatomic, readonly, assign) NSTimeInterval activeDurationSinceLastCrash;
Expand Down Expand Up @@ -126,7 +130,7 @@ NS_ASSUME_NONNULL_BEGIN
/** Report a custom, user defined exception.
* This can be useful when dealing with scripting languages.
*
* If terminateProgram is true, all sentries will be uninstalled and the application will
* If terminateProgram is true, all monitors will be uninstalled and the application will
* terminate with an abort().
*
* @param name The exception name (for namespacing exception types).
Expand All @@ -153,6 +157,13 @@ NS_ASSUME_NONNULL_BEGIN
logAllThreads:(BOOL)logAllThreads
terminateProgram:(BOOL)terminateProgram;

/** Report an NSException as if it's caught by a corresponding monitor.
*
* @param exception The exception to be reported.
* @param logAllThreads If true, suspend all threads and log their state. Note that this incurs a performance penalty.
*/
- (void)reportNSException:(NSException *)exception logAllThreads:(BOOL)logAllThreads;

@end

//! Project version number for KSCrashFramework.
Expand Down
10 changes: 3 additions & 7 deletions Sources/KSCrashRecordingCore/KSCrashMonitor.c
Original file line number Diff line number Diff line change
Expand Up @@ -306,13 +306,9 @@ void kscm_handleException(struct KSCrash_MonitorContext *context)
}

// Restore original handlers if the exception is fatal and not already handled
if (context->currentSnapshotUserReported) {
g_handlingFatalException = false;
} else {
if (g_handlingFatalException && !g_crashedDuringExceptionHandling) {
KSLOG_DEBUG("Exception is fatal. Restoring original handlers.");
kscm_disableAllMonitors();
}
if (g_handlingFatalException && !g_crashedDuringExceptionHandling) {
KSLOG_DEBUG("Exception is fatal. Restoring original handlers.");
kscm_disableAllMonitors();
}

// Done handling the crash
Expand Down
8 changes: 4 additions & 4 deletions Sources/KSCrashRecordingCore/include/KSCrashMonitorContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ extern "C" {
typedef struct KSCrash_MonitorContext {
/** Unique identifier for this event. */
const char *eventID;
/**
If true, so reported user exception will have the current snapshot.
*/
bool currentSnapshotUserReported;

/** If true, the environment has crashed hard, and only async-safe
* functions should be used.
Expand All @@ -64,6 +60,10 @@ typedef struct KSCrash_MonitorContext {
/** True if the crash system has detected a stack overflow. */
bool isStackOverflow;

/** True if crash is reported by a user.
*/
bool isUserReported;

/** The machine context that generated the event. */
struct KSMachineContext *offendingMachineContext;

Expand Down
4 changes: 2 additions & 2 deletions Tests/KSCrashRecordingCoreTests/KSCrashMonitor_Tests.m
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ - (void)testHandleExceptionRestoresOriginalHandlers
kscm_activateMonitors();
XCTAssertTrue(g_dummyMonitor.isEnabled(), @"The monitor should be enabled after activation.");
struct KSCrash_MonitorContext context = { 0 };
context.currentSnapshotUserReported = false; // Simulate that the exception is not user-reported
context.isUserReported = false; // Simulate that the exception is not user-reported
context.monitorFlags = KSCrashMonitorFlagFatal; // Indicate that the exception is fatal
kscm_handleException(&context);
XCTAssertTrue(g_dummyMonitor.isEnabled(),
Expand Down Expand Up @@ -291,7 +291,7 @@ - (void)testHandleExceptionCurrentSnapshotUserReported
kscm_activateMonitors();
XCTAssertTrue(g_dummyMonitor.isEnabled(), @"The monitor should be enabled after activation.");
struct KSCrash_MonitorContext context = { 0 };
context.currentSnapshotUserReported = true; // Simulate that the snapshot is user-reported
context.isUserReported = true; // Simulate that the snapshot is user-reported
context.monitorFlags = KSCrashMonitorFlagFatal; // Indicate that the exception is fatal
kscm_notifyFatalExceptionCaptured(false); // Simulate capturing a fatal exception
kscm_handleException(&context); // Handle the exception
Expand Down

0 comments on commit f7a5182

Please sign in to comment.