#include "cvs.h"

/* for select */
#include <sys/types.h>
#include <sys/time.h>

#if HAVE_SYS_SELECT_H
#include <sys/select.h>
#endif

#if HAVE_FCNTL_H
#include <fcntl.h>
#endif

#ifndef O_NONBLOCK
#define O_NONBLOCK O_NDELAY
#endif


/* Functions which the server calls.  */
int add PROTO((int argc, char **argv));
int admin PROTO((int argc, char **argv));
int checkout PROTO((int argc, char **argv));
int commit PROTO((int argc, char **argv));
int diff PROTO((int argc, char **argv));
int history PROTO((int argc, char **argv));
int import PROTO((int argc, char **argv));
int cvslog PROTO((int argc, char **argv));
int patch PROTO((int argc, char **argv));
int release PROTO((int argc, char **argv));
int cvsremove PROTO((int argc, char **argv));
int rtag PROTO((int argc, char **argv));
int status PROTO((int argc, char **argv));
int tag PROTO((int argc, char **argv));
int update PROTO((int argc, char **argv));

void server_cleanup PROTO((int sig));

/*
 * This is where we stash stuff we are going to use.  Format string
 * which expects a single directory within it, starting with a slash.
 */
static char *server_temp_dir;

/* Nonzero if we should keep the temp directory around after we exit.  */
static int dont_delete_temp;

/*
 * Zero if compression isn't supported or requested; non-zero to indicate
 * a compression level to request from gzip.
 */
int gzip_level;

static char no_mem_error;
#define NO_MEM_ERROR (&no_mem_error)

static void server_write_entries PROTO((void));

/*
 * Read a line from the stream "instream" without command line editing.
 *
 * Action is compatible with "readline", e.g. space for the result is
 * malloc'd and should be freed by the caller.
 *
 * A NULL return means end of file.  A return of NO_MEM_ERROR means
 * that we are out of memory.
 */
static char *read_line PROTO((FILE *));

static char *
read_line (stream)
    FILE *stream;
{
    int c;
    char *result;
    int input_index = 0;
    int result_size = 80;

    fflush (stdout);
    result = (char *) malloc (result_size);
    if (result == NULL)
	return NO_MEM_ERROR;
    
    while (1)
    {
	c = fgetc (stream);
	
	if (c == EOF)
	{
	    free (result);
	    return NULL;
	}
	
	if (c == '\n')
	    break;
	
	result[input_index++] = c;
	while (input_index >= result_size)
	{
	    result_size *= 2;
	    result = (char *) realloc (result, result_size);
	    if (result == NULL)
		return NO_MEM_ERROR;
	}
    }
    
    result[input_index++] = '\0';
    return result;
}

/*
 * Make directory DIR, including all intermediate directories if necessary.
 * Returns 0 for success or errno code.
 */
static int mkdir_p PROTO((char *));

static int
mkdir_p (dir)
     char *dir;
{
    char *p;
    char *q = malloc (strlen (dir) + 1);
    int retval;
    struct stat dummy;

    if (q == NULL)
	return ENOMEM;

    /*
     * Skip over leading slash if present.  We won't bother to try to
     * make '/'.
     */
    p = dir + 1;
    while (1)
    {
	while (*p != '/' && *p != '\0')
	    ++p;
	if (*p == '/')
	{
	    strncpy (q, dir, p - dir);
	    q[p - dir] = '\0';

	    /* If the directory doesn't exist, try to create it.  */
	    if (stat (q, &dummy) < 0
		&& mkdir (q, 0777) < 0)
	    {
	        retval = errno;
		goto done;
	    }
	    ++p;
	}
	else
	{
	    if (mkdir (dir, 0777) < 0)
		retval = errno;
	    else
		retval = 0;
	    goto done;
	}
    }
  done:
    free (q);
    return retval;
}

/*
 * Print the error response for error code STATUS.  The caller is
 * reponsible for making sure we get back to the command loop without
 * any further output occuring.
 */
static void
print_error (status)
    int status;
{
    char *msg;
    printf ("error  ");
    msg = strerror (status);
    if (msg)
	printf ("%s", msg);
    printf ("\n");
}

static int pending_error;
/*
 * Malloc'd text for pending error.  Each line must start with "E ".  The
 * last line should not end with a newline.
 */
static char *pending_error_text;

/* If an error is pending, print it and return 1.  If not, return 0.  */
static int
print_pending_error ()
{
    if (pending_error_text)
    {
	printf ("%s\n", pending_error_text);
	if (pending_error)
	    print_error (pending_error);
	else
	    printf ("error  \n");
	pending_error = 0;
	free (pending_error_text);
	pending_error_text = NULL;
	return 1;
    }
    else if (pending_error)
    {
	print_error (pending_error);
	pending_error = 0;
	return 1;
    }
    else
	return 0;
}

/* Is an error pending?  */
#define error_pending() (pending_error || pending_error_text)

int
supported_response (name)
     char *name;
{
    struct response *rs;

    for (rs = responses; rs->name != NULL; ++rs)
	if (strcmp (rs->name, name) == 0)
	    return rs->status == rs_supported;
    error (1, 0, "internal error: testing support for unknown response?");
}

static void
serve_valid_responses (arg)
     char *arg;
{
    char *p = arg;
    char *q;
    struct response *rs;
    do
    {
	q = strchr (p, ' ');
	if (q != NULL)
	    *q++ = '\0';
	for (rs = responses; rs->name != NULL; ++rs)
	{
	    if (strcmp (rs->name, p) == 0)
		break;
	}
	if (rs->name == NULL)
	    /*
	     * It is a response we have never heard of (and thus never
	     * will want to use).  So don't worry about it.
	     */
	    ;
	else
	    rs->status = rs_supported;
	p = q;
    } while (q != NULL);
    for (rs = responses; rs->name != NULL; ++rs)
    {
	if (rs->status == rs_essential)
	{
	    printf ("E response `%s' not supported by client\nerror  \n",
		    rs->name);
	    exit (1);
	}
	else if (rs->status == rs_optional)
	    rs->status = rs_not_supported;
    }
}

static int use_dir_and_repos = 0;

static void
serve_root (arg)
    char *arg;
{
    char *env;
    extern char *CVSroot;
    char path[PATH_MAX];
    int save_errno;
    
    if (error_pending()) return;
    
    (void) sprintf (path, "%s/%s", arg, CVSROOTADM);
    if (access (path, R_OK | X_OK))
    {
	save_errno = errno;
	printf ("E Cannot access %s\n", path);
	pending_error = save_errno;
    }
    (void) strcat (path, "/");
    (void) strcat (path, CVSROOTADM_HISTORY);
    if (isfile (path) && access (path, R_OK | W_OK))
    {
	save_errno = errno;
	printf ("E \
Sorry, you don't have read/write access to the history file %s", path);
	pending_error = save_errno;
    }

    CVSroot = malloc (strlen (arg) + 1);
    if (CVSroot == NULL)
    {
	pending_error = ENOMEM;
	return;
    }
    strcpy (CVSroot, arg);
#ifdef HAVE_PUTENV
    env = malloc (strlen (CVSROOT_ENV) + strlen (CVSroot) + 1 + 1);
    if (env == NULL)
    {
	pending_error = ENOMEM;
	return;
    }
    (void) sprintf (env, "%s=%s", CVSROOT_ENV, arg);
    (void) putenv (env);
    /* do not free env, as putenv has control of it */
#endif
}

/*
 * Add as many directories to the temp directory as the client tells us it
 * will use "..", so we never try to access something outside the temp
 * directory via "..".
 */
static void
serve_max_dotdot (arg)
    char *arg;
{
    int lim = atoi (arg);
    int i;
    char *p;

    if (lim < 0)
	return;
    p = malloc (strlen (server_temp_dir) + 2 * lim + 10);
    if (p == NULL)
    {
	pending_error = ENOMEM;
	return;
    }
    strcpy (p, server_temp_dir);
    for (i = 0; i < lim; ++i)
	strcat (p, "/d");
    free (server_temp_dir);
    server_temp_dir = p;
}

static void
dirswitch (dir, repos)
    char *dir;
    char *repos;
{
    char *dirname;
    int status;
    FILE *f;

    server_write_entries ();

    if (error_pending()) return;

    dirname = malloc (strlen (server_temp_dir) + strlen (dir) + 40);
    if (dirname == NULL)
    {
	pending_error = ENOMEM;
	return;
    }
    
    strcpy (dirname, server_temp_dir);
    strcat (dirname, "/");
    strcat (dirname, dir);

    status = mkdir_p (dirname);	
    if (status != 0
	&& status != EEXIST)
    {
	pending_error = status;
	return;
    }
    if (chdir (dirname) < 0)
    {
	pending_error = errno;
	return;
    }
    /*
     * This is pretty much like calling Create_Admin, but Create_Admin doesn't
     * report errors in the right way for us.
     */
    if (mkdir ("CVS", 0777) < 0)
    {
	if (errno == EEXIST)
	    /* Don't create the files again.  */
	    return;
	pending_error = errno;
	return;
    }
    f = fopen (CVSADM_REP, "w");
    if (f == NULL)
    {
	pending_error = errno;
	return;
    }
    if (fprintf (f, "%s\n", repos) == EOF)
    {
	pending_error = errno;
	fclose (f);
	return;
    }
    if (fclose (f) == EOF)
    {
	pending_error = errno;
	return;
    }
    f = fopen (CVSADM_ENT, "w+");
    if (f == NULL)
    {
	pending_error = errno;
	return;
    }
    if (fclose (f) == EOF)
    {
	pending_error = errno;
	return;
    }
    free (dirname);
}    

static void
serve_repository (arg)
    char *arg;
{
    dirswitch (arg + 1, arg);
}

static void
serve_directory (arg)
    char *arg;
{
    char *repos;
    use_dir_and_repos = 1;
    repos = read_line (stdin);
    if (repos == NULL)
    {
	pending_error_text = malloc (80 + strlen (arg));
	if (pending_error_text)
	{
	    if (feof (stdin))
		sprintf (pending_error_text,
			 "E end of file reading mode for %s", arg);
	    else
	    {
		sprintf (pending_error_text,
			 "E error reading mode for %s", arg);
		pending_error = errno;
	    }
	}
	else
	    pending_error = ENOMEM;
	return;
    }
    if (repos == NO_MEM_ERROR)
    {
	pending_error = ENOMEM;
	return;
    }
    dirswitch (arg, repos);
}

