File
Blob: src/workerd/api/node/process.c++
| 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 | |
| 16 | namespace workerd::api::node { |
| 17 | |
| 18 | jsg::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 | |
| 78 | jsg::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 | |
| 89 | namespace { |
| 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 | |
| 115 | jsg::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 | |
| 185 | kj::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 | |
| 194 | void 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 | |
| 208 | kj::String ProcessModule::getCwd(jsg::Lock& js) { |
| 209 | KJ_IF_SOME(cwd, getCurrentWorkingDirectory()) { |
| 210 | return cwd.toString(true); |
| 211 | } |
| 212 | return kj::str("/"); |
| 213 | } |
| 214 | |
| 215 | void 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 |