root/usr/src/cmd/lp/filter/postscript/postcomm/postcomm.c
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2005 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*      Copyright (c) 1984, 1986, 1987, 1988, 1989 AT&T */
/*        All Rights Reserved   */

/*
 *
 * A simple program that can be used to filter jobs for PostScript
 * printers. It's a cleaned up version of usg_iox, that I assume was
 * written by Richard Flood.  The most important addition includes some
 * simple processing of printer status reports, usually obtained when
 * \024 is sent to the printer. The returned status lines look like:
 *
 *
 *      %%[ status: idle; source serial 25 ]%%
 *      %%[ status: waiting; source serial 25 ]%%
 *      %%[ status: initializing; source serial 25 ]%%
 *      %%[ status: busy; source serial 25 ]%%
 *      %%[ status: printing; source serial 25 ]%%
 *      %%[ status: PrinterError: out of paper; source serial 25 ]%%
 *      %%[ status: PrinterError: no paper tray; source serial 25 ]%%
 *
 *
 * although the list isn't meant to be complete.
 *
 * Other changes to the original program include the addition of
 * options that let you select the tty line, baud rate, and printer
 * log file. The program seems to work reasonably well, at least for
 * our QMS PS-800 printer, but could still use some work.
 *
 * There were a couple of serious mistakes in the first few versions of
 * postcomm.  Both were made in setting up flow control in routine
 * initialize(). Setting the IXANY flag in c_iflag was wrong, and
 * often caused problems when the printer transmitted a spontaneous
 * status report, which always happens when the paper runs out.
 * Things were kludged up to get around the problems, but they were
 * never exactly right, and probably could never be guaranteed to work
 * 100%.
 *
 * The other mistake was setting the IXOFF flag, again in c_iflag.
 * Although I never saw deadlock in the original versions of postcomm,
 * it could happen.  Apparently the IXANY and IXOFF flags combined to
 * make that an unlikely event.  Anyway both flags should normally be
 * turned off to ensure reliable transmission of jobs.
 *
 * The implications of only setting IXON are obvious. Job transmission
 * should be reliable, but data returned by the printer over the tty
 * line may get lost. That won't cause problems in postcomm, but there
 * may be occasions when you want to run a job and recover data
 * generated by the printer. The -t option sets the IXOFF, IXANY, and
 * IXON flags in c_iflag and causes send() to be far more careful about
 * when data is sent to the printer. In addition anything not
 * recognized as a status report is written on stdout. It seems to
 * work reasonably well, but it's slow and may hang or have flow
 * control problems. Only use the -t option when it's absolutely
 * necessary. A small block size, like 512, should also help.
 *
 * Using two processes, one for reads and the other for writes, may
 * eventually be needed. For now postcomm seems to do a good job
 * transmitting data, and the -t option is probably acceptable for
 * those few jobs that need to recover data from the printer.
 *
 * A typical command line might be:
 *
 *      postcomm -L log -t <file1 > device
 *
 * where -L selects the printer log file and -t sends data from the
 * printer out to the printer.  If you don't choose a log file stderr
 * will be used and the information mailed or written to you.
 *
 */

#include <stdio.h>
#include <stdarg.h>
#include <ctype.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>


# define        OFF             0
# define        ON              1
# define        TRUE            1
# define        FALSE           0
# define        FATAL           1
# define        NON_FATAL       0

#include "postcomm.h"           /* some special definitions */


char    *prog_name = "postcomm";        /* just for error messages */

int     debug = OFF;            /* debug flag */
int     ignore = OFF;           /* what's done for FATAL errors */


char    *block = NULL;          /* input file buffer */
int     blocksize = BLOCKSIZE;  /* and its size in bytes */
int     head = 0;               /* block[head] is the next character */
int     tail = 0;               /* one past the last byte in block[] */

char    mesg[BUFSIZE];          /* exactly what came back on ttyi */
char    sbuf[BUFSIZE];          /* for parsing the message */
int     next = 0;               /* next character goes in sbuf[next] */
Status  status[] = STATUS;      /* for converting status strings */