static void
serve_static_directory (arg)
    char *arg;
{
    FILE *f;
    f = fopen (CVSADM_ENTSTAT, "w+");
    if (f == NULL)
    {
	pending_error = errno;
	return;
    }
    if (fclose (f) == EOF)
    {
	pending_error = errno;
	return;
    }
}

static void
serve_sticky (arg)
    char *arg;
{
    FILE *f;
    f = fopen (CVSADM_TAG, "w+");
    if (f == NULL)
    {
	pending_error = errno;
	return;
    }
    if (fprintf (f, "%s\n", arg) == EOF)
    {
	pending_error = errno;
	return;
    }
    if (fclose (f) == EOF)
    {
	pending_error = errno;
	return;
    }
}

/*
 * Read SIZE bytes from stdin, write them to FILE.
 *
 * Currently this isn't really used for receiving parts of a file --
 * the file is still sent over in one chunk.  But if/when we get
 * spiffy in-process gzip support working, perhaps the compressed
 * pieces could be sent over as they're ready, if the network is fast
 * enough.  Or something.
 */
static void
receive_partial_file (size, file)
     int size;
     int file;
{
    char buf[16*1024], *bufp;
    int toread, nread, nwrote;
    while (size > 0)
    {
	toread = sizeof (buf);
	if (toread > size)
	    toread = size;

	nread = fread (buf, 1, toread, stdin);
	if (nread <= 0)
	{
	    if (feof (stdin))
	    {
		pending_error_text = malloc (80);
		if (pending_error_text)
		{
		    sprintf (pending_error_text,
			     "E premature end of file from client");
		    pending_error = 0;
		}
		else
		    pending_error = ENOMEM;
	    }
	    else if (ferror (stdin))
	    {
		pending_error_text = malloc (40);
		if (pending_error_text)
		    sprintf (pending_error_text,
			     "E error reading from client");
		pending_error = errno;
	    }
	    else
	    {
		pending_error_text = malloc (40);
		if (pending_error_text)
		    sprintf (pending_error_text,
			     "E short read from client");
		pending_error = 0;
	    }
	    return;
	}
	size -= nread;
	bufp = buf;
	while (nread)
	{
	    nwrote = write (file, bufp, nread);
	    if (nwrote < 0)
	    {
		pending_error_text = malloc (40);
		if (pending_error_text)
		    sprintf (pending_error_text, "E unable to write");
		pending_error = errno;
		return;
	    }
	    nread -= nwrote;
	    bufp += nwrote;
	}
    }
}

/* Receive SIZE bytes, write to filename FILE.  */
static void
receive_file (size, file, gzipped)
     int size;
     char *file;
     int gzipped;
{
    char *buf, *p;
    int fd, bytes_left, nbytes;
    char *arg = file;
    pid_t gzip_pid = 0;
    int gzip_status;

    /* Write the file.  */
    fd = open (arg, O_WRONLY | O_CREAT | O_TRUNC, 0600);
    if (fd < 0)
    {
	pending_error_text = malloc (40 + strlen (arg));
	if (pending_error_text)
	    sprintf (pending_error_text, "E cannot open %s", arg);
	pending_error = errno;
	return;
    }

    /*
     * FIXME: This doesn't do anything reasonable with gunzip's stderr, which
     * means that if gunzip writes to stderr, it will cause all manner of
     * protocol violations.
     */
    if (gzipped)
	fd = filter_through_gunzip (fd, 0, &gzip_pid);

    receive_partial_file (size, fd);

    if (pending_error_text)
    {
	char *p = realloc (pending_error_text,
			   strlen (pending_error_text) + strlen (arg) + 30);
	if (p)
	{
	    pending_error_text = p;
	    sprintf (p + strlen (p), ", file %s", arg);
	}
	/* else original string is supposed to be unchanged */
    }

    if (close (fd) < 0 && !error_pending ())
    {
	pending_error_text = malloc (40 + strlen (arg));
	if (pending_error_text)
	    sprintf (pending_error_text, "E cannot close %s", arg);
	pending_error = errno;
	if (gzip_pid)
	    waitpid (gzip_pid, (int *) 0, 0);
	return;
    }

    if (gzip_pid)
    {
	if (waitpid (gzip_pid, &gzip_status, 0) != gzip_pid)
	    error (1, errno, "waiting for gunzip process %d", gzip_pid);
	else if (gzip_status != 0)
	    error (1, 0, "gunzip exited %d", gzip_status);
    }
}

static void
serve_modified (arg)
     char *arg;
{
    int fd;
    char *buf = NULL;
    char *p;

    int size;
    char *size_text;
    char *mode_text;

    int nbytes, bytes_left, gzipped = 0;

    if (error_pending ()) return;

    mode_text = read_line (stdin);
    if (mode_text == NULL)
    {
	pending_error_text = malloc (80 + strlen (arg));
	if (pending_error_text)
	{
	    if (feof (stdin))
		sprintf (pending_error_text,
			 "E end of file reading mode for %s", arg);
	    else
	    {
		sprintf (pending_error_text,
			 "E error reading mode for %s", arg);
		pending_error = errno;
	    }
	}
	else
	    pending_error = ENOMEM;
	return;
    }
    if (mode_text == NO_MEM_ERROR)
    {
	pending_error = ENOMEM;
	return;
    }
    size_text = read_line (stdin);
    if (size_text == NULL)
    {
	pending_error_text = malloc (80 + strlen (arg));
	if (pending_error_text)
	{
	    if (feof (stdin))
		sprintf (pending_error_text,
			 "E end of file reading size for %s", arg);
	    else
	    {
		sprintf (pending_error_text,
			 "E error reading size for %s", arg);
		pending_error = errno;
	    }
	}
	else
	    pending_error = ENOMEM;
	return;
    }
    if (size_text == NO_MEM_ERROR)
    {
	pending_error = ENOMEM;
	return;
    }
    if (size_text[0] == 'z')
      {
	gzipped = 1;
	size = atoi (size_text + 1);
      }
    else
      size = atoi (size_text);
    free (size_text);

    if (size >= 0)
      {
	receive_file (size, arg, gzipped);
	if (error_pending ()) return;
      }

    {
	int status = change_mode (arg, mode_text);
	free (mode_text);
	if (status)
	{
	    pending_error_text = malloc (40 + strlen (arg));
	    if (pending_error_text)
		sprintf (pending_error_text,
			 "E cannot change mode for %s", arg);
	    pending_error = status;
	    return;
	}
    }
}

extern int use_unchanged;
int use_unchanged = 0;

static void
serve_enable_unchanged (arg)
     char *arg;
{
  use_unchanged = 1;
}

static void
serve_lost (arg)
    char *arg;
{
    if (use_unchanged)
    {
	/* A missing file already indicates it is nonexistent.  */
	return;
    }
    else
    {
	struct utimbuf ut;
	int fd = open (arg, O_WRONLY | O_CREAT | O_TRUNC, 0666);
	if (fd < 0 || close (fd) < 0)
	{
	    pending_error = errno;
	    return;
	}
	/*
	 * Set the times to the beginning of the epoch to tell time_stamp()
	 * that the file was lost.
	 */
	ut.actime = 0;
	ut.modtime = 0;
	if (utime (arg, &ut) < 0)
	{
	    pending_error = errno;
	    return;
	}
    }
}

struct an_entry {
    struct an_entry *next;
    char *entry;
};

static struct an_entry *entries;

static void
serve_unchanged (arg)
    char *arg;
{
    if (error_pending ())
	return;
    if (!use_unchanged) 
    {
	/* A missing file already indicates it is unchanged.  */
	return;
    }
    else
    {
	struct an_entry *p;
	char *name;
	char *cp;
	char *timefield;

	/* Rewrite entries file to have `=' in timestamp field.  */
	for (p = entries; p != NULL; p = p->next)
	{
	    name = p->entry + 1;
	    cp = strchr (name, '/');
	    if (cp != NULL
		&& strlen (arg) == cp - name
		&& strncmp (arg, name, cp - name) == 0)
	    {
		timefield = strchr (cp + 1, '/') + 1;
		if (*timefield != '=')
		{
		    cp = timefield + strlen (timefield);
		    cp[1] = '\0';
		    while (cp > timefield)
		    {
			*cp = cp[-1];
			--cp;
		    }
		    *timefield = '=';
		}
		break;
	    }
	}
    }
}

static void
serve_entry (arg)
     char *arg;
{
    struct an_entry *p;
    char *cp;
    if (error_pending()) return;
    p = (struct an_entry *) malloc (sizeof (struct an_entry));
    if (p == NULL)
    {
	pending_error = ENOMEM;
	return;
    }
    /* Leave space for serve_unchanged to write '=' if it wants.  */
    cp = malloc (strlen (arg) + 2);
    if (cp == NULL)
    {
	pending_error = ENOMEM;
	return;
    }
    strcpy (cp, arg);
    p->next = entries;
    p->entry = cp;
    entries = p;
}

static void
server_write_entries ()
{
    FILE *f;
    struct an_entry *p;
    struct an_entry *q;

    if (entries == NULL)
	return;

    f = NULL;
    /* Note that we free all the entries regardless of errors.  */
    if (!error_pending ())
    {
	f = fopen (CVSADM_ENT, "w");
	if (f == NULL)
	{
	    pending_error = errno;
	}
    }
    for (p = entries; p != NULL;)
    {
	if (!error_pending ())
	{
	    if (fprintf (f, "%s\n", p->entry) == EOF)
	    {
		pending_error = errno;
	    }
	}
	free (p->entry);
	q = p->next;
	free (p);
	p = q;
    }
    entries = NULL;
    if (f != NULL && fclose (f) == EOF && !error_pending ())
    {
	pending_error = errno;
    }
}

static int argument_count;
static char **argument_vector;
static int argument_vector_size;

static void
serve_argument (arg)
     char *arg;
{
    char *p;
    
    if (error_pending()) return;
    
    if (argument_vector_size <= argument_count)
    {
	argument_vector_size *= 2;
	argument_vector =
	    (char **) realloc ((char *)argument_vector,
			       argument_vector_size * sizeof (char *));
	if (argument_vector == NULL)
	{
	    pending_error = ENOMEM;
	    return;
	}
    }
    p = malloc (strlen (arg) + 1);
    if (p == NULL)
    {
	pending_error = ENOMEM;
	return;
    }
    strcpy (p, arg);
    argument_vector[argument_count++] = p;
}

