4

I have a short c programme that outputs 3 lines then reads from stdin, and I would like to use a shell script to interact with it as follows:

  1. read c output, line by line while read line; do ...; done <$out_
  2. save 2nd line if [ "$count" == 2 ]; then input=$line; fi
  3. send saved line back to programme echo $input >$in_

I'm using named pipes in the shell script to redirect the output and input of the c programme:

./iotest >$out_ <$in_ &

but I'm not getting the results I'd expect.

Problem

After creating the pipes out_ and in_ with mkfifo, I have a while read loop but it seems to lead to a deadlock. Nothing is read, and the shell script gets stuck on the read command.

I tried adding cat >$in_ & before launching iotest, as proposed here, to open in_for writing without actually writing anything. This unblocks the read but doesn't let me input to in_ (my c programme acts as if I'd written \x00 to stdin, and the echo $input >$in_ command hangs).

What I've tried

I've read here that read blocks if there is no writer, and wihtout cat >$in_ & I have iotest writing to out_, and read reading from it, I'm trying to redirect in_ to iotestwithout writing anything to it from the start.

But why adding the cat command makes the c programme fail at reading, I don't know. The same happens if I exchange cat >$in_ & with while true; do :; done >$in_& or exec 3>$in_ & (exec 3>$in_, as proposed here, also hangs).

Code

This is my c code:

$ cat iotest.c 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main (int argc, char* argv[])
{
    char A[9] = "";
    printf("hello. please send me the following line:\n");
    printf("input me\n");
    printf("now send me the line:\n");
    int ret = read(0,A,8);
    fprintf(stderr,"read returned %i\n",ret);
    if(ret != -1) {
        printf("You sent: %s\n", A);
        if (strcmp(A,"input me") == 0) {
            printf("correct!\n");
        } else {
            printf("wrong line.\n");
        }
    } else {
        printf("read failed.\n");
    }
    return 0;
}

and this is the shell script I've tested:

$ cat runiotest.sh 
#!/bin/bash

in_=/tmp/testpipein
out_=/tmp/testpipeout

mkfifo $in_
mkfifo $out_
count=0
input=""

./iotest >$out_ <$in_ &
while read line
do
    count=$((count+1))
    echo "line number $count:"
    echo $line
    if [ "$count" == 2 ]
    then
        input=$line
    fi
    if [ "$count" == 3 ]
    then
        echo "$input" >$in_
    fi
done < $out_

rm -f $in_
rm -f $out_

I've tested this on 32-bit debian and 64-bit ubuntu, compiling with gcc, and executing the shell script with bash.

Results

without the added cat command:

$ /bin/bash/ ./runiotest.sh
(hangs)

with added cat >$in_ &:

$ /bin/bash ./runiotest.sh
read returned 0
line number 1:
hello. please send me the following line:
line number 2:
input me
line number 3:
now send me the line:
line number 4:
You sent:
line number 5:
wrong line.

Summary

So my question is: What is wrong with my shell script, and how can I modify it to read then write to my c programme? I can't change the c code itself but can fiddle with the shell script.

Thank you for any help!

borizzzzz
  • 620
  • 1
  • 6
  • 17
  • If you can, try running the script under `strace -f` (see the man page for output options etc.). It's really helpful to see which process is blocking on what file descriptor. – Useless Oct 08 '19 at 09:33
  • 1
    Opening a fifo for writing blocks until something else opens it for reading, and vis versa. – Shawn Oct 08 '19 at 09:41
  • You "C" program expects input (from the pipe). Your script has to send the input to the program (or another process should provided it) – dash-o Oct 08 '19 at 10:13
  • Thanks @Useless. I'll have a look at strace! – borizzzzz Oct 08 '19 at 13:54
  • @dash-o, surely I should be able to read the output of the c programme before writing to its input? That's how it works when stdout and stdin are used: the output is printed normally and the programme stops at the stdin read. – borizzzzz Oct 08 '19 at 13:57
  • Is the `cat` getting terminated? It seems that you ought to just explicitly add that to the script. It feels like you are running `cat` in a way that causes it to immediately terminate, so that `read` immediately returns zero. – William Pursell Oct 08 '19 at 14:20
  • For example, if you are literally adding the line `cat > $in_ &` in the script, but executing the script with `runiotest.sh <&-`, then `cat will immediately terminate and the read in iotest will return 0. (s/will/may/, since there are a lot of race conditions here that will depend on the order in which processes execute) – William Pursell Oct 08 '19 at 17:08

2 Answers2

1

Consider:

./iotest >$out_ <$in_ &
while read line; do ...; done < $out_

Here, the subshell attemts to open $out_ for writing and $in_ for reading, and it will not even execute iotest until both of those fifos are open. Without your added cat, the subshell is unable to open $in_ until there is some writer, so iotest never even begins to execute. When you add the cat, the subshell is able to open $in_, and the main shell acts as a reader for $out_, so the subshell is able to open both fifos and execute iotest. As to the behavior you are seeing, it is a bug in your iotest.c. Print the value returned by read (to stderr!), include a declaration of read, and try using != -1 instead of > -1.

William Pursell
  • 204,365
  • 48
  • 270
  • 300
  • Thank you for the answer. It's true that I should be checking for `read(...) =! -1` as this is the value returned on error. In my case, the read function returns 0 for EOF, and including `unistd.h` for the read declaration doesn't change the behaviour I've described in my question. I'll update the code with your corrections, though. – borizzzzz Oct 08 '19 at 13:42
0

The problem was the buffering of the c programme output. Removing the buffering with stdbuf solved it:

cat >$in_ &
stdbuf -o0 ./iotest <$out_ >$in_ &

With this change, the output is as it should:

$ /bin/bash ./runiotest.sh 
line number 1
hello. please send me the following line:
line number 2
input me
line number 3
now send me the line:
read returned 8
line number 4
You sent: input me
line number 5
correct!

Just to add, using stdbuf doesn't work if the executable has the setuid bit set. In this case, you can use unbuffer from the expect utility.

borizzzzz
  • 620
  • 1
  • 6
  • 17