int     stopbits = 1;           /* number of stop bits */
int     tostdout = FALSE;       /* non-status stuff goes to stdout? */
int     curfile = 0;            /* only needed when tostdout is TRUE */

char    *postbegin = POSTBEGIN; /* preceeds all the input files */

int     ttyi;                   /* input */
int     ttyo = 2;               /* and output file descriptors */

FILE    *fp_log = stderr;       /* log file for data from the printer */



static void filter(void);
static int getstatus(int);
static void initialize(void);
static void options(int, char *[]);
static int readblock(int);
static int readline(void);
static void reset(void);
static int writeblock(void);

void
logit(char *mesg, ...)
{

/*
 *
 * Simple routine that's used to write a message to the log file.
 *
 */


    if (mesg != NULL)
    {
        va_list ap;

        va_start(ap, mesg);
        vfprintf(fp_log, mesg, ap);
        va_end(ap);
        fflush(fp_log);
    }

}   /* End of logit */





void
error(int kind, char *mesg, ...)
{


/*
 *
 * Called when we've run into some kind of program error. First *mesg is
 * printed using the control string arguments a?. Then if kind is FATAL
 * and we're not ignoring errors the program will be terminated.
 *
 * If mesg is NULL or *mesg is the NULL string nothing will be printed.
 *
 */


    if ( mesg != NULL  &&  *mesg != '\0' )  {
        va_list ap;

        fprintf(fp_log, "%s: ", prog_name);
        va_start(ap, mesg);
        vfprintf(fp_log, mesg, ap);
        va_end(ap);
        putc('\n', fp_log);
    }   /* End if */

    if ( kind == FATAL  &&  ignore == OFF )  {
        write(ttyo, "\003\004", 2);
        exit(1);
    }   /* End if */

}   /* End of error */





int
main(int argc, char *argv[])
{

/*
 *
 * A simple program that manages input and output for PostScript
 * printers. If you're sending a PostScript program that will be
 * returning useful information add the -ot option to the lp(1) command
 * line. Everything not recognized as a printer status report will go
 * to stdout. The -ot option should only be used when needed! It's slow
 * and doesn't use flow control properly, but it's probably the best
 * that can be done using a single process for reading and writing.
 */

    prog_name = argv[0];        /* really just for error messages */

    options(argc, argv);

    initialize();               /* Set printer up for printing */

    filter();

    reset();                    /* wait 'til it's finished & reset it*/

    return (0);         /* everything probably went OK */

}   /* End of main */





static void
options(int argc, char *argv[])
{


    int         ch;                     /* return value from getopt() */
    char        *names = "tB:L:P:DI";

    extern char *optarg;                /* used by getopt() */

/*
 *
 * Reads and processes the command line options.  The -t option should
 * only be used when absolutely necessary.  It's slow and doesn't do
 * flow control properly.  Selecting a small block size (eg. 512 or
 * less) with with the -B option may help when you need the -t option.
 *
 */


    while ( (ch = getopt(argc, argv, names)) != EOF )
    {
        switch ( ch )
        {
            case 't':           /* non-status stuff goes to stdout */
                    tostdout = TRUE;
                    break;

            case 'B':           /* set the job buffer size */
                    if ((blocksize = atoi(optarg)) <= 0)
                        blocksize = BLOCKSIZE;
                    break;

            case 'L':                   /* printer log file */
                    if ((fp_log = fopen(optarg, "w")) == NULL)
                    {
                        fp_log = stderr;
                        error(NON_FATAL, "can't open log file %s",
                                                                optarg);
                    }   /* End if */
                    break;

            case 'P':                   /* initial PostScript program */
                    postbegin = optarg;
                    break;

            case 'D':                   /* debug flag */
                    debug = ON;
                    break;

            case 'I':                   /* ignore FATAL errors */
                    ignore = ON;
                    break;

            case '?':                   /* don't understand the option */
                    error(FATAL, "");
                    break;

            default:                    /* don't know what to do for ch */
                    error(FATAL, "missing case for option %c\n", ch);
                    break;

        }   /* End switch */

    }   /* End while */
}   /* End of options */