static void
serve_argumentx (arg)
     char *arg;
{
    char *p;
    
    if (error_pending()) return;
    
    p = argument_vector[argument_count - 1];
    p = realloc (p, strlen (p) + 1 + strlen (arg) + 1);
    if (p == NULL)
    {
	pending_error = ENOMEM;
	return;
    }
    strcat (p, "\n");
    strcat (p, arg);
    argument_vector[argument_count - 1] = p;
}

static void
serve_global_option (arg)
    char *arg;
{
    if (arg[0] != '-' || arg[1] == '\0' || arg[2] != '\0')
    {
    error_return:
	pending_error_text = malloc (strlen (arg) + 80);
	sprintf (pending_error_text, "E Protocol error: bad global option %s",
		 arg);
	return;
    }
    switch (arg[1])
    {
	case 'n':
	    noexec = 1;
	    break;
	case 'q':
	    quiet = 1;
	    break;
	case 'r':
	    cvswrite = 0;
	    break;
	case 'Q':
	    really_quiet = 1;
	    break;
	case 'l':
	    logoff = 1;
	    break;
	case 't':
	    trace = 1;
	    break;
	default:
	    goto error_return;
    }
}

/*
 * We must read data from a child process and send it across the
 * network.  We do not want to block on writing to the network, so we
 * store the data from the child process in memory.  A BUFFER
 * structure holds the status of one communication, and uses a linked
 * list of buffer_data structures to hold data.
 */

struct buffer
{
    /* Data.  */
    struct buffer_data *data;

    /* Last buffer on data chain.  */
    struct buffer_data *last;

    /* File descriptor to write to or read from.  */
    int fd;

    /* Nonzero if this is an output buffer (sanity check).  */
    int output;

    /* Nonzero if the file descriptor is in nonblocking mode.  */
    int nonblocking;

    /* Function to call if we can't allocate memory.  */
    void (*memory_error) PROTO((struct buffer *));
};

/* Data is stored in lists of these structures.  */

struct buffer_data
{
    /* Next buffer in linked list.  */
    struct buffer_data *next;

    /*
     * A pointer into the data area pointed to by the text field.  This
     * is where to find data that has not yet been written out.
     */
    char *bufp;

    /* The number of data bytes found at BUFP.  */
    int size;

    /*
     * Actual buffer.  This never changes after the structure is
     * allocated.  The buffer is BUFFER_DATA_SIZE bytes.
     */
    char *text;
};

/* The size we allocate for each buffer_data structure.  */
#define BUFFER_DATA_SIZE (4096)

/* Linked list of available buffer_data structures.  */
static struct buffer_data *free_buffer_data;

static void allocate_buffer_datas PROTO((void));
static struct buffer_data *get_buffer_data PROTO((void));
static int buf_empty_p PROTO((struct buffer *));
static void buf_output PROTO((struct buffer *, const char *, int));
static void buf_output0 PROTO((struct buffer *, const char *));
static void buf_append_char PROTO((struct buffer *, int));
static int buf_send_output PROTO((struct buffer *));
static int set_nonblock PROTO((struct buffer *));
static int set_block PROTO((struct buffer *));
static int buf_send_counted PROTO((struct buffer *));
static void buf_append_data PROTO((struct buffer *,
				     struct buffer_data *,
				     struct buffer_data *));
static int buf_read_file PROTO((FILE *, long, struct buffer_data **,
				  struct buffer_data **));
static int buf_input_data PROTO((struct buffer *, int *));
static void buf_copy_lines PROTO((struct buffer *, struct buffer *, int));
static int buf_copy_counted PROTO((struct buffer *, struct buffer *));

/* Allocate more buffer_data structures.  */

static void
allocate_buffer_datas ()
{
    struct buffer_data *alc;
    char *space;
    int i;

    /* Allocate buffer_data structures in blocks of 16.  */
#define ALLOC_COUNT (16)

    alc = ((struct buffer_data *)
	   malloc (ALLOC_COUNT * sizeof (struct buffer_data)));
    space = (char *) valloc (ALLOC_COUNT * BUFFER_DATA_SIZE);
    if (alc == NULL || space == NULL)
	return;
    for (i = 0; i < ALLOC_COUNT; i++, alc++, space += BUFFER_DATA_SIZE)
    {
	alc->next = free_buffer_data;
	free_buffer_data = alc;
	alc->text = space;
    }	  
}

/* Get a new buffer_data structure.  */

static
#ifdef __GNUC__
__inline__
#endif
struct buffer_data *
get_buffer_data ()
{
    struct buffer_data *ret;

    if (free_buffer_data == NULL)
    {
	allocate_buffer_datas ();
	if (free_buffer_data == NULL)
	    return NULL;
    }

    ret = free_buffer_data;
    free_buffer_data = ret->next;
    return ret;
}

/* See whether a buffer is empty.  */

static int
buf_empty_p (buf)
    struct buffer *buf;
{
    struct buffer_data *data;

    for (data = buf->data; data != NULL; data = data->next)
	if (data->size > 0)
	    return 0;
    return 1;
}

/* Add data DATA of length LEN to BUF.  */

static void
buf_output (buf, data, len)
    struct buffer *buf;
    const char *data;
    int len;
{
    if (! buf->output)
	abort ();

    if (buf->data != NULL
	&& (((buf->last->text + BUFFER_DATA_SIZE)
	     - (buf->last->bufp + buf->last->size))
	    >= len))
    {
	memcpy (buf->last->bufp + buf->last->size, data, len);
	buf->last->size += len;
	return;
    }

    while (1)
    {
	struct buffer_data *newdata;

	newdata = get_buffer_data ();
	if (newdata == NULL)
	{
	    (*buf->memory_error) (buf);
	    return;
	}

	if (buf->data == NULL)
	    buf->data = newdata;
	else
	    buf->last->next = newdata;
	newdata->next = NULL;
	buf->last = newdata;

	newdata->bufp = newdata->text;

	if (len <= BUFFER_DATA_SIZE)
	{
	    newdata->size = len;
	    memcpy (newdata->text, data, len);
	    return;
	}

	newdata->size = BUFFER_DATA_SIZE;
	memcpy (newdata->text, data, BUFFER_DATA_SIZE);

	data += BUFFER_DATA_SIZE;
	len -= BUFFER_DATA_SIZE;
    }

    /*NOTREACHED*/
}

/* Add a '\0' terminated string to BUF.  */

static void
buf_output0 (buf, string)
    struct buffer *buf;
    const char *string;
{
    buf_output (buf, string, strlen (string));
}

/* Add a single character to BUF.  */

static 
#ifdef __GNUC__
__inline__
#endif
void
buf_append_char (buf, ch)
    struct buffer *buf;
    int ch;
{
    if (buf->data != NULL
	&& (buf->last->text + BUFFER_DATA_SIZE
	    != buf->last->bufp + buf->last->size))
    {
	*(buf->last->bufp + buf->last->size) = ch;
	++buf->last->size;
    }
    else
    {
	char b;

	b = ch;
	buf_output (buf, &b, 1);
    }
}

/*
 * Send all the output we've been saving up.  Returns 0 for success or
 * errno code.  If the buffer has been set to be nonblocking, this
 * will just write until the write would block.
 */

static int
buf_send_output (buf)
     struct buffer *buf;
{
    if (! buf->output)
	abort ();

    while (buf->data != NULL)
    {
	struct buffer_data *data;

	data = buf->data;
	while (data->size > 0)
	{
	    int nbytes;

	    nbytes = write (buf->fd, data->bufp, data->size);
	    if (nbytes <= 0)
	    {
		int status;

		if (buf->nonblocking
		    && (nbytes == 0
#ifdef EWOULDBLOCK
			|| errno == EWOULDBLOCK
#endif
			|| errno == EAGAIN))
		{
		    /*
		     * A nonblocking write failed to write any data.
		     * Just return.
		     */
		    return 0;
		}

		/*
		 * An error, or EOF.  Throw away all the data and
		 * return.
		 */
		if (nbytes == 0)
		    status = EIO;
		else
		    status = errno;

		buf->last->next = free_buffer_data;
		free_buffer_data = buf->data;
		buf->data = NULL;
		buf->last = NULL;

		return status;
	    }

	    data->size -= nbytes;
	    data->bufp += nbytes;
	}

	buf->data = data->next;
	data->next = free_buffer_data;
	free_buffer_data = data;
    }

    buf->last = NULL;

    return 0;
}

/*
 * Set buffer BUF to non-blocking I/O.  Returns 0 for success or errno
 * code.
 */

static int
set_nonblock (buf)
     struct buffer *buf;
{
    int flags;

    if (buf->nonblocking)
	return 0;
    flags = fcntl (buf->fd, F_GETFL, 0);
    if (flags < 0)
	return errno;
    if (fcntl (buf->fd, F_SETFL, flags | O_NONBLOCK) < 0)
	return errno;
    buf->nonblocking = 1;
    return 0;
}

/*
 * Set buffer BUF to blocking I/O.  Returns 0 for success or errno
 * code.
 */

static int
set_block (buf)
     struct buffer *buf;
{
    int flags;

    if (! buf->nonblocking)
	return 0;
    flags = fcntl (buf->fd, F_GETFL, 0);
    if (flags < 0)
	return errno;
    if (fcntl (buf->fd, F_SETFL, flags & ~O_NONBLOCK) < 0)
	return errno;
    buf->nonblocking = 0;
    return 0;
}

/*
 * Send a character count and some output.  Returns errno code or 0 for
 * success.
 *
 * Sending the count in binary is OK since this is only used on a pipe
 * within the same system.
 */

static int
buf_send_counted (buf)
     struct buffer *buf;
{
    int size;
    struct buffer_data *data;

    if (! buf->output)
	abort ();

    size = 0;
    for (data = buf->data; data != NULL; data = data->next)
	size += data->size;

    data = get_buffer_data ();
    if (data == NULL)
    {
	(*buf->memory_error) (buf);
	return ENOMEM;
    }

