diff --git a/src/api/hooks.cc b/src/api/hooks.cc index 9e54436ba30fb6..bf4176cc7881c4 100644 --- a/src/api/hooks.cc +++ b/src/api/hooks.cc @@ -166,6 +166,16 @@ void RemoveEnvironmentCleanupHookInternal( handle->info->env->RemoveCleanupHook(RunAsyncCleanupHook, handle->info.get()); } +void RequestInterrupt(Environment* env, void (*fun)(void* arg), void* arg) { + env->RequestInterrupt([fun, arg](Environment* env) { + // Disallow JavaScript execution during interrupt. + Isolate::DisallowJavascriptExecutionScope scope( + env->isolate(), + Isolate::DisallowJavascriptExecutionScope::CRASH_ON_FAILURE); + fun(arg); + }); +} + async_id AsyncHooksGetExecutionAsyncId(Isolate* isolate) { Environment* env = Environment::GetCurrent(isolate); if (env == nullptr) return -1; diff --git a/src/node.h b/src/node.h index f8afff5d357639..a5b370372cb023 100644 --- a/src/node.h +++ b/src/node.h @@ -1088,6 +1088,15 @@ inline void RemoveEnvironmentCleanupHook(AsyncCleanupHookHandle holder) { RemoveEnvironmentCleanupHookInternal(holder.get()); } +// This behaves like V8's Isolate::RequestInterrupt(), but also wakes up +// the event loop if it is currently idle. Interrupt requests are drained +// in `FreeEnvironment()`. The passed callback can not call back into +// JavaScript. +// This function can be called from any thread. +NODE_EXTERN void RequestInterrupt(Environment* env, + void (*fun)(void* arg), + void* arg); + /* Returns the id of the current execution context. If the return value is * zero then no execution has been set. This will happen if the user handles * I/O from native code. */ diff --git a/test/addons/request-interrupt/binding.cc b/test/addons/request-interrupt/binding.cc new file mode 100644 index 00000000000000..ab4e0681608bb5 --- /dev/null +++ b/test/addons/request-interrupt/binding.cc @@ -0,0 +1,72 @@ +#include +#include +#include // NOLINT(build/c++11) + +using node::Environment; +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::HandleScope; +using v8::Isolate; +using v8::Local; +using v8::Maybe; +using v8::Object; +using v8::String; +using v8::Value; + +static std::thread interrupt_thread; + +void ScheduleInterrupt(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + HandleScope handle_scope(isolate); + Environment* env = node::GetCurrentEnvironment(isolate->GetCurrentContext()); + + interrupt_thread = std::thread([=]() { + std::this_thread::sleep_for(std::chrono::seconds(1)); + node::RequestInterrupt( + env, + [](void* data) { + // Interrupt is called from JS thread. + interrupt_thread.join(); + exit(0); + }, + nullptr); + }); +} + +void ScheduleInterruptWithJS(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + HandleScope handle_scope(isolate); + Environment* env = node::GetCurrentEnvironment(isolate->GetCurrentContext()); + + interrupt_thread = std::thread([=]() { + std::this_thread::sleep_for(std::chrono::seconds(1)); + node::RequestInterrupt( + env, + [](void* data) { + // Interrupt is called from JS thread. + interrupt_thread.join(); + Isolate* isolate = static_cast(data); + HandleScope handle_scope(isolate); + Local ctx = isolate->GetCurrentContext(); + Local str = + String::NewFromUtf8(isolate, "interrupt").ToLocalChecked(); + // Calling into JS should abort immediately. + Maybe result = ctx->Global()->Set(ctx, str, str); + // Should not reach here. + if (!result.IsNothing()) { + // Called into JavaScript. + exit(2); + } + // Maybe exception thrown. + exit(1); + }, + isolate); + }); +} + +void init(Local exports) { + NODE_SET_METHOD(exports, "scheduleInterrupt", ScheduleInterrupt); + NODE_SET_METHOD(exports, "ScheduleInterruptWithJS", ScheduleInterruptWithJS); +} + +NODE_MODULE(NODE_GYP_MODULE_NAME, init) diff --git a/test/addons/request-interrupt/binding.gyp b/test/addons/request-interrupt/binding.gyp new file mode 100644 index 00000000000000..55fbe7050f18e4 --- /dev/null +++ b/test/addons/request-interrupt/binding.gyp @@ -0,0 +1,9 @@ +{ + 'targets': [ + { + 'target_name': 'binding', + 'sources': [ 'binding.cc' ], + 'includes': ['../common.gypi'], + } + ] +} diff --git a/test/addons/request-interrupt/test.js b/test/addons/request-interrupt/test.js new file mode 100644 index 00000000000000..75a09b42b7ab36 --- /dev/null +++ b/test/addons/request-interrupt/test.js @@ -0,0 +1,54 @@ +'use strict'; + +const common = require('../../common'); +const assert = require('assert'); +const path = require('path'); +const spawnSync = require('child_process').spawnSync; + +const binding = path.resolve(__dirname, `./build/${common.buildType}/binding`); + +Object.defineProperty(globalThis, 'interrupt', { + get: () => { + return null; + }, + set: () => { + throw new Error('should not calling into js'); + }, +}); + +if (process.argv[2] === 'child-busyloop') { + (function childMain() { + const addon = require(binding); + addon[process.argv[3]](); + while (true) { + /** wait for interrupt */ + } + })(); + return; +} + +if (process.argv[2] === 'child-idle') { + (function childMain() { + const addon = require(binding); + addon[process.argv[3]](); + // wait for interrupt + setTimeout(() => {}, 10_000_000); + })(); + return; +} + +for (const type of ['busyloop', 'idle']) { + { + const child = spawnSync(process.execPath, [ __filename, `child-${type}`, 'scheduleInterrupt' ]); + assert.strictEqual(child.status, 0, `${type} should exit with code 0`); + } + + { + const child = spawnSync(process.execPath, [ __filename, `child-${type}`, 'ScheduleInterruptWithJS' ]); + if (process.platform === 'win32') { + assert.notStrictEqual(child.status, 0, `${type} should not exit with code 0`); + } else { + assert.strictEqual(child.signal, 'SIGTRAP', `${type} should be interrupted with SIGTRAP`); + } + } +} diff --git a/test/cctest/test_environment.cc b/test/cctest/test_environment.cc index f98b22db42ce1a..812962cd5c1a71 100644 --- a/test/cctest/test_environment.cc +++ b/test/cctest/test_environment.cc @@ -12,6 +12,8 @@ using node::AtExit; using node::RunAtExit; using node::USE; +using v8::Context; +using v8::Local; static bool called_cb_1 = false; static bool called_cb_2 = false; @@ -716,3 +718,32 @@ TEST_F(EnvironmentTest, NestedMicrotaskQueue) { node::FreeEnvironment(env); node::FreeIsolateData(isolate_data); } + +static bool interrupted = false; +static void OnInterrupt(void* arg) { + interrupted = true; +} +TEST_F(EnvironmentTest, RequestInterruptAtExit) { + const v8::HandleScope handle_scope(isolate_); + const Argv argv; + + Local context = node::NewContext(isolate_); + CHECK(!context.IsEmpty()); + context->Enter(); + + node::IsolateData* isolate_data = node::CreateIsolateData( + isolate_, &NodeTestFixture::current_loop, platform.get()); + CHECK_NE(nullptr, isolate_data); + std::vector args(*argv, *argv + 1); + std::vector exec_args(*argv, *argv + 1); + node::Environment* environment = + node::CreateEnvironment(isolate_data, context, args, exec_args); + CHECK_NE(nullptr, environment); + + node::RequestInterrupt(environment, OnInterrupt, nullptr); + node::FreeEnvironment(environment); + EXPECT_TRUE(interrupted); + + node::FreeIsolateData(isolate_data); + context->Exit(); +}