Skip to content

Commit

Permalink
process: add replace
Browse files Browse the repository at this point in the history
  • Loading branch information
ShogunPanda committed Jan 7, 2025
1 parent 804d41f commit c3ad479
Show file tree
Hide file tree
Showing 10 changed files with 354 additions and 1 deletion.
8 changes: 8 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -2445,6 +2445,13 @@ An invalid timestamp value was provided for a performance mark or measure.

Invalid options were provided for a performance measure.

<a id="ERR_PROCESS_REPLACE_FAILED"></a>

### `ERR_PROCESS_REPLACE_FAILED`

Replacing the current process with [`process.replace()`][] has failed due to some
system error.

<a id="ERR_PROTO_ACCESS"></a>

### `ERR_PROTO_ACCESS`
Expand Down Expand Up @@ -4264,6 +4271,7 @@ An error occurred trying to allocate memory. This should never happen.
[`postMessage()`]: worker_threads.md#portpostmessagevalue-transferlist
[`postMessageToThread()`]: worker_threads.md#workerpostmessagetothreadthreadid-value-transferlist-timeout
[`process.on('exit')`]: process.md#event-exit
[`process.replace()`]: process.md#processreplacefile-args-env
[`process.send()`]: process.md#processsendmessage-sendhandle-options-callback
[`process.setUncaughtExceptionCaptureCallback()`]: process.md#processsetuncaughtexceptioncapturecallbackfn
[`readable._read()`]: stream.md#readable_readsize
Expand Down
22 changes: 22 additions & 0 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -3303,6 +3303,28 @@ In custom builds from non-release versions of the source tree, only the
`name` property may be present. The additional properties should not be
relied upon to exist.
## \`process.replace(file\[, args\[, env]])\`
<!-- YAML
added: REPLACEME
-->
* `file` {string} The name or path of the executable file to run.
* `args` {string\[]} List of string arguments. No argument can contain a null-byte (`\u0000`).
* `env` {Object} Environment key-value pairs.
No key or value can contain a null-byte (`\u0000`).
**Default:** `process.env`.
Replaces the current process with a new process.
This is achieved by using the `execve` Unix function and therefore no memory or other
resources from the current process are preserved, except for the standard input,
standard output and standard error file descriptor.
All other resources are discarded by system when the processes are swapped.
This function will never return, unless an error occurred.
## `process.report`
<!-- YAML
Expand Down
1 change: 1 addition & 0 deletions lib/internal/bootstrap/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ const rawMethods = internalBinding('process_methods');
process.availableMemory = rawMethods.availableMemory;
process.kill = wrapped.kill;
process.exit = wrapped.exit;
process.replace = wrapped.replace;
process.ref = perThreadSetup.ref;
process.unref = perThreadSetup.unref;

