/*
 * Sample program written for BSD-derived UNIX to talk to an
 * Apple LaserWriter attached to the serial port on an NCD
 * Network Display Station
 *
 * This program is provide "as-is" by NCD with no warranty as to its
 * fitness for any purpose.  It has been compiled and run on SunOS 3.5,
 * SunOS 4, VAX ULTRIX, and RISC ULTRIX.
 */
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <netdb.h>
#include <strings.h>
#include <fcntl.h>
#include <errno.h>

/* for systems that don't have the latest select() */

#ifndef FD_SETSIZE
#include <sys/param.h>
#define FD_SETSIZE	((NOFILE > sizeof (long)) ? sizeof (long) : NOFILE)
#define	FD_ZERO(p)	bzero((char *)(p), sizeof(*(p)))
#define	FD_SET(n, p)	((p)->fds_bits[0] |= (1 << (n)))
#define	FD_ISSET(n, p)	((p)->fds_bits[0] & (1 << (n)))
#endif

#define BUFSIZE		1024

#define	RS232PORT	87	/* the NCD RS-232 daemon TCP port */
#define RESET_TIMEOUT	10	/* seconds to wait on reset before timing out */
#define	MAX_RETRIES	100	/* number of times we'll retry initial reset */

#define	CTRLD		'\004'

#define	TRUE	1
#define	FALSE	0

char	*progname;
char	*printername;

/*
 * This utility is invoked with the host name or IP address of an NCD
 * network display station as it's only argument.  It will take characters
 * from STDIN (assumed to be PostScript) and write them to the serial port
 * on the specified NCD network display station.  The utility will also
 * monitor status from the printer and report any information returned
 * by the printer to STDERR.
 */

main (argc, argv)
    int         argc;
    char      **argv;
{
    int         sfd;
    struct sockaddr_in inaddr;
    struct hostent *hp;

    /* check number of arguments */
    if (argc != 2) {
	perror (argv[0]);
	exit (1);
    }
    progname = argv[0];
    printername = argv[1];

    /* create a socket */
    if ((sfd = socket (AF_INET, SOCK_STREAM, 0)) < 0) {
	stamp (stderr);
	fprintf (stderr, "can't open socket\n");
	exit (1);
    }

    /* resolve network address of NCD unit */
    inaddr.sin_family = AF_INET;
    inaddr.sin_port = htons (RS232PORT);
    if ((inaddr.sin_addr.s_addr = inet_addr (argv[1])) == -1) {
	if ((hp = gethostbyname (argv[1])) == NULL
		|| hp->h_addrtype != AF_INET) {
	    stamp (stderr);
	    fprintf (stderr, "can't resolve name %s\n", argv[1]);
	    exit (1);
	}
	bcopy (hp->h_addr, &inaddr.sin_addr.s_addr, hp->h_length);
    }

    /* connect to NCD RS-232 daemon */
    if (connect (sfd, &inaddr, sizeof (inaddr)) < 0) {
	perror (progname);
	exit (1);
    }

#ifdef DEBUG
    stamp (stderr);
    fprintf (stderr, "got connection, trying to reset printer\n");
#endif

    /* reset the printer */
    reset_printer (sfd, stderr);

#ifdef DEBUG
    stamp (stderr);
    fprintf (stderr, "initial printer reset worked, sending job\n");
#endif

    /* send the PostScript to the printer and wait for echoed ^D */
    print_job (0, sfd, stderr);

#ifdef DEBUG
    stamp (stderr);
    fprintf (stderr, "job done\n");
#endif

    /* clean up */
    exit (0);
}

/*
 * This function attempts to write an EOF (^D) to the LaserWriter
 * and then waits for the ^D to be echoed by the printer.  It will
 * only try to read the echoed ^D MAX_RETRIES times.  Each time it
 * cannot read from the printer it will wait RESET_TIMEOUT seconds
 * before attempting another read.  If it cannot complete it's task
 * it causes the utility to exit with status FAILED_REQUEUE.  This
 * is intended to tell the BSD spooler to requeue the request even
 * though it could not be printed.
 */

