Skip to content
File

Blob: src/workerd/api/node/process.c++

8.7 KB
1// Copyright (c) 2025 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4#include "process.h"
5 
6#include <workerd/api/filesystem.h>
7#include <workerd/api/node/exceptions.h>
8#include <workerd/io/features.h>
9#include <workerd/io/io-context.h>
10#include <workerd/io/tracer.h>
11#include <workerd/io/worker-fs.h>
12#include <workerd/jsg/jsg.h>
13 
14#include <kj/vector.h>
15 
16namespace workerd::api::node {
17 
18jsg::JsValue ProcessModule::getBuiltinModule(jsg::Lock& js, kj::String specifier) {
19 auto rawSpecifier = kj::str(specifier);
20 bool isNode = false;
21 KJ_IF_SOME(spec, jsg::checkNodeSpecifier(specifier)) {
22 isNode = true;
23 specifier = kj::mv(spec);
24 }
25 
26 if (FeatureFlags::get(js).getNewModuleRegistry()) {
27 KJ_IF_SOME(mod, js.resolvePublicBuiltinModule(specifier)) {
28 return mod;
29 }
30 return js.undefined();
31 }
32 
33 auto registry = jsg::ModuleRegistry::from(js);
34 if (registry == nullptr) return js.undefined();
35 
36 // getBuiltinModule is a user-facing API that must only return modules that
37 // are normally importable by user code. Default to BUILTIN_ONLY which only
38 // resolves user-importable built-ins, not internal-only modules.
39 auto resolveOption = jsg::ModuleRegistry::ResolveOption::BUILTIN_ONLY;
40 
41 // Handle process module redirection based on enable_nodejs_process_v2 flag.
42 // This is a special case where we need to redirect to an internal module,
43 // so we escalate to INTERNAL_ONLY only for this specific redirect.
44 if (isNode && specifier == "node:process") {
45 auto featureFlags = FeatureFlags::get(js);
46 if (featureFlags.getEnableNodeJsProcessV2()) {
47 specifier = kj::str("node-internal:public_process");
48 } else {
49 specifier = kj::str("node-internal:legacy_process");
50 }
51 resolveOption = jsg::ModuleRegistry::ResolveOption::INTERNAL_ONLY;
52 }
53 
54 auto path = kj::Path::parse(specifier);
55 
56 KJ_IF_SOME(info,
57 registry->resolve(js, path, kj::none, resolveOption,
58 jsg::ModuleRegistry::ResolveMethod::IMPORT, rawSpecifier.asPtr())) {
59 auto module = info.module.getHandle(js);
60 jsg::instantiateModule(js, module);
61 
62 // For Node.js modules, we want to grab the default export and return that.
63 // For other built-ins, we'll return the module namespace instead. Can be
64 // a bit confusing but it's a side effect of Node.js modules originally
65 // being commonjs and the official getBuiltinModule returning what is
66 // expected to be the default export, while the behavior of other built-ins
67 // is not really defined by Node.js' implementation.
68 if (isNode) {
69 return jsg::JsValue(js.v8Get(module->GetModuleNamespace().As<v8::Object>(), "default"_kj));
70 } else {
71 return jsg::JsValue(module->GetModuleNamespace());
72 }
73 }
74 
75 return js.undefined();
76}
77 
78jsg::JsObject ProcessModule::getEnvObject(jsg::Lock& js) {
79 if (FeatureFlags::get(js).getPopulateProcessEnv()) {
80 KJ_IF_SOME(env, js.getWorkerEnv()) {
81 return jsg::JsObject(env.getHandle(js));
82 }
83 }
84 
85 // Default to empty object.
86 return js.obj();
87}
88 
89namespace {
90[[noreturn]] void handleProcessExit(jsg::Lock& js, int code) {
91 // There are a few things happening here. First, we abort the current IoContext
92 // in order to shut down this specific request....
93 auto message =
94 kj::str("The Node.js process.exit(", code, ") API was called. Canceling the request.");
95 auto& ioContext = IoContext::current();
96 // If we have a tail worker, let's report the error.
97 KJ_IF_SOME(tracer, ioContext.getWorkerTracer()) {
98 // Why create the error like this in tracing? Because we're adding the exception
99 // to the trace and ideally we'd have the JS stack attached to it. Just using
100 // JSG_KJ_EXCEPTION would not give us that, and we only want to incur the cost
101 // of creating and capturing the stack when we actually need it.
102 auto ex = KJ_ASSERT_NONNULL(js.error(message).tryCast<jsg::JsObject>());
103 tracer.addException(ioContext.getInvocationSpanContext(), ioContext.now(),
104 ex.get(js, "name"_kj).toString(js), ex.get(js, "message"_kj).toString(js),
105 ex.get(js, "stack"_kj).toString(js));
106 ioContext.abort(js.exceptionToKj(ex));
107 } else {
108 ioContext.abort(JSG_KJ_EXCEPTION(FAILED, Error, kj::mv(message)));
109 }
110 // ...then we tell the isolate to terminate the current JavaScript execution.
111 js.terminateExecutionNow();
112}
113} // namespace
114 
115jsg::JsObject ProcessModule::getVersions(jsg::Lock& js) const {
116 // Node.js version - represents the most current Node.js version supported
117 // by the platform, as defined in node-version.h
118 // We don't want to provide real versions, these are provided as empty strings just for compat.
119 
120 static const kj::StringPtr keys[] = {
121 "node"_kj,
122 "v8"_kj,
123 "modules"_kj,
124 "zlib"_kj,
125 "brotli"_kj,
126 "zstd"_kj,
127 "ares"_kj,
128 "napi"_kj,
129 "llhttp"_kj,
130 "nghttp2"_kj,
131 "acorn"_kj,
132 "ada"_kj,
133 "simdutf"_kj,
134 "nbytes"_kj,
135 "ncrypto"_kj,
136 "uvwasi"_kj,
137 "uv"_kj,
138 "openssl"_kj,
139 "sqlite"_kj,
140 "icu"_kj,
141 "undici"_kj,
142 "simdjson"_kj,
143 "unicode"_kj,
144 "cldr"_kj,
145 "tz"_kj,
146 "cjs_module_lexer"_kj,
147 "amaro"_kj,
148 "merve"_kj,
149 };
150 
151 jsg::JsValue values[] = {
152 js.str(nodeVersion), // node
153 js.str(""_kj), // v8
154 js.str(""_kj), // modules (Node.js ABI version)
155 js.str(""_kj), // zlib
156 js.str(""_kj), // brotli
157 js.str(""_kj), // zstd
158 js.str(""_kj), // ares
159 js.str(""_kj), // napi
160 js.str(""_kj), // llhttp
161 js.str(""_kj), // nghttp2
162 js.str(""_kj), // acorn
163 js.str(""_kj), // ada
164 js.str(""_kj), // simdutf
165 js.str(""_kj), // nbytes
166 js.str(""_kj), // ncrypto
167 js.str(""_kj), // uvwasi
168 js.str(""_kj), // uv
169 js.str(""_kj), // openssl (BoringSSL compat)
170 js.str(""_kj), // sqlite
171 js.str(""_kj), // icu
172 js.str(""_kj), // undici
173 js.str(""_kj), // simdjson
174 js.str(""_kj), // unicode
175 js.str(""_kj), // cldr
176 js.str(""_kj), // tz
177 js.str(""_kj), // cjs_module_lexer
178 js.str(""_kj), // amaro
179 js.str(""_kj), // merve
180 };
181 
182 return js.obj(kj::arrayPtr(keys, kj::size(keys)), kj::arrayPtr(values, kj::size(values)));
183}
184 
185kj::StringPtr ProcessModule::getPlatform(jsg::Lock& js) const {
186 auto flags = FeatureFlags::get(js);
187 if (flags.getUnsupportedProcessActualPlatform()) {
188 return platform;
189 }
190 // Always return "linux" for production compatibility
191 return "linux"_kj;
192}
193 
194void ProcessModule::exitImpl(jsg::Lock& js, int code) {
195 if (IoContext::hasCurrent()) {
196 handleProcessExit(js, code);
197 }
198 
199 // Create an error object so we can easily capture the stack where the
200 // process.exit call was made.
201 auto err = KJ_ASSERT_NONNULL(
202 js.error("process.exit(...) called without a current request context. Ignoring.")
203 .tryCast<jsg::JsObject>());
204 err.set(js, "name"_kj, js.str());
205 js.logWarning(kj::str(err.get(js, "stack"_kj)));
206}
207 
208kj::String ProcessModule::getCwd(jsg::Lock& js) {
209 KJ_IF_SOME(cwd, getCurrentWorkingDirectory()) {
210 return cwd.toString(true);
211 }
212 return kj::str("/");
213}
214 
215void ProcessModule::setCwd(jsg::Lock& js, kj::String path) {
216 static constexpr size_t MAX_PATH_LENGTH = 4096;
217 if (path.size() > MAX_PATH_LENGTH) {
218 node::THROW_ERR_UV_ENAMETOOLONG(js, "chdir"_kj, nullptr, path);
219 }
220 
221 if (path.size() == 0) {
222 node::THROW_ERR_UV_ENOENT(js, "chdir"_kj, nullptr, path);
223 }
224 
225 auto& vfs = VirtualFileSystem::current(js);
226 
227 // Resolve the path against current working directory if it's relative
228 kj::Path resolvedPath = [&]() {
229 if (path.startsWith("/")) {
230 // Absolute path - parse without leading slash
231 return kj::Path::parse(path.slice(1));
232 } else {
233 // Relative path - resolve against current working directory
234 KJ_IF_SOME(cwd, getCurrentWorkingDirectory()) {
235 return cwd.eval(path);
236 }
237 return kj::Path({"/"}).eval(path);
238 }
239 }();
240 
241 KJ_IF_SOME(stat, vfs.getRoot(js)->stat(js, resolvedPath)) {
242 KJ_SWITCH_ONEOF(stat) {
243 KJ_CASE_ONEOF(fsError, FsError) {
244 node::THROW_ERR_UV_ENOENT(js, "chdir"_kj, nullptr, kj::str(resolvedPath));
245 }
246 KJ_CASE_ONEOF(statInfo, workerd::Stat) {
247 if (statInfo.type != FsType::DIRECTORY) {
248 node::THROW_ERR_UV_ENOTDIR(js, "chdir"_kj, nullptr, kj::str(resolvedPath));
249 }
250 if (!setCurrentWorkingDirectory(resolvedPath.clone())) {
251 node::THROW_ERR_UV_EPERM(js, "chdir"_kj, nullptr, kj::str(resolvedPath));
252 }
253 }
254 }
255 } else {
256 node::THROW_ERR_UV_ENOENT(js, "chdir"_kj, nullptr, kj::str(resolvedPath));
257 }
258}
259 
260} // namespace workerd::api::node