Snaporaz
Snaporaz

Reputation: 133

Comunication to from child process hanging on read in C

I'm trying to communicate with an external program which, if executed, will run a terminal interface. Normally I'll have to provide some inputs (e.g. "1+1") and then read the output of the program (e.g. "2"). Since I need a two-way communication I wasn't able to use popen().

My problem is the following:

Whenever I have a part of the code that asks for inputs, for example containing std::cin >> input I run into the same issue, the read command never exits.

Here I wrote a minimal example, all the child process does is reading the input and repeating it.

When I try to run this code what happens is that I see the first print "Parent says:" and I can provide the input and send it using write. However, when I try to call again the read() function the see the outcome, it never exit.

I've noticed that if I close the pipe that goes from the parent to the child (fd_p2c[1]), then I can read successfully. This is clearly not what I want, since in my application I'd like to keep both communications open.

Any suggestions on what could be done to fix this problem?

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
  int status, buf_length;

  // Input and output
  char buf[256];
  char msg[256];
  char child_read[256];

  int fd_c2p[2]; // file descriptor pipe child -> parent
  int fd_p2c[2]; // file descriptor pipe parent -> child

  pipe(fd_c2p);
  pipe(fd_p2c);

  // Spawn a new process with pid
  pid_t pid = fork(); // Fork process

  if (pid == 0) {
    // Child

    // Close the unused end of the pipe
    if (close(fd_p2c[1]) != 0 || close(fd_c2p[0]) != 0) {
      fprintf(stderr, "Faild to close unused end of pipe\n");
      exit(1);
    }

    // Set the comunication
    if (dup2(fd_p2c[0], STDIN_FILENO) != 0 ||
        dup2(fd_c2p[1], STDOUT_FILENO) != 1 ||
        dup2(fd_c2p[1], STDERR_FILENO) != 2) {
      fprintf(stderr, "Faild to duplicate the end of the pipes\n");
      exit(1);
    }

    // These two pipe ends are not needed anymore
    if (close(fd_p2c[0]) != 0 || close(fd_c2p[1]) != 0) {
      fprintf(stderr, "Faild to close unused end of pipe\n");
      exit(1);
    }

    // ask kernel to deliver SIGTERM in case the parent dies
    prctl(PR_SET_PDEATHSIG, SIGTERM);

    // Moch program
    while (1) {
      fprintf(stdout, "Parent says: ");
      fflush(stdout);
      scanf("%s", child_read);
      fprintf(stdout, " >> Child repeat: %s\n", child_read);
      fflush(stdout);
    }
    exit(1);
  } else {
    // Parent

    // These two pipe ends are not needed anymore
    if (close(fd_p2c[0]) != 0 || close(fd_c2p[1]) != 0) {
      fprintf(stderr, "Faild to close unused end of pipe\n");
      exit(1);
    }
  }

  // Read output and send input
  while (1) {
    // Read from child
    while (buf_length = read(fd_c2p[0], buf, sizeof(buf) - 1)) {
      buf[buf_length] = '\0';
      printf("%s", buf);
    }

    // Enter message to send
    scanf("%s", msg);
    if (strcmp(msg, "exit") == 0)
      break;

    // Send to child
    write(fd_p2c[1], msg, strlen(msg));
    //close(fd_p2c[1]);
  }

  printf("KILL");
  kill(pid, SIGKILL); // send SIGKILL signal to the child process
  waitpid(pid, &status, 0);
}

Upvotes: 0

Views: 752

Answers (4)

Snaporaz
Snaporaz

Reputation: 133

Since in general, I wouldn't know the length of the message in that I need to read form fd_c2p, I need to create a look that listens to the pipe till its empty.

To do it is necessary to add the O_NONBLOCK to the file descriptor in the parent as suggested by @some-programmer-dude:

    // Parent

    // close unused pipe ends
    // These two pipe ends are not needed anymore
    if (close(fd_p2c[0]) != 0 || close(fd_c2p[1]) != 0) {
      fprintf(stderr, "Faild to close unused end of pipe\n");
      exit(1);
    }

    // Add O_NONBLOCK the the fd that reads from the child
    int c2p_flags = fcntl(fd_c2p[0], F_GETFL);
    fcntl(fd_c2p[0], F_SETFL, c2p_flags | O_NONBLOCK);