reset_printer (fd, errfd)
    int         fd;
    int         errfd;
{
    char        buf[BUFSIZE];
    int         len;
    int         err;
    fd_set      rfds;
    struct timeval timeout;
    int         retry_count = 0;

    /* As an aside here:  With the way the NCD RS-232 daemon works
     * we will almost always get to the point where we can send the
     * ^D.  If the NCD network display station is not set up to
     * use the serial port for printing, the NCD network display
     * station side will close the network connection.  This will
     * cause the read of the echoed ^D to fail and the utility
     * will exit.  The reason the NCD network display station doesn't
     * just refuse the connection is one of performance.  Quite simply,
     * it's just a lot cheaper to let the connection be established
     * and then have the NCD side immediately close it.
     */

    /* write ^D to printer */
    buf[0] = CTRLD;
    err = write (fd, buf, 1);
    if (err != 1) {
	stamp (errfd);
	fprintf (errfd, "write of reset EOF returned %d\n", err);
	exit (1);
    }

    /* establish timeout values */
    timeout.tv_sec = RESET_TIMEOUT;
    timeout.tv_usec = 0;

    /* we'll loop until the echoed ^D is read at which time we return */
    while (TRUE) {

	/* see if read on socket will work */
	FD_ZERO (&rfds);
	FD_SET (fd, &rfds);
	if ((err = select (FD_SETSIZE, &rfds, NULL, NULL, &timeout)) < 0) {
	    stamp (errfd);
	    fprintf (errfd, "reset select returned %d\n", err);
	    exit (1);
	}

	/* read from network connection, watch for echoed ^D */
	if (FD_ISSET (fd, &rfds)) {
	    if ((len = read (fd, buf, sizeof (buf)-1)) > 0) {
		buf[len] = '\000';
		if (index ((char *)buf, CTRLD) != (char *)0) {
#ifdef DEBUG
		    stamp (stderr);
		    fprintf (stderr, "printer reset\n");
#endif
		    return;
#ifdef DEBUG
		} else {
		    stamp (stderr);
		    fprintf (stderr,
			"reset got \"%s\" while waiting for EOF\n", buf);
		}
#else
		}
#endif
	    } else {		/* read error or EOF */
		stamp (errfd);
		fprintf (errfd, "reset read returned %d\n", len);
		exit (1);
	    }
	} else {		/* timeout */
	    ++retry_count;
	    if (retry_count < MAX_RETRIES) {
		stamp (errfd);
		fprintf (errfd, "timed out reseting printer %d\n", retry_count);
	    } else {
		stamp (errfd);
		fprintf(errfd, "hit max retries -- giving up\n");
		exit (1);
	    }
	}
    }
}

/*
 * This function ...
 *
 */

print_job (input, printer, errors)
    int		input;
    int		printer;
    int		errors;
{
    int		no_eof_yet = TRUE;
    int		err;
    int		len;
    char	buf[BUFSIZE];
    int		written;
    struct	timeval timeout;

    /* establish timeout values */
    timeout.tv_sec = RESET_TIMEOUT;
    timeout.tv_usec = 0;

    /* make sure we don't get stuck in a write to the socket */
    err = fcntl (printer, F_SETFL, FNDELAY);

    /* we'll copy bytes from the input to the printer until we get
     * an EOF on the input side.
     */
    while (no_eof_yet) {
	fd_set      rfds;
	fd_set      wfds;

	/* see if we can read */
	FD_ZERO (&rfds);
	FD_SET (input, &rfds);
	if ((err = select (FD_SETSIZE, &rfds, NULL, NULL, &timeout)) < 0) {
	    stamp (errors);
	    fprintf (errors, "read select returned %d\n", err);
	    exit (1);
	}

	/* We must be able to read input.  Otherwise we'll delay and
	 * go back to the select.
	 */
	if (FD_ISSET (input, &rfds)) {
	    if ((len = read (input, buf, sizeof (buf))) <= 0) {
		/* EOF or error - manufacture ^D for the printer */
		no_eof_yet = FALSE;
		buf[0] = CTRLD;
		len = 1;
#ifdef DEBUG
		stamp (stderr);
		fprintf (stderr, "manufactured EOF for printer\n");
#endif
	    }
#ifdef DEBUG
	    stamp (stderr);
	    fprintf (stderr, "read %d chars from stdin\n", len);
#endif
	    /* send read data to printer - watch out for partial writes */
	    written = 0;
	    while (written < len) {
		/* To make sure we don't spin on non-blocking write, let's
		 * select first ...
		 */
		FD_ZERO (&wfds);
		FD_SET (printer, &wfds);
		if ((err = select (FD_SETSIZE, NULL, &wfds, NULL, &timeout)) < 0) {
		    stamp (errors);
		    fprintf (errors, "write select returned %d\n", err);
		    exit (1);
		}
		/* make sure we can write something */
		if (FD_ISSET (printer, &wfds)) {
		    err = write (printer, buf+written, len-written);
#ifdef DEBUG
		    stamp (stderr);
		    fprintf (stderr, "write to printer %d chars from %d\n", err, written);
#endif
		    /* this isn't really an error ... */
		    if (err == -1 && errno == EWOULDBLOCK) {
			err = 0;
#ifdef DEBUG
			stamp (stderr);
			fprintf (stderr, "write to printer would block\n");
#endif
		    } else
			if (err < 0) {
			    stamp (errors);
			    fprintf (errors, "write to printer returned %d\n", err);
			    exit (1);
			}
		    /* update # bytes written and check printer status */
		    written += err;
		    err = check_printer_for_errors (printer, errors, 0);
		} else		/* can't write right now ... */
		    /* check printer status - allow some delay */
		    err = check_printer_for_errors (printer, errors, RESET_TIMEOUT);

		/* we shouldn't see ^D from printer yet */
		if (no_eof_yet && (err < 0)) {
		    stamp (errors);
		    fprintf (errors, "Spurious EOF from printer\n");
		}
	    }
	}
    }
    /* watch for final ^D echo */
    while (check_printer_for_errors (printer, errors, RESET_TIMEOUT*2) >= 0)
	;
}

