-
Notifications
You must be signed in to change notification settings - Fork 30k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add input
option to child_process.spawn()
#48786
Comments
Thanks @mscdex, and sorry for not adding that issue in the original message. There is also the underlying issue at #25231. However, I was struck by the discussion straying from being purely technical, or even being on-topic for some of the comments. As @benjamingr put it in that issue:
Some of the discussion was about whether this feature belonged in core Node.js or userland. As someone who tried implementing it in Execa, at the userland level, I think this is rather hard to do so in a way that is stable, fast and cross-platform. I am also hoping to highlight with this issue some of those problems. For example, As you put it in the PR:
Which I also completely agree with. Therefore, if the |
Not really. |
Can you explain why: const child = cp.exec(...);
child.stdin.write(...); Doesn't work? The linked issue seems to be related to timing but I'm not sure I fully understand - does the above code have a potential race condition where it can cause an EPIPE and an |
By the time, const child = spawn('echo')
child.stdin.write('test')
|
I'm having a hard time reproducing this? bgruenbaum@XLOs-MacBook-Pro 1 % node -e 'c = require("child_process").spawn("echo"); console.log(c.stdin.write("foo"));'
true I'm on mac on v20.4.0 (since that's what you used). Ran it 1000 times in a loop and no repro. |
Thanks for trying to run it @benjamingr. I am on Node 20.4.0, but I use Ubuntu 23.04, not macOS. I am not sure whether the difference is OS-related (e.g. between the Linux and macOS syscalls) or hardware-related. Running the same command line as you: $ node -e 'c = require("child_process").spawn("echo"); console.log(c.stdin.write("foo"));'
false
node:events:490
throw er; // Unhandled 'error' event
^
Error: write EPIPE
...
Node.js v20.4.0 There is definitely a timing dimension to this problem. For example, if I make the command slower by adding a long argument to process, it stops failing. By tweaking the size of the argument, I can obtain commands which either succeed or fail half of the time, such as (on my machine): $ node -e 'c = require("child_process").spawn("bash", ["-c", "echo " + "a".repeat(27000)]); console.log(c.stdin.write("foo"));'
true
$ node -e 'c = require("child_process").spawn("bash", ["-c", "echo " + "a".repeat(27000)]); console.log(c.stdin.write("foo"));'
false
node:events:490
throw er; // Unhandled 'error' event
^
Error: write EPIPE
$ node -e 'c = require("child_process").spawn("bash", ["-c", "echo " + "a".repeat(27000)]); console.log(c.stdin.write("foo"));'
true I agree with you that an |
Nice, I can't repro this on macOS but I can confirm this reproduces on an Ubuntu machine. @mscdex what do you think? I think we should either allow cc @nodejs/child_process - the ask here is for: const child = exec("whatever");
child.stdin.write("something"); To not EPIPE on short commands by either accepting an I'm not sure if to add the confirmed bug label I'd like someone from @nodejs/child_process to weigh in. |
If short-lived means something like /bin/false, then the race is intrinsic and not specific to node: the child disappears before the parent gets a chance to write to the pipe that connects them. Adding an |
@bnoordhuis was hoping you'd show up, care to educate me on why? For example if I do something like i.e. there is nothing "special" about shells and there is no system call for "make a process with a preset stdin with that data in it" or some such and it's all multiple call APIs (e.g one to fork and one to set stdin). |
Pretty much. Shells do a lot of bookkeeping. Plus, and unlike node, most programs simply terminate on SIGPIPE and therefore never generate EPIPE errors. |
Thanks @bnoordhuis for giving that additional knowledge about how this works at the OS level. 👍
From looking at the code, it does seem to be as you describe @benjamingr. I might be incorrect but I believe the underlying syscall on Unix being used might be Line 501 in 339eb10
From that perspective, I understand what you mean @bnoordhuis that it is not Node-specific, this is just how things work as the OS level (at least on Unix). So the problem described above should only happen when either:
In both cases, the current behavior of throwing an Am I understanding this correctly? If I do, then that means calling |
Correct.
That's context-dependent. In Programs that want to keep running (like node) suppress the SIGPIPE and deal with the EPIPE error from the read() or write() system call. Node, being a runtime, bubbles it up to you, the programmer. |
In that case, if we can't provide |
Thanks for the additional insights @bnoordhuis, that was super helpful. |
I disagree than attempt to write stdin to the finished process is a programming bug always. E.g. you may start a program, which accepts stdin usually, but under some conditions (e.g. mistake in the program config file) it's hard or not possible to check these conditions before the process starts. So, a workaround is to listen to const child = exec("whatever");
child.stdin.once("error", err => {
// Handle the EPIPE error.
});
child.stdin.write("something"); |
What is the problem this feature will solve?
Users often need a child process's
stdin
to be some input as a string or buffer.This is a use case common enough that Bash has a built-in operator for it:
<<<
.At the moment,
child_process.spawn()
stdio[0]
does not have an easy way to pass some input as a string or buffer.What is the feature you are proposing to solve the problem?
child_process.spawnSync()
implements aninput
option to solve this problem. For feature parity, this could be ported tochild_process.spawn()
as well.What alternatives have you considered?
This issue originated in Execa: sindresorhus/execa#474
We've been considering many ways to implement this at the userland level, but they all come with challenges.
Readable.from()
Converting the string or buffer to a stream using something like
Readable.from()
does not work becausestdio[0]
requires any stream to have an underlying file descriptor.stream.pipe(childProcess.stdin)
orchildProcess.stdin.end(string)
Calling
childProcess.stdin.end(string)
right afterchild_process.spawn()
does not work when the process is very fast. If the process already exited, this results in anEPIPE
error (see #40085).Temporary file or socket
Creating a temporary file or socket, then passing it to
stdio[0]
requires more complex and slower logic to be implemented for what appears to be a simple use case. With--experimental-permissions
, this also requires additional permissions.Additional process
One could spawn a process that just outputs the string or buffer, then pipe its
stdout
to the other process'sstdin
. However, doing so in a way that is both fast and cross-OS is not straightforward.The text was updated successfully, but these errors were encountered: