From 27304fcd0bd9ff8b860f05ec8753bc0dc15c4d54 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Thu, 7 Oct 2021 15:21:34 -0700 Subject: [PATCH] Add error handling to RuntimeScheduler Summary: changelog: [internal] Catch JavaScript errors and forward them to `ErrorUtils` in *RuntimeScheduler*. This makes sure that JS errors are handled by ErrorUtils and do not bubble up to bridge. Reviewed By: philIip Differential Revision: D31429001 fbshipit-source-id: 50f865872e4cd3ba180056099ff40f5962ee7a77 --- .../renderer/runtimescheduler/ErrorUtils.h | 36 ++++++++++ .../runtimescheduler/RuntimeScheduler.cpp | 38 +++++----- .../tests/RuntimeSchedulerTest.cpp | 27 ++++++- .../runtimescheduler/tests/StubErrorUtils.h | 72 +++++++++++++++++++ 4 files changed, 156 insertions(+), 17 deletions(-) create mode 100644 ReactCommon/react/renderer/runtimescheduler/ErrorUtils.h create mode 100644 ReactCommon/react/renderer/runtimescheduler/tests/StubErrorUtils.h diff --git a/ReactCommon/react/renderer/runtimescheduler/ErrorUtils.h b/ReactCommon/react/renderer/runtimescheduler/ErrorUtils.h new file mode 100644 index 00000000000..a6da8b49b85 --- /dev/null +++ b/ReactCommon/react/renderer/runtimescheduler/ErrorUtils.h @@ -0,0 +1,36 @@ +/* + * 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. + */ + +#include + +namespace facebook { +namespace react { + +inline static void handleFatalError( + jsi::Runtime &runtime, + const jsi::JSError &error) { + auto reportFatalError = "reportFatalError"; + auto errorUtils = runtime.global().getProperty(runtime, "ErrorUtils"); + if (errorUtils.isUndefined() || !errorUtils.isObject() || + !errorUtils.getObject(runtime).hasProperty(runtime, reportFatalError)) { + // ErrorUtils was not set up. This probably means the bundle didn't + // load properly. + throw jsi::JSError( + runtime, + "ErrorUtils is not set up properly. Something probably went wrong trying to load the JS bundle. Trying to report error " + + error.getMessage(), + error.getStack()); + } + + auto func = errorUtils.asObject(runtime).getPropertyAsFunction( + runtime, reportFatalError); + + func.call(runtime, error.value()); +} + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp b/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp index fd47ab43033..ecf2589dddc 100644 --- a/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp +++ b/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp @@ -6,6 +6,7 @@ */ #include "RuntimeScheduler.h" +#include "ErrorUtils.h" namespace facebook { namespace react { @@ -89,27 +90,32 @@ void RuntimeScheduler::executeNowOnTheSameThread( void RuntimeScheduler::startWorkLoop(jsi::Runtime &runtime) const { auto previousPriority = currentPriority_; isPerformingWork_ = true; - while (!taskQueue_.empty()) { - auto topPriorityTask = taskQueue_.top(); - auto now = now_(); - auto didUserCallbackTimeout = topPriorityTask->expirationTime <= now; + try { + while (!taskQueue_.empty()) { + auto topPriorityTask = taskQueue_.top(); + auto now = now_(); + auto didUserCallbackTimeout = topPriorityTask->expirationTime <= now; - if (!didUserCallbackTimeout && shouldYield_) { - // This task hasn't expired and we need to yield. - break; - } - currentPriority_ = topPriorityTask->priority; - auto result = topPriorityTask->execute(runtime); + if (!didUserCallbackTimeout && shouldYield_) { + // This task hasn't expired and we need to yield. + break; + } + currentPriority_ = topPriorityTask->priority; + auto result = topPriorityTask->execute(runtime); - if (result.isObject() && result.getObject(runtime).isFunction(runtime)) { - topPriorityTask->callback = - result.getObject(runtime).getFunction(runtime); - } else { - if (taskQueue_.top() == topPriorityTask) { - taskQueue_.pop(); + if (result.isObject() && result.getObject(runtime).isFunction(runtime)) { + topPriorityTask->callback = + result.getObject(runtime).getFunction(runtime); + } else { + if (taskQueue_.top() == topPriorityTask) { + taskQueue_.pop(); + } } } + } catch (jsi::JSError &error) { + handleFatalError(runtime, error); } + currentPriority_ = previousPriority; isPerformingWork_ = false; } diff --git a/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp b/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp index abb6a96b482..3fade3594ee 100644 --- a/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp +++ b/ReactCommon/react/renderer/runtimescheduler/tests/RuntimeSchedulerTest.cpp @@ -10,7 +10,9 @@ #include #include #include + #include "StubClock.h" +#include "StubErrorUtils.h" #include "StubQueue.h" namespace facebook::react { @@ -22,6 +24,7 @@ class RuntimeSchedulerTest : public testing::Test { void SetUp() override { hostFunctionCallCount_ = 0; runtime_ = facebook::hermes::makeHermesRuntime(); + stubErrorUtils_ = StubErrorUtils::createAndInstallIfNeeded(*runtime_); stubQueue_ = std::make_unique(); RuntimeExecutor runtimeExecutor = @@ -52,7 +55,7 @@ class RuntimeSchedulerTest : public testing::Test { jsi::Runtime &, jsi::Value const &, jsi::Value const *arguments, - size_t) noexcept -> jsi::Value { + size_t) -> jsi::Value { ++hostFunctionCallCount_; auto didUserCallbackTimeout = arguments[0].getBool(); return callback(didUserCallbackTimeout); @@ -65,6 +68,7 @@ class RuntimeSchedulerTest : public testing::Test { std::unique_ptr stubClock_; std::unique_ptr stubQueue_; std::unique_ptr runtimeScheduler_; + std::shared_ptr stubErrorUtils_; }; TEST_F(RuntimeSchedulerTest, now) { @@ -469,4 +473,25 @@ TEST_F(RuntimeSchedulerTest, scheduleTaskFromTask) { EXPECT_EQ(stubQueue_->size(), 0); } +TEST_F(RuntimeSchedulerTest, handlingError) { + bool didRunTask = false; + auto firstCallback = createHostFunctionFromLambda([this, &didRunTask](bool) { + didRunTask = true; + jsi::detail::throwJSError(*runtime_, "Test error"); + return jsi::Value::undefined(); + }); + + runtimeScheduler_->scheduleTask( + SchedulerPriority::NormalPriority, std::move(firstCallback)); + + EXPECT_FALSE(didRunTask); + EXPECT_EQ(stubQueue_->size(), 1); + + stubQueue_->tick(); + + EXPECT_TRUE(didRunTask); + EXPECT_EQ(stubQueue_->size(), 0); + EXPECT_EQ(stubErrorUtils_->getReportFatalCallCount(), 1); +} + } // namespace facebook::react diff --git a/ReactCommon/react/renderer/runtimescheduler/tests/StubErrorUtils.h b/ReactCommon/react/renderer/runtimescheduler/tests/StubErrorUtils.h new file mode 100644 index 00000000000..ca2de84a0fe --- /dev/null +++ b/ReactCommon/react/renderer/runtimescheduler/tests/StubErrorUtils.h @@ -0,0 +1,72 @@ +/* + * 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. + */ + +#pragma once + +#include +#include + +namespace facebook { +namespace react { + +/* + * Exposes StubErrorUtils to JavaScript realm. + */ +class StubErrorUtils : public jsi::HostObject { + public: + static std::shared_ptr createAndInstallIfNeeded( + jsi::Runtime &runtime) { + auto errorUtilsModuleName = "ErrorUtils"; + auto errorUtilsValue = + runtime.global().getProperty(runtime, errorUtilsModuleName); + + if (errorUtilsValue.isUndefined()) { + auto stubErrorUtils = std::make_shared(); + auto object = jsi::Object::createFromHostObject(runtime, stubErrorUtils); + runtime.global().setProperty( + runtime, errorUtilsModuleName, std::move(object)); + return stubErrorUtils; + } + + auto stubErrorUtilsObject = errorUtilsValue.asObject(runtime); + return stubErrorUtilsObject.getHostObject(runtime); + } + + /* + * `jsi::HostObject` specific overloads. + */ + jsi::Value get(jsi::Runtime &runtime, jsi::PropNameID const &name) override { + auto propertyName = name.utf8(runtime); + + if (propertyName == "reportFatalError") { + return jsi::Function::createFromHostFunction( + runtime, + name, + 1, + [this]( + jsi::Runtime &runtime, + jsi::Value const &, + jsi::Value const *arguments, + size_t) noexcept -> jsi::Value { + reportFatalCallCount_++; + return jsi::Value::undefined(); + }); + } + + return jsi::Value::undefined(); + } + + int getReportFatalCallCount() const { + return reportFatalCallCount_; + } + + private: + int reportFatalCallCount_; +}; + +} // namespace react +} // namespace facebook