///////////////
// lightweight connection from qmail to filters e.g. spamassassin
// (hi-q filter, get it?)

// TODO:  Panic stop should signal all children.
// TODO:  Possibly:  Wait for all kids in parallel?
//	That's because they might finish out of order.

#include <unistd.h>
#include <stdlib.h>		/* for malloc() */
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>		/* for fork(), wait() */
#include <sys/stat.h>
#include <sys/wait.h>

//  error exit codes, as stated in qmail.c
const int ex_spam = 21;
const int ex_syserr = 71;
const int ex_comerr = 74;

#define bufsize 16384

void panic(const int sts) {
  // FIXME: stop other children
  exit(sts);
}

void slurp(const int inch, const int ouch){
  char buf[bufsize];  
  ssize_t todo;
  for (;;) {
    ssize_t got = read(inch, buf, bufsize);
    if (got == 0) {		// EoF
      break;
    }
    if (got < 0) {
      fprintf(stderr, "hi-q: input error: ");
      perror(0);
      panic(ex_comerr);
    }

    todo = got;
    while (todo) {
      ssize_t sent = write(ouch, buf, todo);
      if (sent < 0 && errno != EINTR) {
	fprintf(stderr, "hi-q: output error: ");
	perror(0);
	panic(ex_comerr);
      }
      todo -= sent;
    }
  }
}


void probe_fd(){
  int ii;
  struct stat buf;
  for (ii = 0; ii < 16; ii++) {
    int rslt = fstat(ii, &buf);
    fprintf(stderr, "fd %2d status %2d", ii, rslt);
    if (rslt==0) 
      fprintf(stderr, " : %d", (int)buf.st_dev);
    fprintf(stderr, "\n");
  }
  fprintf(stderr, "============\n");
}


void blurb(const int ii, const pid_t* kidpid) {
  int kidstatus;
  /*pid_t somekid = */ waitpid(kidpid[ii], &kidstatus, WUNTRACED);
  if (WIFEXITED(kidstatus)) 
    fprintf(stderr, "kid #%d (%d) exited with status %d\n", 
	      ii, kidpid[ii], WEXITSTATUS(kidstatus));
  if (WIFSIGNALED(kidstatus))
    fprintf(stderr, "kid #%d (%d) killed by signal %d\n", 
	      ii, kidpid[ii], WTERMSIG(kidstatus));

}

////////////////////////////////////////
// Here with data coming in on fd 0.
// and control coming in on ft 1.

