/* * $Id: //devel/tools/main/lpdcat/lpdcat.c#1 $ * * written by : Stephen J. Friedl * Software Consultant * friedl@mtndew.com * 29 July 1994 * * ===NOTE: I wrote this >10 years ago, haven't done any updates since. * * This program is meant to run out of a System V line printer interface * script and to send the file so mentioned to a remote line printer * daemon server somewhere. It was primarily motivated by the desire * to print to a remote PC's local LPD server from a UNIX host on the * other end of a 56k digital line, where the only way to get printouts * from UNIX was to walk a couple of blocks to the main office. Bummer. * * This was written based entirely on RFC1179 - Line Printer Daemon * Protocol (October 1990) and was tested with PC/TCP v3.0's LPD * server under Windows. It runs on SCO UNIX. * * We communicate with the daemon via a TCP connection, and the commands * are all very simple and in ASCII with newlines at the end. The * acknowledgement from the remote daemon is either a NUL byte for * "all OK" or a non-NUL byte for some kind of error. There is no * specification for what error states or codes mean. * * IMPLEMENTATION * * This program was build to talk to PC/TCP v3.0's LP server, and some * of the implementation issues are because it's not a UNIX system. We * talk about them here. * * TO DO * - add cancel logic * - make smarter with timers and the like * - deal with stdin printing * * TO BUILD * * SCO UNIX: cc lpdcat.c -o lpdcat -lsocket * NCR SVR4: cc lpdcat.c -o lpdcat -lsocket -lnet -lnsl * * HISTORY * Fri Jul 29 11:08:09 PDT 1994 by SJF * - program created */ #include #include #include #include #include #include #include #include /* for getXXXbyYYY */ #include #include #include #include /* for sockaddr */ #include /* for sockaddr_in */ #include /* for addr -> name conversion */ #include #ifndef __GNUC__ # define __attribute__(x) /*nothing*/ #endif #define UNUSED_PARAMETER(x) (void)(x) #ifndef MAXHOSTNAMELEN # define MAXHOSTNAMELEN 256 #endif #define dprintf printf #define TRUE 1 #define FALSE 0 static char *LPD_host = 0, /* hostname for LPD server */ *Printer_queue = 0, /* printer name to print to */ *My_host_name = 0, /* ... for identification */ *My_user_name = 0, /* ... for identification */ *Job_name = 0, /* job name for ID */ *Job_file = 0, /* filename for job number */ *Mail_user = 0, /* username for mailing */ *Prog_name = 0; /* program name (argv[0]) */ static int Job_number = 1, /* current job being printed */ Unlink_file = 0, /* unlink file when done */ Ncopies = 1, /* # of copies to print */ Timeout_secs = 0, /* timeout for writes */ ACK_debug = 0, /* more extensive ACK debug */ Debug = 0; /* enable/disable debug */ static void usage(int); static void eprintf(const char *, ...) __attribute__((format(printf, 1, 2))); static void die(const char *, ...) __attribute__((noreturn, format(printf, 1, 2))); static int getport(const char *); static long next_jobno(const char *, long); static int print_file(int, const char *); static void catch_signal(int); extern char *optarg; extern int optind; /*------------------------------------------------------------------------ * This section includes the command-line options and their associated * help information. We put them all together to (hopefully) reduce the * chance that something will be overlooked when one is added. */ #define OPTS "h:p:n:H:U:DuM:J:j:c:T:A:" #define USAGE "-p prtr [-H myhost] [-U user] [-h LPD_host] [-D] [-j job#]" static const char *Help_text[] = { "-h rmthost specify name of remote host (required)", "-p ptr specify name of remote printer queue (required)", "-n jobname specify jobname for listing in the queue", "-H hostname specify local hostname for job-ID purposes", "-U user specify local username for job-ID purposes", "-D enable debugging", "-u unlink data file when done printing", "-M username specify name for mailing when finished", "-J jobfile specify filename for job-number control", "-c copies specify number of copies (default = 1)", "-T timeout specify timeout seconds (default = none)", "-j jobno specify job number (up to 3 digits)", 0 /* ENDMARKER */ }; static int careful_fd_copy(int ifd, int ofd, int alarmtime); int main(int argc, char **argv) { int c; Prog_name = argv[0]; Job_number = getpid() * 10; /*---------------------------------------------------------------- * quick check -- see if the user wants help or obviously doesn't * know what s/he is doing. */ if (argc <= 1 || strcmp(argv[1], "-help") == 0) usage(1); while ( (c = getopt(argc, argv, OPTS)) != EOF) { switch (c) { case 'A': ACK_debug = 1; break; case 'c': /* # of copies */ Ncopies = atoi(optarg); break; case 'T': /* timeout in seconds */ Timeout_secs = atoi(optarg); break; case 'J': /* specify file with job number */ Job_file = optarg; break; case 'u': /* unlink file when finished */ Unlink_file = TRUE; break; case 'M': /* mail user name */ Mail_user = optarg; break; case 'n': /* job name */ Job_name = optarg; break; case 'D': /* increment debugging level */ Debug++; break; case 'H': /* my host name for ID */ My_host_name = optarg; break; case 'U': /* my user name for ID */ My_user_name = optarg; break; case 'j': /* job number to use here */ Job_number = atoi(optarg); break; case 'p': /* which printer? */ Printer_queue = optarg; break; case 'h': /* remote printing host */ LPD_host = optarg; break; default: usage(0); } } if (Timeout_secs > 0) signal(SIGALRM, catch_signal); signal(SIGPIPE, catch_signal); /*---------------------------------------------------------------- * do some validation on the command-line arguments and all that */ if (LPD_host == 0) die("missing LPD host!"); if (Printer_queue == 0) die("missing remote printer-queue name!"); if (Debug) dprintf("printing to %s:%s\n", LPD_host, Printer_queue); /*---------------------------------------------------------------- * now fill in some of the arguments that were omitted on the * command line. */ if (My_host_name == 0) { static char hostbuf[MAXHOSTNAMELEN + 1]; if (gethostname(hostbuf, sizeof hostbuf) != -1) My_host_name = hostbuf; else My_host_name = "unknown"; } if (My_user_name == 0) My_user_name = getenv("LOGNAME"); if (My_user_name == 0) die("missing username!"); if (Debug) dprintf("You are <%s> at <%s>\n", My_user_name, My_host_name); if (optind >= argc) { int fd; if ( (fd = open("/tmp/pctcp.tmp", O_CREAT|O_RDWR, 0666)) < 0) die("cannot copy stdin to temp file! [%s]", strerror(errno)); unlink("/tmp/pctcp.tmp"); if (Debug) dprintf("copying stdin to temp file..."); careful_fd_copy(fileno(stdin), fd, 0); if (Debug) dprintf("done!\n"); lseek(fd, 0L, SEEK_SET); print_file(fd, ""); close(fd); } else { while (optind < argc) { char *fname = argv[optind++]; int ifd; if ( (ifd = open(fname, O_RDONLY)) >= 0) { if (print_file(ifd, fname) < 0) exit(1); close(ifd); } else { eprintf("cannot open file %s [%s]", fname, strerror(errno)); } } } return EXIT_SUCCESS; } /* * careful_write() * * This routine writes the given buffer to the socket descriptor * provided, and it does it a chunk at a time in case the buffer * is quite large. Return is 0 if all is OK or <0 on error. Note * that the error number should be valid when this returns. */ static int careful_write(int sfd, const char *buf, int len, int alarmtime) { int nwrite = 0; assert(sfd >= 0); assert(buf != 0); alarm(alarmtime); while (len > 0 && (nwrite = write(sfd, buf, len)) > 0) { buf += nwrite; len -= nwrite; } if (alarmtime > 0) alarm(0); return (nwrite >= 0) ? 0 : nwrite; } /* * careful_fd_copy() * * Given a pair of file descriptors, copy from input to output * being careful to notice short reads and short writes and to * adjust accordingly. */ static int careful_fd_copy(int ifd, int ofd, int alarmtime) { char iobuf[256]; int nread; while ( (nread = read(ifd, iobuf, sizeof iobuf)) > 0) if (careful_write(ofd, iobuf, nread, alarmtime) != 0) return -1; return 0; } /* * getport() * * Given the name of a remote host, build a connection to that * machine and return a TCP socket connected for it. Return is * the socket descriptor if made, or <0 on error. */ static int getport(const char *rhost) { struct hostent *hp; struct servent *sp; struct sockaddr_in sin; int sfd; int lport = IPPORT_RESERVED - 1; assert(rhost != 0); /*---------------------------------------------------------------- * look up the IP address of this host and return failure if not * found. */ if ( (hp = gethostbyname(rhost)) == 0) { eprintf("getport: cannot find host \"%s\"", rhost); return -1; } /*---------------------------------------------------------------- * fetch the port number for the lpd service. While we could have * hardcoded the port number here or at least default to it, it's * probably best to just bail if we don't know the service info. */ if ( (sp = getservbyname("printer", "tcp")) == 0) { eprintf("warning: cannot find printer/tcp port #\n"); return -1; } /*---------------------------------------------------------------- * Now we have everything we need for the connection -- the remote * IP address plus the port number to connect to -- so build up * the full socket address info structure. Print a bit of debug * if required. */ (void) memcpy(&sin.sin_addr.s_addr, hp->h_addr, hp->h_length); sin.sin_port = sp->s_port; sin.sin_family = hp->h_addrtype; if (Debug) { dprintf("Constructed address for %s/printer/tcp is %s/%d\n", hp->h_name, inet_ntoa(sin.sin_addr), ntohs(sp->s_port)); } /*---------------------------------------------------------------- * Now we have to make our local side of the connection, and we * need to have a reserved port. * * ===IS THIS REALLY NECESSARY */ #if 0 if ( (sfd = rresvport(&lport)) < 0) die("cannot rresvport [%s]", strerror(errno)); #else if ( (sfd = socket(PF_INET, SOCK_STREAM, 0)) < 0 ) die("cannot create socket [%s]", strerror(errno)); #endif if (Debug) dprintf("Local port = %d\n", lport); if (connect(sfd, (struct sockaddr *)&sin, sizeof(sin)) < 0) { eprintf("cannot connect to %s.%d [%s]\n", inet_ntoa(sin.sin_addr), ntohs(sp->s_port), strerror(errno)); return -1; } if (Debug) dprintf("connected with fd = %d\n", sfd); return sfd; } /* * die() * * Given a printf-style format string, send this string to the standard * error stream prefixed with the program name and followed by a newline. * Then exit with error status. */ static void die(const char *format, ...) { va_list args; va_start(args, format); eprintf("%s: ", Prog_name); vfprintf(stderr, format, args); putc('\n', stderr); va_end(args); exit(EXIT_FAILURE); } /* * eprintf() * * Send the given printf-like string to the standard error stream. */ static void eprintf(const char *format, ...) { va_list args; va_start(args, format); vfprintf(stderr, format, args); va_end(args); } /* * sprcat() * * This routine does an sprintf to the *end* of the given buffer * (sort of like printf after strcat). Return is the length of * the constructed buffer, including the initial part. */ static int sprcat(char *buffer, const char *format, ...) { va_list args; int n; va_start(args, format); n = strlen(buffer); n += vsprintf(buffer + n, format, args); va_end(args); return n; } /* * send_with_ack() * * Given a socket descriptor that's connected to an lp daemon, send * the given command (with length) to that buffer and wait for the * single-byte ack from it. * * Return is the ack byte, or -1 for some kind of network error * (such as "cannot write" or "cannot read"). * * ===TODO=== put in some kind of wait timer */ static int send_with_ack(int sfd, const char *cmdbuf, int len, int alarmtime) { int nr; char c; assert(cmdbuf != 0); if (ACK_debug) { dprintf("ACK: careful write %*.*s\n", len, len, cmdbuf); } /*---------------------------------------------------------------- * first write the big buffer to the remote socket */ if ( careful_write(sfd, cmdbuf, len, alarmtime) != 0 ) { dprintf("cannot write buffer to printer! [%s]\n", strerror(errno)); return -1; } /*---------------------------------------------------------------- * now fetch the response from the remote end and return the byte * provided from the daemon. Return the byte received or -1 on * error. */ if (ACK_debug) dprintf("ACK: reading from network\n"); nr = read(sfd, &c, sizeof c); if (ACK_debug) { dprintf("ACK: read %d bytes", nr); if (nr > 0) dprintf(" (byte = %d)", c); dprintf("\n"); } return (nr == 1) ? c : -1; } /* * send_file_to_lpd() * * This is The Big Routine that does the hard work. We tell LPD * on the other end that we want to print a file, then we send down * the data and control filenames. This should do it... * * Return is 0 if all OK or <0 on error */ static int send_file_to_lpd(int sfd, const char *printer, int dfd, const char *filename, int jobno, int alarmtime) { int i, cmdlen, /* length of control "file" */ len; /* general length variable */ char cmdbuf[50]; char ctlbuf[200]; /* control file buffer */ char dfilename[100], /* name for data file */ cfilename[100]; /* name for control file */ struct stat stbuf; UNUSED_PARAMETER(jobno); assert(printer != 0); assert(filename != 0); /*---------------------------------------------------------------- * construct the names we'll be using for the control and data * files. We build these names to the RFC though it seems that * PC/TCP doesn't require this. Note that neither of these names * exist on the UNIX side -- they are just names used on the * remote machine. */ sprintf(dfilename, "dfA%03d%s", Job_number % 1000, My_host_name); sprintf(cfilename, "cfA%03d%s", Job_number % 1000, My_host_name); /*---------------------------------------------------------------- * first tell the LP daemon that we want to start a job. We send * the receive-job command for the specified printer. After this * we send subcommands related to enqueing this job. */ len = sprintf(cmdbuf, "%c%s\n", 2, printer); if (send_with_ack(sfd, cmdbuf, len, alarmtime) != 0) { eprintf("cannot send receive-job command [%s]\n", strerror(errno) ); /* abort job? */ return -1; } /*---------------------------------------------------------------- * construct the control file that we'll be sending down. Rather * than build a real file, however, we just make it in memory to * keep things easier, and we build it up one line at a time. * * The control file is build to contain the following. Note that * * H hostname host name of requester * U username user name of requester * N filename name of file to print (for ID only) * * J jobname name of this job (for ID only) * * M user@host send mail to user * l name literally print file "name" * * U name unlink file "name" when done * * Items with "*" before them are optional */ ctlbuf[0] = '\0'; sprcat(ctlbuf, "H%s\n", My_host_name); sprcat(ctlbuf, "P%s\n", My_user_name); sprcat(ctlbuf, "N%s\n", filename); if (Job_name ) sprcat (ctlbuf, "J%s\n", Job_name); if (Mail_user) sprcat (ctlbuf, "M%s\n", Mail_user); /*---------------------------------------------------------------- * count out multiple copies if requested */ for (i = 0; i < Ncopies; i++) cmdlen = sprcat(ctlbuf, "l%s\n", cfilename); if (Unlink_file) cmdlen = sprcat(ctlbuf, "U%s\n", dfilename); /*---------------------------------------------------------------- * Send the receive-data-file command, then send down the file * itself. */ if (Debug) dprintf("sending receive-data-file command\n"); fstat(dfd, &stbuf); len = sprintf(cmdbuf, "%c%ld %s\n", 3, /* subcommand number */ stbuf.st_size, /* file size */ dfilename); /* data filename */ if (send_with_ack(sfd, cmdbuf, len, alarmtime) != 0) { eprintf("cannot send receive-data-file command\n"); return -1; } /*---------------------------------------------------------------- * copy down the data file directly, close it, and send down the * final NUL byte used for an ACK. */ careful_fd_copy(dfd, sfd, alarmtime); close(dfd); if (send_with_ack(sfd, "", 1, alarmtime) != 0) die("no ack on data-file write"); /*---------------------------------------------------------------- * now build and send the "receive control file" command, then * send down the control file itself. */ if (Debug) dprintf("sending receive-control-file command\n"); len = sprintf(cmdbuf, "%c%d %s\n", 2, /* command number */ cmdlen, /* size of ctl "file" */ cfilename); if (send_with_ack(sfd, cmdbuf, len, alarmtime) != 0) die("cannot send 'rcv-control-file' command!"); if (send_with_ack(sfd, ctlbuf, cmdlen+1, alarmtime) != 0) die("cannot send control file\n"); if (Debug) dprintf("all finished!\n"); return 0; } /* * usage() * * Print a usage message and exit. We either print full or * brief info depending on the user's request. */ static void usage(int full) { eprintf("usage: %s %s\n", Prog_name, USAGE); if (full) { const char **p = Help_text; eprintf("\n"); for ( ; *p; p++) eprintf("%s\n", *p); eprintf("\n"); } else eprintf("\nType \"%s -help\" for more info\n", Prog_name); exit(EXIT_FAILURE); } /* * next_jobno() * * return the job number used for this next job. */ static long next_jobno(const char *jobfile, long dflt) { long jobno = dflt; int fd; if ( (fd = open(jobfile, O_RDWR)) < 0) return dflt; /*---------------------------------------------------------------- * read the job number (ignoring errors), increment it, and write * the job number back out. Return the pre-incremented number. */ read(fd, &jobno, sizeof jobno); jobno++; lseek(fd, 0L, SEEK_SET); write(fd, &jobno, sizeof jobno); close(fd); if (Debug) dprintf("Job number = %ld\n", jobno - 1); return (jobno - 1); } /* * print_file() * * Given a file descriptor, send it to the remote printer daemon. * The filename is given just for identification purposes. * * Return is 0 if all OK or <0 on error */ static int print_file(int ifd, const char *fname) { int sfd, rv; if (Debug) dprintf("Printing file %s to %s:%s\n", fname, LPD_host, Printer_queue); if ( (sfd = getport(LPD_host)) < 0) die("cannot connect to LPD host \"%s\"", LPD_host); if (Job_file) Job_number = next_jobno(Job_file, Job_number + 1); rv = send_file_to_lpd(sfd, Printer_queue, ifd, fname, Job_number++, Timeout_secs); (void) close(sfd); return rv; } /* * catch_signal() */ static void catch_signal(int signo) { die("Caught signal %d!", signo); }