From 61be5e385b0550389063665742779daa9d10ef42 Mon Sep 17 00:00:00 2001 From: legendecas Date: Sun, 21 Aug 2022 10:42:40 +0800 Subject: [PATCH] src: expose environment RequestInterrupt api Allow add-ons to interrupt JavaScript execution, and wake up loop if it is currently idle. --- src/api/hooks.cc | 12 ++++ src/node.h | 8 +++ test/addons/request-interrupt/binding.cc | 69 +++++++++++++++++++++++ test/addons/request-interrupt/binding.gyp | 9 +++ test/addons/request-interrupt/test.js | 51 +++++++++++++++++ 5 files changed, 149 insertions(+) create mode 100644 test/addons/request-interrupt/binding.cc create mode 100644 test/addons/request-interrupt/binding.gyp create mode 100644 test/addons/request-interrupt/test.js diff --git a/src/api/hooks.cc b/src/api/hooks.cc index 9e54436ba30fb6..7de02d41c79c84 100644 --- a/src/api/hooks.cc +++ b/src/api/hooks.cc @@ -166,6 +166,18 @@ 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..ac91846afc0a54 100644 --- a/src/node.h +++ b/src/node.h @@ -1088,6 +1088,14 @@ 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. 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..43825d3b161676 --- /dev/null +++ b/test/addons/request-interrupt/binding.cc @@ -0,0 +1,69 @@ +#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); + if (!result.IsNothing() && result.ToChecked()) { + exit(2); + } + 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..08f739ad2592e0 --- /dev/null +++ b/test/addons/request-interrupt/test.js @@ -0,0 +1,51 @@ +'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', { + 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`); + } + } +}