Skip to content

Commit 8438f3b

Browse files
authored
process,worker: ensure code after exit() effectless
Cope with the delay(to the next function call) of v8::Isolate::TerminateExecution() PR-URL: #45620 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Minwoo Jung <nodecorelab@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent d5a08c7 commit 8438f3b

10 files changed

+76
-5
lines changed

lib/internal/process/per_thread.js

+12
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ function hrtimeBigInt() {
100100
return hrBigintValues[0];
101101
}
102102

103+
function nop() {}
104+
103105
// The execution of this function itself should not cause any side effects.
104106
function wrapProcessMethods(binding) {
105107
const {
@@ -194,6 +196,16 @@ function wrapProcessMethods(binding) {
194196
// in the user land. Either document it, or deprecate it in favor of a
195197
// better public alternative.
196198
process.reallyExit(process.exitCode || kNoFailure);
199+
200+
// If this is a worker, v8::Isolate::TerminateExecution() is called above.
201+
// That function spoofs the stack pointer to cause the stack guard
202+
// check to throw the termination exception. Because v8 performs
203+
// stack guard check upon every function call, we give it a chance.
204+
//
205+
// Without this, user code after `process.exit()` would take effect.
206+
// test/parallel/test-worker-voluntarily-exit-followed-by-addition.js
207+
// test/parallel/test-worker-voluntarily-exit-followed-by-throw.js
208+
nop();
197209
}
198210

199211
function kill(pid, sig) {

src/js_native_api_v8.cc

+3
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,9 @@ class CallbackWrapperBase : public CallbackWrapper {
314314
env->CallIntoModule([&](napi_env env) { result = cb(env, cbinfo_wrapper); },
315315
[&](napi_env env, v8::Local<v8::Value> value) {
316316
exceptionOccurred = true;
317+
if (env->terminatedOrTerminating()) {
318+
return;
319+
}
317320
env->isolate->ThrowException(value);
318321
});
319322

src/js_native_api_v8.h

+11
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,20 @@ struct napi_env__ {
7272
}
7373

7474
static inline void HandleThrow(napi_env env, v8::Local<v8::Value> value) {
75+
if (env->terminatedOrTerminating()) {
76+
return;
77+
}
7578
env->isolate->ThrowException(value);
7679
}
7780

81+
// i.e. whether v8 exited or is about to exit
82+
inline bool terminatedOrTerminating() {
83+
return this->isolate->IsExecutionTerminating() || !can_call_into_js();
84+
}
85+
86+
// v8 uses a special exception to indicate termination, the
87+
// `handle_exception` callback should identify such case using
88+
// terminatedOrTerminating() before actually handle the exception
7889
template <typename T, typename U = decltype(HandleThrow)>
7990
inline void CallIntoModule(T&& call, U&& handle_exception = HandleThrow) {
8091
int open_handle_scopes_before = open_handle_scopes;

src/node_api.cc

+3
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ template <bool enforceUncaughtExceptionPolicy, typename T>
9595
void node_napi_env__::CallbackIntoModule(T&& call) {
9696
CallIntoModule(call, [](napi_env env_, v8::Local<v8::Value> local_err) {
9797
node_napi_env__* env = static_cast<node_napi_env__*>(env_);
98+
if (env->terminatedOrTerminating()) {
99+
return;
100+
}
98101
node::Environment* node_env = env->node_env();
99102
if (!node_env->options()->force_node_api_uncaught_exceptions_policy &&
100103
!enforceUncaughtExceptionPolicy) {

test/cctest/test_environment.cc

+3-1
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,9 @@ TEST_F(EnvironmentTest, ExitHandlerTest) {
553553
callback_calls++;
554554
node::Stop(*env);
555555
});
556-
node::LoadEnvironment(*env, "process.exit(42)").ToLocalChecked();
556+
// When terminating, v8 throws makes the current embedder call bail out
557+
// with MaybeLocal<>()
558+
EXPECT_TRUE(node::LoadEnvironment(*env, "process.exit(42)").IsEmpty());
557559
EXPECT_EQ(callback_calls, 1);
558560
}
559561

test/node-api/test_worker_terminate/test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ if (isMainThread) {
1919
const { Test } = require(`./build/${common.buildType}/test_worker_terminate`);
2020

2121
const { counter } = workerData;
22-
// Test() tries to call a function twice and asserts that the second call does
23-
// not work because of a pending exception.
22+
// Test() tries to call a function and asserts it fails because of a
23+
// pending termination exception.
2424
Test(() => {
2525
Atomics.add(counter, 0, 1);
2626
process.exit();

test/node-api/test_worker_terminate/test_worker_terminate.c

-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ napi_value Test(napi_env env, napi_callback_info info) {
1717
NODE_API_ASSERT(env, t == napi_function,
1818
"Wrong first argument, function expected.");
1919

20-
status = napi_call_function(env, recv, argv[0], 0, NULL, NULL);
21-
assert(status == napi_ok);
2220
status = napi_call_function(env, recv, argv[0], 0, NULL, NULL);
2321
assert(status == napi_pending_exception);
2422

test/parallel/test-async-hooks-worker-asyncfn-terminate-4.js

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const { Worker } = require('worker_threads');
1212
const workerData = new Int32Array(new SharedArrayBuffer(4));
1313
const w = new Worker(`
1414
const { createHook } = require('async_hooks');
15+
const { workerData } = require('worker_threads');
1516
1617
setImmediate(async () => {
1718
createHook({ init() {} }).enable();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const { Worker, isMainThread } = require('worker_threads');
5+
6+
if (isMainThread) {
7+
const workerData = new Int32Array(new SharedArrayBuffer(4));
8+
new Worker(__filename, {
9+
workerData,
10+
});
11+
process.on('beforeExit', common.mustCall(() => {
12+
assert.strictEqual(workerData[0], 0);
13+
}));
14+
} else {
15+
const { workerData } = require('worker_threads');
16+
process.exit();
17+
workerData[0] = 1;
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const { Worker, isMainThread } = require('worker_threads');
5+
6+
if (isMainThread) {
7+
const workerData = new Int32Array(new SharedArrayBuffer(4));
8+
new Worker(__filename, {
9+
workerData,
10+
});
11+
process.on('beforeExit', common.mustCall(() => {
12+
assert.strictEqual(workerData[0], 0);
13+
}));
14+
} else {
15+
const { workerData } = require('worker_threads');
16+
try {
17+
process.exit();
18+
throw new Error('xxx');
19+
// eslint-disable-next-line no-unused-vars
20+
} catch (err) {
21+
workerData[0] = 1;
22+
}
23+
}

0 commit comments

Comments
 (0)