    data->next = buf->data;
    buf->data = data;
    if (buf->last == NULL)
	buf->last = data;

    data->bufp = data->text;
    data->size = sizeof (int);

    *((int *) data->text) = size;

    return buf_send_output (buf);
}

/* Append a list of buffer_data structures to an buffer.  */

static
#ifdef __GNUC__
__inline__
#endif
void
buf_append_data (buf, data, last)
     struct buffer *buf;
     struct buffer_data *data;
     struct buffer_data *last;
{
    if (data != NULL)
    {
	if (buf->data == NULL)
	    buf->data = data;
	else
	    buf->last->next = data;
	buf->last = last;
    }
}

/*
 * Copy the contents of file F into buffer_data structures.  We can't
 * copy directly into an buffer, because we want to handle failure and
 * succeess differently.  Returns 0 on success, or -2 if out of
 * memory, or a status code on error.  Since the caller happens to
 * know the size of the file, it is passed in as SIZE.  On success,
 * this function sets *RETP and *LASTP, which may be passed to
 * buf_append_data.
 */

static int
buf_read_file (f, size, retp, lastp)
    FILE *f;
    long size;
    struct buffer_data **retp;
    struct buffer_data **lastp;
{
    int status;

    *retp = NULL;
    *lastp = NULL;

    while (size > 0)
    {
	struct buffer_data *data;
	int get;

	data = get_buffer_data ();
	if (data == NULL)
	{
	    status = -2;
	    goto error_return;
	}

	if (*retp == NULL)
	    *retp = data;
	else
	    (*lastp)->next = data;
	data->next = NULL;
	*lastp = data;

	data->bufp = data->text;
	data->size = 0;

	if (size > BUFFER_DATA_SIZE)
	    get = BUFFER_DATA_SIZE;
	else
	    get = size;

	errno = EIO;
	if (fread (data->text, get, 1, f) != 1)
	{
	    status = errno;
	    goto error_return;
	}

	data->size += get;
	size -= get;
    }

    return 0;

  error_return:
    if (*retp != NULL)
    {
	(*lastp)->next = free_buffer_data;
	free_buffer_data = *retp;
    }
    return status;
}

static int
buf_read_file_to_eof (f, retp, lastp)
     FILE *f;
     struct buffer_data **retp;
     struct buffer_data **lastp;
{
    int status;

    *retp = NULL;
    *lastp = NULL;

    while (!feof (f))
    {
	struct buffer_data *data;
	int get, nread;

	data = get_buffer_data ();
	if (data == NULL)
	{
	    status = -2;
	    goto error_return;
	}

	if (*retp == NULL)
	    *retp = data;
	else
	    (*lastp)->next = data;
	data->next = NULL;
	*lastp = data;

	data->bufp = data->text;
	data->size = 0;

	get = BUFFER_DATA_SIZE;

	errno = EIO;
	nread = fread (data->text, 1, get, f);
	if (nread == 0 && !feof (f))
	{
	    status = errno;
	    goto error_return;
	}

	data->size = nread;
    }

    return 0;

  error_return:
    if (*retp != NULL)
    {
	(*lastp)->next = free_buffer_data;
	free_buffer_data = *retp;
    }
    return status;
}

static int
buf_chain_length (buf)
     struct buffer_data *buf;
{
    int size = 0;
    while (buf)
    {
	size += buf->size;
	buf = buf->next;
    }
    return size;
}

/*
 * Read an arbitrary amount of data from a file descriptor into an
 * input buffer.  The file descriptor will be in nonblocking mode, and
 * we just grab what we can.  Return 0 on success, or -1 on end of
 * file, or -2 if out of memory, or an error code.  If COUNTP is not
 * NULL, *COUNTP is set to the number of bytes read.
 */

static int
buf_input_data (buf, countp)
     struct buffer *buf;
     int *countp;
{
    if (buf->output)
	abort ();

    if (countp != NULL)
	*countp = 0;

    while (1)
    {
	int get;
	int nbytes;

	if (buf->data == NULL
	    || (buf->last->bufp + buf->last->size
		== buf->last->text + BUFFER_DATA_SIZE))
	{
	    struct buffer_data *data;

	    data = get_buffer_data ();
	    if (data == NULL)
	    {
		(*buf->memory_error) (buf);
		return -2;
	    }

	    if (buf->data == NULL)
		buf->data = data;
	    else
		buf->last->next = data;
	    data->next = NULL;
	    buf->last = data;

	    data->bufp = data->text;
	    data->size = 0;
	}

	get = ((buf->last->text + BUFFER_DATA_SIZE)
	       - (buf->last->bufp + buf->last->size));
	nbytes = read (buf->fd, buf->last->bufp + buf->last->size, get);
	if (nbytes <= 0)
	{
	    if (nbytes == 0)
	    {
		/*
		 * This assumes that we are using POSIX or BSD style
		 * nonblocking I/O.  On System V we will get a zero
		 * return if there is no data, even when not at EOF.
		 */
		return -1;
	    }

	    if (errno == EAGAIN
#ifdef EWOULDBLOCK
		|| errno == EWOULDBLOCK
#endif
		)
	      return 0;

	    return errno;
	}

	buf->last->size += nbytes;
	if (countp != NULL)
	    *countp += nbytes;
    }

    /*NOTREACHED*/
}

/*
 * Copy lines from an input buffer to an output buffer.  This copies
 * all complete lines (characters up to a newline) from INBUF to
 * OUTBUF.  Each line in OUTBUF is preceded by the character COMMAND
 * and a space.
 */

static void
buf_copy_lines (outbuf, inbuf, command)
     struct buffer *outbuf;
     struct buffer *inbuf;
     int command;
{
    if (! outbuf->output || inbuf->output)
	abort ();

    while (1)
    {
	struct buffer_data *data;
	struct buffer_data *nldata;
	char *nl;
	int len;

	/* See if there is a newline in INBUF.  */
	nldata = NULL;
	nl = NULL;
	for (data = inbuf->data; data != NULL; data = data->next)
	{
	    nl = memchr (data->bufp, '\n', data->size);
	    if (nl != NULL)
	    {
		nldata = data;
		break;
	    }
	}

	if (nldata == NULL)
	{
	    /* There are no more lines in INBUF.  */
	    return;
	}

	/* Put in the command.  */
	buf_append_char (outbuf, command);
	buf_append_char (outbuf, ' ');

	if (inbuf->data != nldata)
	{
	    /*
	     * Simply move over all the buffers up to the one containing
	     * the newline.
	     */
	    for (data = inbuf->data; data->next != nldata; data = data->next)
		;
	    data->next = NULL;
	    buf_append_data (outbuf, inbuf->data, data);
	    inbuf->data = nldata;
	}

	/*
	 * If the newline is at the very end of the buffer, just move
	 * the buffer onto OUTBUF.  Otherwise we must copy the data.
	 */
	len = nl + 1 - nldata->bufp;
	if (len == nldata->size)
	{
	    inbuf->data = nldata->next;
	    if (inbuf->data == NULL)
		inbuf->last = NULL;

	    nldata->next = NULL;
	    buf_append_data (outbuf, nldata, nldata);
	}
	else
	{
	    buf_output (outbuf, nldata->bufp, len);
	    nldata->bufp += len;
	    nldata->size -= len;
	}
    }
}

/*
 * Copy counted data from one buffer to another.  The count is an
 * integer, host size, host byte order (it is only used across a
 * pipe).  If there is enough data, it should be moved over.  If there
 * is not enough data, it should remain on the original buffer.  This
 * returns the number of bytes it needs to see in order to actually
 * copy something over.
 */

static int
buf_copy_counted (outbuf, inbuf)
     struct buffer *outbuf;
     struct buffer *inbuf;
{
    if (! outbuf->output || inbuf->output)
	abort ();

    while (1)
    {
	struct buffer_data *data;
	int need;
	union
	{
	    char intbuf[sizeof (int)];
	    int i;
	} u;
	char *intp;
	int count;
	struct buffer_data *start;
	int startoff;
	struct buffer_data *stop;
	int stopwant;

	/* See if we have enough bytes to figure out the count.  */
	need = sizeof (int);
	intp = u.intbuf;
	for (data = inbuf->data; data != NULL; data = data->next)
	{
	    if (data->size >= need)
	    {
		memcpy (intp, data->bufp, need);
		break;
	    }
	    memcpy (intp, data->bufp, data->size);
	    intp += data->size;
	    need -= data->size;
	}
	if (data == NULL)
	{
	    /* We don't have enough bytes to form an integer.  */
	    return need;
	}

	count = u.i;
	start = data;
	startoff = need;

	/*
	 * We have an integer in COUNT.  We have gotten all the data
	 * from INBUF in all buffers before START, and we have gotten
	 * STARTOFF bytes from START.  See if we have enough bytes
	 * remaining in INBUF.
	 */
	need = count - (start->size - startoff);
	if (need <= 0)
	{
	    stop = start;
	    stopwant = count;
	}
	else
	{
	    for (data = start->next; data != NULL; data = data->next)
	    {
		if (need <= data->size)
		    break;
		need -= data->size;
	    }
	    if (data == NULL)
	    {
		/* We don't have enough bytes.  */
		return need;
	    }
	    stop = data;
	    stopwant = need;
	}

	/*
	 * We have enough bytes.  Free any buffers in INBUF before
	 * START, and remove STARTOFF bytes from START, so that we can
	 * forget about STARTOFF.
	 */
	start->bufp += startoff;
	start->size -= startoff;

	if (start->size == 0)
	    start = start->next;

	if (stop->size == stopwant)
	{
	    stop = stop->next;
	    stopwant = 0;
	}

	while (inbuf->data != start)
	{
	    data = inbuf->data;
	    inbuf->data = data->next;
	    data->next = free_buffer_data;
	    free_buffer_data = data;
	}

	/*
	 * We want to copy over the bytes from START through STOP.  We
	 * only want STOPWANT bytes from STOP.
	 */

	if (start != stop)
	{
	    /* Attach the buffers from START through STOP to OUTBUF.  */
	    for (data = start; data->next != stop; data = data->next)
		;
	    inbuf->data = stop;
	    data->next = NULL;
	    buf_append_data (outbuf, start, data);
	}

	if (stopwant > 0)
	{
	    buf_output (outbuf, stop->bufp, stopwant);
	    stop->bufp += stopwant;
	    stop->size -= stopwant;
	}
    }

