test/t40e.c
/* t40e.c
 *
 * Test sockets
 *
 * Select works on regular files, (pseudo) terminal devices, streams-based
 * files, FIFOs, pipes, and sockets. This test verifies selecting for sockets.
 *
 * This test is part of a bigger select test. It expects as argument which sub-
 * test it is.
 *
 * Specific rules for sockets:
 * If a socket has a pending error, it shall be considered to have an
 * exceptional condition pending. Otherwise, what constitutes an exceptional
 * condition is file type-specific. For a file descriptor for use with a
 * socket, it is protocol-specific except as noted below. For other file types
 * it is implementation-defined. If the operation is meaningless for a
 * particular file type, pselect() or select() shall indicate that the
 * descriptor is ready for read or write operations, and shall indicate that
 * the descriptor has no exceptional condition pending.
 *
 * [1] If a descriptor refers to a socket, the implied input function is the
 * recvmsg()function with parameters requesting normal and ancillary data, such
 * that the presence of either type shall cause the socket to be marked as
 * readable. The presence of out-of-band data shall be checked if the socket
 * option SO_OOBINLINE has been enabled, as out-of-band data is enqueued with
 * normal data. If the socket is currently listening, then it shall be marked
 * as readable if an incoming connection request has been received, and a call
 * to the accept() function shall complete without blocking.
 *
 * [2] If a descriptor refers to a socket, the implied output function is the
 * sendmsg() function supplying an amount of normal data equal to the current
 * value of the SO_SNDLOWAT option for the socket. If a non-blocking call to
 * the connect() function has been made for a socket, and the connection
 * attempt has either succeeded or failed leaving a pending error, the socket
 * shall be marked as writable.
 * 
 * [3] A socket shall be considered to have an exceptional condition pending if
 * a receive operation with O_NONBLOCK clear for the open file description and
 * with the MSG_OOB flag set would return out-of-band data without blocking.
 * (It is protocol-specific whether the MSG_OOB flag would be used to read
 * out-of-band data.) A socket shall also be considered to have an exceptional
 * condition pending if an out-of-band data mark is present in the receive
 * queue. Other circumstances under which a socket may be considered to have an
 * exceptional condition pending are protocol-specific and
 * implementation-defined.
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <time.h>
#include <assert.h>
#include <netdb.h>

#define DO_HANDLEDATA 1
#define DO_PAUSE 3
#define DO_TIMEOUT 7
#define MYPORT 3490
#define NUMCHILDREN 5
#define MAX_ERROR 10

int errct = 0, subtest = -1;
char errbuf[1000];

void e(int n, char *s) {
  printf("Subtest %d, error %d, %s\n", subtest, n, s);

  if (errct++ > MAX_ERROR) {
    printf("Too many errors; test aborted\n");
    exit(errct);
  }
}

/* All *_fds routines are helping routines. They intentionally use FD_* macros
   in order to prevent making assumptions on how the macros are implemented.*/

int count_fds(int nfds, fd_set *fds) {
  /* Return number of bits set in fds */
  int i, result = 0;
  assert(fds != NULL && nfds > 0);
  for(i = 0; i < nfds; i++) {
    if(FD_ISSET(i, fds)) result++;
  }
  return result;
}

int empty_fds(int nfds, fd_set *fds) {
  /* Returns nonzero if the first bits up to nfds in fds are not set */
  int i;
  assert(fds != NULL && nfds > 0);
  for(i = 0; i < nfds; i++) if(FD_ISSET(i, fds)) return 0;
  return 1;
}

int compare_fds(int nfds, fd_set *lh, fd_set *rh) {
  /* Returns nonzero if lh equals rh up to nfds bits */
  int i;
  assert(lh != NULL && rh != NULL && nfds > 0);
  for(i = 0; i < nfds; i++) {
    if((FD_ISSET(i, lh) && !FD_ISSET(i, rh)) ||
       (!FD_ISSET(i, lh) && FD_ISSET(i, rh))) {
      return 0;
    }
  }
  return 1;
}

