Skip to content

Commit

Permalink
src: expose environment RequestInterrupt api
Browse files Browse the repository at this point in the history
Allow add-ons to interrupt JavaScript execution, and wake up loop if it
is currently idle.

PR-URL: #44362
Reviewed-By: Anna Henningsen <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Gerhard Stöbich <[email protected]>
  • Loading branch information
legendecas authored and juanarbol committed Oct 11, 2022
1 parent 7100bae commit 2ba547a
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 0 deletions.
10 changes: 10 additions & 0 deletions src/api/hooks.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions src/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,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. */
Expand Down
72 changes: 72 additions & 0 deletions test/addons/request-interrupt/binding.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#include <node.h>
#include <v8.h>
#include <thread> // 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<Value>& 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<Value>& 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<Isolate*>(data);
HandleScope handle_scope(isolate);
Local<Context> ctx = isolate->GetCurrentContext();
Local<String> str =
String::NewFromUtf8(isolate, "interrupt").ToLocalChecked();
// Calling into JS should abort immediately.
Maybe<bool> 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<Object> exports) {
NODE_SET_METHOD(exports, "scheduleInterrupt", ScheduleInterrupt);
NODE_SET_METHOD(exports, "ScheduleInterruptWithJS", ScheduleInterruptWithJS);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, init)
9 changes: 9 additions & 0 deletions test/addons/request-interrupt/binding.gyp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.cc' ],
'includes': ['../common.gypi'],
}
]
}
50 changes: 50 additions & 0 deletions test/addons/request-interrupt/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'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' ]);
assert(common.nodeProcessAborted(child.status, child.signal));
}
}
31 changes: 31 additions & 0 deletions test/cctest/test_environment.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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> 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<std::string> args(*argv, *argv + 1);
std::vector<std::string> 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();
}

0 comments on commit 2ba547a

Please sign in to comment.