    /*NOTREACHED*/
}

static struct buffer protocol;

static void
protocol_memory_error (buf)
    struct buffer *buf;
{
    error (1, ENOMEM, "Virtual memory exhausted");
}

/*
 * Process IDs of the subprocess, or negative if that subprocess
 * does not exist.
 */
static pid_t command_pid;

static void
outbuf_memory_error (buf)
    struct buffer *buf;
{
    static char msg[] = "E Fatal error\n\
error ENOMEM Virtual memory exhausted.\n";
    if (command_pid > 0)
	kill (command_pid, SIGTERM);

    /*
     * We have arranged things so that printing this now either will
     * be legal, or the "E fatal error" line will get glommed onto the
     * end of an existing "E" or "M" response.
     */

    /* If this gives an error, not much we could do.  syslog() it?  */
    write (STDOUT_FILENO, msg, sizeof (msg) - 1);
    server_cleanup (0);
    exit (1);
}

static void
input_memory_error (buf)
     struct buffer *buf;
{
    outbuf_memory_error (buf);
}

/* Execute COMMAND in a subprocess with the approriate funky things done.  */

static void
do_cvs_command (command)
    int (*command) PROTO((int argc, char **argv));
{
    /*
     * The following file descriptors are set to -1 if that file is not
     * currently open.
     */

    /* Data on these pipes is a series of '\n'-terminated lines.  */
    int stdout_pipe[2];
    int stderr_pipe[2];

    /*
     * Data on this pipe is a series of counted (see buf_send_counted)
     * packets.  Each packet must be processed atomically (i.e. not
     * interleaved with data from stdout_pipe or stderr_pipe).
     */
    int protocol_pipe[2];
    
    int dev_null_fd = -1;

    int errs;

    command_pid = -1;
    stdout_pipe[0] = -1;
    stdout_pipe[1] = -1;
    stderr_pipe[0] = -1;
    stderr_pipe[1] = -1;
    protocol_pipe[0] = -1;
    protocol_pipe[1] = -1;

    server_write_entries ();

    if (print_pending_error ())
	goto free_args_and_return;

    /*
     * We use a child process which actually does the operation.  This
     * is so we can intercept its standard output.  Even if all of CVS
     * were written to go to some special routine instead of writing
     * to stdout or stderr, we would still need to do the same thing
     * for the RCS commands.
     */

    if (pipe (stdout_pipe) < 0)
    {
	print_error (errno);
	goto error_exit;
    }
    if (pipe (stderr_pipe) < 0)
    {
	print_error (errno);
	goto error_exit;
    }
    if (pipe (protocol_pipe) < 0)
    {
	print_error (errno);
	goto error_exit;
    }

    dev_null_fd = open ("/dev/null", O_RDONLY);
    if (dev_null_fd < 0)
    {
	print_error (errno);
	goto error_exit;
    }

    /* Don't use vfork; we're not going to exec().  */
    command_pid = fork ();
    if (command_pid < 0)
    {
	print_error (errno);
	goto error_exit;
    }
    if (command_pid == 0)
    {
	int exitstatus;

	protocol.data = protocol.last = NULL;
	protocol.fd = protocol_pipe[1];
	protocol.output = 1;
	protocol.nonblocking = 0;
	protocol.memory_error = protocol_memory_error;

	if (dup2 (dev_null_fd, STDIN_FILENO) < 0)
	    error (1, errno, "can't set up pipes");
	if (dup2 (stdout_pipe[1], STDOUT_FILENO) < 0)
	    error (1, errno, "can't set up pipes");
	if (dup2 (stderr_pipe[1], STDERR_FILENO) < 0)
	    error (1, errno, "can't set up pipes");
	close (stdout_pipe[0]);
	close (stderr_pipe[0]);
	close (protocol_pipe[0]);

	/*
	 * Set this in .bashrc if you want to give yourself time to attach
	 * to the subprocess with a debugger.
	 */
	if (getenv ("CVS_SERVER_SLEEP"))
	{
	    int secs = atoi (getenv ("CVS_SERVER_SLEEP"));
	    sleep (secs);
	}

	exitstatus = (*command) (argument_count, argument_vector);

	/*
	 * When we exit, that will close the pipes, giving an EOF to
	 * the parent.
	 */
	exit (exitstatus);
    }

    /* OK, sit around getting all the input from the child.  */
    {
	struct buffer outbuf;
	struct buffer stdoutbuf;
	struct buffer stderrbuf;
	struct buffer protocol_inbuf;
	/* Number of file descriptors to check in select ().  */
	int num_to_check;
	int count_needed = 0;

	num_to_check = stdout_pipe[0];
	if (stderr_pipe[0] > num_to_check)
	  num_to_check = stderr_pipe[0];
	if (protocol_pipe[0] > num_to_check)
	  num_to_check = protocol_pipe[0];
	if (STDOUT_FILENO > num_to_check)
	  num_to_check = STDOUT_FILENO;
	/*
	 * File descriptors are numbered from 0, so num_to_check needs to
	 * be one larger than the largest descriptor.
	 */
	++num_to_check;
	if (num_to_check > FD_SETSIZE)
	{
	    printf ("E internal error: FD_SETSIZE not big enough.\nerror  \n");
	    goto error_exit;
	}

	outbuf.data = outbuf.last = NULL;
	outbuf.fd = STDOUT_FILENO;
	outbuf.output = 1;
	outbuf.nonblocking = 0;
	outbuf.memory_error = outbuf_memory_error;

	stdoutbuf.data = stdoutbuf.last = NULL;
	stdoutbuf.fd = stdout_pipe[0];
	stdoutbuf.output = 0;
	stdoutbuf.nonblocking = 0;
	stdoutbuf.memory_error = input_memory_error;

	stderrbuf.data = stderrbuf.last = NULL;
	stderrbuf.fd = stderr_pipe[0];
	stderrbuf.output = 0;
	stderrbuf.nonblocking = 0;
	stderrbuf.memory_error = input_memory_error;

	protocol_inbuf.data = protocol_inbuf.last = NULL;
	protocol_inbuf.fd = protocol_pipe[0];
	protocol_inbuf.output = 0;
	protocol_inbuf.nonblocking = 0;
	protocol_inbuf.memory_error = input_memory_error;

	set_nonblock (&outbuf);
	set_nonblock (&stdoutbuf);
	set_nonblock (&stderrbuf);
	set_nonblock (&protocol_inbuf);

	if (close (stdout_pipe[1]) < 0)
	{
	    print_error (errno);
	    goto error_exit;
	}
	stdout_pipe[1] = -1;

	if (close (stderr_pipe[1]) < 0)
	{
	    print_error (errno);
	    goto error_exit;
	}
	stderr_pipe[1] = -1;

	if (close (protocol_pipe[1]) < 0)
	{
	    print_error (errno);
	    goto error_exit;
	}
	protocol_pipe[1] = -1;

	if (close (dev_null_fd) < 0)
	{
	    print_error (errno);
	    goto error_exit;
	}
	dev_null_fd = -1;

	while (stdout_pipe[0] >= 0
	       || stderr_pipe[0] >= 0
	       || protocol_pipe[0] >= 0)
	{
	    fd_set readfds;
	    fd_set writefds;
	    fd_set exceptfds;
	    int numfds;

	    FD_ZERO (&readfds);
	    FD_ZERO (&writefds);
	    FD_ZERO (&exceptfds);
	    if (! buf_empty_p (&outbuf))
	      FD_SET (STDOUT_FILENO, &writefds);
	    FD_SET (STDOUT_FILENO, &exceptfds);
	    if (stdout_pipe[0] >= 0)
	    {
		FD_SET (stdout_pipe[0], &readfds);
		FD_SET (stdout_pipe[0], &exceptfds);
	    }
	    if (stderr_pipe[0] >= 0)
	    {
		FD_SET (stderr_pipe[0], &readfds);
		FD_SET (stderr_pipe[0], &exceptfds);
	    }
	    if (protocol_pipe[0] >= 0)
	    {
		FD_SET (protocol_pipe[0], &readfds);
		FD_SET (protocol_pipe[0], &exceptfds);
	    }

	    do {
	      numfds = select (num_to_check, &readfds, &writefds,
				 &exceptfds, (struct timeval *)NULL);
		if (numfds < 0
		    && errno != EINTR)
		{
		    print_error (errno);
		    goto error_exit;
		}
	    } while (numfds < 0);
	    
	    if (FD_ISSET (STDOUT_FILENO, &writefds)
		|| FD_ISSET (STDOUT_FILENO, &exceptfds))
	    {
		/* What should we do with errors?  syslog() them?  */
		buf_send_output (&outbuf);
	    }

	    if (stdout_pipe[0] >= 0
		&& (FD_ISSET (stdout_pipe[0], &readfds)
		    || FD_ISSET (stdout_pipe[0], &exceptfds)))
	    {
	        int status;

	        status = buf_input_data (&stdoutbuf, (int *) NULL);

		buf_copy_lines (&outbuf, &stdoutbuf, 'M');

		if (status == -1)
		    stdout_pipe[0] = -1;
		else if (status > 0)
		{
		    print_error (status);
		    goto error_exit;
		}

		/* What should we do with errors?  syslog() them?  */
		buf_send_output (&outbuf);
	    }

	    if (stderr_pipe[0] >= 0
		&& (FD_ISSET (stderr_pipe[0], &readfds)
		    || FD_ISSET (stderr_pipe[0], &exceptfds)))
	    {
	        int status;

	        status = buf_input_data (&stderrbuf, (int *) NULL);

		buf_copy_lines (&outbuf, &stderrbuf, 'E');

		if (status == -1)
		    stderr_pipe[0] = -1;
		else if (status > 0)
		{
		    print_error (status);
		    goto error_exit;
		}

		/* What should we do with errors?  syslog() them?  */
		buf_send_output (&outbuf);
	    }

	    if (protocol_pipe[0] >= 0
		&& (FD_ISSET (protocol_pipe[0], &readfds)
		    || FD_ISSET (protocol_pipe[0], &exceptfds)))
	    {
		int status;
		int count_read;
		
		status = buf_input_data (&protocol_inbuf, &count_read);

		/*
		 * We only call buf_copy_counted if we have read
		 * enough bytes to make it worthwhile.  This saves us
		 * from continually recounting the amount of data we
		 * have.
		 */
		count_needed -= count_read;
		if (count_needed <= 0)
		  count_needed = buf_copy_counted (&outbuf, &protocol_inbuf);

		if (status == -1)
		    protocol_pipe[0] = -1;
		else if (status > 0)
		{
		    print_error (status);
		    goto error_exit;
		}

		/* What should we do with errors?  syslog() them?  */
		buf_send_output (&outbuf);
	    }
	}

	/*
	 * OK, we've gotten EOF on all the pipes.  If there is
	 * anything left on stdoutbuf or stderrbuf (this could only
	 * happen if there was no trailing newline), send it over.
	 */
	if (! buf_empty_p (&stdoutbuf))
	{
	    buf_append_char (&stdoutbuf, '\n');
	    buf_copy_lines (&outbuf, &stdoutbuf, 'M');
	}
	if (! buf_empty_p (&stderrbuf))
	{
	    buf_append_char (&stderrbuf, '\n');
	    buf_copy_lines (&outbuf, &stderrbuf, 'E');
	}
	if (! buf_empty_p (&protocol_inbuf))
	    buf_output0 (&outbuf,
			 "E Protocol error: uncounted data discarded\n");

	errs = 0;

	while (command_pid > 0)
	{
	    int status;
	    pid_t waited_pid;
	    waited_pid = waitpid (command_pid, &status, 0);
	    if (waited_pid < 0)
	    {
		/*
		 * Intentionally ignoring EINTR.  Other errors
		 * "can't happen".
		 */
		continue;
	    }
	    
	    if (WIFEXITED (status))
		errs += WEXITSTATUS (status);
	    else
	    {
	        int sig = WTERMSIG (status);
		/*
		 * This is really evil, because signals might be numbered
		 * differently on the two systems.  We should be using
		 * signal names (either of the "Terminated" or the "SIGTERM"
		 * variety).  But cvs doesn't currently use libiberty...we
		 * could roll our own....  FIXME.
		 */
		printf ("E Terminated with fatal signal %d\n", sig);

		/* Test for a core dump.  Is this portable?  */
		if (status & 0x80)
		{
		    printf ("E Core dumped; preserving %s on server.\n\
E CVS locks may need cleaning up.\n",
			    server_temp_dir);
		    dont_delete_temp = 1;
		}
		++errs;
	    }
	    if (waited_pid == command_pid)
		command_pid = -1;
	}

	/*
	 * OK, we've waited for the child.  By now all CVS locks are free
	 * and it's OK to block on the network.
	 */
	set_block (&outbuf);
	buf_send_output (&outbuf);
    }

    if (errs)
	/* We will have printed an error message already.  */
	printf ("error  \n");
    else
	printf ("ok\n");
    goto free_args_and_return;

 error_exit:
    if (command_pid > 0)
	kill (command_pid, SIGTERM);

    while (command_pid > 0)
    {
	pid_t waited_pid;
	waited_pid = waitpid (command_pid, (int *) 0, 0);
	if (waited_pid < 0 && errno == EINTR)
	    continue;
	if (waited_pid == command_pid)
	    command_pid = -1;
    }

    close (dev_null_fd);
    close (protocol_pipe[0]);
    close (protocol_pipe[1]);
    close (stderr_pipe[0]);
    close (stderr_pipe[1]);
    close (stdout_pipe[0]);
    close (stdout_pipe[1]);

 free_args_and_return:
    /* Now free the arguments.  */

    {
	/* argument_vector[0] is a dummy argument, we don't mess with it.  */
	char **cp;
	for (cp = argument_vector + 1;
	     cp < argument_vector + argument_count;
	     ++cp)
	    free (*cp);

	argument_count = 1;
    }
    return;
}

