[BACK]Return to printer.c CVS log [TXT][DIR] Up to [local] / src / usr.sbin / lpd

File: [local] / src / usr.sbin / lpd / printer.c (download)

Revision 1.4, Wed Dec 28 21:30:17 2022 UTC (17 months, 1 week ago) by jmc
Branch: MAIN
CVS Tags: OPENBSD_7_5_BASE, OPENBSD_7_5, OPENBSD_7_4_BASE, OPENBSD_7_4, OPENBSD_7_3_BASE, OPENBSD_7_3, HEAD
Changes since 1.3: +4 -4 lines

spelling fixes; from paul tagliamonte
any parts of his diff not taken are noted on tech

/*	$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;
	}
}