diff --git a/napi-inl.h b/napi-inl.h index ee1c37104..5da37d6f0 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -2373,6 +2373,11 @@ inline void Error::ThrowAsJavaScriptException() const { napi_status status = napi_throw(_env, Value()); + if (status == napi_pending_exception) { + // The environment could be terminating. + return; + } + #ifdef NAPI_CPP_EXCEPTIONS if (status != napi_ok) { throw Error::New(_env); diff --git a/test/error.cc b/test/error.cc index 832cad525..1be01cf53 100644 --- a/test/error.cc +++ b/test/error.cc @@ -1,9 +1,54 @@ +#include #include "napi.h" using namespace Napi; namespace { +std::promise promise_for_child_process_; +std::promise promise_for_worker_thread_; + +void ResetPromises(const CallbackInfo&) { + promise_for_child_process_ = std::promise(); + promise_for_worker_thread_ = std::promise(); +} + +void WaitForWorkerThread(const CallbackInfo&) { + std::future future = promise_for_worker_thread_.get_future(); + + std::future_status status = future.wait_for(std::chrono::seconds(5)); + + if (status != std::future_status::ready) { + Error::Fatal("WaitForWorkerThread", "status != std::future_status::ready"); + } +} + +void ReleaseAndWaitForChildProcess(const CallbackInfo& info, + const uint32_t index) { + if (info.Length() < index + 1) { + return; + } + + if (!info[index].As().Value()) { + return; + } + + promise_for_worker_thread_.set_value(); + + std::future future = promise_for_child_process_.get_future(); + + std::future_status status = future.wait_for(std::chrono::seconds(5)); + + if (status != std::future_status::ready) { + Error::Fatal("ReleaseAndWaitForChildProcess", + "status != std::future_status::ready"); + } +} + +void ReleaseWorkerThread(const CallbackInfo&) { + promise_for_child_process_.set_value(); +} + void DoNotCatch(const CallbackInfo& info) { Function thrower = info[0].As(); thrower({}); @@ -18,16 +63,22 @@ void ThrowApiError(const CallbackInfo& info) { void ThrowJSError(const CallbackInfo& info) { std::string message = info[0].As().Utf8Value(); + + ReleaseAndWaitForChildProcess(info, 1); throw Error::New(info.Env(), message); } void ThrowTypeError(const CallbackInfo& info) { std::string message = info[0].As().Utf8Value(); + + ReleaseAndWaitForChildProcess(info, 1); throw TypeError::New(info.Env(), message); } void ThrowRangeError(const CallbackInfo& info) { std::string message = info[0].As().Utf8Value(); + + ReleaseAndWaitForChildProcess(info, 1); throw RangeError::New(info.Env(), message); } @@ -83,16 +134,22 @@ void CatchAndRethrowErrorThatEscapesScope(const CallbackInfo& info) { void ThrowJSError(const CallbackInfo& info) { std::string message = info[0].As().Utf8Value(); + + ReleaseAndWaitForChildProcess(info, 1); Error::New(info.Env(), message).ThrowAsJavaScriptException(); } void ThrowTypeError(const CallbackInfo& info) { std::string message = info[0].As().Utf8Value(); + + ReleaseAndWaitForChildProcess(info, 1); TypeError::New(info.Env(), message).ThrowAsJavaScriptException(); } void ThrowRangeError(const CallbackInfo& info) { std::string message = info[0].As().Utf8Value(); + + ReleaseAndWaitForChildProcess(info, 1); RangeError::New(info.Env(), message).ThrowAsJavaScriptException(); } @@ -187,6 +244,8 @@ void ThrowDefaultError(const CallbackInfo& info) { Error::Fatal("ThrowDefaultError", "napi_get_named_property"); } + ReleaseAndWaitForChildProcess(info, 1); + // The macro creates a `Napi::Error` using the factory that takes only the // env, however, it heeds the exception mechanism to be used. NAPI_THROW_IF_FAILED_VOID(env, status); @@ -209,5 +268,8 @@ Object InitError(Env env) { Function::New(env, CatchAndRethrowErrorThatEscapesScope); exports["throwFatalError"] = Function::New(env, ThrowFatalError); exports["throwDefaultError"] = Function::New(env, ThrowDefaultError); + exports["resetPromises"] = Function::New(env, ResetPromises); + exports["waitForWorkerThread"] = Function::New(env, WaitForWorkerThread); + exports["releaseWorkerThread"] = Function::New(env, ReleaseWorkerThread); return exports; } diff --git a/test/error_terminating_environment.js b/test/error_terminating_environment.js new file mode 100644 index 000000000..10c417fb0 --- /dev/null +++ b/test/error_terminating_environment.js @@ -0,0 +1,88 @@ +'use strict'; +const buildType = process.config.target_defaults.default_configuration; +const assert = require('assert'); + +// These tests ensure that Error types can be used in a terminating +// environment without triggering any fatal errors. + +if (process.argv[2] === 'runInChildProcess') { + const binding_path = process.argv[3]; + const index_for_test_case = Number(process.argv[4]); + + const binding = require(binding_path); + + // Use C++ promises to ensure the worker thread is terminated right + // before running the testable code in the binding. + + binding.error.resetPromises() + + const { Worker } = require('worker_threads'); + + const worker = new Worker( + __filename, + { + argv: [ + 'runInWorkerThread', + binding_path, + index_for_test_case, + ] + } + ); + + binding.error.waitForWorkerThread() + + worker.terminate(); + + binding.error.releaseWorkerThread() + + return; +} + +if (process.argv[2] === 'runInWorkerThread') { + const binding_path = process.argv[3]; + const index_for_test_case = Number(process.argv[4]); + + const binding = require(binding_path); + + switch (index_for_test_case) { + case 0: + binding.error.throwJSError('test', true); + break; + case 1: + binding.error.throwTypeError('test', true); + break; + case 2: + binding.error.throwRangeError('test', true); + break; + case 3: + binding.error.throwDefaultError(false, true); + break; + case 4: + binding.error.throwDefaultError(true, true); + break; + default: assert.fail('Invalid index'); + } + + assert.fail('This should not be reachable'); +} + +test(`./build/${buildType}/binding.node`); +test(`./build/${buildType}/binding_noexcept.node`); + +function test(bindingPath) { + const number_of_test_cases = 5; + + for (let i = 0; i < number_of_test_cases; ++i) { + const child_process = require('./napi_child').spawnSync( + process.execPath, + [ + __filename, + 'runInChildProcess', + bindingPath, + i, + ] + ); + + assert(child_process.status === 0, `Test case ${i} failed`); + } +} diff --git a/test/index.js b/test/index.js index 2d963ca10..13c89f0a9 100644 --- a/test/index.js +++ b/test/index.js @@ -106,6 +106,7 @@ if (napiVersion < 6) { if (majorNodeVersion < 12) { testModules.splice(testModules.indexOf('objectwrap_worker_thread'), 1); + testModules.splice(testModules.indexOf('error_terminating_environment'), 1); } (async function() {