src/usr.sbin/lpd/printer.c

1401 lines
28 KiB
C

/* $OpenBSD: printer.c,v 1.4 2022/12/28 21:30:17 jmc Exp $ */
/*
* Copyright (c) 2017 Eric Faurot <eric@openbsd.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/tree.h>
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <pwd.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include <vis.h>
#include "lpd.h"
#include "lp.h"
#include "log.h"
#define RETRY_MAX 5
#define JOB_OK 0
#define JOB_AGAIN 1
#define JOB_IGNORE 2
#define JOB_ERROR 3
enum {
OK = 0,
ERR_TRANSIENT, /* transient error */
ERR_ACCOUNT, /* account required on the local machine */
ERR_ACCESS, /* cannot read file */
ERR_INODE, /* inode changed */
ERR_NOIMPL, /* unimplemented feature */
ERR_REJECTED, /* remote server rejected a job */
ERR_ERROR, /* filter report an error */
ERR_FILTER, /* filter return invalid status */
};
struct job {
char *class;
char *host;
char *literal;
char *mail;
char *name;
char *person;
char *statinfo;
char *title;
int indent;
int pagewidth;
};
struct prnstate {
int pfd; /* printer fd */
int ofilter; /* use output filter when printing */
int ofd; /* output filter fd */
pid_t opid; /* output filter process */
int tof; /* true if at top of form */
int count; /* number of printed files */
char efile[64]; /* filename for filter stderr */
};
static void sighandler(int);
static char *xstrdup(const char *);
static int openfile(const char *, const char *, struct stat *, FILE **);
static int printjob(const char *, int);
static void printbanner(struct job *);
static int printfile(struct job *, int, const char *, const char *);
static int sendjob(const char *, int);
static int sendcmd(const char *, ...);
static int sendfile(int, const char *, const char *);
static int recvack(void);
static void mailreport(struct job *, int);
static void prn_open(void);
static int prn_connect(void);
static void prn_close(void);
static int prn_fstart(void);
static void prn_fsuspend(void);
static void prn_fresume(void);
static void prn_fclose(void);
static int prn_formfeed(void);
static int prn_write(const char *, size_t);
static int prn_writefile(FILE *);
static int prn_puts(const char *);
static ssize_t prn_read(char *, size_t);
static struct lp_printer *lp;
static struct prnstate *prn;
void
printer(int debug, int verbose, const char *name)
{
struct sigaction sa;
struct passwd *pw;
struct lp_queue q;
int fd, jobidx, qstate, r, reload, retry;
char buf[64], curr[1024];
/* Early initialisation. */
log_init(debug, LOG_LPR);
log_setverbose(verbose);
snprintf(buf, sizeof(buf), "printer:%s", name);
log_procinit(buf);
setproctitle("%s", buf);
if ((lpd_hostname = malloc(HOST_NAME_MAX+1)) == NULL)
fatal("%s: malloc", __func__);
gethostname(lpd_hostname, HOST_NAME_MAX+1);
/* Detach from lpd session if not in debug mode. */
if (!debug)
if (setsid() == -1)
fatal("%s: setsid", __func__);
/* Read printer config. */
if ((lp = calloc(1, sizeof(*lp))) == NULL)
fatal("%s: calloc", __func__);
if (lp_getprinter(lp, name) == -1)
exit(1);
/*
* Redirect stderr if not in debug mode.
* This must be done before dropping privileges.
*/
if (!debug) {
fd = open(LP_LF(lp), O_WRONLY|O_APPEND);
if (fd == -1)
fatal("%s: open: %s", __func__, LP_LF(lp));
if (fd != STDERR_FILENO) {
if (dup2(fd, STDERR_FILENO) == -1)
fatalx("%s: dup2", __func__);
(void)close(fd);
}
}
/* Drop privileges. */
if ((pw = getpwnam(LPD_USER)) == NULL)
fatalx("unknown user " LPD_USER);
if (setgroups(1, &pw->pw_gid) ||
setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid))
fatal("cannot drop privileges");
/* Initialize the printer state. */
if ((prn = calloc(1, sizeof(*prn))) == NULL)
fatal("%s: calloc", __func__);
prn->pfd = -1;
prn->ofd = -1;
/* Setup signals */
memset(&sa, 0, sizeof(sa));
sa.sa_handler = sighandler;
sa.sa_flags = SA_RESTART;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGINT); /* for kill() in sighandler */
sigaction(SIGHUP, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
sigaction(SIGQUIT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
/* Grab lock file. */
if (lp_lock(lp) == -1) {
if (errno == EWOULDBLOCK) {
log_debug("already locked");
exit(0);
}
fatalx("cannot open lock file");
}
/* Pledge. */
switch (lp->lp_type) {
case PRN_LOCAL:
pledge("stdio rpath wpath cpath flock getpw tty proc exec",
NULL);
break;
case PRN_NET:
pledge("stdio rpath wpath cpath inet flock dns getpw proc exec",
NULL);
break;
case PRN_LPR:
pledge("stdio rpath wpath cpath inet flock dns getpw", NULL);
break;
}
/* Start processing the queue. */
memset(&q, 0, sizeof(q));
jobidx = 0;
reload = 1;
retry = 0;
curr[0] = '\0';
for (;;) {
/* Check the queue state. */
if (lp_getqueuestate(lp, 1, &qstate) == -1)
fatalx("cannot get queue state");
if (qstate & LPQ_PRINTER_DOWN) {
log_debug("printing disabled");
break;
}
if (qstate & LPQ_QUEUE_UPDATED) {
log_debug("queue updated");
if (reload == 0)
lp_clearqueue(&q);
reload = 1;
}
/* Read the queue if needed. */
if (reload || q.count == 0) {
if (lp_readqueue(lp, &q) == -1)
fatalx("cannot read queue");
jobidx = 0;
reload = 0;
}
/* If the queue is empty, all done */
if (q.count <= jobidx) {
log_debug("queue empty");
break;
}
/* Open the printer if needed. */
if (prn->pfd == -1) {
prn_open();
/*
* Opening the printer might take some time.
* Re-read the queue in case its state has changed.
*/
lp_clearqueue(&q);
reload = 1;
continue;
}
if (strcmp(curr, q.cfname[jobidx]))
retry = 0;
else
strlcpy(curr, q.cfname[jobidx], sizeof(curr));
lp_setcurrtask(lp, q.cfname[jobidx]);
if (lp->lp_type == PRN_LPR)
r = sendjob(q.cfname[jobidx], retry);
else
r = printjob(q.cfname[jobidx], retry);
lp_setcurrtask(lp, NULL);
switch (r) {
case JOB_OK:
log_info("job %s %s successfully", q.cfname[jobidx],
(lp->lp_type == PRN_LPR) ? "relayed" : "printed");
break;
case JOB_AGAIN:
retry++;
continue;
case JOB_IGNORE:
break;
case JOB_ERROR:
log_warnx("job %s could not be printed",
q.cfname[jobidx]);
break;
}
curr[0] = '\0';
jobidx++;
retry = 0;
}
if (prn->pfd != -1) {
if (prn->count) {
prn_formfeed();
if (lp->lp_tr)
prn_puts(lp->lp_tr);
}
prn_close();
}
exit(0);
}
static void
sighandler(int code)
{
log_info("got signal %d", code);
exit(0);
}
static char *
xstrdup(const char *s)
{
char *r;
if ((r = strdup(s)) == NULL)
fatal("strdup");
return r;
}
/*
* Open control/data file, and check that the inode information is valid.
* On success, fill the "st" structure and set "fpp" and return 0 (OK).
* Return an error code on error.
*/
static int
openfile(const char *fname, const char *inodeinfo, struct stat *st, FILE **fpp)
{
FILE *fp;
char buf[64];
if (inodeinfo) {
log_warnx("cannot open %s: symlink not implemented", fname);
return ERR_NOIMPL;
}
else {
if ((fp = lp_fopen(lp, fname)) == NULL) {
log_warn("cannot open %s", fname);
return ERR_ACCESS;
}
}
if (fstat(fileno(fp), st) == -1) {
log_warn("%s: fstat: %s", __func__, fname);
fclose(fp);
return ERR_ACCESS;
}
if (inodeinfo) {
snprintf(buf, sizeof(buf), "%d %llu", st->st_dev, st->st_ino);
if (strcmp(inodeinfo, buf)) {
log_warnx("inode changed for %s", fname);
fclose(fp);
return ERR_INODE;
}
}
*fpp = fp;
return OK;
}
/*
* Print the job described by the control file.
*/
static int
printjob(const char *cfname, int retry)
{
struct job job;
FILE *fp;
ssize_t len;
size_t linesz = 0;
char *line = NULL;
const char *errstr;
long long num;
int r, ret = JOB_OK;
log_debug("printing job %s...", cfname);
prn->efile[0] = '\0';
memset(&job, 0, sizeof(job));
job.pagewidth = lp->lp_pw;
if ((fp = lp_fopen(lp, cfname)) == NULL) {
if (errno == ENOENT) {
log_info("missing control file %s", cfname);
return JOB_IGNORE;
}
/* XXX no fatal? */
fatal("cannot open %s", cfname);
}
/* First pass: setup the job structure, print banner and print data. */
while ((len = getline(&line, &linesz, fp)) != -1) {
if (line[len-1] == '\n')
line[len-1] = '\0';
switch (line[0]) {
case 'C': /* Classification */
if (line[1]) {
free(job.class);
job.class = xstrdup(line + 1);
}
else if (job.class == NULL)
job.class = xstrdup(lpd_hostname);
break;
case 'H': /* Host name */
free(job.host);
job.host = xstrdup(line + 1);
if (job.class == NULL)
job.class = xstrdup(line + 1);
break;
case 'I': /* Indent */
errstr = NULL;
num = strtonum(line + 1, 0, INT_MAX, &errstr);
if (errstr == NULL)
job.indent = num;
else
log_warnx("strtonum: %s", errstr);
break;
case 'J': /* Job Name */
free(job.name);
if (line[1])
job.name = strdup(line + 1);
else
job.name = strdup(" ");
break;
case 'L': /* Literal */
free(job.literal);
job.literal = xstrdup(line + 1);
if (!lp->lp_sh && !lp->lp_hl)
printbanner(&job);
break;
case 'M': /* Send mail to the specified user */
free(job.mail);
job.mail = xstrdup(line + 1);
break;
case 'N': /* Filename */
break;
case 'P': /* Person */
free(job.person);
job.person = xstrdup(line + 1);
if (lp->lp_rs && getpwnam(job.person) == NULL) {
mailreport(&job, ERR_ACCOUNT);
ret = JOB_ERROR;
goto remove;
}
break;
case 'S': /* Stat info for symlink protection */
job.statinfo = xstrdup(line + 1);
break;
case 'T': /* Title for pr */
job.title = xstrdup(line + 1);
break;
case 'U': /* Unlink */
break;
case 'W': /* Width */
errstr = NULL;
num = strtonum(line + 1, 0, INT_MAX, &errstr);
if (errstr == NULL)
job.pagewidth = num;
else
log_warnx("strtonum: %s", errstr);
break;
case '1': /* troff fonts */
case '2':
case '3':
case '4':
/* XXX not implemented */
break;
default:
if (line[0] < 'a' || line[0] > 'z')
break;
r = printfile(&job, line[0], line+1, job.statinfo);
free(job.statinfo);
job.statinfo = NULL;
free(job.title);
job.title = NULL;
if (r) {
if (r == ERR_TRANSIENT && retry < RETRY_MAX) {
ret = JOB_AGAIN;
goto done;
}
mailreport(&job, r);
ret = JOB_ERROR;
goto remove;
}
}
}
remove:
if (lp_unlink(lp, cfname) == -1)
log_warn("cannot unlink %s", cfname);
/* Second pass: print trailing banner, mail report, and remove files. */
rewind(fp);
while ((len = getline(&line, &linesz, fp)) != -1) {
if (line[len-1] == '\n')
line[len-1] = '\0';
switch (line[0]) {
case 'L': /* Literal */
if (ret != JOB_OK)
break;
if (!lp->lp_sh && lp->lp_hl)
printbanner(&job);
break;
case 'M': /* Send mail to the specified user */
if (ret == JOB_OK)
mailreport(&job, ret);
break;
case 'U': /* Unlink */
if (lp_unlink(lp, line + 1) == -1)
log_warn("cannot unlink %s", line + 1);
break;
}
}
done:
if (prn->efile[0])
unlink(prn->efile);
(void)fclose(fp);
free(job.class);
free(job.host);
free(job.literal);
free(job.mail);
free(job.name);
free(job.person);
free(job.statinfo);
free(job.title);
return ret;
}
static void
printbanner(struct job *job)
{
time_t t;
time(&t);
prn_formfeed();
if (lp->lp_sb) {
if (job->class) {
prn_puts(job->class);
prn_puts(":");
}
prn_puts(job->literal);
prn_puts(" Job: ");
prn_puts(job->name);
prn_puts(" Date: ");
prn_puts(ctime(&t));
prn_puts("\n");
} else {
prn_puts("\n\n\n");
lp_banner(prn->pfd, job->literal, lp->lp_pw);
prn_puts("\n\n");
lp_banner(prn->pfd, job->name, lp->lp_pw);
if (job->class) {
prn_puts("\n\n\n");
lp_banner(prn->pfd, job->class, lp->lp_pw);
}
prn_puts("\n\n\n\n\t\t\t\t\tJob: ");
prn_puts(job->name);
prn_puts("\n\t\t\t\t\tDate: ");
prn_puts(ctime(&t));
prn_puts("\n");
}
prn_formfeed();
}
static int
printfile(struct job *job, int fmt, const char *fname, const char *inodeinfo)
{
pid_t pid;
struct stat st;
FILE *fp;
size_t n;
int ret, argc, efd, status;
char *argv[16], *prog, width[16], length[16], indent[16], tmp[512];
log_debug("printing file %s...", fname);
switch (fmt) {
case 'f': /* print file as-is */
case 'o': /* print postscript file */
case 'l': /* print file as-is but pass control chars */
break;
case 'p': /* print using pr(1) */
case 'r': /* print fortran text file */
case 't': /* print troff output */
case 'n': /* print ditroff output */
case 'd': /* print tex output */
case 'c': /* print cifplot output */
case 'g': /* print plot output */
case 'v': /* print raster output */
default:
log_warn("unrecognized output format '%c'", fmt);
return ERR_NOIMPL;
}
if ((ret = openfile(fname, inodeinfo, &st, &fp)) != OK)
return ret;
prn_formfeed();
/*
* No input filter, just write the raw file.
*/
if (!lp->lp_if) {
if (prn_writefile(fp) == -1)
ret = ERR_TRANSIENT;
else
ret = OK;
(void)fclose(fp);
return ret;
}
/*
* Otherwise, run the input filter with proper plumbing.
*/
/* Prepare filter arguments. */
snprintf(width, sizeof(width), "-w%d", job->pagewidth);
snprintf(length, sizeof(length), "-l%ld", lp->lp_pl);
snprintf(indent, sizeof(indent), "-i%d", job->indent);
prog = strrchr(lp->lp_if, '/');
argc = 0;
argv[argc++] = prog ? (prog + 1) : lp->lp_if;
if (fmt == 'l')
argv[argc++] = "-c";
argv[argc++] = width;
argv[argc++] = length;
argv[argc++] = indent;
argv[argc++] = "-n";
argv[argc++] = job->person;
if (job->name) {
argv[argc++] = "-j";
argv[argc++]= job->name;
}
argv[argc++] = "-h";
argv[argc++] = job->host;
argv[argc++] = lp->lp_af;
argv[argc++] = NULL;
/* Open the stderr file. */
strlcpy(prn->efile, "/tmp/prn.XXXXXXXX", sizeof(prn->efile));
if ((efd = mkstemp(prn->efile)) == -1) {
log_warn("%s: mkstemp", __func__);
(void)fclose(fp);
return ERR_TRANSIENT;
}
/* Disable output filter. */
prn_fsuspend();
/* Run input filter */
switch ((pid = fork())) {
case -1:
log_warn("%s: fork", __func__);
close(efd);
prn_fresume();
return ERR_TRANSIENT;
case 0:
if (dup2(fileno(fp), STDIN_FILENO) == -1)
fatal("%s:, dup2", __func__);
if (dup2(prn->pfd, STDOUT_FILENO) == -1)
fatal("%s:, dup2", __func__);
if (dup2(efd, STDERR_FILENO) == -1)
fatal("%s:, dup2", __func__);
if (closefrom(3) == -1)
fatal("%s:, closefrom", __func__);
execv(lp->lp_if, argv);
log_warn("%s:, execv", __func__);
exit(2);
default:
break;
}
log_debug("waiting for ifilter...");
/* Wait for input filter to finish. */
while (waitpid(pid, &status, 0) == -1)
log_warn("%s: waitpid", __func__);
log_debug("ifilter done, status %d", status);
/* Resume output filter */
prn_fresume();
prn->tof = 0;
/* Copy efd to stderr */
if (lseek(efd, 0, SEEK_SET) == -1)
log_warn("%s: lseek", __func__);
while ((n = read(efd, tmp, sizeof(tmp))) > 0)
(void)write(STDERR_FILENO, tmp, n);
close(efd);
if (!WIFEXITED(status)) {
log_warn("filter terminated (termsig=%d)", WTERMSIG(status));
return ERR_FILTER;
}
switch (WEXITSTATUS(status)) {
case 0:
prn->tof = 1;
return OK;
case 1:
return ERR_TRANSIENT;
case 2:
return ERR_ERROR;
default:
log_warn("filter exited (exitstatus=%d)", WEXITSTATUS(status));
return ERR_FILTER;
}
}
static int
sendjob(const char *cfname, int retry)
{
struct job job;
FILE *fp;
ssize_t len;
size_t linesz = 0;
char *line = NULL;
int ret = JOB_OK, r;
log_debug("sending job %s...", cfname);
memset(&job, 0, sizeof(job));
if ((fp = lp_fopen(lp, cfname)) == NULL) {
if (errno == ENOENT) {
log_info("missing control file %s", cfname);
return JOB_IGNORE;
}
/* XXX no fatal? */
fatal("cannot open %s", cfname);
}
/* First pass: setup the job structure, and forward data files. */
while ((len = getline(&line, &linesz, fp)) != -1) {
if (line[len-1] == '\n')
line[len-1] = '\0';
switch (line[0]) {
case 'P':
free(job.person);
job.person = xstrdup(line + 1);
break;
case 'S':
free(job.statinfo);
job.statinfo = xstrdup(line + 1);
break;
default:
if (line[0] < 'a' || line[0] > 'z')
break;
r = sendfile('\3', line+1, job.statinfo);
free(job.statinfo);
job.statinfo = NULL;
if (r) {
if (r == ERR_TRANSIENT && retry < RETRY_MAX) {
ret = JOB_AGAIN;
goto done;
}
mailreport(&job, r);
ret = JOB_ERROR;
goto remove;
}
}
}
/* Send the control file. */
if ((r = sendfile('\2', cfname, ""))) {
if (r == ERR_TRANSIENT && retry < RETRY_MAX) {
ret = JOB_AGAIN;
goto done;
}
mailreport(&job, r);
ret = JOB_ERROR;
}
remove:
if (lp_unlink(lp, cfname) == -1)
log_warn("cannot unlink %s", cfname);
/* Second pass: remove files. */
rewind(fp);
while ((len = getline(&line, &linesz, fp)) != -1) {
if (line[len-1] == '\n')
line[len-1] = '\0';
switch (line[0]) {
case 'U':
if (lp_unlink(lp, line + 1) == -1)
log_warn("cannot unlink %s", line + 1);
break;
}
}
done:
(void)fclose(fp);
free(line);
free(job.person);
free(job.statinfo);
return ret;
}
/*
* Send a LPR command to the remote lpd server and return the ack.
* Return 0 for ack, 1 or nack, -1 and set errno on error.
*/
static int
sendcmd(const char *fmt, ...)
{
va_list ap;
unsigned char line[1024];
int len;
va_start(ap, fmt);
len = vsnprintf(line, sizeof(line), fmt, ap);
va_end(ap);
if (len < 0) {
log_warn("%s: vsnprintf", __func__);
return -1;
}
if (prn_puts(line) == -1)
return -1;
return recvack();
}
static int
sendfile(int type, const char *fname, const char *inodeinfo)
{
struct stat st;
FILE *fp = NULL;
int ret;
log_debug("sending file %s...", fname);
if ((ret = openfile(fname, inodeinfo, &st, &fp)) != OK)
return ret;
ret = ERR_TRANSIENT;
if (sendcmd("%c%lld %s\n", type, (long long)st.st_size, fname)) {
if (errno == 0)
ret = ERR_REJECTED;
goto fail;
}
lp_setstatus(lp, "sending %s to %s", fname, lp->lp_rm);
if (prn_writefile(fp) == -1 || prn_write("\0", 1) == -1)
goto fail;
if (recvack()) {
if (errno == 0)
ret = ERR_REJECTED;
goto fail;
}
ret = OK;
fail:
(void)fclose(fp);
if (ret == ERR_REJECTED)
log_warnx("%s rejected by remote host", fname);
return ret;
}
/*
* Read a ack response from the server.
* Return 0 for ack, 1 or nack, -1 and set errno on error.
*/
static int
recvack(void)
{
char visbuf[256 * 4 + 1];
unsigned char line[1024];
ssize_t n;
if ((n = prn_read(line, sizeof(line))) == -1)
return -1;
if (n == 1) {
errno = 0;
if (line[0])
log_warnx("%s: \\%d", lp->lp_host, line[0]);
return line[0] ? 1 : 0;
}
if (n > 256)
n = 256;
line[n] = '\0';
if (line[n-1] == '\n')
line[--n] = '\0';
strvisx(visbuf, line, n, VIS_NL | VIS_CSTYLE);
log_warnx("%s: %s", lp->lp_host, visbuf);
errno = 0;
return -1;
}
static void
mailreport(struct job *job, int result)
{
struct stat st;
FILE *fp = NULL, *efp;
const char *user;
char *cp;
int p[2], c;
if (job->mail)
user = job->mail;
else
user = job->person;
if (user == NULL) {
log_warnx("no user to send report to");
return;
}
if (pipe(p) == -1) {
log_warn("pipe");
return;
}
switch (fork()) {
case -1:
(void)close(p[0]);
(void)close(p[1]);
log_warn("fork");
return;
case 0:
if (dup2(p[0], 0) == -1)
fatal("%s: dup2", __func__);
(void)closefrom(3);
if ((cp = strrchr(_PATH_SENDMAIL, '/')))
cp++;
else
cp = _PATH_SENDMAIL;
execl(_PATH_SENDMAIL, cp, "-t", (char *)NULL);
fatal("%s: execl: %s", __func__, _PATH_SENDMAIL);
default:
(void)close(p[0]);
if ((fp = fdopen(p[1], "w")) == NULL) {
(void)close(p[1]);
log_warn("fdopen");
return;
}
}
fprintf(fp, "Auto-Submitted: auto-generated\n");
fprintf(fp, "To: %s@%s\n", user, job->host);
fprintf(fp, "Subject: %s printer job \"%s\"\n", lp->lp_name,
job->name ? job->name : "<unknown>");
fprintf(fp, "Reply-To: root@%s\n\n", lpd_hostname);
fprintf(fp, "Your printer job ");
if (job->name)
fprintf(fp, " (%s) ", job->name);
fprintf(fp, "\n");
switch (result) {
case OK:
fprintf(fp, "completed successfully");
break;
case ERR_ACCOUNT:
fprintf(fp, "could not be printed without an account on %s",
lpd_hostname);
break;
case ERR_ACCESS:
fprintf(fp, "could not be printed because the file could "
" not be read");
break;
case ERR_INODE:
fprintf(fp, "was not printed because it was not linked to"
" the original file");
break;
case ERR_NOIMPL:
fprintf(fp, "was not printed because some feature is missing");
break;
case ERR_FILTER:
efp = fopen(prn->efile, "r");
if (efp && fstat(fileno(efp), &st) == 0 && st.st_size) {
fprintf(fp,
"had the following errors and may not have printed:\n");
while ((c = getc(efp)) != EOF)
putc(c, fp);
}
else
fprintf(fp,
"had some errors and may not have printed\n");
if (efp)
fclose(efp);
break;
default:
printf("could not be printed");
break;
}
fprintf(fp, "\n");
fclose(fp);
wait(NULL);
}
static void
prn_open(void)
{
const char *status, *oldstatus;
int i;
switch (lp->lp_type) {
case PRN_LOCAL:
lp_setstatus(lp, "opening %s", LP_LP(lp));
break;
case PRN_NET:
case PRN_LPR:
lp_setstatus(lp, "connecting to %s:%s", lp->lp_host,
lp->lp_port ? lp->lp_port : "printer");
break;
}
status = oldstatus = NULL;
for (i = 0; prn->pfd == -1; i += (i < 6) ? 1 : 0) {
if (status != oldstatus) {
lp_setstatus(lp, "%s", status);
oldstatus = status;
}
if (i)
sleep(1 << i);
if ((prn->pfd = prn_connect()) == -1) {
status = "waiting for printer to come up";
continue;
}
if (lp->lp_type == PRN_LPR) {
/* Send a recvjob request. */
if (sendcmd("\2%s\n", LP_RP(lp))) {
if (errno == 0)
log_warnx("remote queue is disabled");
(void)close(prn->pfd);
prn->pfd = -1;
status = "waiting for queue to be enabled";
}
}
}
switch (lp->lp_type) {
case PRN_LOCAL:
lp_setstatus(lp, "printing to %s", LP_LP(lp));
break;
case PRN_NET:
lp_setstatus(lp, "printing to %s:%s", lp->lp_host, lp->lp_port);
break;
case PRN_LPR:
lp_setstatus(lp, "sending to %s", lp->lp_host);
break;
}
prn->tof = lp->lp_fo ? 0 : 1;
prn->count = 0;
prn_fstart();
}
/*
* Open the printer device, or connect to the remote host.
* Return the printer file descriptor, or -1 on error.
*/
static int
prn_connect(void)
{
struct addrinfo hints, *res, *res0;
int save_errno;
int fd, e, mode;
const char *cause = NULL, *host, *port;
if (lp->lp_type == PRN_LOCAL) {
mode = lp->lp_rw ? O_RDWR : O_WRONLY;
if ((fd = open(LP_LP(lp), mode)) == -1) {
log_warn("failed to open %s", LP_LP(lp));
return -1;
}
if (isatty(fd)) {
lp_stty(lp, fd);
return -1;
}
return fd;
}
host = lp->lp_host;
port = lp->lp_port ? lp->lp_port : "printer";
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if ((e = getaddrinfo(host, port, &hints, &res0))) {
log_warnx("%s:%s: %s", host, port, gai_strerror(e));
return -1;
}
fd = -1;
for (res = res0; res && fd == -1; res = res->ai_next) {
fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (fd == -1)
cause = "socket";
else if (connect(fd, res->ai_addr, res->ai_addrlen) == -1) {
cause = "connect";
save_errno = errno;
(void)close(fd);
errno = save_errno;
fd = -1;
}
}
if (fd == -1)
log_warn("%s", cause);
else
log_debug("connected to %s:%s", host, port);
freeaddrinfo(res0);
return fd;
}
static void
prn_close(void)
{
prn_fclose();
(void)close(prn->pfd);
prn->pfd = -1;
}
/*
* Fork the output filter process if needed.
*/
static int
prn_fstart(void)
{
char width[32], length[32], *cp;
int fildes[2], i;
if (lp->lp_type == PRN_LPR || (!lp->lp_of))
return 0;
pipe(fildes);
for (i = 0; i < 20; i++) {
if (i)
sleep(i);
if ((prn->opid = fork()) != -1)
break;
log_warn("%s: fork", __func__);
}
if (prn->opid == -1) {
log_warnx("cannot fork output filter");
return -1;
}
if (prn->opid == 0) {
/* child */
dup2(fildes[0], 0);
dup2(prn->pfd, 1);
(void)closefrom(3);
cp = strrchr(lp->lp_of, '/');
if (cp)
cp += 1;
else
cp = lp->lp_of;
snprintf(width, sizeof(width), "-w%ld", lp->lp_pw);
snprintf(length, sizeof(length), "-l%ld", lp->lp_pl);
execl(lp->lp_of, cp, width, length, (char *)NULL);
log_warn("%s: execl", __func__);
exit(1);
}
close(fildes[0]);
prn->ofd = fildes[1];
prn->ofilter = 1;
return 0;
}
/*
* Suspend the output filter process.
*/
static void
prn_fsuspend(void)
{
pid_t pid;
int status;
if (prn->opid == 0)
return;
prn_puts("\031\1");
while ((pid = waitpid(WAIT_ANY, &status, WUNTRACED)) && pid != prn->opid)
;
prn->ofilter = 0;
if (!WIFSTOPPED(status)) {
log_warn("output filter died (exitstatus=%d termsig=%d)",
WEXITSTATUS(status), WTERMSIG(status));
prn->opid = 0;
prn_fclose();
}
}
/*
* Resume the output filter process.
*/
static void
prn_fresume(void)
{
if (prn->opid == 0)
return;
if (kill(prn->opid, SIGCONT) == -1)
fatal("cannot restart output filter");
prn->ofilter = 1;
}
/*
* Close the output filter socket and wait for the process to terminate
* if currently running.
*/
static void
prn_fclose(void)
{
pid_t pid;
close(prn->ofd);
prn->ofd = -1;
while (prn->opid) {
pid = wait(NULL);
if (pid == -1)
log_warn("%s: wait", __func__);
else if (pid == prn->opid)
prn->opid = 0;
}
}
/*
* Write a form-feed if the printer cap requires it, and if not currently
* at top of form. Return 0 on success, or -1 on error and set errno.
*/
static int
prn_formfeed(void)
{
if (!lp->lp_sf && !prn->tof)
if (prn_puts(LP_FF(lp)) == -1)
return -1;
prn->tof = 1;
return 0;
}
/*
* Write data to the printer (or output filter process).
* Return 0 on success, or -1 and set errno.
*/
static int
prn_write(const char *buf, size_t len)
{
ssize_t n;
int fd;
fd = prn->ofilter ? prn->ofd : prn->pfd;
log_debug("prn_write(fd=%d len=%zu, of=%d pfd=%d ofd=%d)", fd, len,
prn->ofilter, prn->pfd, prn->ofd);
if (fd == -1) {
log_warnx("printer socket not opened");
errno = EPIPE;
return -1;
}
while (len) {
if ((n = write(fd, buf, len)) == -1) {
if (errno == EINTR)
continue;
log_warn("%s: write", __func__);
/* XXX close the printer */
return -1;
}
len -= n;
buf += n;
prn->tof = 0;
}
return 0;
}
/*
* Write a string to the printer (or output filter process).
* Return 0 on success, or -1 and set errno.
*/
static int
prn_puts(const char *buf)
{
return prn_write(buf, strlen(buf));
}
/*
* Write the FILE content to the printer (or output filter process).
* Return 0 on success, or -1 and set errno.
*/
static int
prn_writefile(FILE *fp)
{
char buf[BUFSIZ];
size_t r;
while (!feof(fp)) {
r = fread(buf, 1, sizeof(buf), fp);
if (ferror(fp)) {
log_warn("%s: fread", __func__);
return -1;
}
if (r && (prn_write(buf, r) == -1))
return -1;
}
return 0;
}
/*
* Read data from the printer socket into the given buffer.
* Return 0 on success, or -1 and set errno.
*/
static ssize_t
prn_read(char *buf, size_t sz)
{
ssize_t n;
for (;;) {
if ((n = read(prn->pfd, buf, sz)) == 0) {
errno = ECONNRESET;
n = -1;
}
if (n == -1) {
if (errno == EINTR)
continue;
/* XXX close printer? */
log_warn("%s: read", __func__);
return -1;
}
return n;
}
}