My problem
I have a simple sh
script that behaves exactly as I want, unless it's called from a node.js
script.
What the sh script is supposed to do
- Convert the (potentially binary) data passed via
stdin
to base64 - Store that base64 string in a variable
- Print the contents of that variable to
stdout
- If no data is passed to
stdin
, exit immediately without printing tostdout
My sh script
/tmp/aaa
:
#!/bin/sh
! [ -t 0 ] && stdin_base64=$(base64 -w 0) || stdin_base64=""
echo -n "$stdin_base64"
When called from a terminal it works as expected
Without stdin
:
$ /tmp/aaa
With stdin
:
$ echo foo | /tmp/aaa
Zm9vCg==
With binary stdin
:
$ echo -n -e '\x00\x00\x00' | /tmp/aaa
AAAA
With a ton of binary stdin
that takes multiple seconds to be written:
$ dd if=/dev/urandom bs=1 count=10M | /tmp/aaa | rev | head -c 1
When called from node.js it breaks
When the exact same script gets called from node.js
using execFile like this:
const { execFile } = require('child_process');
execFile('/tmp/aaa', [], {}, (error, stdout, stderr) => {
console.log(error, stdout, stderr);
});
it just gets stuck indefinitely, without exiting, no errors and nothing gets printed to stdout
or stderr
. I assume it just waits for stdin
forever because when I change the script to simply echo something, it exits immediately after printing:
#!/bin/sh
echo "test"
What I can/cannot do
- I cannot change the
node.js
script - I cannot use Bash (I'm using an Alpine-based Docker image that only supports basic
POSIX sh
.) - I cannot install additional software.
- The sh script needs to be changed in a way that it can handle
stdin
(or the lack thereof) properly, so that I always get the same behavior that I'm seeing when calling the script on thesh
terminal directly. - It must support binary
stdin
data including null bytes. - I cannot use something like
timeout 1 base64 -w
because reading all data fromstdin
may take well more than 1 second.
Ideas
The closest thing I could come up with was this:
#!/bin/sh
stdin_file=$(mktemp)
cat - | base64 -w 0 > $stdin_file &
base64_pid=$!
timeout 1 sh -c "while [ ! -s $stdin_file ]; do sleep 0.1; done" && wait $base64_pid || kill $base64_pid 2&> /dev/null
stdin_base64=$(cat $stdin_file 2> /dev/null)
rm -f $stdin_file
echo -n "$stdin_base64"
But this would of course always result in a wasted second when called from node.js
:
$ node -e "require('child_process').execFile('/tmp/aaa', [], {}, console.log)"
and on the other hand if it takes more than a second for data to arrive on stdin
, it will fail, thinking there was no stdin:
$ (sleep 2; echo 123) | /tmp/aaa