-
-
Notifications
You must be signed in to change notification settings - Fork 32.3k
Description
Edit: If someone can come up with a better shim for execve
for Windows, that'd be far better. The form below is very expensive and very horrible.
Edit 2: Linked relevant SO question.
Edit 3: Clarify FS changes
Edit 4: Here's the text from that SO question as of July 6, 2018 (so you don't have to search for it), where I asked about how to do the Windows part.
Click to show (warning: lots of text)
So, in a feature request I filed against Node.js, I was looking for a way to replace the current Node process with another. In Linux and friends (really, any POSIX-compliant system), this is easy: use execve
and friends and call it a day. But obviously, that won't work on Windows, since it only has CreateProcess
(which execve
and friends delegate to, complete with async behavior). And it's not like people haven't wanted to do similar, leading to numerous duplicate questions on this site. (This isn't a duplicate because it's explicitly seeking a workaround given certain constraints, not just asking for direct replacement.)
Process replacement has several facets that have to addressed:
- All console I/O streams have to be forwarded to the new process.
- All signals need transparently forwarded to the new process.
- The data from the old process have to be destroyed, with as many resources reclaimed as possible.
- All pre-existing threads and child processes should be destroyed.
- All pre-existing handles should be destroyed apart from open file descriptors and named pipes/etc.
- Optimally, the old process's memory should be kept to a minimum after the process is created.
- For my particular use case, retaining the process ID is not important.
And for my particular case, there are a few constraints:
- I can control the initial process's startup as well as the location of my "process replacement" function.
- I could load arbitrary native code via add-ons at potentially any stack offset.
- Implication: I can't even dream of tracking
malloc
calls, handles, thread manipulation, or process manipulation to track and free them all, since DLL rewriting isn't exactly practical.
- Implication: I can't even dream of tracking
- I have no control over when my "process replacement" is called. It could be called through an add-on, which could've been called through either interpreted code via FFI or even another add-on recursively. It could even be called during add-on initialization.
- Implication: I would have no ability to know what's in the stack, even if I perfectly instrumented my side. And rewriting all their
call
s andpush
es is far from practical, and would just be all-around slow for obvious reasons.
- Implication: I would have no ability to know what's in the stack, even if I perfectly instrumented my side. And rewriting all their
So, here's the gist of what I was thinking: use something similar to a pseudo-trampoline.
- Statically allocate the following:
- A single pointer for the stack pointer.
MAX_PATH + 1
chars for the application path +'\0'
.MAX_PATH + 1
chars for the current working directory path +'\0'
.- 32768 chars for the arguments +
'\0'
. - 32768 chars for the environment +
'\0'
.
- On entry, set the global stack pointer reference to the stack pointer.
- On "replacement":
- Do relevant process cleanup and lock/release everything you can.
- Set the stack pointer to the stored original global one.
- Terminate each child thread.
- Kill each child process.
- Free each open handle.
- If possible (i.e. not in a UWP program), For each heap, destroy it if it's not the default heap or the temporary heap (if it exists).
- If possible, close each open handle.
- If possible, walk the default heap and free each segment associated with it.
- Create a new process with the statically allocated file/arguments/environment/etc. with no new window created.
- Proxy all future received signals, exceptions, etc. without modification to this process somehow. The standard signals are easy, but not so much with the exceptions.
- Wait for the process to end.
- Return with the process's exit code.
The idea here is to use a process-based trampoline and drop the current process size to an absolute minimum while the newly created one is started.
But where I'm not very familiar with Windows, I probably made quite a few mistakes here. Also, the above seems extremely inefficient and to an extent it just feels horribly wrong for something a kernel could just release a few memory pages, deallocate a bunch of memory handles, and move some memory around for the next process.
So, to summarize, what's the ideal way to emulate process replacement on Windows with the fewest limitations?
I would like a means to "replace" the current Node process with another, keeping the same process ID. It would be something morally similar to this function, but it wouldn't return. This would be most useful for conditionally replacing Node flags in a startup script - for example, if someone wants to enable modules and your behavior needs to change non-trivially in the presence of them (like if you need to install a default loader), you'll want to respawn the process with --experimental-modules --loader <file>
so you can install the loader.
This is also for scenarios when you want to run a module as a main
module. If you want to do logic after the process ends, you should be using child_process.spawn
regardless - you shouldn't be attempting to "replace" it in any capacity.
Here's what I propose:
-
child_process.replaceSpawn(command [ , args] [ , options ])
command
is the path to the new command.args
is the args to replace the arguments with. This defaults to the empty array.options
is for the various options for replacing the process. This defaults to an empty object.options.cwd
is the new cwd to use. (Default:process.cwd()
)options.env
is the new environment to use. (Default:process.env
)options.argv0
is the binary to spawn as. (Default:command
)
-
child_process.replaceFork(mainPath [ , args] [ , options ])
works similarly to above.mainPath
is the path to the newrequire.main
.options.execPath
is the new binary to spawn as. (Default:process.execPath
)options.execArgv
are the new Node flags to spawn with. (Default:process.execArgv
)options.argv0
is the binary to spawn as. (Default:process.argv0
)- The command is the original binary itself.
-
Add a
napi_terminating
member fornapi_status
to representtry_catch.HasTerminated()
and the result of each call after replacement termination. -
Add a
napi_set_terminate_hook(napi_env env, void (*fun)(void*), void* data)
function to register a callback called on termination, to make it easier to clean up resources.
Internally, there are two cases you need to cover, and the simulated part for Windows is where it gets really hairy due to all the edge cases. Here's pseudocode for the basic algorithm (I'm not really familiar with Node internals, so take this as a rough guideline):
- Stop the main event loop.
- Go through the standard shutdown routine.
- Destroy any open libuv handles and cancel any remaining event loop tasks.
- If we're on a platform that supports process replacement (like Linux or Mac):
- Invoke
execve
or equivalent with the new process path, arguments, and environment.
- Invoke
- Else, if we're on Windows (the only supported OS that doesn't), we have to simulate it entirely:
- Terminate execution via
v8::V8::TerminateExecution()
. All N-API callbacks should returnnapi_terminated
during this step. - For each loaded native module:
- If the native module has a terminate hook, call it.
- Unload the native module's DLL.
- Close the event loop.
- Dispose the isolate.
- Do the rest according to whatever happens to this SO question.
- Else, on other OSs without a process replacement function, it'd look similar to Windows.
- Terminate execution via
In addition, file system requests will have to generally create each file descriptor with O_CLOEXEC
.
As for precedent where this could be used immediately:
- Liftoff works very similarly, just with a little extra opinionated sugar, and that's used natively in Gulp. This kind of thing would speed that up quite a bit.
- I do very similar to transparently pass through unknown Node flags.
- Babel attempts to use
kexec
where available, which is a POSIX-only module that replaces the process literally. Absent that, it falls back to its own implementation that works like the other two examples.