Holger Hoefling
Holger Hoefling

Reputation: 428

Redirect stdout and stderr separately over a socket connection

I am trying to run a script on the other side of a unix-socket connection. For this I am trying to use socat. The script is

#!/bin/bash
read MESSAGE1
echo "PID: $$"
echo "$MESSAGE1"
sleep 2
read MESSAGE2
echo "$MESSAGE2" 1>&2

As the listener for socat I have

socat unix-listen:my_socket,fork exec:./getmsg.sh,stderr

as the client I use:

echo $'message 1\nmessage 2\n' | socat -,ignoreeof unix:my_socket 2> stderr.txt

and I get the output

PID: 57248
message 1
message 2

whereas the file stderr.txt is empty.

My expectation however was that

That is the file stderr.txt should have had the content message 2 instead of being empty.

Any idea on how I can achieve it that stdout and stderr are transferred separately and not combined?

Thanks

Upvotes: 2

Views: 2059

Answers (2)

Andrej Podzimek
Andrej Podzimek

Reputation: 2768

If the input and output are just text with reasonably finite line lengths, then you can easily write muxing and demuxing commands in pure Bash.

The only issue is how socat (mis)handles stderr; it basically either forces it to be the same file as stdout or ignores it completely. At which point it is better to use one’s own file descriptor convention in the handler script, with unusual file descriptors that don’t conflict with 0, 1 or 2.

Let’s pick 11 for stdout and 12 for stderr, for example. For stdin we can just keep using 0 as usual.

getmsg.sh

#!/bin/bash
set -e -o pipefail

read message
echo "PID: $$"  1>&11   # to stdout
echo "$message" 1>&11   # to stdout
sleep 2
read message
echo "$message" 1>&12   # to stderr

mux.sh

#!/bin/bash

"$@" \
11> >(while read line; do printf '%s\n' "stdout: ${line}"; done) \
12> >(while read line; do printf '%s\n' "stderr: ${line}"; done)

demux.sh

#!/bin/bash
set -e -o pipefail

declare -ri stdout="${1:-1}"
declare -ri stderr="${2:-2}"
while IFS= read -r line; do
  if [[ "$line" = 'stderr: '* ]]; then
    printf '%s\n' "${line#stderr: }" 1>&"$((stderr))"
  elif [[ "$line" = 'stdout: '* ]]; then
    printf '%s\n' "${line#stdout: }" 1>&"$((stdout))"
  else
    exit 3  # report malformed stream
  fi
done

A few examples

#!/bin/bash
set -e -o pipefail

socat unix-listen:my_socket,fork exec:'./mux.sh ./getmsg.sh' &

declare -ir server_pid="$!"
trap 'kill "$((server_pid))"
      wait -n "$((server_pid))" || :' EXIT

until [[ -S my_socket ]]; do :; done  # ugly

echo '================= raw data from the socket ================='
echo $'message 1\nmessage 2\n' | socat -,ignoreeof unix:my_socket

echo '================= normal mode of operation ================='
echo $'message 1\nmessage 2\n' | socat -,ignoreeof unix:my_socket \
| ./demux.sh

echo '================= demux / mux test for fun ================='
echo $'message 1\nmessage 2\n' | socat -,ignoreeof unix:my_socket \
| ./mux.sh ./demux.sh 11 12

Upvotes: 3

KamilCuk
KamilCuk

Reputation: 140880

My expectation however

There is only one socket and via one socket one stream of data can be sent, not two. You can't send stdout and stderr (two streams) via one handle (I mean, without like inter-mixing them, i.e. without loosing information what data is from which stream). Also see explanation of stderr flag with exec in man socat and see man dup. Both stderr and stdout of the script redirect to the same output.

The expectation would be that stderr.txt is empty, because socat does not write anything to stderr.

how I can achieve it that stdout and stderr are transferred separately and not combined?

Use two sockets separately for each stream.

Transfer messages using a protocol that would differentiate two streams. For example a simple line-based protocol that prefixes the messages:

# script.sh
echo "stdout: this is stdout"
echo "stderr: this is stderr"

# client
... | socat ... | tee >(sed -n 's/stderr: //p' >&2) | sed -n 's/stdout: //p'

Upvotes: 0

Related Questions