Skip to content

Commit ca91921

Browse files
committed
fix: avoid duplicate crashOnException swizzles
1 parent 242b208 commit ca91921

2 files changed

Lines changed: 50 additions & 21 deletions

File tree

Sources/Sentry/SentryUncaughtNSExceptions.m

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -40,34 +40,37 @@ + (void)swizzleNSApplicationCrashOnException
4040
# if SENTRY_TEST || SENTRY_TEST_CI
4141
# pragma clang diagnostic ignored "-Wunused-variable"
4242
# endif
43-
// AppKit calls _crashOnException: when an exception is caught during CATransaction flush
44-
// or view layout, bypassing reportException: entirely. Depending on macOS version, AppKit
45-
// may call the class method +[NSApplication _crashOnException:] or the instance method
46-
// -[NSApp _crashOnException:], so we swizzle both.
47-
SEL selector = NSSelectorFromString(@"_crashOnException:");
48-
49-
SentrySwizzleClassMethod(NSApplication, selector, SentrySWReturnType(void),
50-
SentrySWArguments(NSException * exception), SentrySWReplacement({
51-
[SentryUncaughtNSExceptions capture:exception];
43+
static dispatch_once_t onceToken;
44+
dispatch_once(&onceToken, ^{
45+
// AppKit calls _crashOnException: when an exception is caught during CATransaction flush
46+
// or view layout, bypassing reportException: entirely. Depending on macOS version, AppKit
47+
// may call the class method +[NSApplication _crashOnException:] or the instance method
48+
// -[NSApp _crashOnException:], so we swizzle both.
49+
SEL selector = NSSelectorFromString(@"_crashOnException:");
50+
51+
SentrySwizzleClassMethod(NSApplication, selector, SentrySWReturnType(void),
52+
SentrySWArguments(NSException * exception), SentrySWReplacement({
53+
[SentryUncaughtNSExceptions capture:exception];
5254
# if SENTRY_TEST || SENTRY_TEST_CI
53-
// Don't call the original in tests as it would abort() the process.
54-
swizzleInfo.originalCalled = YES;
55+
// Don't call the original in tests as it would abort() the process.
56+
swizzleInfo.originalCalled = YES;
5557
# else
56-
return SentrySWCallOriginal(exception);
58+
return SentrySWCallOriginal(exception);
5759
# endif
58-
}));
60+
}));
5961

60-
SentrySwizzleInstanceMethod(NSApplication, selector, SentrySWReturnType(void),
61-
SentrySWArguments(NSException * exception), SentrySWReplacement({
62-
[SentryUncaughtNSExceptions capture:exception];
62+
SentrySwizzleInstanceMethod(NSApplication, selector, SentrySWReturnType(void),
63+
SentrySWArguments(NSException * exception), SentrySWReplacement({
64+
[SentryUncaughtNSExceptions capture:exception];
6365
# if SENTRY_TEST || SENTRY_TEST_CI
64-
// Don't call the original in tests as it would abort() the process.
65-
swizzleInfo.originalCalled = YES;
66+
// Don't call the original in tests as it would abort() the process.
67+
swizzleInfo.originalCalled = YES;
6668
# else
67-
return SentrySWCallOriginal(exception);
69+
return SentrySWCallOriginal(exception);
6870
# endif
69-
}),
70-
SentrySwizzleModeOncePerClassAndSuperclasses, (void *)selector);
71+
}),
72+
SentrySwizzleModeOncePerClassAndSuperclasses, (void *)selector);
73+
});
7174
# pragma clang diagnostic pop
7275
}
7376

Tests/SentryTests/Integrations/SentryCrash/SentryUncaughtNSExceptionsTests.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,30 @@ final class SentryUncaughtNSExceptionsTests: XCTestCase {
5151

5252
XCTAssertTrue(wasUncaughtExceptionHandlerCalled)
5353
}
54+
55+
func testSwizzleNSApplicationCrashOnException_whenCalledMultipleTimes_shouldCaptureClassMethodOnce() throws {
56+
let crashReporter = SentryDependencyContainer.sharedInstance().crashReporter
57+
58+
defer {
59+
crashReporter.uncaughtExceptionHandler = nil
60+
wasUncaughtExceptionHandlerCalled = false
61+
uncaughtExceptionHandlerCallCount = 0
62+
}
63+
64+
wasUncaughtExceptionHandlerCalled = false
65+
uncaughtExceptionHandlerCallCount = 0
66+
crashReporter.uncaughtExceptionHandler = uncaughtExceptionHandler
67+
68+
SentryUncaughtNSExceptions.swizzleNSApplicationCrashOnException()
69+
SentryUncaughtNSExceptions.swizzleNSApplicationCrashOnException()
70+
71+
// Call the class method +[NSApplication _crashOnException:].
72+
// In tests, SentrySWCallOriginal is skipped so this won't abort().
73+
NSApplication.perform(NSSelectorFromString("_crashOnException:"), with: uncaughtInternalInconsistencyException)
74+
75+
XCTAssertTrue(wasUncaughtExceptionHandlerCalled)
76+
XCTAssertEqual(1, uncaughtExceptionHandlerCallCount)
77+
}
5478

5579
func testSwizzleNSApplicationCrashOnException_instanceMethod() throws {
5680
let crashReporter = SentryDependencyContainer.sharedInstance().crashReporter
@@ -108,10 +132,12 @@ final class SentryUncaughtNSExceptionsTests: XCTestCase {
108132
// We need to declare this on the file level because otherwise we get the error:
109133
// A C function pointer cannot be formed from a closure that captures context.
110134
var wasUncaughtExceptionHandlerCalled = false
135+
var uncaughtExceptionHandlerCallCount = 0
111136
func uncaughtExceptionHandler(exception: NSException) {
112137
XCTAssertEqual(uncaughtInternalInconsistencyException.name, exception.name)
113138
XCTAssertEqual(uncaughtInternalInconsistencyException.reason, exception.reason)
114139
wasUncaughtExceptionHandlerCalled = true
140+
uncaughtExceptionHandlerCallCount += 1
115141
}
116142

117143
let uncaughtInternalInconsistencyException = NSException(name: .internalInconsistencyException, reason: "reason", userInfo: nil)

0 commit comments

Comments
 (0)