static void output_dir PROTO((char *, char *));

static void
output_dir (update_dir, repository)
    char *update_dir;
    char *repository;
{
    if (use_dir_and_repos)
    {
	if (update_dir[0] == '\0')
	    buf_output0 (&protocol, ".");
	else
	    buf_output0 (&protocol, update_dir);
	buf_output0 (&protocol, "/\n");
    }
    buf_output0 (&protocol, repository);
    buf_output0 (&protocol, "/");
}

/*
 * Entries line that we are squirreling away to send to the client when
 * we are ready.
 */
static char *entries_line;

/*
 * File which has been Scratch_File'd, we are squirreling away that fact
 * to inform the client when we are ready.
 */
static char *scratched_file;

/*
 * The scratched_file will need to be removed as well as having its entry
 * removed.
 */
static int kill_scratched_file;

void
server_register (name, version, timestamp, options, tag, date, conflict)
    char *name;
    char *version;
    char *timestamp;
    char *options;
    char *tag;
    char *date;
    char *conflict;
{
    int len;

    if (options == NULL)
	options = "";

    if (entries_line != NULL)
    {
	/*
	 * If CVS decides to Register it more than once (which happens
	 * on "cvs update foo/foo.c" where foo and foo.c are already
	 * checked out), use the last of the entries lines Register'd.
	 */
	free (entries_line);
    }

    /*
     * I have reports of Scratch_Entry and Register both happening, in
     * two different cases.  Using the last one which happens is almost
     * surely correct; I haven't tracked down why they both happen (or
     * even verified that they are for the same file).
     */
    if (scratched_file != NULL)
    {
	free (scratched_file);
	scratched_file = NULL;
    }

    len = (strlen (name) + strlen (version) + strlen (options) + 80);
    if (tag)
	len += strlen (tag);
    if (date)
	len += strlen (date);
    
    entries_line = xmalloc (len);
    sprintf (entries_line, "/%s/%s/", name, version);
    if (conflict != NULL)
    {
	strcat (entries_line, "+=");
    }
    strcat (entries_line, "/");
    strcat (entries_line, options);
    strcat (entries_line, "/");
    if (tag != NULL)
    {
	strcat (entries_line, "T");
	strcat (entries_line, tag);
    }
    else if (date != NULL)
    {
	strcat (entries_line, "D");
	strcat (entries_line, date);
    }
}

void
server_scratch (fname)
    char *fname;
{
    /*
     * I have reports of Scratch_Entry and Register both happening, in
     * two different cases.  Using the last one which happens is almost
     * surely correct; I haven't tracked down why they both happen (or
     * even verified that they are for the same file).
     */
    if (entries_line != NULL)
    {
	free (entries_line);
	entries_line = NULL;
    }

    if (scratched_file != NULL)
    {
	buf_output0 (&protocol,
		     "E CVS server internal error: duplicate Scratch_Entry\n");
	buf_send_counted (&protocol);
	return;
    }
    scratched_file = xstrdup (fname);
    kill_scratched_file = 1;
}

void
server_scratch_entry_only ()
{
    kill_scratched_file = 0;
}

/* Print a new entries line, from a previous server_register.  */
static void
new_entries_line (file, repository)
    char *file;
    char *repository;
{
    if (entries_line)
    {
	buf_output0 (&protocol, entries_line);
	buf_output (&protocol, "\n", 1);
    }
    else
	/* Return the error message as the Entries line.  */
	buf_output0 (&protocol,
		     "CVS server internal error: Register missing\n");
    free (entries_line);
    entries_line = NULL;
}

static void
serve_ci (arg)
    char *arg;
{
    do_cvs_command (commit);
}

void
server_checked_in (file, update_dir, repository)
    char *file;
    char *update_dir;
    char *repository;
{
    if (noexec)
	return;
    if (scratched_file != NULL && entries_line == NULL)
    {
	/*
	 * This happens if we are now doing a "cvs remove" after a previous
	 * "cvs add" (without a "cvs ci" in between).
	 */
	buf_output0 (&protocol, "Remove-entry ");
	output_dir (update_dir, repository);
	buf_output0 (&protocol, file);
	buf_output (&protocol, "\n", 1);
	free (scratched_file);
	scratched_file = NULL;
    }
    else
    {
	buf_output0 (&protocol, "Checked-in ");
	output_dir (update_dir, repository);
	buf_output0 (&protocol, file);
	buf_output (&protocol, "\n", 1);
	new_entries_line (file, repository);
    }
    buf_send_counted (&protocol);
}

void
server_update_entries (file, update_dir, repository, updated)
    char *file;
    char *update_dir;
    char *repository;
    enum server_updated_arg4 updated;
{
    if (noexec)
	return;
    if (updated == SERVER_UPDATED)
	buf_output0 (&protocol, "Checked-in ");
    else
    {
	if (!supported_response ("New-entry"))
	    return;
	buf_output0 (&protocol, "New-entry ");
    }

    output_dir (update_dir, repository);
    buf_output0 (&protocol, file);
    buf_output (&protocol, "\n", 1);
    new_entries_line (file, repository);
    buf_send_counted (&protocol);
}

static void
serve_update (arg)
    char *arg;
{
    do_cvs_command (update);
}

static void
serve_diff (arg)
    char *arg;
{
    do_cvs_command (diff);
}

static void
serve_log (arg)
    char *arg;
{
    do_cvs_command (cvslog);
}

static void
serve_add (arg)
    char *arg;
{
    do_cvs_command (add);
}

static void
serve_remove (arg)
    char *arg;
{
    do_cvs_command (cvsremove);
}

static void
serve_status (arg)
    char *arg;
{
    do_cvs_command (status);
}

static void
serve_rdiff (arg)
    char *arg;
{
    do_cvs_command (patch);
}

static void
serve_tag (arg)
    char *arg;
{
    do_cvs_command (tag);
}

static void
serve_rtag (arg)
    char *arg;
{
    do_cvs_command (rtag);
}

static void
serve_import (arg)
    char *arg;
{
    do_cvs_command (import);
}

static void
serve_admin (arg)
    char *arg;
{
    do_cvs_command (admin);
}

static void
serve_history (arg)
    char *arg;
{
    do_cvs_command (history);
}

static void
serve_release (arg)
    char *arg;
{
    do_cvs_command (release);
}

