From ff398e4e2632ece8a6f22d744e96e489ca3b9d92 Mon Sep 17 00:00:00 2001 From: Paige Sun Date: Thu, 20 Oct 2022 21:12:41 -0700 Subject: [PATCH] 4/n Display a RedBox with the JS stack (instead of native stack) when an unhandled JS exceptions occurs Summary: Changelog: [iOS][Changed] Display a RedBox with the JS stack (instead of native stack) when an unhandled JS exceptions occurs ----- # A0) Bridge mode, Unhandled Exception, FBiOS Unhandled exception goes to FBReactModule, but the JS Stack is not parsed correctly. https://www.internalfb.com/code/fbsource/[312d5cbdd7278247a84619786b12a44c4400fcc0]/fbobjc/Apps/Wilde/FBReactModule2/FBReactModuleAPI/FBReactModuleAPI/Exported/FBReactModule.mm?lines=1488%2C1493 See `****** FBReactModule handleFatalError` in the logs P539306390, and compare with correct behavior in (A1) in the Test Plan. https://pxl.cl/2h6h3 {F782257996} ----- # A) Before diff, rn-tester Open to rn-tester -> APIs -> Crash Examples -> JS Crash. Set `RCTParseUnhandledJSErrorStackNatively` to YES. https://www.internalfb.com/code/fbsource/[98880e52ee78be3614e5d9a2ce3292f6a7b5e413]/xplat/js/react-native-github/React/Base/RCTConstants.m?lines=73 {F783395297} Reviewed By: sammy-SC Differential Revision: D40387938 fbshipit-source-id: 2abea657476d3bf61ad5b1c643f129e44c6f3f35 --- BUCK | 1 + React/CxxBridge/RCTCxxBridge.mm | 20 ++++++----- React/CxxModule/RCTCxxUtils.mm | 36 +++++++++++++++---- ReactCommon/React-Fabric.podspec | 2 -- ReactCommon/jserrorhandler/BUCK | 2 -- ReactCommon/jserrorhandler/JsErrorHandler.cpp | 8 +++-- ReactCommon/jserrorhandler/JsErrorHandler.h | 3 ++ .../React-jserrorhandler.podspec | 36 +++++++++++++++++++ packages/rn-tester/Podfile.lock | 9 ++++- scripts/react_native_pods.rb | 2 +- 10 files changed, 94 insertions(+), 25 deletions(-) create mode 100644 ReactCommon/jserrorhandler/React-jserrorhandler.podspec diff --git a/BUCK b/BUCK index 24538162ef..299c6347ab 100644 --- a/BUCK +++ b/BUCK @@ -200,6 +200,7 @@ rn_xplat_cxx_library2( visibility = ["PUBLIC"], deps = [ "//xplat/folly:dynamic", + react_native_xplat_target("jserrorhandler:jserrorhandler"), ], ) diff --git a/React/CxxBridge/RCTCxxBridge.mm b/React/CxxBridge/RCTCxxBridge.mm index 047a7c2c4e..b8fd6f4460 100644 --- a/React/CxxBridge/RCTCxxBridge.mm +++ b/React/CxxBridge/RCTCxxBridge.mm @@ -1137,10 +1137,7 @@ struct RCTInstanceCallback : public InstanceCallback { // In state 3: do nothing. if (self->_valid && !self->_loading) { - if ([error userInfo][RCTJSRawStackTraceKey]) { - [self.redBox showErrorMessage:[error localizedDescription] withRawStack:[error userInfo][RCTJSRawStackTraceKey]]; - } - + [self showJsError:error onRedBox:self.redBox]; RCTFatal(error); // RN will stop, but let the rest of the app keep going. @@ -1171,15 +1168,20 @@ struct RCTInstanceCallback : public InstanceCallback { [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidFailToLoadNotification object:self->_parentBridge userInfo:@{@"bridge" : self, @"error" : error}]; - - if ([error userInfo][RCTJSRawStackTraceKey]) { - [redBox showErrorMessage:[error localizedDescription] withRawStack:[error userInfo][RCTJSRawStackTraceKey]]; - } - + [self showJsError:error onRedBox:redBox]; RCTFatal(error); }); } +- (void)showJsError:(NSError *)error onRedBox:(RCTRedBox *)redbox +{ + if ([error userInfo][RCTJSStackTraceKey]) { + [redbox showErrorMessage:[error localizedDescription] withStack:[error userInfo][RCTJSStackTraceKey]]; + } else if ([error userInfo][RCTJSRawStackTraceKey]) { + [redbox showErrorMessage:[error localizedDescription] withRawStack:[error userInfo][RCTJSRawStackTraceKey]]; + } +} + RCT_NOT_IMPLEMENTED(-(instancetype)initWithDelegate : (__unused id)delegate bundleURL : (__unused NSURL *)bundleURL moduleProvider diff --git a/React/CxxModule/RCTCxxUtils.mm b/React/CxxModule/RCTCxxUtils.mm index 03f0ad0c83..6be964fabd 100644 --- a/React/CxxModule/RCTCxxUtils.mm +++ b/React/CxxModule/RCTCxxUtils.mm @@ -7,6 +7,9 @@ #import "RCTCxxUtils.h" +#include + +#import #import #import #import @@ -20,8 +23,6 @@ namespace facebook { namespace react { -using facebook::jsi::JSError; - std::vector> createNativeModules(NSArray *modules, RCTBridge *bridge, const std::shared_ptr &instance) { @@ -42,13 +43,34 @@ createNativeModules(NSArray *modules, RCTBridge *bridge, const static NSError *errorWithException(const std::exception &e) { - NSString *msg = @(e.what()); + NSString *msg; NSMutableDictionary *errorInfo = [NSMutableDictionary dictionary]; - const auto *jsError = dynamic_cast(&e); - if (jsError) { - errorInfo[RCTJSRawStackTraceKey] = @(jsError->getStack().c_str()); - msg = [@"Unhandled JS Exception: " stringByAppendingString:msg]; + const auto *jsError = dynamic_cast(&e); + if (jsError && RCTGetParseUnhandledJSErrorStackNatively()) { + MapBuffer errorMap = JsErrorHandler::parseErrorStack(*jsError, true, false); + + NSString *message = [NSString stringWithCString:errorMap.getString(JSErrorHandlerKey::kErrorMessage).c_str() + encoding:[NSString defaultCStringEncoding]]; + auto frames = errorMap.getMapBufferList(JSErrorHandlerKey::kAllStackFrames); + NSMutableArray *stack = [[NSMutableArray alloc] init]; + for (auto const &mapBuffer : frames) { + NSDictionary *frame = @{ + @"file" : [NSString stringWithCString:mapBuffer.getString(JSErrorHandlerKey::kFrameFileName).c_str() + encoding:[NSString defaultCStringEncoding]], + @"methodName" : [NSString stringWithCString:mapBuffer.getString(JSErrorHandlerKey::kFrameMethodName).c_str() + encoding:[NSString defaultCStringEncoding]], + @"lineNumber" : [NSNumber numberWithInt:mapBuffer.getInt(JSErrorHandlerKey::kFrameLineNumber)], + @"column" : [NSNumber numberWithInt:mapBuffer.getInt(JSErrorHandlerKey::kFrameColumnNumber)], + }; + [stack addObject:frame]; + } + + msg = [@"Unhandled JS Exception: " stringByAppendingString:message]; + errorInfo[RCTJSStackTraceKey] = stack; + errorInfo[RCTJSRawStackTraceKey] = @(e.what()); + } else { + msg = @(e.what()); } NSError *nestedError; diff --git a/ReactCommon/React-Fabric.podspec b/ReactCommon/React-Fabric.podspec index c28eee60d0..b9a13624a5 100644 --- a/ReactCommon/React-Fabric.podspec +++ b/ReactCommon/React-Fabric.podspec @@ -254,8 +254,6 @@ Pod::Spec.new do |s| end s.subspec "mapbuffer" do |ss| - ss.dependency folly_dep_name, folly_version - ss.compiler_flags = folly_compiler_flags ss.source_files = "react/renderer/mapbuffer/**/*.{m,mm,cpp,h}" ss.exclude_files = "react/renderer/mapbuffer/tests" ss.header_dir = "react/renderer/mapbuffer" diff --git a/ReactCommon/jserrorhandler/BUCK b/ReactCommon/jserrorhandler/BUCK index e7ef279bb1..e051503309 100644 --- a/ReactCommon/jserrorhandler/BUCK +++ b/ReactCommon/jserrorhandler/BUCK @@ -23,8 +23,6 @@ rn_xplat_cxx_library( "PUBLIC", ], deps = [ - "//xplat/folly:dynamic", - "//xplat/folly:json", "//xplat/jsi:jsi", react_native_xplat_target("react/renderer/mapbuffer:mapbuffer"), ], diff --git a/ReactCommon/jserrorhandler/JsErrorHandler.cpp b/ReactCommon/jserrorhandler/JsErrorHandler.cpp index 0902e68b2b..be74dc5da2 100644 --- a/ReactCommon/jserrorhandler/JsErrorHandler.cpp +++ b/ReactCommon/jserrorhandler/JsErrorHandler.cpp @@ -17,8 +17,10 @@ namespace react { using facebook::react::JSErrorHandlerKey; -static MapBuffer -parseErrorStack(const jsi::JSError &error, bool isFatal, bool isHermes) { +MapBuffer JsErrorHandler::parseErrorStack( + const jsi::JSError &error, + bool isFatal, + bool isHermes) { /** * This parses the different stack traces and puts them into one format * This borrows heavily from TraceKit (https://github.com/occ/TraceKit) @@ -99,7 +101,7 @@ JsErrorHandler::~JsErrorHandler() {} void JsErrorHandler::handleJsError(const jsi::JSError &error, bool isFatal) { // TODO: Current error parsing works and is stable. Can investigate using // REGEX_HERMES to get additional Hermes data, though it requires JS setup. - MapBuffer errorMap = parseErrorStack(error, isFatal, false); + MapBuffer errorMap = JsErrorHandler::parseErrorStack(error, isFatal, false); _jsErrorHandlingFunc(std::move(errorMap)); } diff --git a/ReactCommon/jserrorhandler/JsErrorHandler.h b/ReactCommon/jserrorhandler/JsErrorHandler.h index 90383e17ca..ca5af45425 100644 --- a/ReactCommon/jserrorhandler/JsErrorHandler.h +++ b/ReactCommon/jserrorhandler/JsErrorHandler.h @@ -26,6 +26,9 @@ class JsErrorHandler { public: using JsErrorHandlingFunc = std::function; + static MapBuffer + parseErrorStack(const jsi::JSError &error, bool isFatal, bool isHermes); + JsErrorHandler(JsErrorHandlingFunc jsErrorHandlingFunc); ~JsErrorHandler(); diff --git a/ReactCommon/jserrorhandler/React-jserrorhandler.podspec b/ReactCommon/jserrorhandler/React-jserrorhandler.podspec new file mode 100644 index 0000000000..cfb7594ee2 --- /dev/null +++ b/ReactCommon/jserrorhandler/React-jserrorhandler.podspec @@ -0,0 +1,36 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "..", "..", "package.json"))) +version = package['version'] + +source = { :git => 'https://github.com/facebook/react-native.git' } +if version == '1000.0.0' + # This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in. + source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1") +else + source[:tag] = "v#{version}" +end + +Pod::Spec.new do |s| + s.name = "React-jserrorhandler" + s.version = version + s.summary = "-" # TODO + s.homepage = "https://reactnative.dev/" + s.license = package["license"] + s.author = "Facebook, Inc. and its affiliates" + s.platforms = { :ios => "12.4", :tvos => "12.4" } + s.public_header_files = [ "JsErrorHandler.h" ] + s.source = source + s.source_files = "*.{cpp,h}" + s.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "" } + s.header_dir = "jserrorhandler" + + s.dependency "React-jsi", version + s.dependency "React-Fabric/mapbuffer", version + +end diff --git a/packages/rn-tester/Podfile.lock b/packages/rn-tester/Podfile.lock index fedd81b021..336f7d5170 100644 --- a/packages/rn-tester/Podfile.lock +++ b/packages/rn-tester/Podfile.lock @@ -620,6 +620,9 @@ PODS: - React-jsiexecutor (= 1000.0.0) - React-jsinspector (= 1000.0.0) - React-perflogger (= 1000.0.0) + - React-jserrorhandler (1000.0.0): + - React-Fabric/mapbuffer (= 1000.0.0) + - React-jsi (= 1000.0.0) - React-jsi (1000.0.0): - hermes-engine - React-jsidynamic (1000.0.0): @@ -796,6 +799,7 @@ DEPENDENCIES: - React-Fabric (from `../../ReactCommon`) - React-graphics (from `../../ReactCommon/react/renderer/graphics`) - React-hermes (from `../../ReactCommon/hermes`) + - React-jserrorhandler (from `../../ReactCommon/jserrorhandler`) - React-jsi (from `../../ReactCommon/jsi`) - React-jsidynamic (from `../../ReactCommon/jsi`) - React-jsiexecutor (from `../../ReactCommon/jsiexecutor`) @@ -879,6 +883,8 @@ EXTERNAL SOURCES: :path: "../../ReactCommon/react/renderer/graphics" React-hermes: :path: "../../ReactCommon/hermes" + React-jserrorhandler: + :path: "../../ReactCommon/jserrorhandler" React-jsi: :path: "../../ReactCommon/jsi" React-jsidynamic: @@ -958,9 +964,10 @@ SPEC CHECKSUMS: React-Core: 3965263aa4b4e1ebf7b4fdb50d2f49ce7bf28f63 React-CoreModules: 675170bccf156da3a3348e04e2036ce401b2010d React-cxxreact: 7276467c246302fedf598cc40d7003896ddb20ba - React-Fabric: 141459e61c825acf02d26ece099acbd9cbd87b99 + React-Fabric: e177589b59ae3ae3dd3340190adcde9cf01ebceb React-graphics: 5ccc9cc0d91794fd42bc1c693e9aea207554bbef React-hermes: 0a5145bae4207edf0def8e28fbcb6a8fd6e806c2 + React-jserrorhandler: f0e756378ad46f5f3448f097a736eb5249de262b React-jsi: c24dbcfdf7ea075138b73372387c7f17c0db56ef React-jsidynamic: 2b14ac1b6d3a1b7daa1e5a424b98de87da981698 React-jsiexecutor: 14e899380e3fe9ca74c4e19727540a03e7574721 diff --git a/scripts/react_native_pods.rb b/scripts/react_native_pods.rb index 73b44c12ba..326cb50ee4 100644 --- a/scripts/react_native_pods.rb +++ b/scripts/react_native_pods.rb @@ -101,10 +101,10 @@ def use_react_native! ( else setup_jsc!(:react_native_path => prefix, :fabric_enabled => fabric_enabled) end + pod 'React-jserrorhandler', :path => "#{prefix}/ReactCommon/jserrorhandler" pod 'React-jsidynamic', :path => "#{prefix}/ReactCommon/jsi" pod 'React-jsiexecutor', :path => "#{prefix}/ReactCommon/jsiexecutor" pod 'React-jsinspector', :path => "#{prefix}/ReactCommon/jsinspector" - pod 'React-callinvoker', :path => "#{prefix}/ReactCommon/callinvoker" pod 'React-runtimeexecutor', :path => "#{prefix}/ReactCommon/runtimeexecutor" pod 'React-perflogger', :path => "#{prefix}/ReactCommon/reactperflogger"