/*
 * Cannibalized from the following (in no particular order):
 *	/TekXP-V8.1/tekxp/examples/data_xp/data_xp.c
 *	ftp://axis/npipe/prosd.c
 *	/usr/sbin/WireFullDuplex.pm (which should live in /usr/local/lib/perl5/site_perl :-)
 *
 * V1.8  28 Sep 11  With -reconnect-when-idle option for "hold print" bugs on Ricoh
 *  1.7  24 Feb 09  Better handle errors
 *  1.6   9 Jan 09  Handle "read (from printer) returned 0, errno=0" from Kyocera
 *  1.5  15 Feb 05  Rewrite for Debian
 *  1.4  19 Jul 00  Allow for ETIMEDOUT errors
 *  1.3  10 Sep 99  Make timestamp Y2K compliant
 *  1.2  17 Jun 98  Allow ports up to 9100 at least (for HP JetDirect)
 *  1.1  12 Jun 98  Minor cleanup
 *  1.0  26 Sep 97
 *
Paul Szabo - psz@maths.usyd.edu.au  http://www.maths.usyd.edu.au:8000/u/psz/
School of Mathematics and Statistics  University of Sydney   2006  Australia
 *
 * Utility to send data to a printer connected to the serial or parallel
 * port of a Tektronix X-terminal; it is assumed that the X-terminal is
 * configured properly (and will send the data to the correct peripheral
 * port). This same utility may be used to send data to a data_wr process
 * running on a UNIX host (and thus to a printer connected to that host),
 * or to an HP printer equipped with a JetDirect card.
 *
 * Takes characters from STDIN or a file or a pipe, and writes them
 * (without any translation) to the serial or parallel port on the
 * specified X-station. Also monitors the status from the printer and
 * reports any information returned by the printer to STDERR.
 *
 * Design notes:
 *
 * We do not attempt to make any sense of the data stream, not even to
 * 'split' it into jobs. The one input file may contain more than one
 * job, properly delimited with ctrl-D for PostScript or 'ESC @' for
 * Epsons. Since such delimiters may also occur as part of graphics or
 * whatever, to scan for them we would need to do a full printer-specific
 * syntax check on the stream.
 *
 * We do not attempt to make any sense of the printer status stream, not
 * even to split it into lines.
 */


#define BUFSIZE 4096
#define IDLE_DEFAULT 20


#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#ifdef __linux__
#include <time.h>
#else
#include <sys/time.h>
#endif
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>

#ifdef __linux__
/* Avoid bcopy warning */
#include <strings.h>
#endif


extern time_t time();
extern int errno;
extern struct tm *localtime();

char *progname;
char *addrstr = NULL;
int portnum = 0;


usage()
{
    fprintf(stderr,"Usage: %s\n",progname);
    fprintf(stderr," -addr <string>  address/name of the X station to send to. REQUIRED\n");
    fprintf(stderr," -port <num>     port number (87?) on the X station to send to. REQUIRED\n");
    fprintf(stderr," -file <file>    take input from file <file>, default stdin\n");
    fprintf(stderr," -pipe <file>    take input from named pipe <file>\n");
    fprintf(stderr," -idle <secs>    seconds to wait for printer status output, default %d\n", IDLE_DEFAULT);
    fprintf(stderr,"      Consider the speed of the communication line when setting -idle:\n");
    fprintf(stderr,"      you will get repeated 'Have data to send, waiting for printer' messages\n");
    fprintf(stderr,"      unless all of the buffer of size [%d] can be sent within IDLE.\n", BUFSIZE);
    fprintf(stderr," -reconnect-when-idle\n");
    fprintf(stderr,"      When idle after finishing communication, drop (close) the connection\n");
    fprintf(stderr,"      and reconnect straight away. It is hoped this will work around the\n");
    fprintf(stderr,"      \"hold print\" issue with the Ricoh copiers, and may help with the\n");
    fprintf(stderr,"      bug when they often get stuck.\n");
    fprintf(stderr," -debug <int>    debug level (bit pattern)\n");
    fprintf(stderr,"     dec   hex   oct    messages about\n");
    fprintf(stderr,"       1     1     1    open/close files, sockets\n");
    fprintf(stderr,"       2     2     2    connect to X station\n");
    fprintf(stderr,"       4     4     4    select call output\n");
    fprintf(stderr,"       8     8    10    read from printer\n");
    fprintf(stderr,"      16    10    20    write to printer\n");
    fprintf(stderr,"      32    20    40    read from file/pipe\n");
    fprintf(stderr,"      64    40   100    repeated errors\n");
    fprintf(stderr,"ABORTING\n");
    exit(1);
}