/*
 * This function reads text back from the LaserWriter and parses it
 * into lines.  It then emits these lines on the specified file
 * (usually stderr).
 */

int	prtbufi = 0;
char	prtbuf[BUFSIZE];

int check_printer_for_errors (fd, errfd, delay)
    int		fd;
    int		errfd;
    int		delay;
{
    int		state;
    char	*left;
    char	*right;
    int		err;
    int		len;
    int		retval = 0;
    fd_set      rfds;
    struct	timeval timeout;

#ifdef DEBUG
    stamp (stderr);
    fprintf (stderr, "check printer with delay %d\n", delay);
#endif

    /* get set for the select - timeout is specified to this function */
    FD_ZERO (&rfds);
    FD_SET (fd, &rfds);
    timeout.tv_sec = delay;
    timeout.tv_usec = 0;

    /* make sure we can read something */
    if ((err = select (FD_SETSIZE, &rfds, NULL, NULL, &timeout)) < 0) {
	stamp (errfd);
	fprintf (errfd, "error select returned %d\n", err);
	exit (1);
    }

    /* If we can't read, then return */
    if (!FD_ISSET (fd, &rfds)) {
#ifdef DEBUG
	stamp (stderr);
	fprintf (stderr, "no printer status to read\n");
#endif
	return (retval);
    }

    /* if we can read, then let's do it ... */
    len = read (fd, prtbuf+prtbufi, sizeof (prtbuf)-prtbufi-1);
    if (len < 0 && errno == EWOULDBLOCK)
	return (retval);	/* just paranoid about blocking ... */
    if (len < 0) {
	stamp (errfd);
	fprintf (errfd, "read from printer returned %d\n", len);
	exit (1);
    }
    prtbufi += len;

    /* terminate the current input buffer and parse away */
    prtbuf[prtbufi] = '\000';
    left = right = prtbuf;
    while (*right) {
	switch (*right) {
	    case CTRLD:		/* ^D seen - make sure we return -1 */
		retval = -1;
#ifdef DEBUG
		stamp (stderr);
		fprintf (stderr, "EOF from printer\n");
#endif
		/* drop through and treat ^D as line break */
	    case '\012':	/* LF */
	    case '\015':	/* CR */
		/* if we see adjacent line break characters ignore them */
		if (left == right) {
		    left = ++right;
		    break;
		}
		/* terminate the line string and pass it to the reporter */
		*right = '\000';
		printer_status (left, errfd);
		left = ++right;
		break;
	    default:		/* just skip over other chars */
		right++;
	}
    }

    /* If we've not collected anything to report, just return */
    if (left == prtbuf) {
#ifdef DEBUG
	stamp (stderr);
	fprintf (stderr, "no printer status line collected\n");
#endif
	return (retval);
    }

    /* Otherwise copy the partial line back to the beginning of the buffer */
    right = left;
    left = prtbuf;
    while (*right)
	*left++ = *right++;
    prtbufi = (left-prtbuf);

    return (retval);
}

/*
 * Just report the status line to the specified file.
 */

printer_status (line, fd)
    char	*line;
    int		fd;
{
    stamp (fd);
    fprintf (fd, "%s\n", line);
}

/*
 * generate time stamp for logging messages.
 */

stamp (fd)
    int		fd;
{
	struct tm *tm;
	long now;

	now = time (0);
	tm = localtime (&now);
	fprintf (fd, "%02d%02d%02d %2d:%02d:%02d: %s: %s: ",
	    tm->tm_year, tm->tm_mon, tm->tm_mday,
	    tm->tm_hour, tm->tm_min, tm->tm_sec,
	    progname, printername);
}
