From 1a67a518241c422907b9c920ace6c0a02b240c94 Mon Sep 17 00:00:00 2001 From: Pavlos Vinieratos Date: Mon, 25 Feb 2019 13:50:56 +0100 Subject: [PATCH 1/8] add RCTFatalException and the surrounding funcs, to handle exceptions from native ios modules, before losing the context by converting it to an NSError --- React/Base/RCTAssert.h | 10 ++++++++-- React/Base/RCTAssert.m | 30 ++++++++++++++++++++++++++++++ React/CxxModule/RCTNativeModule.mm | 5 +---- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/React/Base/RCTAssert.h b/React/Base/RCTAssert.h index 4a6580676971f5..bb9912086bbf10 100644 --- a/React/Base/RCTAssert.h +++ b/React/Base/RCTAssert.h @@ -39,9 +39,11 @@ RCT_EXTERN void _RCTAssertFormat( /** * Report a fatal condition when executing. These calls will _NOT_ be compiled out * in production, and crash the app by default. You can customize the fatal behaviour - * by setting a custom fatal handler through `RCTSetFatalHandler`. + * by setting a custom fatal handler through `RCTSetFatalHandler` and + * `RCTSetFatalExceptionHandler`. */ RCT_EXTERN void RCTFatal(NSError *error); +RCT_EXTERN void RCTFatalException(NSException *exception); /** * The default error domain to be used for React errors. @@ -73,6 +75,7 @@ typedef void (^RCTAssertFunction)(NSString *condition, NSString *message); typedef void (^RCTFatalHandler)(NSError *error); +typedef void (^RCTFatalExceptionHandler)(NSException *exception); /** * Convenience macro for asserting that a parameter is non-nil/non-zero. @@ -114,10 +117,13 @@ RCT_EXTERN void RCTAddAssertFunction(RCTAssertFunction assertFunction); RCT_EXTERN void RCTPerformBlockWithAssertFunction(void (^block)(void), RCTAssertFunction assertFunction); /** - These methods get and set the current fatal handler called by the RCTFatal method. + * These methods get and set the current fatal handler called by the `RCTFatal` + * and `RCTFatalException` methods. */ RCT_EXTERN void RCTSetFatalHandler(RCTFatalHandler fatalHandler); RCT_EXTERN RCTFatalHandler RCTGetFatalHandler(void); +RCT_EXTERN void RCTSetFatalExceptionHandler(RCTFatalExceptionHandler fatalExceptionHandler); +RCT_EXTERN RCTFatalExceptionHandler RCTGetFatalExceptionHandler(void); /** * Get the current thread's name (or the current queue, if in debug mode) diff --git a/React/Base/RCTAssert.m b/React/Base/RCTAssert.m index cec11448004091..0f7f4de3090d82 100644 --- a/React/Base/RCTAssert.m +++ b/React/Base/RCTAssert.m @@ -18,6 +18,7 @@ RCTAssertFunction RCTCurrentAssertFunction = nil; RCTFatalHandler RCTCurrentFatalHandler = nil; +RCTFatalExceptionHandler RCTCurrentFatalExceptionHandler = nil; NSException *_RCTNotImplementedException(SEL, Class); NSException *_RCTNotImplementedException(SEL cmd, Class cls) @@ -187,3 +188,32 @@ RCTFatalHandler RCTGetFatalHandler(void) return [NSString stringWithFormat:@"%@%@", message, prettyStack]; } + +void RCTFatalException(NSException *exception) +{ + _RCTLogNativeInternal(RCTLogLevelFatal, NULL, 0, @"%@: %@", exception.name, exception.reason); + + RCTFatalExceptionHandler fatalExceptionHandler = RCTGetFatalExceptionHandler(); + if (fatalExceptionHandler) { + fatalExceptionHandler(exception); + } else { +#if DEBUG + @try { +#endif + @throw exception; +#if DEBUG + } @catch (NSException *e) {} +#endif + } +} + +void RCTSetFatalExceptionHandler(RCTFatalExceptionHandler fatalExceptionHandler) +{ + RCTCurrentFatalExceptionHandler = fatalExceptionHandler; +} + +RCTFatalExceptionHandler RCTGetFatalExceptionHandler(void) +{ + return RCTCurrentFatalExceptionHandler; +} + diff --git a/React/CxxModule/RCTNativeModule.mm b/React/CxxModule/RCTNativeModule.mm index ec88558b873da8..025b24d0c3a1ff 100644 --- a/React/CxxModule/RCTNativeModule.mm +++ b/React/CxxModule/RCTNativeModule.mm @@ -110,10 +110,7 @@ static MethodCallResult invokeInner(RCTBridge *bridge, RCTModuleData *moduleData @throw exception; } - NSString *message = [NSString stringWithFormat: - @"Exception '%@' was thrown while invoking %s on target %@ with params %@\ncallstack: %@", - exception, method.JSMethodName, moduleData.name, objcParams, exception.callStackSymbols]; - RCTFatal(RCTErrorWithMessage(message)); + RCTFatalException(exception); } return folly::none; From e749ca728e54898089a68a93ef614b79573b8993 Mon Sep 17 00:00:00 2001 From: Pavlos Vinieratos Date: Mon, 25 Feb 2019 13:51:44 +0100 Subject: [PATCH 2/8] add example of js and native crash, in RNTester --- RNTester/RNTester.xcodeproj/project.pbxproj | 15 +++++ .../NativeExampleModules/CrashyCrash.h | 18 ++++++ .../NativeExampleModules/CrashyCrash.m | 23 +++++++ RNTester/js/CrashExample.js | 61 +++++++++++++++++++ RNTester/js/RNTesterList.android.js | 4 ++ RNTester/js/RNTesterList.ios.js | 4 ++ 6 files changed, 125 insertions(+) create mode 100644 RNTester/RNTester/NativeExampleModules/CrashyCrash.h create mode 100644 RNTester/RNTester/NativeExampleModules/CrashyCrash.m create mode 100644 RNTester/js/CrashExample.js diff --git a/RNTester/RNTester.xcodeproj/project.pbxproj b/RNTester/RNTester.xcodeproj/project.pbxproj index c9a7d90f1277d1..1f7caf9966899d 100644 --- a/RNTester/RNTester.xcodeproj/project.pbxproj +++ b/RNTester/RNTester.xcodeproj/project.pbxproj @@ -126,6 +126,7 @@ 83636F8F1B53F22C009F943E /* RCTUIManagerScenarioTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 83636F8E1B53F22C009F943E /* RCTUIManagerScenarioTests.m */; }; 8385CEF51B873B5C00C6273E /* RCTImageLoaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8385CEF41B873B5C00C6273E /* RCTImageLoaderTests.m */; }; 8385CF041B87479200C6273E /* RCTImageLoaderHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 8385CF031B87479200C6273E /* RCTImageLoaderHelpers.m */; }; + AFEACA842223EB05004E5198 /* CrashyCrash.m in Sources */ = {isa = PBXBuildFile; fileRef = AFEACA832223EB05004E5198 /* CrashyCrash.m */; }; BC9C03401DC9F1D600B1C635 /* RCTDevMenuTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BC9C033F1DC9F1D600B1C635 /* RCTDevMenuTests.m */; }; C60A228221C9726800B820FE /* RCTFormatErrorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C60A228121C9726800B820FE /* RCTFormatErrorTests.m */; }; C60A228321C9726800B820FE /* RCTFormatErrorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C60A228121C9726800B820FE /* RCTFormatErrorTests.m */; }; @@ -563,6 +564,8 @@ 8385CEF41B873B5C00C6273E /* RCTImageLoaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoaderTests.m; sourceTree = ""; }; 8385CF031B87479200C6273E /* RCTImageLoaderHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoaderHelpers.m; sourceTree = ""; }; 8385CF051B8747A000C6273E /* RCTImageLoaderHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTImageLoaderHelpers.h; sourceTree = ""; }; + AFEACA822223EB05004E5198 /* CrashyCrash.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CrashyCrash.h; sourceTree = ""; }; + AFEACA832223EB05004E5198 /* CrashyCrash.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CrashyCrash.m; sourceTree = ""; }; BC9C033F1DC9F1D600B1C635 /* RCTDevMenuTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDevMenuTests.m; sourceTree = ""; }; C60A228121C9726800B820FE /* RCTFormatErrorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTFormatErrorTests.m; sourceTree = ""; }; C654F0B21EB34A73000B7A9A /* RNTesterTestModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNTesterTestModule.m; sourceTree = ""; }; @@ -774,6 +777,7 @@ 272E6B3A1BEA846C001FCF37 /* NativeExampleViews */, 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 13B07FB01A68108700A75B9A /* AppDelegate.m */, + AFEACAB12223EB2C004E5198 /* NativeExampleModules */, 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, 13B07FB71A68108700A75B9A /* main.m */, 1323F18D1C04ABAC0091BED0 /* Supporting Files */, @@ -1021,6 +1025,16 @@ name = Products; sourceTree = ""; }; + AFEACAB12223EB2C004E5198 /* NativeExampleModules */ = { + isa = PBXGroup; + children = ( + AFEACA822223EB05004E5198 /* CrashyCrash.h */, + AFEACA832223EB05004E5198 /* CrashyCrash.m */, + ); + name = NativeExampleModules; + path = RNTester/NativeExampleModules; + sourceTree = ""; + }; D85B82921AB6D5CE003F4FE2 /* Products */ = { isa = PBXGroup; children = ( @@ -1724,6 +1738,7 @@ 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, 27F441EC1BEBE5030039B79C /* FlexibleSizeExampleView.m in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, + AFEACA842223EB05004E5198 /* CrashyCrash.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/RNTester/RNTester/NativeExampleModules/CrashyCrash.h b/RNTester/RNTester/NativeExampleModules/CrashyCrash.h new file mode 100644 index 00000000000000..b6e0ea77f441dc --- /dev/null +++ b/RNTester/RNTester/NativeExampleModules/CrashyCrash.h @@ -0,0 +1,18 @@ +// +// CrashyCrash.h +// RNTester +// +// Created by Pavlos Vinieratos on 25/02/2019. +// Copyright © 2019 Facebook. All rights reserved. +// + +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +@interface CrashyCrash : NSObject +@end + +NS_ASSUME_NONNULL_END diff --git a/RNTester/RNTester/NativeExampleModules/CrashyCrash.m b/RNTester/RNTester/NativeExampleModules/CrashyCrash.m new file mode 100644 index 00000000000000..cd26ef01ad0a4f --- /dev/null +++ b/RNTester/RNTester/NativeExampleModules/CrashyCrash.m @@ -0,0 +1,23 @@ +// +// CrashyCrash.m +// RNTester +// +// Created by Pavlos Vinieratos on 25/02/2019. +// Copyright © 2019 Facebook. All rights reserved. +// + +#import "CrashyCrash.h" + + +@implementation CrashyCrash + +RCT_EXPORT_MODULE(); + +RCT_EXPORT_METHOD(letsCrash) +{ + NSArray *a = @[@"wow"]; + NSString *s = [a objectAtIndex:42]; // native crash here + NSLog(@"%@", s); +} + +@end diff --git a/RNTester/js/CrashExample.js b/RNTester/js/CrashExample.js new file mode 100644 index 00000000000000..ff2a254ca7f466 --- /dev/null +++ b/RNTester/js/CrashExample.js @@ -0,0 +1,61 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +'use strict'; + +import React from 'react'; +import {NativeModules, Button, StyleSheet} from 'react-native'; + +const { CrashyCrash } = NativeModules + + +exports.displayName = (undefined: ?string); +exports.framework = 'React'; +exports.title = 'Crash'; +exports.description = 'Crash examples.'; + +exports.examples = [ + { + title: 'JS crash', + render() { + return ( +