Skip to content

Commit 4331bbe

Browse files
legendecasRafaelGSS
authored andcommitted
src: expose environment RequestInterrupt api
Allow add-ons to interrupt JavaScript execution, and wake up loop if it is currently idle. PR-URL: #44362 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de>
1 parent c5630ad commit 4331bbe

File tree

6 files changed

+181
-0
lines changed

6 files changed

+181
-0
lines changed

src/api/hooks.cc

+10
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,16 @@ void RemoveEnvironmentCleanupHookInternal(
166166
handle->info->env->RemoveCleanupHook(RunAsyncCleanupHook, handle->info.get());
167167
}
168168

169+
void RequestInterrupt(Environment* env, void (*fun)(void* arg), void* arg) {
170+
env->RequestInterrupt([fun, arg](Environment* env) {
171+
// Disallow JavaScript execution during interrupt.
172+
Isolate::DisallowJavascriptExecutionScope scope(
173+
env->isolate(),
174+
Isolate::DisallowJavascriptExecutionScope::CRASH_ON_FAILURE);
175+
fun(arg);
176+
});
177+
}
178+
169179
async_id AsyncHooksGetExecutionAsyncId(Isolate* isolate) {
170180
Environment* env = Environment::GetCurrent(isolate);
171181
if (env == nullptr) return -1;

src/node.h

+9
Original file line numberDiff line numberDiff line change
@@ -1030,6 +1030,15 @@ inline void RemoveEnvironmentCleanupHook(AsyncCleanupHookHandle holder) {
10301030
RemoveEnvironmentCleanupHookInternal(holder.get());
10311031
}
10321032

1033+
// This behaves like V8's Isolate::RequestInterrupt(), but also wakes up
1034+
// the event loop if it is currently idle. Interrupt requests are drained
1035+
// in `FreeEnvironment()`. The passed callback can not call back into
1036+
// JavaScript.
1037+
// This function can be called from any thread.
1038+
NODE_EXTERN void RequestInterrupt(Environment* env,
1039+
void (*fun)(void* arg),
1040+
void* arg);
1041+
10331042
/* Returns the id of the current execution context. If the return value is
10341043
* zero then no execution has been set. This will happen if the user handles
10351044
* I/O from native code. */
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#include <node.h>
2+
#include <v8.h>
3+
#include <thread> // NOLINT(build/c++11)
4+
5+
using node::Environment;
6+
using v8::Context;
7+
using v8::FunctionCallbackInfo;
8+
using v8::HandleScope;
9+
using v8::Isolate;
10+
using v8::Local;
11+
using v8::Maybe;
12+
using v8::Object;
13+
using v8::String;
14+
using v8::Value;
15+
16+
static std::thread interrupt_thread;
17+
18+
void ScheduleInterrupt(const FunctionCallbackInfo<Value>& args) {
19+
Isolate* isolate = args.GetIsolate();
20+
HandleScope handle_scope(isolate);
21+
Environment* env = node::GetCurrentEnvironment(isolate->GetCurrentContext());
22+
23+
interrupt_thread = std::thread([=]() {
24+
std::this_thread::sleep_for(std::chrono::seconds(1));
25+
node::RequestInterrupt(
26+
env,
27+
[](void* data) {
28+
// Interrupt is called from JS thread.
29+
interrupt_thread.join();
30+
exit(0);
31+
},
32+
nullptr);
33+
});
34+
}
35+
36+
void ScheduleInterruptWithJS(const FunctionCallbackInfo<Value>& args) {
37+
Isolate* isolate = args.GetIsolate();
38+
HandleScope handle_scope(isolate);
39+
Environment* env = node::GetCurrentEnvironment(isolate->GetCurrentContext());
40+
41+
interrupt_thread = std::thread([=]() {
42+
std::this_thread::sleep_for(std::chrono::seconds(1));
43+
node::RequestInterrupt(
44+
env,
45+
[](void* data) {
46+
// Interrupt is called from JS thread.
47+
interrupt_thread.join();
48+
Isolate* isolate = static_cast<Isolate*>(data);
49+
HandleScope handle_scope(isolate);
50+
Local<Context> ctx = isolate->GetCurrentContext();
51+
Local<String> str =
52+
String::NewFromUtf8(isolate, "interrupt").ToLocalChecked();
53+
// Calling into JS should abort immediately.
54+
Maybe<bool> result = ctx->Global()->Set(ctx, str, str);
55+
// Should not reach here.
56+
if (!result.IsNothing()) {
57+
// Called into JavaScript.
58+
exit(2);
59+
}
60+
// Maybe exception thrown.
61+
exit(1);
62+
},
63+
isolate);
64+
});
65+
}
66+
67+
void init(Local<Object> exports) {
68+
NODE_SET_METHOD(exports, "scheduleInterrupt", ScheduleInterrupt);
69+
NODE_SET_METHOD(exports, "ScheduleInterruptWithJS", ScheduleInterruptWithJS);
70+
}
71+
72+
NODE_MODULE(NODE_GYP_MODULE_NAME, init)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
'targets': [
3+
{
4+
'target_name': 'binding',
5+
'sources': [ 'binding.cc' ],
6+
'includes': ['../common.gypi'],
7+
}
8+
]
9+
}