int main(int argc, char* argv[], char* env[]) {
  int kidstatus;
  pid_t somekid;

  int rslt;
  int loose_end = 0;

#ifdef SpareStuff
  char* slurp2_args[] = {"/home/jsd/hack/slurp2", 0};
  char* echo_args[] = {"/bin/echo", "hi there", 0};
  char* wc_args[] = {"/usr/bin/wc", 0};
  char* cat_args[] = {"/bin/cat", 0};
  char* spama_args[] = {"/usr/local/bin/spamassassin", "-e", 0};
  char* spamc_args[] = {"/usr/local/bin/spamc", "-Z", "7", 0};
  char* qq_args[] = {"/var/qmail/bin/qmail-queue", 0};
#endif
  char* spamc_args[] = {"/usr/local/bin/spamc", "-Z", "7", 0};
  char* qq_args[] = {"/var/qmail/bin/qmail-queue", 0};
  
#ifdef testing
  char** todo[] = {
    cat_args,
    slurp2_args,
    0		// required: zero terminates the list
  };
#else
  char** todo[] = {
    spamc_args,
    qq_args,
    0		// required: zero terminates the list
  };
#endif

  int nkids;
  pid_t* kidpid;	// indexed by kid number

  for (nkids = 0; todo[nkids]; nkids++) {}  // count 'em
  kidpid = malloc(nkids * sizeof(pid_t));

// At this point, there is some loop invariants;
// (a) fd0 is open and ready for the next child to read, and
// (b) fd1 is open but is something children can (and should) 
//  throw away as soon as convenient.

  {int ii; for (ii=0; todo[ii]; ii++){	/* loop over all kids */
    int datapipe[2];
    int kid_end;
    int lastkid = !todo[ii+1];
#define flip(a,b) (lastkid ? b : a)

//xx fprintf(stderr, "Top of loop %d loose: %d\n", ii, loose_end);

// Create a pipe, which will be used to connect 
// this child's fd1 to the next child's fd0 ...
// except for the last kid, which reads fd1 rather
// than writing it.
    rslt = pipe(datapipe);
    if (rslt < 0) {
      fprintf(stderr, "hi-q: could not create datapipe: ");
      perror(0);
      panic(ex_syserr);
    }

//xx fprintf(stderr, "pipe: %d %d\n", datapipe[0], datapipe[1]);
    if (loose_end) {
      close(0);
      dup2(loose_end, 0);
      close(loose_end);
    }

    loose_end = datapipe[flip(0,1)];
    kid_end = datapipe[flip(1,0)];

    kidpid[ii] = fork();
    if (!kidpid[ii]) {  /*** child code ***/
      char** prog;

// Now that we are through creating pipes, get rid
// of the placeholder:
      close(1);

      close(loose_end); 	 // the reading end is none of our business

      rslt = dup2(kid_end, 1);
      if (rslt < 0) {
	fprintf(stderr, "hi-q: dup2(kid(%d),1) failed: ", kid_end);
	perror(0);
	exit(ex_syserr);
      }

      close(kid_end);		// use fd1 instead now
      // OK, at this point we are set up to read fd0
      // and write fd1 (except last kid reads fd1).
////  probe_fd();

      prog = todo[ii];
      rslt = execve(prog[0], prog, env);
      fprintf(stderr, "hi-q: failed to exec '%s': ", prog[0]);
      perror(0);
      exit(ex_syserr);
    }

    /*** parent code ***/
    if (kidpid[ii] < 0) {
      fprintf(stderr, "hi-q: failure to fork kid#%d: ", ii);
      perror(0);
      panic(ex_syserr);
    }
    close(kid_end);
#ifdef more_testing
    fprintf(stderr, "forked kid #%d (%d) piping %d\n", 
    		ii, kidpid[ii], kid_end);
//    sleep(1);	/* let kid run a while */
#endif
  }}

// here with the whole pipeline of kids running

  close(0);		// the reading end of stdin was
  			// delegated to the first child


  {int ii; for (ii=0; ii<nkids-1; ii++){	/* loop over N-1 kids */
  					/* not last kid */
  
#ifdef testing
    blurb(ii, kidpid);
#else
    somekid = waitpid(kidpid[ii], &kidstatus, WUNTRACED);
    if (WIFEXITED(kidstatus)) {
      if (WEXITSTATUS(kidstatus) == 1) panic(ex_spam);
      if (WEXITSTATUS(kidstatus) != 0) panic(ex_syserr);
      /* otherwise kidstatus==0 and we fall through */
    } 
    else panic(ex_syserr);	// any kill, not a normal exit
#endif
  
  
  }}

//xx fprintf(stderr, "slurping %d %d\n", 1, loose_end);
  slurp(1, loose_end);
  close(1);
  close(loose_end);

  {
    int ii = nkids-1;

#ifdef moretesting
fprintf(stderr, "About to wait for kid #%d (%d)\n", 
  		ii, kidpid[ii]);
    blurb(nkids-1, kidpid);
#endif
    somekid = waitpid(kidpid[ii], &kidstatus, WUNTRACED);
    if (WIFEXITED(kidstatus)) return WEXITSTATUS(kidstatus);
  }

#ifdef testing
  sleep(1);
#endif
  return ex_syserr;		// any kill, not a normal exit

}