Now, when I read the output of the child from the file descriptor fd_c2p[0], it returns an error whenever we attempt to read from an empty file. Reading the error code in errno should match EWOULDBLOCK.

To know when to stop to read from fd_c2p[0] some knowledge on the output is needed. This particular reading should stop when the last character of the line when it reaches EWOULDBLOCK and the previous message ended with :.

    // Read from child
    end_of_message = false;
    while (1) {
      buf_length = read(fd_c2p[0], buf, sizeof(buf) - 1);
      if (buf_length == -1)
      {
        if (end_of_message && errno == EWOULDBLOCK)
          break;
        else if (errno == EWOULDBLOCK)
          continue;
        else {
          fprintf(stderr, "reading from pd_c2p returned an error different "
                          "from `EWOULDBLOCK'\n");
          exit(errno);
        }
      }
      buf[buf_length] = '\0';
      printf("%s", buf);
      end_of_message = buf[buf_length - 1] == ':';
    }

This patch solves the problem of reading from the file when one is not sure about how many lines are there before the program asks for inputs.

It should also be safe when the message contains : at any position. To test it, one can reduce the different buffers to a smaller size (e.g. from 256 to 1).

It was also pointed out from by @prog-fh, that in principle one would like to have inputs that contains spaces as well. To accomodate can use fgets instead of scanf:

   // Enter message and send it over to the chid process
    while (fgets(msg, 256, stdin) != NULL) {
      if (msg[strlen(msg)] == '\0')
        write(fd_p2c[1], msg, strlen(msg));
      else {
        fprintf(stderr, "Error encounter while reading input\n");
        exit(1);
      }

      if (msg[strlen(msg) - 1] == '\n')
        break;
      else
        continue;
    }

Among the advantages of using fgets there is the fact that the string will keep the newline \n at the end, meaning that there is no need to push an extra character to the write buffer once we are done reading the message.

The complete code is then

#include <cerrno>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
  int status, buf_length;
  bool end_of_message = false;

  int fd_c2p[2]; // file descriptor pipe child -> parent
  int fd_p2c[2]; // file descriptor pipe parent -> child

  // Input and output
  char buf[256];
  char msg[256];
  char child_read[256];

  // We need two pipes if we want a two way comunication.
  pipe(fd_c2p);
  pipe(fd_p2c);

  // Spawn a new process with pid
  pid_t pid = fork(); // Fork process

  if (pid == 0) {
    // Child

    // Close the unused end of the pipe
    if (close(fd_p2c[1]) != 0 || close(fd_c2p[0]) != 0) {
      fprintf(stderr, "Faild to close unused end of pipe\n");
      exit(1);
    }

    // Set the comunications
    if (dup2(fd_p2c[0], STDIN_FILENO) != 0 ||
        dup2(fd_c2p[1], STDOUT_FILENO) != 1 ||
        dup2(fd_c2p[1], STDERR_FILENO) != 2) {
      fprintf(stderr, "Faild to duplicate the end of the pipes\n");
      exit(1);
    }

    // These two pipe ends are not needed anymore
    if (close(fd_p2c[0]) != 0 || close(fd_c2p[1]) != 0) {
      fprintf(stderr, "Faild to close unused end of pipe\n");
      exit(1);
    }

    // ask kernel to deliver SIGTERM in case the parent dies
    prctl(PR_SET_PDEATHSIG, SIGTERM);

    // Moch Program
    while (1) {
      fprintf(stdout, "Parent says:");
      fflush(stdout);

      fgets(child_read, 256, stdin);
      fprintf(stdout, " >> Child repeat: %s", child_read);
      while (child_read[strlen(child_read) - 1] != '\n') {
        fgets(child_read, 256, stdin);
        fprintf(stdout, " >> Child repeat: %s", child_read);
      }
      fflush(stdout);
    }

    // Nothing below this line should be executed by child process.
    // If so, it means that thera has beed a problem so lets exit:
    exit(1);
  } else {
    // Parent

    // close unused pipe ends
    // These two pipe ends are not needed anymore
    if (close(fd_p2c[0]) != 0 || close(fd_c2p[1]) != 0) {
      fprintf(stderr, "Faild to close unused end of pipe\n");
      exit(1);
    }

    // Add O_NONBLOCK the the fd that reads from the child
    int c2p_flags = fcntl(fd_c2p[0], F_GETFL);
    fcntl(fd_c2p[0], F_SETFL, c2p_flags | O_NONBLOCK);
  }

  // Now, you can write to fd_p2c[1] and read from fd_c2p[0] :
  while (1) {

    // Read from child
    end_of_message = false;
    while (1) {
      buf_length = read(fd_c2p[0], buf, sizeof(buf) - 1);
      if (buf_length == -1)
      {
        if (end_of_message && errno == EWOULDBLOCK)
          break;
        else if (errno == EWOULDBLOCK)
          continue;
        else {
          fprintf(stderr, "reading from pd_c2p returned an error different "
                          "from `EWOULDBLOCK'\n");
          exit(errno);
        }
      }
      buf[buf_length] = '\0';
      printf("%s", buf);
      end_of_message = buf[buf_length - 1] == ':';
    }

    // Enter message and send it over to the chid process
    while (fgets(msg, 256, stdin) != NULL) {
      if (msg[strlen(msg)] == '\0')
        write(fd_p2c[1], msg, strlen(msg));
      else {
        fprintf(stderr, "Error encounter while reading input\n");
        exit(1);
      }

      if (msg[strlen(msg) - 1] == '\n')
        break;
      else
        continue;
    }

    // Check if the user wants to exit the program
    if (strcmp(msg, "exit\n") == 0)
      break;
  }

  printf("KILL");
  kill(pid, SIGKILL); // send SIGKILL signal to the child process
  waitpid(pid, &status, 0);
}

