jiuyiUniapp/service/node_modules/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp

422 lines
13 KiB
C++

/*
* 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.
*/
#include "JsErrorHandler.h"
#include <cxxreact/ErrorUtils.h>
#include <glog/logging.h>
#include <react/bridging/Bridging.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <string>
#include "StackTraceParser.h"
using namespace facebook;
namespace {
std::string quote(const std::string& view) {
return "\"" + view + "\"";
}
int nextExceptionId() {
static int exceptionId = 0;
return exceptionId++;
}
bool isLooselyNull(const jsi::Value& value) {
return value.isNull() || value.isUndefined();
}
bool isEmptyString(jsi::Runtime& runtime, const jsi::Value& value) {
return jsi::Value::strictEquals(
runtime, value, jsi::String::createFromUtf8(runtime, ""));
}
std::string stringifyToCpp(jsi::Runtime& runtime, const jsi::Value& value) {
return value.toString(runtime).utf8(runtime);
}
bool isTruthy(jsi::Runtime& runtime, const jsi::Value& value) {
auto Boolean = runtime.global().getPropertyAsFunction(runtime, "Boolean");
return Boolean.call(runtime, value).getBool();
}
void objectAssign(
jsi::Runtime& runtime,
jsi::Object& target,
const jsi::Object& value) {
auto Object = runtime.global().getPropertyAsObject(runtime, "Object");
auto assign = Object.getPropertyAsFunction(runtime, "assign");
assign.callWithThis(runtime, Object, target, value);
}
jsi::Object wrapInErrorIfNecessary(
jsi::Runtime& runtime,
const jsi::Value& value) {
auto Error = runtime.global().getPropertyAsFunction(runtime, "Error");
auto isError =
value.isObject() && value.asObject(runtime).instanceOf(runtime, Error);
auto error = isError
? value.getObject(runtime)
: Error.callAsConstructor(runtime, value).getObject(runtime);
return error;
}
class SetFalseOnDestruct {
std::shared_ptr<bool> _value;
public:
SetFalseOnDestruct(const SetFalseOnDestruct&) = delete;
SetFalseOnDestruct& operator=(const SetFalseOnDestruct&) = delete;
SetFalseOnDestruct(SetFalseOnDestruct&&) = delete;
SetFalseOnDestruct& operator=(SetFalseOnDestruct&&) = delete;
explicit SetFalseOnDestruct(std::shared_ptr<bool> value)
: _value(std::move(value)) {}
~SetFalseOnDestruct() {
*_value = false;
}
};
void logErrorWhileReporting(
std::string message,
jsi::JSError& error,
jsi::JSError& originalError) {
LOG(ERROR) << "JsErrorHandler::" << message << std::endl
<< "Js error message: " << error.getMessage() << std::endl
<< "Original js error message: " << originalError.getMessage()
<< std::endl;
}
jsi::Value getBundleMetadata(jsi::Runtime& runtime, jsi::JSError& error) {
auto jsGetBundleMetadataValue =
runtime.global().getProperty(runtime, "__getBundleMetadata");
if (!jsGetBundleMetadataValue.isObject() ||
!jsGetBundleMetadataValue.asObject(runtime).isFunction(runtime)) {
return jsi::Value::null();
}
auto jsGetBundleMetadataValueFn =
jsGetBundleMetadataValue.asObject(runtime).asFunction(runtime);
try {
auto bundleMetadataValue = jsGetBundleMetadataValueFn.call(runtime);
if (bundleMetadataValue.isObject()) {
return bundleMetadataValue;
}
return bundleMetadataValue;
} catch (jsi::JSError& ex) {
logErrorWhileReporting(
"getBundleMetadata(): Error raised while calling __getBundleMetadata(). Returning null.",
ex,
error);
}
return jsi::Value::null();
}
} // namespace
namespace facebook::react {
template <>
struct Bridging<JsErrorHandler::ParsedError::StackFrame> {
static jsi::Value toJs(
jsi::Runtime& runtime,
const JsErrorHandler::ParsedError::StackFrame& frame) {
auto stackFrame = jsi::Object(runtime);
auto file = bridging::toJs(runtime, frame.file, nullptr);
auto lineNumber = bridging::toJs(runtime, frame.lineNumber, nullptr);
auto column = bridging::toJs(runtime, frame.column, nullptr);
stackFrame.setProperty(runtime, "file", file);
stackFrame.setProperty(runtime, "methodName", frame.methodName);
stackFrame.setProperty(runtime, "lineNumber", lineNumber);
stackFrame.setProperty(runtime, "column", column);
return stackFrame;
}
};
template <>
struct Bridging<JsErrorHandler::ParsedError> {
static jsi::Value toJs(
jsi::Runtime& runtime,
const JsErrorHandler::ParsedError& error) {
auto data = jsi::Object(runtime);
data.setProperty(runtime, "message", error.message);
data.setProperty(
runtime,
"originalMessage",
bridging::toJs(runtime, error.originalMessage, nullptr));
data.setProperty(
runtime, "name", bridging::toJs(runtime, error.name, nullptr));
data.setProperty(
runtime,
"componentStack",
bridging::toJs(runtime, error.componentStack, nullptr));
auto stack = jsi::Array(runtime, error.stack.size());
for (size_t i = 0; i < error.stack.size(); i++) {
auto& frame = error.stack[i];
stack.setValueAtIndex(runtime, i, bridging::toJs(runtime, frame));
}
data.setProperty(runtime, "stack", stack);
data.setProperty(runtime, "id", error.id);
data.setProperty(runtime, "isFatal", error.isFatal);
data.setProperty(runtime, "extraData", error.extraData);
return data;
}
};
std::ostream& operator<<(
std::ostream& os,
const JsErrorHandler::ParsedError::StackFrame& frame) {
auto file = frame.file ? quote(*frame.file) : "nil";
auto methodName = quote(frame.methodName);
auto lineNumber =
frame.lineNumber ? std::to_string(*frame.lineNumber) : "nil";
auto column = frame.column ? std::to_string(*frame.column) : "nil";
os << "StackFrame { .file = " << file << ", .methodName = " << methodName
<< ", .lineNumber = " << lineNumber << ", .column = " << column << " }";
return os;
}
std::ostream& operator<<(
std::ostream& os,
const JsErrorHandler::ParsedError& error) {
auto message = quote(error.message);
auto originalMessage =
error.originalMessage ? quote(*error.originalMessage) : "nil";
auto name = error.name ? quote(*error.name) : "nil";
auto componentStack =
error.componentStack ? quote(*error.componentStack) : "nil";
auto id = std::to_string(error.id);
auto isFatal = std::to_string(static_cast<int>(error.isFatal));
auto extraData = "jsi::Object{ <omitted> } ";
os << "ParsedError {\n"
<< " .message = " << message << "\n"
<< " .originalMessage = " << originalMessage << "\n"
<< " .name = " << name << "\n"
<< " .componentStack = " << componentStack << "\n"
<< " .stack = [\n";
for (const auto& frame : error.stack) {
os << " " << frame << ", \n";
}
os << " ]\n"
<< " .id = " << id << "\n"
<< " .isFatal " << isFatal << "\n"
<< " .extraData = " << extraData << "\n"
<< "}";
return os;
}
JsErrorHandler::JsErrorHandler(JsErrorHandler::OnJsError onJsError)
: _onJsError(std::move(onJsError)),
_inErrorHandler(std::make_shared<bool>(false)){
};
JsErrorHandler::~JsErrorHandler() {}
void JsErrorHandler::handleError(
jsi::Runtime& runtime,
jsi::JSError& error,
bool isFatal,
bool logToConsole) {
// TODO: Current error parsing works and is stable. Can investigate using
// REGEX_HERMES to get additional Hermes data, though it requires JS setup
if (!ReactNativeFeatureFlags::useAlwaysAvailableJSErrorHandling() &&
_isRuntimeReady) {
try {
handleJSError(runtime, error, isFatal);
return;
} catch (jsi::JSError& ex) {
logErrorWhileReporting(
"handleError(): Error raised while reporting using js pipeline. Using c++ pipeline instead.",
ex,
error);
// Re-try reporting using the c++ pipeline
_hasHandledFatalError = false;
}
}
handleErrorWithCppPipeline(runtime, error, isFatal, logToConsole);
}
void JsErrorHandler::handleErrorWithCppPipeline(
jsi::Runtime& runtime,
jsi::JSError& error,
bool isFatal,
bool logToConsole) {
*_inErrorHandler = true;
SetFalseOnDestruct temp{_inErrorHandler};
auto message = error.getMessage();
auto errorObj = wrapInErrorIfNecessary(runtime, error.value());
auto componentStackValue = errorObj.getProperty(runtime, "componentStack");
if (!isLooselyNull(componentStackValue)) {
message += "\n" + stringifyToCpp(runtime, componentStackValue);
}
auto nameValue = errorObj.getProperty(runtime, "name");
auto name = (isLooselyNull(nameValue) || isEmptyString(runtime, nameValue))
? std::nullopt
: std::optional(stringifyToCpp(runtime, nameValue));
if (name && !message.starts_with(*name + ": ")) {
message = *name + ": " + message;
}
auto jsEngineValue = errorObj.getProperty(runtime, "jsEngine");
if (!isLooselyNull(jsEngineValue)) {
message += ", js engine: " + stringifyToCpp(runtime, jsEngineValue);
}
auto extraDataKey = jsi::PropNameID::forUtf8(runtime, "RN$ErrorExtraDataKey");
auto extraDataValue = errorObj.getProperty(runtime, extraDataKey);
auto extraData = jsi::Object(runtime);
if (extraDataValue.isObject()) {
objectAssign(runtime, extraData, extraDataValue.asObject(runtime));
}
auto isDEV =
isTruthy(runtime, runtime.global().getProperty(runtime, "__DEV__"));
extraData.setProperty(runtime, "jsEngine", jsEngineValue);
extraData.setProperty(runtime, "rawStack", error.getStack());
extraData.setProperty(runtime, "__DEV__", isDEV);
extraData.setProperty(
runtime, "bundleMetadata", getBundleMetadata(runtime, error));
auto cause = errorObj.getProperty(runtime, "cause");
if (cause.isObject()) {
auto causeObj = cause.asObject(runtime);
// TODO: Consider just forwarding all properties. For now, just forward the
// stack properties to maintain symmetry with js pipeline
auto stackSymbols = causeObj.getProperty(runtime, "stackSymbols");
extraData.setProperty(runtime, "stackSymbols", stackSymbols);
auto stackReturnAddresses =
causeObj.getProperty(runtime, "stackReturnAddresses");
extraData.setProperty(
runtime, "stackReturnAddresses", stackReturnAddresses);
auto stackElements = causeObj.getProperty(runtime, "stackElements");
extraData.setProperty(runtime, "stackElements", stackElements);
}
auto originalMessage = message == error.getMessage()
? std::nullopt
: std::optional(error.getMessage());
auto componentStack = !componentStackValue.isString()
? std::nullopt
: std::optional(componentStackValue.asString(runtime).utf8(runtime));
auto isHermes = runtime.global().hasProperty(runtime, "HermesInternal");
auto stackFrames = StackTraceParser::parse(isHermes, error.getStack());
auto id = nextExceptionId();
ParsedError parsedError = {
.message =
_isRuntimeReady ? message : ("[runtime not ready]: " + message),
.originalMessage = originalMessage,
.name = name,
.componentStack = componentStack,
.stack = stackFrames,
.id = id,
.isFatal = isFatal,
.extraData = std::move(extraData),
};
auto data = bridging::toJs(runtime, parsedError).asObject(runtime);
auto isComponentError =
isTruthy(runtime, errorObj.getProperty(runtime, "isComponentError"));
data.setProperty(runtime, "isComponentError", isComponentError);
if (logToConsole) {
auto console = runtime.global().getPropertyAsObject(runtime, "console");
auto errorFn = console.getPropertyAsFunction(runtime, "error");
auto finalMessage =
jsi::String::createFromUtf8(runtime, parsedError.message);
errorFn.callWithThis(runtime, console, finalMessage);
}
std::shared_ptr<bool> shouldPreventDefault = std::make_shared<bool>(false);
auto preventDefault = jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "preventDefault"),
0,
[shouldPreventDefault](
jsi::Runtime& /*rt*/,
const jsi::Value& /*thisVal*/,
const jsi::Value* /*args*/,
size_t /*count*/) {
*shouldPreventDefault = true;
return jsi::Value::undefined();
});
data.setProperty(runtime, "preventDefault", preventDefault);
for (auto& errorListener : _errorListeners) {
try {
errorListener(runtime, jsi::Value(runtime, data));
} catch (jsi::JSError& ex) {
logErrorWhileReporting(
"handleErrorWithCppPipeline(): Error raised inside an error listener. Executing next listener.",
ex,
error);
}
}
if (*shouldPreventDefault) {
return;
}
if (isFatal) {
if (_hasHandledFatalError) {
return;
}
_hasHandledFatalError = true;
}
_onJsError(runtime, parsedError);
}
void JsErrorHandler::registerErrorListener(
const std::function<void(jsi::Runtime&, jsi::Value)>& errorListener) {
_errorListeners.push_back(errorListener);
}
bool JsErrorHandler::hasHandledFatalError() {
return _hasHandledFatalError;
}
void JsErrorHandler::setRuntimeReady() {
_isRuntimeReady = true;
}
bool JsErrorHandler::isRuntimeReady() {
return _isRuntimeReady;
}
void JsErrorHandler::notifyOfFatalError() {
_hasHandledFatalError = true;
}
bool JsErrorHandler::inErrorHandler() {
return *_inErrorHandler;
}
} // namespace facebook::react