void dump_fds(int nfds, fd_set *fds) {
  /* Print a graphical representation of bits in fds */
  int i;
  if(fds != NULL && nfds > 0) {
    for(i = 0; i < nfds; i++) printf("%d ", (FD_ISSET(i, fds) ? 1 : 0));
    printf("\n");
  }
}

void do_child(int childno) {
  int fd_sock, port;
  int retval;

  fd_set fds_read, fds_write, fds_error;
  fd_set fds_compare_write;

  struct hostent *he;
  struct sockaddr_in server;
  struct timeval tv;

  if((fd_sock = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
    perror("Error getting socket\n");
    exit(-1);
  }

  if((he = gethostbyname("127.0.0.1")) == NULL){/*"localhost" might be unknown*/
    perror("Error resolving");
    exit(-1);
  }

  /* Child 4 connects to the wrong port. See Actual testing description below.*/
  port = (childno == 3 ? MYPORT + 1 : MYPORT);

  memcpy(&server.sin_addr, he->h_addr_list[0], he->h_length);
  server.sin_family = AF_INET;
  server.sin_port = htons(port);

#if 0
  printf("Going to connect to: %s:%d\n", inet_ntoa(server.sin_addr),
	 ntohs(server.sin_port));
#endif

  /* Normally we'd zerofill sin_zero, but there is no such thing on Minix */
#ifndef _MINIX
  memset(server.sin_zero, '\0', sizeof server.sin_zero);
#endif

  /* Wait for parent to set up connection */
  tv.tv_sec = (childno <= 1 ? DO_PAUSE : DO_TIMEOUT);
  tv.tv_usec = 0;
  retval = select(0, NULL, NULL, NULL, &tv);

  /* All set, let's do some testing */
  /* Children 3 and 4 do a non-blocking connect */
  if(childno == 2 || childno == 3)
    fcntl(fd_sock, F_SETFL, fcntl(fd_sock, F_GETFL, 0) | O_NONBLOCK);
 
  if(connect(fd_sock, (struct sockaddr *) &server, sizeof(server)) < 0) {
    /* Well, we don't actually care. The connect is non-blocking and is
       supposed to "in progress" at this point. */
  }

  if(childno == 2 || childno == 3) { /* Children 3 and 4 */
    /* Open Group: "If a non-blocking call to the connect() function has been
       made for a socket, and the connection attempt has either succeeded or
       failed leaving a pending error, the socket shall be marked as writable.
       ...
       A socket shall be considered to have an exceptional condition pending if
       a receive operation with O_NONBLOCK clear for the open file description
       and with the MSG_OOB flag set would return out-of-band data without
       blocking. (It is protocol-specific whether the MSG_OOB flag would be used
       to read out-of-band data.) A socket shall also be considered to have an
       exceptional condition pending if an out-of-band data mark is present in
       the receive queue. Other circumstances under which a socket may be
       considered to have an exceptional condition pending are protocol-specific
       and implementation-defined."

       In other words, it only makes sense for us to check the write set as the
       read set is not expected to be set, but is allowed to be set (i.e.,
       unspecified) and whether the error set is set is implementation-defined.
    */
    FD_ZERO(&fds_read); FD_ZERO(&fds_write); FD_ZERO(&fds_error);
    FD_SET(fd_sock, &fds_write);
    tv.tv_sec = DO_TIMEOUT;
    tv.tv_usec = 0;
    retval = select(fd_sock+1, NULL, &fds_write, NULL, &tv);
    

    if(retval <= 0) e(6, "expected one fd to be ready");
    
    FD_ZERO(&fds_compare_write); FD_SET(fd_sock, &fds_compare_write);
    if(!compare_fds(fd_sock+1, &fds_compare_write, &fds_compare_write))
      e(7, "write should be set");
  }

  if(close(fd_sock) < 0) {
    perror("Error disconnecting");
    exit(-1);
  }

  exit(errct);
}

void do_parent(void) {
#ifndef _MINIX
  int yes = 1;
#endif
  int fd_sock, fd_new, exitstatus;
  int sockets[NUMCHILDREN], i;
  fd_set fds_read, fds_write, fds_error;
  fd_set fds_compare_read, fds_compare_write;
  struct timeval tv;
  int retval, childresults = 0;

  struct sockaddr_in my_addr;
  struct sockaddr_in other_addr;
  socklen_t other_size;

  if((fd_sock = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
    perror("Error getting socket\n");
    exit(-1);
  }

  my_addr.sin_family = AF_INET;
  my_addr.sin_port = htons(MYPORT); /* Short, network byte order */
  my_addr.sin_addr.s_addr = INADDR_ANY;
  /* Normally we'd zerofill sin_zero, but there is no such thing on Minix */
#ifndef _MINIX
  memset(my_addr.sin_zero, '\0', sizeof my_addr.sin_zero);
#endif
  
  /* Reuse port number. Not implemented in Minix. */
#ifndef _MINIX
  if(setsockopt(fd_sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) < 0) {
    perror("Error setting port reuse option");
    exit(-1);
  }
#endif

  /* Bind to port */
  if(bind(fd_sock, (struct sockaddr *) &my_addr, sizeof my_addr) < 0) {
    perror("Error binding to port");
    exit(-1);
  }
  
  /* Mark socket to be used for incoming connections */
  if(listen(fd_sock, 20) < 0) {
    perror("Listen");
    exit(-1);
  }
    
  /*                              Actual testing                              */
  /* While sockets resemble file descriptors, they are not the same at all.
     We can read/write from/to and close file descriptors, but we cannot open
     them O_RDONLY or O_WRONLY; they are always O_RDWR (other flags do not make
     sense regarding sockets). As such, we cannot provide wrong file descriptors
     to select, except for descriptors that are not in use.
     We will test standard behavior and what is described in [2]. [1] and [3]
     are not possible to test on Minix, as Minix does not support OOB data. That
     is, the TCP layer can handle it, but there is no socket interface for it.
     Our test consists of waiting for input from the first two children and
     waiting to write output [standard usage]. Then the first child closes its
     connection we select for reading. This should fail with error set. Then we
     close child number two on our side and select for reading. This should fail
     with EBADF. Child number three shall then do a non-blocking connect (after
     waiting for DO_PAUSE seconds) and do a select, resulting in being marked
     ready for writing. Subsequently child number four also does a non-blocking
     connect to loclhost on MYPORT+1 (causing the connect to fail) and then does
     a select. This should result in write and error being set (error because of
     pending error).
  */

  /* Accept and store connections from the first two children */
  other_size = sizeof(other_addr);
  for(i = 0; i < 2; i++) {
    fd_new = accept(fd_sock, (struct sockaddr *) &other_addr, &other_size);
    if(fd_new < 0) break;
    sockets[i] = fd_new;
  }

  /* If we break out of the for loop, we ran across an error and want to exit.
     Check whether we broke out. */
  if(fd_new < 0) {
    perror("Error accepting connection");
    exit(-1);
  }

  /* Select error condition checking */
  for(childresults = 0; childresults < 2; childresults++) {
    FD_ZERO(&fds_read); FD_ZERO(&fds_write); FD_ZERO(&fds_error);
    FD_SET(sockets[childresults], &fds_read);
    FD_SET(sockets[childresults], &fds_write);
    FD_SET(sockets[childresults], &fds_error);
    tv.tv_sec = DO_TIMEOUT;
    tv.tv_usec = 0;
    
    retval = select(sockets[childresults]+1, &fds_read, &fds_write, &fds_error,
		    &tv);
    
    if(retval <= 0) {
      snprintf(errbuf, sizeof(errbuf),
	       "two fds should be set%s", (retval == 0 ? " (TIMEOUT)" : ""));
      e(1, errbuf);
    }

    FD_ZERO(&fds_compare_read); FD_ZERO(&fds_compare_write);
    FD_SET(sockets[childresults], &fds_compare_write);

    /* We can't say much about being ready for reading at this point or not. It
       is not specified and the other side might have data ready for us to read
    */
    if(!compare_fds(sockets[childresults]+1, &fds_compare_write, &fds_write))
      e(2, "write should be set");

    if(!empty_fds(sockets[childresults]+1, &fds_error))
      e(3, "no error should be set");
  }


  /* We continue by accepting a connection of child 3 */
  fd_new = accept(fd_sock, (struct sockaddr *) &other_addr, &other_size);
  if(fd_new < 0) {
    perror("Error accepting connection\n");
    exit(-1);
  }
  sockets[2] = fd_new;
   
  /* Child 4 will never connect */

  /* Child 5 is still pending to be accepted. Open Group: "If the socket is
     currently listening, then it shall be marked as readable if an incoming
     connection request has been received, and a call to the accept() function
     shall complete without blocking."*/
  FD_ZERO(&fds_read);
  FD_SET(fd_sock, &fds_read);
  tv.tv_sec = DO_TIMEOUT;
  tv.tv_usec = 0;
  retval = select(fd_sock+1, &fds_read, NULL, NULL, &tv);
  if(retval <= 0) {
    snprintf(errbuf, sizeof(errbuf),
	     "one fd should be set%s", (retval == 0 ? " (TIMEOUT)" : ""));
    e(4, errbuf);
  }

  /* Check read bit is set */
  FD_ZERO(&fds_compare_read); FD_SET(fd_sock, &fds_compare_read);
  if(!compare_fds(fd_sock+1, &fds_compare_read, &fds_read))
    e(5, "read should be set");


  /* Accept incoming connection to unblock child 5 */
  fd_new = accept(fd_sock, (struct sockaddr *) &other_addr, &other_size);
  if(fd_new < 0) {
    perror("Error accepting connection\n");
    exit(-1);
  }
  sockets[4] = fd_new;


  /* We're done, let's wait a second to synchronize children and parent. */
  tv.tv_sec = DO_HANDLEDATA;
  tv.tv_usec = 0;
  select(0, NULL, NULL, NULL, &tv);

  /* Close connection with children. */
  for(i = 0; i < NUMCHILDREN; i++) {
    if(i == 3) /* No need to disconnect child 4 that failed to connect. */
      continue;

    if(close(sockets[i]) < 0) {
      perror(NULL);
    }
  }

  /* Close listening socket */
  if(close(fd_sock) < 0) {
    perror("Closing listening socket");
    errct++;
  }

  for(i = 0; i < NUMCHILDREN; i++) {
    wait(&exitstatus); /* Wait for children */
    if(exitstatus > 0)
      errct += WEXITSTATUS(exitstatus); /* and count their errors, too. */
  }

  exit(errct);
}

int main(int argc, char **argv) {
  int forkres, i;

  /* Get subtest number */
  if(argc != 2) {
    printf("Usage: %s subtest_no\n", argv[0]);
    exit(-2);
  } else if(sscanf(argv[1], "%d", &subtest) != 1) {
    printf("Usage: %s subtest_no\n", argv[0]);
    exit(-2);
  }
  
  /* Fork off a bunch of children */
  for(i = 0; i < NUMCHILDREN; i++) {
      forkres = fork();
      if(forkres == 0) do_child(i);
      else if(forkres < 0) {
	perror("Unable to fork");
	exit(-1);
      }
  }
  /* do_child always calls exit(), so when we end up here, we're the parent. */
  do_parent();

  exit(-2); /* We're not supposed to get here. Both do_* routines should exit.*/
}