static void
initialize(void)
{
    if ((block = malloc(blocksize)) == NULL)
        error(FATAL, "no memory");

    ttyi = fileno(stdout);

    if ((ttyo = dup(ttyi)) == -1)
        error(FATAL, "can't dup file descriptor for stdout");

/*
 *
 * Makes sure the printer is in the
 * IDLE state before any real data is sent.
 *
 */


    logit("printer startup\n");

    while ( 1 )
        switch (getstatus(1))
        {
            case IDLE:
                    if (postbegin != NULL)
                        write(ttyo, postbegin, strlen(postbegin));
                    else
                        write(ttyo, "\n", 1);
                    return;

            case WAITING:
            case BUSY:
            case ERROR:
                    write(ttyo, "\003\004", 2);
                    sleep(1);
                    break;

            case FLUSHING:
                    write(ttyo, "\004", 1);
                    sleep(1);
                    break;

            case PRINTERERROR:
            case INITIALIZING:
                    sleep(15);
                    break;

            case DISCONNECT:
                    /* talk to spooler w/S_FAULT_ALERT */
                    error(FATAL, "printer appears to be offline");
                    break;

            default:
                    sleep(1);
                    break;

        }   /* End switch */

}   /* End of initialize */





static void
filter(void)
{
    static int  wflag = 0;      /* nonzero if we've written a block */
    int         fd_in = fileno(stdin);

/*
 *
 * Responsible for sending the next file to the printer.
 * Most of the hard stuff is done in getstatus() and readline().
 * All this routine really does is control what happens for the
 * different printer states.
 *
 */


    logit("sending file\n");

    curfile++;

    while (readblock(fd_in))
        switch (getstatus(0))
        {
            case WAITING:
                    writeblock();
                    wflag = 1;
                    break;

            case BUSY:
            case PRINTING:
            case PRINTERERROR:
                    if (tostdout == FALSE)
                    {
                        writeblock();
                        wflag = 1;
                    }
                    else
                        sleep(1);
                    break;

            case UNKNOWN:
                    if (tostdout == FALSE)
                    {
                        writeblock();
                        wflag = 1;
                    }
                    break;

            case NOSTATUS:
                    if (tostdout == FALSE)
                    {
                        if (wflag)
                            writeblock();
                    }
                    else
                        sleep(1);
                    break;

            case IDLE:
                    if (wflag)
                        error(FATAL, "printer is idle");
                    write(ttyo, "\n", 1);
                    break;

            case ERROR:
                    fprintf(stderr, "%s", mesg);        /* for csw */
                    error(FATAL, "PostScript error");
                    break;

            case FLUSHING:
                    error(FATAL, "PostScript error");
                    break;

            case INITIALIZING:
                    error(FATAL, "printer booting");
                    break;

            case DISCONNECT:
                    error(FATAL, "printer appears to be offline");
                    break;

        }   /* End switch */

}   /* End of print */





static int
readblock(int fd_in)
    /* current input file */
{

/*
 *
 * Fills the input buffer with the next block, provided we're all done
 * with the last one.  Blocks from fd_in are stored in array block[].
 * Head is the index of the next byte in block[] that's supposed to go
 * to the printer.   tail points one past the last byte in the current
 * block.  head is adjusted in writeblock() after each successful
 * write, while head and tail are reset here each time a new block is
 * read.  Returns the number of bytes left in the current block.   Read
 * errors cause the program to abort.
 *
 */

    if (head >= tail)
    {           /* done with the last block */
        if ((tail = read(fd_in, block, blocksize)) == -1)
            error(FATAL, "error reading input file");
        head = 0;
    }

    return(tail - head);

}   /* End of readblock */





static int
writeblock(void)
{
    int         count;          /* bytes successfully written */

/*
 *
 * Called from send() when it's OK to send the next block to the
 * printer. head is adjusted after the write, and the number of bytes
 * that were successfully written is returned to the caller.
 *
 */


    if ((count = write(ttyo, &block[head], tail - head)) == -1)
        error(FATAL, "error writing to stdout");
    else
        if (count == 0)
            error(FATAL, "printer appears to be offline");

    head += count;
    return(count);
}   /* End of writeblock */





