Skip to content

Commit

Permalink
Implement process.exit(...) in the runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
jasnell committed Oct 8, 2024
1 parent 8e7faed commit d4b027b
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 0 deletions.
5 changes: 5 additions & 0 deletions src/node/internal/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,13 @@ export function getBuiltinModule(id: string): any {
return utilImpl.getBuiltinModule(id);
}

export function exit(code: number) {
utilImpl.processExitImpl(code);
}

export default {
nextTick,
env,
exit,
getBuiltinModule,
};
1 change: 1 addition & 0 deletions src/node/internal/util.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,4 @@ export function isBoxedPrimitive(

export function getBuiltinModule(id: string): any;
export function getCallSite(frames: number): Record<string, string>[];
export function processExitImpl(code: number): void;
7 changes: 7 additions & 0 deletions src/workerd/api/node/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ wd_cc_library(
],
visibility = ["//visibility:public"],
deps = [
"//src/workerd/io",
"//src/workerd/io:compatibility-date_capnp",
"//src/workerd/jsg",
"//src/workerd/util:mimetype",
Expand Down Expand Up @@ -207,3 +208,9 @@ wd_test(
args = ["--experimental"],
data = ["tests/module-create-require-test.js"],
)

wd_test(
src = "tests/process-exit-test.wd-test",
args = ["--experimental"],
data = ["tests/process-exit-test.js"],
)
26 changes: 26 additions & 0 deletions src/workerd/api/node/tests/process-exit-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { rejects, ok } from 'assert';

let called = false;

export const test = {
async test(_, env) {
// We don't really have a way to verifying that the correct log messages
// we emitted. For now, we need to manually check the log out, and here
// we check only that we received an internal error and that no other
// lines of code ran after the process.exit call.
await rejects(env.subrequest.fetch('http://example.org'), {
message: /^The Node.js process.exit/,
});
ok(!called);
},
};

export default {
async fetch() {
try {
process.exit(123);
} finally {
called = true;
}
},
};
18 changes: 18 additions & 0 deletions src/workerd/api/node/tests/process-exit-test.wd-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Workerd = import "/workerd/workerd.capnp";

const unitTests :Workerd.Config = (
services = [
( name = "process-exit-test",
worker = (
modules = [
(name = "worker", esModule = embed "process-exit-test.js")
],
compatibilityDate = "2024-10-01",
compatibilityFlags = ["nodejs_compat"],
bindings = [
(name = "subrequest", service = "process-exit-test")
]
)
),
],
);
27 changes: 27 additions & 0 deletions src/workerd/api/node/util.c++
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// https://opensource.org/licenses/Apache-2.0
#include "util.h"

#include <workerd/io/io-context.h>
#include <workerd/jsg/jsg.h>

#include <kj/vector.h>
Expand Down Expand Up @@ -240,4 +241,30 @@ jsg::JsValue UtilModule::getBuiltinModule(jsg::Lock& js, kj::String specifier) {
return js.undefined();
}

void UtilModule::processExitImpl(jsg::Lock& js, int code) {
if (IoContext::hasCurrent()) {
auto ex = JSG_KJ_EXCEPTION(FAILED, Error,
kj::str("The Node.js process.exit(", code, ") API was called. Canceling the request."));
// There are a few things happening here. First, we abort the current IoContext
// in order to shut down this specific request....
IoContext::current().abort(kj::cp(ex));
// ...then we tell the isolate to terminate the current JavaScript execution.
// Oddly however, this does not appear to *actually* terminate the thread of
// execution unless we trigger the Isolate to handle the intercepts, which
// calling v8::JSON::Stringify does. Weird... but ok? As long as it works
// TODO(soon): Investigate if there is a better approach to triggering the
// interrupt handling.
js.v8Isolate->TerminateExecution();
v8::JSON::Stringify(js.v8Context(), js.str()).IsEmpty();
} else {
// Create an error object so we can easily capture the stack where the
// process.exit call was made.
auto err = KJ_ASSERT_NONNULL(
js.error("process.exit(...) called without a current request context. Ignoring.")
.tryCast<jsg::JsObject>());
err.set(js, "name"_kj, js.str());
js.logWarning(kj::str(err.get(js, "stack"_kj)));
}
}

} // namespace workerd::api::node
8 changes: 8 additions & 0 deletions src/workerd/api/node/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,13 @@ class UtilModule final: public jsg::Object {

jsg::JsValue getBuiltinModule(jsg::Lock& js, kj::String specifier);

// This is used in the implementation of process.exit(...). Contrary
// to what the name suggests, it does not actually exit the process.
// Instead, it will cause the IoContext, if any, and will stop javascript
// from further executing in that request. If there is no active IoContext,
// then it becomes a non-op.
void processExitImpl(jsg::Lock& js, int code);

JSG_RESOURCE_TYPE(UtilModule) {
JSG_NESTED_TYPE(MIMEType);
JSG_NESTED_TYPE(MIMEParams);
Expand Down Expand Up @@ -243,6 +250,7 @@ class UtilModule final: public jsg::Object {
JSG_METHOD(isBoxedPrimitive);

JSG_METHOD(getBuiltinModule);
JSG_METHOD(processExitImpl);
}
};

Expand Down

0 comments on commit d4b027b

Please sign in to comment.