Upvotes: 1

If you need a two-ways communication, you could use some unix(7) socket, or several pipe(7)-s, or some fifo(7).

You could use some JSONRPC library. Or XDR (perhaps ONC/RPC/XDR) or ASN/1 for binary communication between heterogeneous computers in a data center, or MPI. If you can use some supercomputer, it probably has proprietary libraries to ease message passing between processes running on different nodes.

Consider using OpenMPI.

I'm trying to communicate with an external program which, if executed, will run a terminal interface.

Maybe you then need some pty(7) with termios(3) ? Then take inspiration from the source code of xterm or of rxvt

You probably need some event loop around a poll(2) before attempting a read(2) (or recv(2)...) or a write(2) (or send(2))

You could find open source libraries (e.g. Glib, libev, ...) to help you, and you certainly should study for inspiration their source code.

Upvotes: 1

prog-fh
prog-fh

Reputation: 16870

Let's call a word what scanf("%s") is able to extract. This is a contiguous sequence of characters that are not separators (space, tab, new-line...).

The (redirected) standard input of the child reads a word with scanf("%s", child_read);. This word is known as ended when a separator is read or EOF is reached. In the parent, write(fd_p2c[1], msg, strlen(msg)); sends a word (and nothing more right after it) because msg is extracted just before as a word.

Note that when you input a word with the keyboard, you also hit the enter key which sends the new-line separator in the standard input. At this time, the terminal makes this line available to scanf(), the word is known as ended and the separator is ignored (in this specific situation, but we could obtain it with fgetc()).

For example, if in the standard input of the parent we input "abc\n", the parent obtains the word "abc" which is sent as is to the child. Then the child receives a word starting with "abc" but not ended yet: scanf("%s") is still waiting some other characters after c to make this word longer or a separator or EOF to detect the end of this word.

You can for example send a separator after this word.

// Send to child
write(fd_p2c[1], msg, strlen(msg));
char lf='\n';
write(fd_p2c[1], &lf, 1);

Or maybe it's better to rely on fgets() (instead of scanf("%s")) to obtain a line (not just a word) both in the child and the parent.

By the way, the while/read in the parent looks weird to me. I would do something like this.

// Read from child
buf_length = (int)read(fd_c2p[0], buf, sizeof(buf) - 1);
if (buf_length <= 0) {
  break;
}

Upvotes: 1

Some programmer dude
Some programmer dude

Reputation: 409176

One problem is in the child-process with:

scanf("%s", child_read);

With the %s format there's only three things that will stop scanf from waiting for more input:

  1. Error
  2. End of file
  3. Space

Assuming nothing goes wrong, there will be no errors. And since the parent process keeps the pipe open there will be no end of file. And since the parent process writes only what it itself reads with scanf("%s", ...) there will be no spaces in the data sent.

All in all, the child process will wait indefinitely for scanf to return, which it never will.

Upvotes: 2

Related Questions