test/addons/request-interrupt/test.js

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict';
2+
3+
const common = require('../../common');
4+
const assert = require('assert');
5+
const path = require('path');
6+
const spawnSync = require('child_process').spawnSync;
7+
8+
const binding = path.resolve(__dirname, `./build/${common.buildType}/binding`);
9+
10+
Object.defineProperty(globalThis, 'interrupt', {
11+
get: () => {
12+
return null;
13+
},
14+
set: () => {
15+
throw new Error('should not calling into js');
16+
},
17+
});
18+
19+
if (process.argv[2] === 'child-busyloop') {
20+
(function childMain() {
21+
const addon = require(binding);
22+
addon[process.argv[3]]();
23+
while (true) {
24+
/** wait for interrupt */
25+
}
26+
})();
27+
return;
28+
}
29+
30+
if (process.argv[2] === 'child-idle') {
31+
(function childMain() {
32+
const addon = require(binding);
33+
addon[process.argv[3]]();
34+
// wait for interrupt
35+
setTimeout(() => {}, 10_000_000);
36+
})();
37+
return;
38+
}
39+
40+
for (const type of ['busyloop', 'idle']) {
41+
{
42+
const child = spawnSync(process.execPath, [ __filename, `child-${type}`, 'scheduleInterrupt' ]);
43+
assert.strictEqual(child.status, 0, `${type} should exit with code 0`);
44+
}
45+
46+
{
47+
const child = spawnSync(process.execPath, [ __filename, `child-${type}`, 'ScheduleInterruptWithJS' ]);
48+
assert(common.nodeProcessAborted(child.status, child.signal));
49+
}
50+
}

test/cctest/test_environment.cc

+31
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
using node::AtExit;
1313
using node::RunAtExit;
1414
using node::USE;
15+
using v8::Context;
16+
using v8::Local;
1517

1618
static bool called_cb_1 = false;
1719
static bool called_cb_2 = false;
@@ -716,3 +718,32 @@ TEST_F(EnvironmentTest, NestedMicrotaskQueue) {
716718
node::FreeEnvironment(env);
717719
node::FreeIsolateData(isolate_data);
718720
}
721+
722+
static bool interrupted = false;
723+
static void OnInterrupt(void* arg) {
724+
interrupted = true;
725+
}
726+
TEST_F(EnvironmentTest, RequestInterruptAtExit) {
727+
const v8::HandleScope handle_scope(isolate_);
728+
const Argv argv;
729+
730+
Local<Context> context = node::NewContext(isolate_);
731+
CHECK(!context.IsEmpty());
732+
context->Enter();
733+
734+
node::IsolateData* isolate_data = node::CreateIsolateData(
735+
isolate_, &NodeTestFixture::current_loop, platform.get());
736+
CHECK_NE(nullptr, isolate_data);
737+
std::vector<std::string> args(*argv, *argv + 1);
738+
std::vector<std::string> exec_args(*argv, *argv + 1);
739+
node::Environment* environment =
740+
node::CreateEnvironment(isolate_data, context, args, exec_args);
741+
CHECK_NE(nullptr, environment);
742+
743+
node::RequestInterrupt(environment, OnInterrupt, nullptr);
744+
node::FreeEnvironment(environment);
745+
EXPECT_TRUE(interrupted);
746+
747+
node::FreeIsolateData(isolate_data);
748+
context->Exit();
749+
}

0 commit comments

Comments
 (0)