static void
serve_co (arg)
    char *arg;
{
    char *tempdir;
    int status;

    if (print_pending_error ())
	return;

    if (!isdir ("CVS"))
    {
	/*
	 * The client has not sent a "Repository" line.  Check out
	 * into a pristine directory.
	 */
	tempdir = malloc (strlen (server_temp_dir) + 80);
	if (tempdir == NULL)
	{
	    printf ("E Out of memory\n");
	    return;
	}
	strcpy (tempdir, server_temp_dir);
	strcat (tempdir, "/checkout-dir");
	status = mkdir_p (tempdir);
	if (status != 0 && status != EEXIST)
	{
	    printf ("E Cannot create %s\n", tempdir);
	    print_error (errno);
	    free (tempdir);
	    return;
	}

	if (chdir (tempdir) < 0)
	{
	    printf ("E Cannot change to directory %s\n", tempdir);
	    print_error (errno);
	    free (tempdir);
	    return;
	}
	free (tempdir);
    }
    do_cvs_command (checkout);
}

static void
serve_export (arg)
    char *arg;
{
    /* Tell checkout() to behave like export not checkout.  */
    command_name = "export";
    serve_co (arg);
}

void
server_copy_file (file, update_dir, repository, newfile)
    char *file;
    char *update_dir;
    char *repository;
    char *newfile;
{
    if (!supported_response ("Copy-file"))
	return;
    buf_output0 (&protocol, "Copy-file ");
    output_dir (update_dir, repository);
    buf_output0 (&protocol, file);
    buf_output0 (&protocol, "\n");
    buf_output0 (&protocol, newfile);
    buf_output0 (&protocol, "\n");
}

void
server_updated (file, update_dir, repository, updated, file_info, checksum)
    char *file;
    char *update_dir;
    char *repository;
    enum server_updated_arg4 updated;
    struct stat *file_info;
    unsigned char *checksum;
{
    char *short_pathname;

    if (noexec)
	return;

    short_pathname = xmalloc (strlen (update_dir) + strlen (file) + 10);
    if (update_dir[0] == '\0')
	strcpy (short_pathname, file);
    else
	sprintf (short_pathname, "%s/%s", update_dir, file);

    if (entries_line != NULL && scratched_file == NULL)
    {
	FILE *f;
	struct stat sb;
	struct buffer_data *list, *last;
	unsigned long size;
	char size_text[80];

	if (stat (file, &sb) < 0)
	{
	    if (errno == ENOENT)
	    {
		/*
		 * If we have a sticky tag for a branch on which the
		 * file is dead, and cvs update the directory, it gets
		 * a T_CHECKOUT but no file.  So in this case just
		 * forget the whole thing.
		 */
		free (entries_line);
		entries_line = NULL;
		goto done;
	    }
	    error (1, errno, "reading %s", short_pathname);
	}

	if (checksum != NULL)
	{
	    static int checksum_supported = -1;

	    if (checksum_supported == -1)
	    {
		checksum_supported = supported_response ("Checksum");
	    }

	    if (checksum_supported)
	    {
	        int i;
		char buf[3];

	        buf_output0 (&protocol, "Checksum ");
		for (i = 0; i < 16; i++)
		{
		    sprintf (buf, "%02x", (unsigned int) checksum[i]);
		    buf_output0 (&protocol, buf);
		}
		buf_append_char (&protocol, '\n');
	    }
	}

	if (updated == SERVER_UPDATED)
	    buf_output0 (&protocol, "Updated ");
	else if (updated == SERVER_MERGED)
	    buf_output0 (&protocol, "Merged ");
	else if (updated == SERVER_PATCHED)
	    buf_output0 (&protocol, "Patched ");
	else
	    abort ();
	output_dir (update_dir, repository);
	buf_output0 (&protocol, file);
	buf_output (&protocol, "\n", 1);

	new_entries_line (file, repository);

        {
	    char *mode_string;

	    if (file_info != NULL)
	        mode_string = mode_to_string (file_info);
	    else
	        mode_string = mode_to_string (&sb);
	    buf_output0 (&protocol, mode_string);
	    buf_output0 (&protocol, "\n");
	    free (mode_string);
	}

	list = last = NULL;
	size = 0;
	if (sb.st_size > 0)
	{
	    if (gzip_level
		/*
		 * For really tiny files, the gzip process startup
		 * time will outweigh the compression savings.  This
		 * might be computable somehow; using 100 here is just
		 * a first approximation.
		 */
		&& sb.st_size > 100)
	    {
		int status, fd, gzip_status;
		pid_t gzip_pid;

		fd = open (file, O_RDONLY, 0);
		if (fd < 0)
		    error (1, errno, "reading %s", short_pathname);
		fd = filter_through_gzip (fd, 1, gzip_level, &gzip_pid);
		f = fdopen (fd, "r");
		status = buf_read_file_to_eof (f, &list, &last);
		size = buf_chain_length (list);
		if (status == -2)
		    (*protocol.memory_error) (&protocol);
		else if (status != 0)
		    error (1, ferror (f) ? errno : 0, "reading %s",
			   short_pathname);
		if (fclose (f) == EOF)
		    error (1, errno, "reading %s", short_pathname);
		if (waitpid (gzip_pid, &gzip_status, 0) == -1)
		    error (1, errno, "waiting for gzip process %d", gzip_pid);
		else if (gzip_status != 0)
		    error (1, 0, "gzip exited %d", gzip_status);
		/* Prepending length with "z" is flag for using gzip here.  */
		buf_output0 (&protocol, "z");
	    }
	    else
	    {
		long status;

		size = sb.st_size;
		f = fopen (file, "r");
		if (f == NULL)
		    error (1, errno, "reading %s", short_pathname);
		status = buf_read_file (f, sb.st_size, &list, &last);
		if (status == -2)
		    (*protocol.memory_error) (&protocol);
		else if (status != 0)
		    error (1, ferror (f) ? errno : 0, "reading %s",
			   short_pathname);
		if (fclose (f) == EOF)
		    error (1, errno, "reading %s", short_pathname);
	    }
	}

	sprintf (size_text, "%lu\n", size);
	buf_output0 (&protocol, size_text);

	buf_append_data (&protocol, list, last);
	/* Note we only send a newline here if the file ended with one.  */

	/*
	 * Avoid using up too much disk space for temporary files.
	 * A file which does not exist indicates that the file is up-to-date,
	 * which is now the case.  If this is SERVER_MERGED, the file is
	 * not up-to-date, and we indicate that by leaving the file there.
	 * I'm thinking of cases like "cvs update foo/foo.c foo".
	 */
	if ((updated == SERVER_UPDATED || updated == SERVER_PATCHED)
	    /* But if we are joining, we'll need the file when we call
	       join_file.  */
	    && !joining ())
	    unlink (file);
    }
    else if (scratched_file != NULL && entries_line == NULL)
    {
	if (strcmp (scratched_file, file) != 0)
	    error (1, 0,
		   "CVS server internal error: `%s' vs. `%s' scratched",
		   scratched_file,
		   file);
	free (scratched_file);
	scratched_file = NULL;

	if (kill_scratched_file)
	    buf_output0 (&protocol, "Removed ");
	else
	    buf_output0 (&protocol, "Remove-entry ");
	output_dir (update_dir, repository);
	buf_output0 (&protocol, file);
	buf_output (&protocol, "\n", 1);
    }
    else if (scratched_file == NULL && entries_line == NULL)
    {
	/*
	 * This can happen with death support if we were processing
	 * a dead file in a checkout.
	 */
    }
    else
	error (1, 0,
	       "CVS server internal error: Register *and* Scratch_Entry.\n");
    buf_send_counted (&protocol);
  done:
    free (short_pathname);
}

void
server_set_entstat (update_dir, repository)
    char *update_dir;
    char *repository;
{
    static int set_static_supported = -1;
    if (set_static_supported == -1)
	set_static_supported = supported_response ("Set-static-directory");
    if (!set_static_supported) return;

    buf_output0 (&protocol, "Set-static-directory ");
    output_dir (update_dir, repository);
    buf_output0 (&protocol, "\n");
    buf_send_counted (&protocol);
}

void
server_clear_entstat (update_dir, repository)
     char *update_dir;
     char *repository;
{
    static int clear_static_supported = -1;
    if (clear_static_supported == -1)
	clear_static_supported = supported_response ("Clear-static-directory");
    if (!clear_static_supported) return;

    if (noexec)
	return;

    buf_output0 (&protocol, "Clear-static-directory ");
    output_dir (update_dir, repository);
    buf_output0 (&protocol, "\n");
    buf_send_counted (&protocol);
}

void
server_set_sticky (update_dir, repository, tag, date)
    char *update_dir;
    char *repository;
    char *tag;
    char *date;
{
    static int set_sticky_supported = -1;
    if (set_sticky_supported == -1)
	set_sticky_supported = supported_response ("Set-sticky");
    if (!set_sticky_supported) return;

    if (noexec)
	return;

    if (tag == NULL && date == NULL)
    {
	buf_output0 (&protocol, "Clear-sticky ");
	output_dir (update_dir, repository);
	buf_output0 (&protocol, "\n");
    }
    else
    {
	buf_output0 (&protocol, "Set-sticky ");
	output_dir (update_dir, repository);
	buf_output0 (&protocol, "\n");
	if (tag != NULL)
	{
	    buf_output0 (&protocol, "T");
	    buf_output0 (&protocol, tag);
	}
	else
	{
	    buf_output0 (&protocol, "D");
	    buf_output0 (&protocol, date);
	}
	buf_output0 (&protocol, "\n");
    }
    buf_send_counted (&protocol);
}

static void
serve_gzip_contents (arg)
     char *arg;
{
    int level;
    level = atoi (arg);
    if (level == 0)
	level = 6;
    gzip_level = level;
}

static void
serve_ignore (arg)
    char *arg;
{
    /*
     * Just ignore this command.  This is used to support the
     * update-patches command, which is not a real command, but a signal
     * to the client that update will accept the -u argument.
     */
}

static int
expand_proc (pargc, argv, where, mwhere, mfile, shorten,
	     local_specified, omodule, msg)
    int *pargc;
    char *argv[];
    char *where;
    char *mwhere;
    char *mfile;
    int shorten;
    int local_specified;
    char *omodule;
    char *msg;
{
    int i;
    char *dir = argv[0];
    if (*pargc == 1)
	printf ("Module-expansion %s\n", dir);
    else
	for (i = 1; i < *pargc; ++i)
	    printf ("Module-expansion %s/%s\n", dir, argv[i]);
    return 0;
}