Expand Down
47 changes: 47 additions & 0 deletions lib/internal/process/per_thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const {
FunctionPrototypeCall,
NumberMAX_SAFE_INTEGER,
ObjectDefineProperty,
ObjectEntries,
ObjectFreeze,
ReflectApply,
RegExpPrototypeExec,
Expand All @@ -24,6 +25,7 @@ const {
SetPrototypeEntries,
SetPrototypeValues,
StringPrototypeEndsWith,
StringPrototypeIncludes,
StringPrototypeReplace,
StringPrototypeSlice,
Symbol,
Expand All @@ -38,13 +40,15 @@ const {
ERR_INVALID_ARG_VALUE,
ERR_OUT_OF_RANGE,
ERR_UNKNOWN_SIGNAL,
ERR_WORKER_UNSUPPORTED_OPERATION,
},
} = require('internal/errors');
const format = require('internal/util/inspect').format;
const {
validateArray,
validateNumber,
validateObject,
validateString,
} = require('internal/validators');

const constants = internalBinding('constants').os.signals;
Expand Down Expand Up @@ -101,6 +105,7 @@ function wrapProcessMethods(binding) {
rss,
resourceUsage: _resourceUsage,
loadEnvFile: _loadEnvFile,
replace: _replace,
} = binding;

function _rawDebug(...args) {
Expand Down Expand Up @@ -223,6 +228,47 @@ function wrapProcessMethods(binding) {
return true;
}

function replace(execPath, args, env) {
const { isMainThread } = require('internal/worker');

if (!isMainThread) {
throw new ERR_WORKER_UNSUPPORTED_OPERATION('Calling process.replace');
}

validateString(execPath, 'execPath');
validateArray(args, 'args');

for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (typeof arg !== 'string' || StringPrototypeIncludes(arg, '\u0000')) {
throw new ERR_INVALID_ARG_VALUE(`args[${i}]`, arg, 'must be a string without null bytes');
}
}

if (env !== undefined) {
validateObject(env, 'env');

for (const { 0: key, 1: value } of ObjectEntries(env)) {
if (
typeof key !== 'string' ||
typeof value !== 'string' ||
StringPrototypeIncludes(key, '\u0000') ||
StringPrototypeIncludes(value, '\u0000')
) {
throw new ERR_INVALID_ARG_VALUE(
'env', env, 'must be an object with string keys and values without null bytes',
);
}
}
}

// Construct the environment array.
const envArray = ObjectEntries(env || {}).map(({ 0: key, 1: value }) => `${key}=${value}`);

// Perform the system call
_replace(execPath, args, envArray);
}

const resourceValues = new Float64Array(16);
function resourceUsage() {
_resourceUsage(resourceValues);
Expand Down Expand Up @@ -267,6 +313,7 @@ function wrapProcessMethods(binding) {
memoryUsage,
kill,
exit,
replace,
loadEnvFile,
};
}
Expand Down
14 changes: 13 additions & 1 deletion src/node_errors.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
#include <sstream>

namespace node {
// This forward declaration is to have the method available in error messages.
namespace errors {
const char* errno_string(int errorno);
}

enum ErrorHandlingMode { CONTEXTIFY_ERROR, FATAL_ERROR, MODULE_ERROR };
void AppendExceptionLine(Environment* env,
Expand Down Expand Up @@ -111,7 +115,8 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
V(ERR_WASI_NOT_STARTED, Error) \
V(ERR_ZLIB_INITIALIZATION_FAILED, Error) \
V(ERR_WORKER_INIT_FAILED, Error) \
V(ERR_PROTO_ACCESS, Error)
V(ERR_PROTO_ACCESS, Error) \
V(ERR_PROCESS_REPLACE_FAILED, Error)

#define V(code, type) \
template <typename... Args> \
Expand Down Expand Up @@ -253,6 +258,13 @@ inline v8::Local<v8::Object> ERR_STRING_TOO_LONG(v8::Isolate* isolate) {
return ERR_STRING_TOO_LONG(isolate, message);
}

inline void THROW_ERR_PROCESS_REPLACE_FAILED(Environment* env, int code) {
std::ostringstream message;
message << "Replacing process failed with error code "
<< errors::errno_string(code);
THROW_ERR_PROCESS_REPLACE_FAILED(env, message.str().c_str());
}

#define THROW_AND_RETURN_IF_NOT_BUFFER(env, val, prefix) \
do { \
if (!Buffer::HasInstance(val)) \
Expand Down
100 changes: 100 additions & 0 deletions src/node_process_methods.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@
#if defined(_MSC_VER)
#include <direct.h>
#include <io.h>
#include <process.h>
#define umask _umask
typedef int mode_t;
#else
#include <pthread.h>
#include <sys/resource.h> // getrlimit, setrlimit
#include <termios.h> // tcgetattr, tcsetattr
#include <unistd.h>
#endif

namespace node {
Expand Down Expand Up @@ -471,6 +473,102 @@ static void ReallyExit(const FunctionCallbackInfo<Value>& args) {
env->Exit(code);
}

inline char** copy_js_strings_array(
Environment* env, Local<Array> js_array, int* target_length) {
Local<Context> context = env->context();
char** target = nullptr;
int length = js_array->Length();

CHECK_LT(length, INT_MAX);

target = new char*[length + 1];
target[length] = nullptr;

if (length > 0) {
for (int i = 0; i < length; i++) {
node::Utf8Value str(
env->isolate(), js_array->Get(context, i).ToLocalChecked());
target[i] = strdup(*str);
CHECK_NOT_NULL(target[i]);
}
}

*target_length = length;
return target;
}

static void Replace(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Local<Context> context = env->context();

CHECK_GT(args.Length(), 1);
CHECK(args[0]->IsString());

// Prepare arguments and environment
char **argv = nullptr;
char **envp = nullptr;;
int argv_length = 0;
int envp_length = 0;

// Prepare arguments - Note that the pathname is always the first argument
if (args.Length() > 1) {
CHECK(args[1]->IsArray());

Local<Array> argv_array = args[1].As<Array>();

Local<Array> full_argv_array = Array::New(
env->isolate(), argv_array->Length() + 1);
full_argv_array->Set(context, 0, args[0].As<String>()).Check();
for (unsigned int i = 0; i < argv_array->Length(); i++) {
full_argv_array->Set(
context, i + 1, argv_array->Get(context, i).ToLocalChecked()).Check();
}

argv = copy_js_strings_array(env, full_argv_array, &argv_length);
} else {
node::Utf8Value pathname_string(env->isolate(), args[0].As<String>());

argv = new char*[2];
argv[0] = strdup(*pathname_string);
argv[1] = nullptr;
}

if (args.Length() > 2) {
CHECK(args[2]->IsArray());
envp = copy_js_strings_array(env, args[2].As<Array>(), &envp_length);
}

// Set stdin, stdout and stderr to be non-close-on-exec
// so that the new process will inherit it.
for (int i = 0; i < 3; i ++) {
int flags = fcntl(i, F_GETFD, 0);
if (flags >= 0) {
flags &= ~FD_CLOEXEC;
fcntl(i, F_SETFD, flags);
}
}

// Perform the execve operation.
// If it returns, it means that the execve operation failed.
RunAtExit(env);
execve(argv[0], argv, envp);
int error_code = errno;

// Return with that error code.
for (int i = 0; i < argv_length; i++) {
free(argv[i]);
}

for (int i = 0; i < envp_length; i++) {
free(envp[i]);
}

delete argv;
delete envp;

THROW_ERR_PROCESS_REPLACE_FAILED(env, error_code);
}

static void LoadEnvFile(const v8::FunctionCallbackInfo<v8::Value>& args) {
Environment* env = Environment::GetCurrent(args);
std::string path = ".env";
Expand Down Expand Up @@ -662,6 +760,7 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,
SetMethodNoSideEffect(isolate, target, "cwd", Cwd);
SetMethod(isolate, target, "dlopen", binding::DLOpen);
SetMethod(isolate, target, "reallyExit", ReallyExit);
SetMethod(isolate, target, "replace", Replace);
SetMethodNoSideEffect(isolate, target, "uptime", Uptime);
SetMethod(isolate, target, "patchProcessObject", PatchProcessObject);

Expand Down Expand Up @@ -704,6 +803,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(Cwd);
registry->Register(binding::DLOpen);
registry->Register(ReallyExit);
registry->Register(Replace);
registry->Register(Uptime);
registry->Register(PatchProcessObject);

Expand Down
Loading

0 comments on commit c3ad479

Please sign in to comment.