1

I have a server application that prints to standard output something along Listening on http://localhost:12345, and then waits until its output is closed (e.g. via Ctrl-C). I want to start it, get its address, run commands using it, and then close or kill it. How can I do this?

I came up with this:

trap 'kill $(jobs -p)' EXIT
server > tempfile &
url=$(tail -f tempfile | perl -nle "m/(http\S+)/; print \$1; exit 0;")
run-thing --using-server="$url"

Runnable code that approximates the above:

$ cat test.sh
trap 'kill $(jobs -p)' EXIT
python3 -uc 'import time; print("Listening on http://localhost:123"); time.sleep(99)' > tempfile &
url=$(tail -f tempfile | perl -nle "m/(http\S+)/; print \$1; exit 0;")
echo "using server at $url, running jobs: $(jobs -p)"
$ bash test.sh; ps aux | grep [p]ython || echo server successfully killed
using server at http://localhost:123, running jobs: 2898
server successfully killed

I feel that this approach is pretty awful for a number of reasons. What else can I do?

squirrel
  • 5,114
  • 4
  • 31
  • 43

2 Answers2

1

You could use a fifo and a custom file descriptor (#3 in the code):

tmpdir=$(mktemp -d)
mkfifo "$tmpdir/fifo"
exec 3<> "$tmpdir/fifo"

tail -f tempfile | perl -lne 'm/(http\S+)/; print $1; exit 0;' >&3 &

IFS='' read -r url <&3   # will block until one line is read

printf 'We got this: %s\n' "$url"

kill %1   # kill the background job
exec 3>&-
rm -rf "$tmpdir"

remark: with tail -f file | somecommand you might not get any output (for an undefined amount of time) because of somecommand internal buffering. That said, you did work around that with the exit 0 of your perl one-liner.

Fravadona
  • 13,917
  • 1
  • 23
  • 35
  • That's a much cleaner way to approach it. I'm still trying to wrap my head around the purpose for the pattern to begin with. Though I guess if there is some process writing to a tempfile or fifo and you have to grab the first url produced, that's about as good as anything I can think of. May want to make sure you note the use of the multiple file descriptors (which is apparent to those with sufficient bash experience), but may be a head-scratcher for those just learning. – David C. Rankin Jun 08 '22 at 00:23
  • If I may ask, what's the advantage of a fifo? And what's the advantage of using a file descriptor instead of a file name? I'm assuming `exec 3<>` opens the fifo for reading and writing with descriptor `3`, and `exec 3>&-` closes it. Also, what does `IFS` do? Sorry, this is quite confusing . – squirrel Jun 08 '22 at 00:38
1

Using coprocesses

trap '[[ -v SERVER_PID ]] && pkill -P $SERVER_PID' EXIT
coproc SERVER { server; }
url=$(<&${SERVER[0]} perl -nle 'm/(http\S+)/; print $1; exit 0;')
run-thing --using-server="$url"

Here, -v checks if the variable was set, in case the script fails before launching the server. Since the coprocess is not launched directly but in a subshell, you want to use pkill -P that kills a process by its parent PID. Finally, <&${SERVER[0]} reads from its stdout. Runnable code that approximates the above:

$ cat test.sh
trap 'pkill -P $COPROC_PID' EXIT
coproc { python3 -uc 'import time; print("Listening on http://localhost:123"); time.sleep(99)'; }
url=$(<&${COPROC[0]} perl -nle 'm/(http\S+)/; print $1; exit 0;')
echo "using server at $url, server $(pgrep python >/dev/null && echo is running)"
$ bash test.sh; ps aux | pgrep python || echo server successfully killed
using server at http://localhost:123, server is running
Terminated
server successfully killed

Using process substitution

trap "kill 0" EXIT
{ url=$(perl -nle 'm/(http\S+)/; print $1; exit 0;'); } < <(server)
run-thing --using-server="$url"

This was found thanks to the helpful advice from libera's #bash. Runnable code that approximates the above:

$ cat test.sh
trap "kill 0" EXIT
{ url=$(perl -nle 'm/(http\S+)/; print $1; exit 0;'); } \
< <(python3 -uc 'import time; print("Listening on http://localhost:123"); time.sleep(99)')
echo "using server at $url, server $(pgrep python >/dev/null && echo is running)"
$ bash test.sh; ps aux | pgrep python || echo server successfully killed
using server at http://localhost:123, server is running
server successfully killed

I'm not entirely sure why I need {}, which is a command grouping. It's like () but it doesn't spawn a subshell. kill 0 will kill all processes in the current process group. This may kill the process that started current script. You can also kill the server directly. Just like coproc does, <() spawns a subshell, so you want to find the process by parent:

{ url=$(perl -nle 'm/(http\S+)/; print $1; exit 0;'); 
  run-thing --using-server="$url";
} < <(server)
pkill -P $!
squirrel
  • 5,114
  • 4
  • 31
  • 43