Search code examples
shellprocesstelnet

Killing a specific process in a pipeline, from later in the pipeline


I'm writing a shell script that uses telnet to connect to a server, send a query, and then grab the one-line response for further processing. The classic solution is to set up a subshell which echos the query and then runs sleep to keep the connection open long enough for the response to be returned:

#!/bin/sh

SERVER_ADD=localhost
SERVER_PORT=9995

result=$( ( echo '{"op":"get","path":"access"}'; sleep 30 ) | telnet "$SERVER_ADD" "$SERVER_PORT" )
    
echo "$result"

This works, except that the sleep has to be longer than the longest the server might take to respond. This means that every invocation will take that long, which pushes the minimum time out from tens of milliseconds to tens of seconds. I need my script to wait for that first line of response, and then terminate the process so it can go on to do other things.

(Yes: I know the obvious answer is "use expect." Unfortunately, I'm targeting an embedded system, and adding expect and the TcL script engine that it is written in would add about a megabyte to my image size. No can do.)

After much trial and error, I came up with the following:

#!/bin/sh

SERVER_ADD=localhost
SERVER_PORT=9995

result=$( ( echo '{"op":"get","path":"access"}'; sleep 30 ) |
    telnet "$SERVER_ADD" "$SERVER_PORT" |
    while read -r line; do
        echo "$line"
        killall sleep
    done ) 2>/dev/null
echo "$result"

Explanation

  • Set up a subshell that echoes the query and then sleeps long enough for the longest response time
  • Pipe to telnet to make the connection
  • Pipe the server response to a loop, one line at a time
  • Once the first line has been captured and echoed, kill the sleep command by name, which ends the subshell and thus the telnet connection
  • The 2>/dev/null consumes the "Terminated" message that the sleep command prints when it is terminated

This works well, EXCEPT for that killall sleep. This will kill EVERY instance of sleep under this user's control. Most times this won't be a problem, but the other times it will be a seriously confusing source of bugs.

How can I kill that sleep (or the entire subshell) when needed, without collateral damage?


Solution

  • You could try using a fifo to send data to the telnet, so you can close it at the wanted time.

    rm -f myfifo
    mkfifo myfifo
    result=$(
        telnet "$SERVER_ADD" "$SERVER_PORT"  <myfifo  |
        (  echo '{"op":"get","path":"access"}'  >&5
           while read -r line; do
              echo "$line"
              exec 5>&-
           done
        ) 5>myfifo
    )
    

    The syntax 5>myfifo (no space) opens a new output file description number 5 writing to the fifo. The echo >&5 writes to this fd. The exec 5>&- closes this fd. This syntax should work in ash.