main (argc, argv)
    int		argc;
    char	**argv;
{
    struct sockaddr_in inaddr;
    struct hostent *hp;
    struct linger net_linger;
    struct timeval timeout;
    fd_set rfds, wfds, efds;

    unsigned char rbuf[BUFSIZE];
    unsigned char wbuf[BUFSIZE];

    int stat, i;

    int pipe = 0;
    int pfd = 0;
    int sfd = 0;

    int cntd = 0;
    int cflg = -2;
    int pflg = -2;
    int rlen = 0;
    int rflg = -2;
    int wlen = 0;
    int wpos = 0;
    int wtot = 0;
    int wflg = -2;
    int eoff = 0;

    int idle = IDLE_DEFAULT;	/* seconds to wait before timing out */

    int debug = 0;
    int reconnect_when_idle = 0;
    int sleep_after_close = 0;


    stamp("Daemon started");

    progname = argv[0];

    if (argc < 1) {
	usage();
    }


    for (i = 1; i < argc; i++) {
	if (!strcmp(argv[i], "-addr")) {
	    if (++i >= argc) usage();
	    addrstr = argv[i];
	}
	else if (!strcmp(argv[i], "-port")) {
	    if (++i >= argc) usage();
	    portnum = atoi(argv[i]);
	    if (portnum <= 0 || portnum > 10000) {
		stamp("Bad port number %s (%d)", argv[i], portnum);
		stamp("ABORTING (use -help for usage info)");
		exit(1);
	    }
	}
	else if (!strcmp(argv[i], "-file")) {
	    if (++i >= argc) usage();
	    pipe = 0;
	    if (debug&0x01) stamp("About to open file %s", argv[i]);
	    pfd = open(argv[i], O_RDONLY, 0777);
	    if (pfd <= 0) {
		stamp("Cannot open input file %s", argv[i]);
		stamp("open(O_RDONLY) returned %d, errno=%d", pfd, errno);
		perror ("errno means");
		stamp("ABORTING (use -help for usage info)");
		exit(1);
	    }
	    if (debug&0x01) stamp("Open successful");
	}
	else if (!strcmp(argv[i], "-pipe")) {
	    if (++i >= argc) usage();
	    pipe = 1;
	    if (debug&0x01) stamp("About to open pipe %s", argv[i]);
	    /* 
	     * Open the pipe O_RDWR even though we never write to it.
	     * If we used O_RDONLY then the open would block until some
	     * data was written to the pipe, and we would get EOF "error"
	     * (which is handled below, but prefer not...).
	     */
	    pfd = open(argv[i], O_RDWR, 0777);
	    if (pfd <= 0) {
		stamp("Cannot open named pipe %s", argv[i]);
		stamp("open(O_RDWR) returned %d, errno=%d", pfd, errno);
		perror ("errno means");
		stamp("ABORTING (use -help for usage info)");
		exit(1);
	    }
	    if (debug&0x01) stamp("Open successful");
	}
	else if (!strcmp(argv[i], "-idle")) {
	    if (++i >= argc) usage();
	    idle = atoi(argv[i]);
	    if (idle <= 0 || idle > 200) {
		stamp("Bad delay %s (%d)", argv[i], idle);
		stamp("ABORTING (use -help for usage info)");
		exit(1);
	    }
	}
	else if (!strcmp(argv[i], "-reconnect-when-idle")) {
	    reconnect_when_idle = 1;
	}
	else if (!strcmp(argv[i], "-debug")) {
	    if (++i >= argc) usage();
	    debug = atoi(argv[i]);
	}
	else {
	    stamp("Bad argument %s", argv[i]);
	    usage();
	}
    }

    /* make sure required parameters were specified */
    if (addrstr == NULL) {
	stamp("No station name/address specified, use -addr");
	usage();
    }
    if (portnum == 0) {
	stamp("No port number specified, use -port");
	usage();
    }


    if (debug) stamp("Running with debug=%d (0x%x, \\0%o)", debug, debug, debug);


    /* resolve network address of unit */
    inaddr.sin_family = AF_INET;
    inaddr.sin_addr.s_addr = inet_addr(addrstr);
    if (inaddr.sin_addr.s_addr == -1) {
	hp = gethostbyname(addrstr);
	if (hp == NULL || hp->h_addrtype != AF_INET || hp->h_length != sizeof(inaddr.sin_addr.s_addr)) {
	    stamp("Cannot resolve name %s", addrstr);
	    stamp("ABORTING (use -help for usage info)");
	    exit(1);
	}
	bcopy(hp->h_addr, (char *)&inaddr.sin_addr.s_addr, hp->h_length);
    }

    /* port number */
    inaddr.sin_port = htons(portnum);


    while (1) {
	if (!cntd) {
	    if (sfd) {
		/*
		 * It seems we cannot reuse the same socket from a failed
		 * connection. Close it, then re-create.
		 */
		if (debug&0x01) stamp("About to close socket sfd=%d", sfd);
		stat = close(sfd);
		if (stat) {
		    stamp("Close socket sfd=%d failed, will re-create new anyway", sfd);
		    stamp(" ... close returned %d, errno=%d", stat, errno);
		}
		if (sleep_after_close) {
		    sleep(sleep_after_close);
		    sleep_after_close = 0;
		}
	    }

	    /* create a new socket */
	    if (debug&0x01) stamp("About to create socket");
	    sfd = socket(AF_INET, SOCK_STREAM, 0);
	    if (sfd < 0) {
		stamp("Cannot create socket: returned %d, errno=%d", sfd, errno);
		perror ("errno means");
		stamp("ABORTING");
		exit(1);
	    }
	    if (debug&0x01) stamp("Socket created sfd=%d", sfd);

	    /* enable linger on close of socket, with 360 second timeout */
	    /* Whatever for? We do not exit until all done. */
	    net_linger.l_onoff = 1;
	    net_linger.l_linger = 360;
	    setsockopt(sfd,SOL_SOCKET,SO_LINGER,&net_linger,sizeof(net_linger));


	    /* connect to unit, may need to keep re-trying */
	    if (debug&0x02) {
		stamp("About to connect to %s:%d", addrstr, portnum);
		stamp(" addr=%08x, family=%d, port=%d", inaddr.sin_addr.s_addr, inaddr.sin_family, inaddr.sin_port);
	    }
	    stat = connect(sfd,
#ifdef __linux__
		(__CONST_SOCKADDR_ARG)
#endif
			   &inaddr,
			   sizeof(inaddr));
	    if (stat < 0) {
		if (cflg != errno || debug&0x0040) {
		    stamp("Cannot connect to X-station %s:%d", addrstr, portnum);
		    stamp(" ... connect returned %d, errno=%d", stat, errno);
		    perror ("errno means");
		    stamp("Will retry later ...");
		    cflg = errno;
		}
		sleep(idle);
		continue;
	    }
	    if (cflg != -2) {
		stamp("Connect to X-station %s:%d succeeded", addrstr, portnum);
	    }
	    cntd = 1;
	    cflg = -2;
	}


	FD_ZERO(&rfds);
	FD_ZERO(&wfds);
	FD_ZERO(&efds);
	FD_SET(sfd, &rfds);
	if (debug&0x04) FD_SET(sfd, &efds);
	if (wlen == 0) {
	    if (debug&0x04) FD_SET(pfd, &efds);
	    if (!eoff) {
		FD_SET(pfd, &rfds);
	    }
	}
	else {
	    FD_SET(sfd, &wfds);
	}

	/* establish timeout value */
	timeout.tv_sec = idle;
	timeout.tv_usec = 0;


	if (debug&0x04) stamp("Starting select with wlen=%d, wtot=%d", wlen, wtot);
	stat = select(FD_SETSIZE, &rfds, &wfds, &efds, &timeout);
	if (debug&0x04) {
	    int r,w,e;
	    stamp("select returned %d", stat);
	    stamp("       read   write  err");
	    r=FD_ISSET(sfd, &rfds); w=FD_ISSET(sfd, &wfds); e=FD_ISSET(sfd, &efds);
	    stamp("socket:  %d     %d     %d",r,w,e);
	    r=FD_ISSET(pfd, &rfds); w=FD_ISSET(pfd, &wfds); e=FD_ISSET(pfd, &efds);
	    stamp("pipe:    %d     %d     %d",r,w,e);
	}
	if (stat < 0) {
	    stamp("select returned %d, errno=%d", stat, errno);
	    perror ("errno means");
	    stamp("ABORTING");
	    exit(1);
	}
	else if (stat == 0) {
	    /* We are idle */
	    if (wlen > 0) {
		if (wflg != -1) {
		    stamp("Have data to send, waiting for printer");
		    wflg = -1;
		}
	    }
	    else {
		if (!pipe || eoff) {
		    stamp("Sent %d bytes to printer, exiting", wtot);
		    exit(0);
		}
		if (wtot > 0) {
		    stamp("Sent %d bytes to printer", wtot);
		    wtot = 0;
		    wflg = -2;
		    if (reconnect_when_idle) {
			stamp("Reconnect when idle: dropping connection, will try to re-connect");
			cntd = 0;
			cflg = -1;
			/* sleep(idle); */ /* Right now, do not wait */
			sleep_after_close = 1;	/* ... otherwise would get "Connection refused" */
			continue;
		    }
		}
	    }
	    continue;
	}


	if (FD_ISSET(sfd, &rfds)) {
	    rlen = read(sfd, rbuf, sizeof(rbuf)-1);
	    if (rlen <= 0) {
		if (errno == ECONNRESET) {
		    stamp("Connection (read) reset by peer? Will try to re-connect");
		    cntd = 0;
		    cflg = errno;
		    sleep(idle);
		    continue;
		}
		if (errno == ETIMEDOUT) {
		    stamp("Connection (read) timed out? Will try to re-connect");
		    cntd = 0;
		    cflg = errno;
		    sleep(idle);
		    continue;
		}
		if (rlen == 0 && errno == 0) {
		    stamp("Printer idle disconnect? Trying to re-connect");
		    cntd = 0;
		    cflg = -1;
		    /* sleep(idle); */ /* Right now, no need to wait */
		    continue;
		}
		if (rlen == 0) {
		    stamp("read (from printer) returned nothing (0), errno=%d", errno);
		    perror ("errno means");
		    stamp("Will try to re-connect");
		    cntd = 0;
		    cflg = errno;
		    sleep(idle);
		    continue;
		}
		if (rflg != errno || debug&0x0040) {
		    stamp("read (from printer) returned %d, errno=%d", rlen, errno);
		    perror ("errno means");
		    stamp("(Keeping connection open)");
		    rflg = errno;
		}
		sleep(idle); /* Do not fill up the log file too quickly */
	    }
	    else {
		if (debug&0x08) stamp("%d bytes returned by printer:", rlen);
		rbuf[rlen] = 0;
		fprintf (stderr,"%s", rbuf);
		if (debug&0x08) {
		    fprintf (stderr,"\n");
		    stamp("Printer data above");
		}
		rflg = -2;
	    }
	}

	if (wlen == 0 && FD_ISSET(pfd, &rfds)) {
	    wlen = read(pfd, wbuf, sizeof(wbuf));
	    if (wlen == 0 && errno == 1) {
		/* Weird way of signalling EOF (errno=1 means "Not owner") */
		if (pipe) {
		    if (pflg != errno) {
			stamp("EOF reached on pipe, weird");
			pflg = errno;
		    }
		    sleep(idle); /* Do not fill up the log file too quickly */
		}
		else {
		    /* close(pfd); */
		    stamp("EOF reached on file, will not read again");
		    eoff = 1;
		}
	    }
	    else if (wlen <= 0) {
		if (pflg != errno || debug&0x0040) {
		    stamp("read (from pipe/file) returned %d, errno=%d", wlen, errno);
		    perror ("errno means");
		    pflg = errno;
		}
		sleep(idle); /* Do not fill up the log file too quickly */
		wlen = 0;
	    }
	    else {
		pflg = -2;
		if (debug&0x0020) stamp("%d bytes read from file/pipe", wlen);
	    }
	}
	else if (wlen > 0 && FD_ISSET(sfd, &wfds)) {
	    stat = write(sfd, wbuf+wpos, wlen-wpos);
	    if (stat <= 0) {
		if (errno == ECONNRESET) {
		    stamp("Connection (write) reset by peer? Will try to re-connect");
		    cntd = 0;
		    cflg = errno;
		    sleep(idle);
		    continue;
		}
		if (errno == ETIMEDOUT) {
		    stamp("Connection (write) timed out? Will try to re-connect");
		    cntd = 0;
		    cflg = errno;
		    sleep(idle);
		    continue;
		}
		if (wflg != errno || debug&0x0040) {
		    stamp("write (to printer) returned %d, errno=%d", stat, errno);
		    perror ("errno means");
		    stamp("(Keeping connection open)");
		    wflg = errno;
		}
		sleep(idle); /* Do not fill up the log file too quickly */
	    }
	    else {
		if (debug&0x0010) stamp("%d bytes written to printer (out of buffer of %d)", stat, wlen-wpos);
		if (stat < wlen-wpos) {
		    wpos += stat;
		    if (wpos >= wlen) { /* This is sure not to happen, but... */
			wlen = 0;
			wpos = 0;
		    }
		}
		else {
		    wlen = 0;
		    wpos = 0;
		}
		wtot += stat;
		wflg = -2;
	    }
	}
    }
}


/*
 * log messages with time stamp
 */
stamp (fmt, err1, err2, err3)
void *fmt, *err1, *err2, *err3;
{
	register struct tm *tm_ptr;
	time_t now;

	now = time((time_t *)NULL);
	tm_ptr = localtime(&now);

	/* No need to log program name, X-station address or port number:
	   deducible from name of log file */
/*
 *	fprintf(stderr, "%02d/%02d/%02d %02d:%02d:%02d %s: %s:%d: ",
 *	    tm_ptr->tm_year, tm_ptr->tm_mon + 1, tm_ptr->tm_mday,
 *	    tm_ptr->tm_hour, tm_ptr->tm_min, tm_ptr->tm_sec,
 *	    progname, addrstr, portnum);
 */
	fprintf(stderr, "%04d/%02d/%02d %02d:%02d:%02d ",
	    tm_ptr->tm_year + 1900, tm_ptr->tm_mon + 1, tm_ptr->tm_mday,
	    tm_ptr->tm_hour, tm_ptr->tm_min, tm_ptr->tm_sec);
	fprintf(stderr, fmt, err1, err2, err3);
	fprintf(stderr, "\n");
}