static int
getstatus(int t)
    /* sleep time after sending '\024' */
{
    char        *state;         /* new printer state - from sbuf[] */
    int         i;              /* index of new state in status[] */
    static int  laststate = NOSTATUS;
                                /* last state we found out about */

/*
 *
 * Sends a status request to the printer and tries to read the response.
 * If an entire line is available readline() returns TRUE and the
 * string in sbuf[] is parsed and converted into an integer code that
 * represents the printer's state.  If readline() returns FALSE,
 * meaning an entire line wasn't available, NOSTATUS is returned.
 *
 */

    if (readline() == TRUE)
    {
        state = sbuf;

        if (strncmp(sbuf, "%%[", 3) == 0)
        {
            strtok(sbuf, " ");          /* skip the leading "%%[ " */
            if (strcmp(state = strtok(NULL, " :;"), "status") == 0)
                state = strtok(NULL, " :;");
        }

        for (i = 0; status[i].state != NULL; i++)
            if (strcmp(state, status[i].state) == 0)
                break;

        if (status[i].val != laststate || debug == ON)
            logit("%s", mesg);

        if (tostdout == TRUE && status[i].val == UNKNOWN && curfile > 0)
            fprintf(stdout, "%s", mesg);

        return(laststate = status[i].val);
    }   /* End if */

    if ( write(ttyo, "\024", 1) != 1 )
        error(FATAL, "printer appears to be offline");

    if ( t > 0 )
        sleep(t);

    return(NOSTATUS);

}   /* End of getstatus */





static void
reset(void)
{
    int         sleeptime = 15;         /* for 'out of paper' etc. */
    int         senteof = FALSE;

/*
 *
 * We're all done sending the input files, so we'll send an EOF to the
 * printer and wait until it tells us it's done.
 *
 */


    logit("waiting for end of job\n");

    while (1)
    {
        switch (getstatus(2))
        {
            case WAITING:
                write(ttyo, "\004", 1);
                senteof = TRUE;
                sleeptime = 15;
                break;

            case ENDOFJOB:
                if (senteof == TRUE)
                {
                    logit("job complete\n");
                    return;
                }
                sleeptime = 15;
                break;

            case BUSY:
            case PRINTING:
                sleeptime = 15;
                sleep(1);
                break;

            case PRINTERERROR:
                sleep(sleeptime++);
                break;

            case ERROR:
                fprintf(stderr, "%s", mesg);    /* for csw */
                error(FATAL, "PostScript error");
                return;

            case FLUSHING:
                error(FATAL, "PostScript error");
                return;

            case IDLE:
                error(FATAL, "printer is idle");
                return;

            case INITIALIZING:
                error(FATAL, "printer booting");
                return;

            case DISCONNECT:
                error(FATAL, "printer appears to be offline");
                return;

            default:
                sleep(1);
                break;

        }   /* End switch */

        if (sleeptime > 60)
            sleeptime = 60;

    }   /* End while */

}   /* End of reset */









static int
readline(void)
{
    char        ch;                     /* next character from ttyi */
    int         n;                      /* read() return value */

/*
 *
 * Reads the printer's tty line up to a newline (or EOF) or until no
 * more characters are available. As characters are read they're
 * converted to lower case and put in sbuf[next] until a newline (or
 * EOF) are found. The string is then terminated with '\0', next is
 * reset to zero, and TRUE is returned.
 *
 */


    while ((n = read(ttyi, &ch, 1)) != 0)
    {
        if (n < 0)
            error(FATAL, "error reading stdout");
        mesg[next] = ch;
        sbuf[next++] = tolower(ch);
        if (ch == '\n'  ||  ch == '\004')
        {
            mesg[next] = sbuf[next] = '\0';
            if (ch == '\004')
                sprintf(sbuf, "%%%%[ status: endofjob ]%%%%\n");
            next = 0;
            return(TRUE);
        }   /* End if */
    }   /* End while */

    return(FALSE);

}   /* End of readline */