C – Monitor bytes in stdin, stdout, and stderr in C

Monitor bytes in stdin, stdout, and stderr in C… here is a solution to the problem.

Monitor bytes in stdin, stdout, and stderr in C

I need to calculate how many bytes are sent to the child process via stdin, and the number of bytes the child process writes to stdout and stderr. The child process calls execvp, so I can’t monitor these statistics from inside the process itself. My current strategy involves creating 3 additional child processes, each of which monitors each std stream through the pipe (or in the case of stdin, just reading from stdin).

This tactic looks really fragile at best, and I’m doing something weird that makes it impossible for processes monitoring stdout/err to read from the end of their respective pipes (and hang them indefinitely). The code is as follows.

This creates three worker child processes and should allow them to calculate statistics:

void controles(struct fds *des)
{
    int ex[2];
    int err[2];

int n_in = 0;
    int c_in;

int n_ex = 0;
    int c_ex;

int n_err = 0;
    int c_err;

pipe(ex);
    pipe(err);

/*has two fields, for the write end of the stdout pipe and the stderr pipe. */
    des->err = err[1];
    des->ex = ex[1];
    switch (fork()) {
    case 0:     /*stdin */
        while (read(0, &c_in, 1) == 1)
            n_in++;
        if (n_in > 0)
            printf("%d bytes to stdin\n", n_in);
        exit(n_in);
    default:
        break;
    }

switch (fork()) {
    case 0:     /*stdout */
        close(ex[1]);
        /*pretty sure this is wrong */
        while (read(ex[0], &c_ex, 1) == 1) {
            n_ex++;
            write(1, &c_ex, 1);
        }
        if (n_ex > 0)
            printf("%d bytes to stdout\n", n_ex);
        close(ex[0]);
        exit(n_ex);
    default:
        close(ex[0]);
    }
    switch (fork()) {
    case 0:     /*error */
        close(err[1]);
        /*also probably have a problem here */
        while (read(err[0], &c_err, 1) == 1) {
            n_err++;
            write(2, &c_err, 1);
        }
        if (n_err > 0)
            printf("%d bytes to stderr\n", n_err);
        close(err[0]);
        exit(n_err);
    default:
        close(err[0]);
    }
}

This is a code snippet (in a child process) that sets up two FDs from the FDS structure so that the child process should write to the pipe instead of stdin/stderr.

    dup2(des.ex, 1);
dup2(des.err, 2);
close(des.ex); close(des.err); /*Is this right?*/
execvp(opts->exec, opts->options); /*sure this is working fine*/

I got lost and any help would be appreciated.

Solution

I think your code could be improved by breaking it down a bit; The accounting and copying routines are basically the same task, and if you choose to continue using multiple processes, you can simply write :

void handle_fd_pair(char *name, int in, int out) {
    char buf[1024];
    int count = 0, n;
    char fn[PATH_MAX];
    snprintf(fn, PATH_MAX - 1, "/tmp/%s_count", name);
    fn[PATH_MAX-1] = '\0';
    FILE *output = fopen(fn, "w");
    /* handle error */

while((n = read(in, buf, 1024)) > 0) {
        count+=n;
        writen(out, buf, n); /* see below */
    }

fprintf(output, "%s copied %d bytes\n", name, count);
    fclose(output);
}

Instead of writing one character at a time, we can handle partial writes using Advanced Programming in the Unix Environment. This is inefficient for medium data volumes. Source code:

ssize_t             /* Write "n" bytes to a descriptor  */
writen(int fd, const void *ptr, size_t n)
{
  size_t      nleft;
  ssize_t     nwritten;

nleft = n;
  while (nleft > 0) {
      if ((nwritten = write(fd, ptr, nleft)) < 0) {
          if (nleft == n)
              return(-1); /* error, return -1 */
          else
              break;      /* error, return amount written so far */
      } else if (nwritten == 0) {
          break;
      }
      nleft -= nwritten;
      ptr   += nwritten;
  }
  return(n - nleft);      /* return >= 0 */
}

With the assistant, I think the rest will be easier. Fork one
Each stream is a new child, and gives in[0] read, out[1] and
err[1] writes the end of the pipe to a subkey.

All those close() calls for every child are very ugly, but attempted
Write a small wrapper around the array of all fds and dispense
Those passed as parameters, also look cumbersome.

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

#ifndef PATH_MAX
#define PATH_MAX 128
#endif

void handle_fd_pair(char *name, int in, int out) {
    char buf[1024];
    int count = 0, n;
    char fn[PATH_MAX];
    snprintf(fn, PATH_MAX - 1, "/tmp/%s_count", name);
    fn[PATH_MAX-1] = '\0';
    FILE *output = fopen(fn, "w");
    /* handle error */

while((n = read(in, buf, 1024)) > 0) {
        count+=n;
        writen(out, buf, n); /* see below */
    }

fprintf(output, "%s copied %d bytes\n", name, count);
    fclose(output);
}

int main(int argc, char* argv[]) {
    int in[2], out[2], err[2];
    pid_t c1, c2, c3;

pipe(in);
    pipe(out);
    pipe(err);

if ((c1 = fork()) < 0) {
        perror("can't fork first child");
        exit(1);
    } else if (c1 == 0) {
        close(in[0]);
        close(out[0]);
        close(out[1]);
        close(err[0]);
        close(err[1]);
        handle_fd_pair("stdin", 0, in[1]);
        exit(0);
    }

if ((c2 = fork()) < 0) {
        perror("can't fork second child");
        exit(1);
    } else if (c2 == 0) {
        close(in[0]);
        close(in[1]);
        close(out[1]);
        close(err[0]);
        close(err[1]);
        handle_fd_pair("stdout", out[0], 1);
        exit(0);
    }

if ((c3 = fork()) < 0) {
        perror("can't fork third child");
        exit(1);
    } else if (c3 == 0) {
        close(in[0]);
        close(in[1]);
        close(out[0]);
        close(out[1]);
        close(err[1]);
        handle_fd_pair("stderr", err[0], 2);
        exit(0);
    }

/* parent falls through to here, no children */

close(in[1]);
   close(out[0]);
   close(err[0]);
   close(0);
   close(1);
   close(2);
   dup2(in[0], 0);
   dup2(out[1], 1);
   dup2(err[1], 2);
   system(argv[1]);
   exit(1); /* can't reach */
}

It seems to work for toy applications anyway 🙂

$ ./dup cat
hello
hello
$ ls -l *count
-rw-r--r-- 1 sarnold sarnold 22 2011-05-26 17:41 stderr_count
-rw-r--r-- 1 sarnold sarnold 21 2011-05-26 17:41 stdin_count
-rw-r--r-- 1 sarnold sarnold 22 2011-05-26 17:41 stdout_count
$ cat *count
stderr copied 0 bytes
stdin copied 6 bytes
stdout copied 6 bytes

I think it’s worth pointing out that you can implement this as well
There is only one process of the program, and select (2) is used to determine which
File descriptors need to be read and written.

Related Problems and Solutions