static void
serve_expand_modules (arg)
    char *arg;
{
    int i;
    int err;
    DBM *db;
    err = 0;

    /*
     * FIXME: error handling is bogus; do_module can write to stdout and/or
     * stderr and we're not using do_cvs_command.
     */

    server_expanding = 1;
    db = open_module ();
    for (i = 1; i < argument_count; i++)
	err += do_module (db, argument_vector[i],
			  CHECKOUT, "Updating", expand_proc,
			  NULL, 0, 0, 0,
			  (char *) NULL);
    close_module (db);
    server_expanding = 0;
    {
	/* argument_vector[0] is a dummy argument, we don't mess with it.  */
	char **cp;
	for (cp = argument_vector + 1;
	     cp < argument_vector + argument_count;
	     ++cp)
	    free (*cp);

	argument_count = 1;
    }
    if (err)
	/* We will have printed an error message already.  */
	printf ("error  \n");
    else
	printf ("ok\n");
}

void
server_prog (dir, name, which)
    char *dir;
    char *name;
    enum progs which;
{
    if (!supported_response ("Set-checkin-prog"))
    {
	printf ("E \
warning: this client does not support -i or -u flags in the modules file.\n");
	return;
    }
    switch (which)
    {
	case PROG_CHECKIN:
	    printf ("Set-checkin-prog ");
	    break;
	case PROG_UPDATE:
	    printf ("Set-update-prog ");
	    break;
    }
    printf ("%s\n%s\n", dir, name);
}

static void
serve_checkin_prog (arg)
    char *arg;
{
    FILE *f;
    f = fopen (CVSADM_CIPROG, "w+");
    if (f == NULL)
    {
	pending_error = errno;
	return;
    }
    if (fprintf (f, "%s\n", arg) == EOF)
    {
	pending_error = errno;
	return;
    }
    if (fclose (f) == EOF)
    {
	pending_error = errno;
	return;
    }
}

static void
serve_update_prog (arg)
    char *arg;
{
    FILE *f;
    f = fopen (CVSADM_UPROG, "w+");
    if (f == NULL)
    {
	pending_error = errno;
	return;
    }
    if (fprintf (f, "%s\n", arg) == EOF)
    {
	pending_error = errno;
	return;
    }
    if (fclose (f) == EOF)
    {
	pending_error = errno;
	return;
    }
}

static void serve_valid_requests PROTO((char *arg));

struct request requests[] =
{
  {"Root", serve_root, rq_essential},
  {"Valid-responses", serve_valid_responses, rq_essential},
  {"valid-requests", serve_valid_requests, rq_essential},
  {"Repository", serve_repository, rq_essential},
  {"Directory", serve_directory, rq_optional},
  {"Max-dotdot", serve_max_dotdot, rq_optional},
  {"Static-directory", serve_static_directory, rq_optional},
  {"Sticky", serve_sticky, rq_optional},
  {"Checkin-prog", serve_checkin_prog, rq_optional},
  {"Update-prog", serve_update_prog, rq_optional},
  {"Entry", serve_entry, rq_essential},
  {"Modified", serve_modified, rq_essential},
  {"Lost", serve_lost, rq_optional},
  {"UseUnchanged", serve_enable_unchanged, rq_enableme},
  {"Unchanged", serve_unchanged, rq_optional},
  {"Argument", serve_argument, rq_essential},
  {"Argumentx", serve_argumentx, rq_essential},
  {"Global_option", serve_global_option, rq_optional},
  {"expand-modules", serve_expand_modules, rq_optional},
  {"ci", serve_ci, rq_essential},
  {"co", serve_co, rq_essential},
  {"update", serve_update, rq_essential},
  {"diff", serve_diff, rq_optional},
  {"log", serve_log, rq_optional},
  {"add", serve_add, rq_optional},
  {"remove", serve_remove, rq_optional},
  {"update-patches", serve_ignore, rq_optional},
  {"gzip-file-contents", serve_gzip_contents, rq_optional},
  {"status", serve_status, rq_optional},
  {"rdiff", serve_rdiff, rq_optional},
  {"tag", serve_tag, rq_optional},
  {"rtag", serve_rtag, rq_optional},
  {"import", serve_import, rq_optional},
  {"admin", serve_admin, rq_optional},
  {"export", serve_export, rq_optional},
  {"history", serve_history, rq_optional},
  {"release", serve_release, rq_optional},
  {NULL, NULL, rq_optional}
};

static void
serve_valid_requests (arg)
     char *arg;
{
    struct request *rq;
    printf ("Valid-requests");
    for (rq = requests; rq->name != NULL; rq++)
	if (rq->func != NULL)
	    printf (" %s", rq->name);
    printf ("\nok\n");
}

/*
 * Delete temporary files.  SIG is the signal making this happen, or
 * 0 if not called as a result of a signal.
 */
void
server_cleanup (sig)
    int sig;
{
    /* Do "rm -rf" on the temp directory.  */
    int len;
    char *tempbuf;
    char *cmd;
    char *temp_dir;

    if (dont_delete_temp)
	return;

    /* This might be set by the user in ~/.bashrc, ~/.cshrc, etc.  */
    temp_dir = getenv ("TMPDIR");
    if (temp_dir == NULL || temp_dir[0] == '\0')
        temp_dir = "/var/tmp";
    chdir(temp_dir);

    len = strlen (server_temp_dir) + 80;
    cmd = malloc (len);
    if (cmd == NULL)
    {
	printf ("E Cannot delete %s on server; out of memory\n",
		server_temp_dir);
	return;
    }
    sprintf (cmd, "rm -rf %s", server_temp_dir);
    system (cmd);
    free (cmd);
}

int server_active = 0;
int server_expanding = 0;

int
server (argc, argv)
     int argc;
     char **argv;
{
    if (argc == -1)
    {
	static char *msg[] =
	{
	    "Usage: %s %s\n",
	    "  Normally invoked by a cvs client on a remote machine.\n",
	    NULL
	};
	usage (msg);
    }
    /* Ignore argc and argv.  They might be from .cvsrc.  */

    /*
     * Put Rcsbin at the start of PATH, so that rcs programs can find
     * themselves.
     */
#ifdef HAVE_PUTENV
    if (Rcsbin != NULL && *Rcsbin)
    {
        char *p;
	char *env;

	p = getenv ("PATH");
	if (p != NULL)
	{
	    env = malloc (strlen (Rcsbin) + strlen (p) + sizeof "PATH=:");
	    if (env != NULL)
	        sprintf (env, "PATH=%s:%s", Rcsbin, p);
	}
	else
	{
	    env = malloc (strlen (Rcsbin) + sizeof "PATH=");
	    if (env != NULL)
	        sprintf (env, "PATH=%s", Rcsbin);
	}
	if (env == NULL)
	{
	    printf ("E Fatal error, aborting.\n\
error ENOMEM Virtual memory exhausted.\n");
	    exit (1);
	}
	putenv (env);
    }
#endif

    /* OK, now figure out where we stash our temporary files.  */
    {
	char *p;

	/* This might be set by the user in ~/.bashrc, ~/.cshrc, etc.  */
	char *temp_dir = getenv ("TMPDIR");
	if (temp_dir == NULL || temp_dir[0] == '\0')
	    temp_dir = "/var/tmp";

	server_temp_dir = malloc (strlen (temp_dir) + 80);
	if (server_temp_dir == NULL)
	{
	    /*
	     * Strictly speaking, we're not supposed to output anything
	     * now.  But we're about to exit(), give it a try.
	     */
	    printf ("E Fatal error, aborting.\n\
error ENOMEM Virtual memory exhausted.\n");
	    exit (1);
	}
	strcpy (server_temp_dir, temp_dir);

	/* Remove a trailing slash from TMPDIR if present.  */
	p = server_temp_dir + strlen (server_temp_dir) - 1;
	if (*p == '/')
	    *p = '\0';

	/*
	 * I wanted to use cvs-serv/PID, but then you have to worry about
	 * the permissions on the cvs-serv directory being right.  So
	 * use cvs-servPID.
	 */
	strcat (server_temp_dir, "/cvs-serv");

	p = server_temp_dir + strlen (server_temp_dir);
	sprintf (p, "%d", getpid ());
    }

    (void) SIG_register (SIGHUP, server_cleanup);
    (void) SIG_register (SIGINT, server_cleanup);
    (void) SIG_register (SIGQUIT, server_cleanup);
    (void) SIG_register (SIGPIPE, server_cleanup);
    (void) SIG_register (SIGTERM, server_cleanup);
    
    /* Now initialize our argument vector (for arguments from the client).  */

    /* Small for testing.  */
    argument_vector_size = 1;
    argument_vector =
	(char **) malloc (argument_vector_size * sizeof (char *));
    if (argument_vector == NULL)
    {
	/*
	 * Strictly speaking, we're not supposed to output anything
	 * now.  But we're about to exit(), give it a try.
	 */
	printf ("E Fatal error, aborting.\n\
error ENOMEM Virtual memory exhausted.\n");
	exit (1);
    }

    argument_count = 1;
    argument_vector[0] = "Dummy argument 0";

    server_active = 1;
    while (1)
    {
	char *cmd, *orig_cmd;
	struct request *rq;
	
	orig_cmd = cmd = read_line (stdin);
	if (cmd == NULL)
	    break;
	if (cmd == NO_MEM_ERROR)
	{
	    printf ("E Fatal error, aborting.\n\
error ENOMEM Virtual memory exhausted.\n");
	    break;
	}
	for (rq = requests; rq->name != NULL; ++rq)
	    if (strncmp (cmd, rq->name, strlen (rq->name)) == 0)
	    {
		int len = strlen (rq->name);
		if (cmd[len] == '\0')
		    cmd += len;
		else if (cmd[len] == ' ')
		    cmd += len + 1;
		else
		    /*
		     * The first len characters match, but it's a different
		     * command.  e.g. the command is "cooperate" but we matched
		     * "co".
		     */
		    continue;
		(*rq->func) (cmd);
		break;
	    }
	if (rq->name == NULL)
	{
	    if (!print_pending_error ())
		printf ("error  unrecognized request `%s'\n", cmd);
	}
	free (orig_cmd);
    }
    server_cleanup (0);
    return 0;
}
