faxQueueApp.c++
上传用户:weiyuanprp
上传日期:2020-05-20
资源大小:1169k
文件大小:116k
- /* $Id: faxQueueApp.c++,v 1.88 2009/10/17 21:20:59 faxguy Exp $ */
- /*
- * Copyright (c) 1990-1996 Sam Leffler
- * Copyright (c) 1991-1996 Silicon Graphics, Inc.
- * HylaFAX is a trademark of Silicon Graphics
- *
- * Permission to use, copy, modify, distribute, and sell this software and
- * its documentation for any purpose is hereby granted without fee, provided
- * that (i) the above copyright notices and this permission notice appear in
- * all copies of the software and related documentation, and (ii) the names of
- * Sam Leffler and Silicon Graphics may not be used in any advertising or
- * publicity relating to the software without the specific, prior written
- * permission of Sam Leffler and Silicon Graphics.
- *
- * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND,
- * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY
- * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
- *
- * IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR
- * ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND,
- * OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
- * WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF
- * LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
- * OF THIS SOFTWARE.
- */
- #include "Sys.h"
- #include <ctype.h>
- #include <errno.h>
- #include <math.h>
- #include <limits.h>
- #include <sys/file.h>
- #include <tiffio.h>
- #include "Dispatcher.h"
- #include "FaxMachineInfo.h"
- #include "FaxAcctInfo.h"
- #include "FaxRequest.h"
- #include "FaxTrace.h"
- #include "FaxRecvInfo.h"
- #include "Timeout.h"
- #include "UUCPLock.h"
- #include "DialRules.h"
- #include "RE.h"
- #include "Modem.h"
- #include "Trigger.h"
- #include "faxQueueApp.h"
- #include "HylaClient.h"
- #include "MemoryDecoder.h"
- #include "FaxSendInfo.h"
- #include "config.h"
- /*
- * HylaFAX Spooling and Command Agent.
- */
- const fxStr faxQueueApp::sendDir = FAX_SENDDIR;
- const fxStr faxQueueApp::docDir = FAX_DOCDIR;
- const fxStr faxQueueApp::clientDir = FAX_CLIENTDIR;
- fxStr strTime(time_t t) { return fxStr(fmtTime(t)); }
- #define JOBHASH(pri) (((pri) >> 4) & (NQHASH-1))
- faxQueueApp::SchedTimeout::SchedTimeout()
- {
- started = false;
- pending = false;
- lastRun = Sys::now() - 1;
- }
- faxQueueApp::SchedTimeout::~SchedTimeout() {}
- void
- faxQueueApp::SchedTimeout::timerExpired(long, long)
- {
- if (pending && lastRun <= Sys::now()) pending = false;
- if (faxQueueApp::instance().scheduling() ) {
- start(0);
- return;
- }
- faxQueueApp::instance().runScheduler();
- started = false;
- }
- void
- faxQueueApp::SchedTimeout::start(u_short s)
- {
- /*
- * If we don't throttle the scheduler then large
- * queues can halt the system with CPU consumption.
- * So we keep the scheduler from running more than
- * once per second.
- */
- if (!started && Sys::now() != lastRun) {
- started = true;
- pending = false;
- Dispatcher::instance().startTimer(s, 1, this);
- lastRun = Sys::now() + s;
- } else {
- if (!pending && lastRun <= Sys::now()) {
- /*
- * The scheduler is either running now or has been run
- * within the last second and there are no timers set
- * to trigger another scheduler run. So we set a
- * timer to go off in one second to avoid a stalled
- * run queue.
- */
- Dispatcher::instance().startTimer(s + 1, 0, this);
- lastRun = Sys::now() + 1 + s;
- pending = true;
- }
- }
- }
- faxQueueApp* faxQueueApp::_instance = NULL;
- faxQueueApp::faxQueueApp()
- : configFile(FAX_CONFIG)
- {
- fifo = -1;
- quit = false;
- dialRules = NULL;
- inSchedule = false;
- setupConfig();
- fxAssert(_instance == NULL, "Cannot create multiple faxQueueApp instances");
- _instance = this;
- }
- faxQueueApp::~faxQueueApp()
- {
- HylaClient::purge();
- delete dialRules;
- }
- faxQueueApp& faxQueueApp::instance() { return *_instance; }
- void
- faxQueueApp::initialize(int argc, char** argv)
- {
- updateConfig(configFile); // read config file
- faxApp::initialize(argc, argv);
- for (GetoptIter iter(argc, argv, getOpts()); iter.notDone(); iter++)
- switch (iter.option()) {
- case 'c': // set configuration parameter
- readConfigItem(iter.optArg());
- break;
- }
- logInfo("%s", HYLAFAX_VERSION);
- logInfo("%s", "Copyright (c) 1990-1996 Sam Leffler");
- logInfo("%s", "Copyright (c) 1991-1996 Silicon Graphics, Inc.");
- scanForModems();
- }
- void
- faxQueueApp::open()
- {
- faxApp::open();
- scanQueueDirectory();
- Modem::broadcast("HELLO"); // announce queuer presence
- scanClientDirectory(); // announce queuer presence
- pokeScheduler();
- }
- void
- faxQueueApp::blockSignals()
- {
- sigset_t block;
- sigemptyset(&block);
- sigaddset(&block, SIGCHLD);
- sigprocmask(SIG_BLOCK, &block, NULL);
- }
- void
- faxQueueApp::releaseSignals()
- {
- sigset_t release;
- sigemptyset(&release);
- sigaddset(&release, SIGCHLD);
- sigprocmask (SIG_UNBLOCK, &release, NULL);
- }
- /*
- * Scan the spool area for modems. We can't be certain the
- * modems are actively working without probing them; this
- * work is done simply to buildup the internal database for
- * broadcasting a ``HELLO'' message. Later on, individual
- * modems are enabled for use based on messages received
- * through the FIFO.
- */
- void
- faxQueueApp::scanForModems()
- {
- DIR* dir = Sys::opendir(".");
- if (dir == NULL) {
- logError("Could not scan directory for modems");
- return;
- }
- fxStr fifoMatch(fifoName | ".");
- for (dirent* dp = readdir(dir); dp; dp = readdir(dir)) {
- if (dp->d_name[0] != fifoName[0])
- continue;
- if (!strneq(dp->d_name, fifoMatch, fifoMatch.length()))
- continue;
- if (Sys::isFIFOFile(dp->d_name)) {
- fxStr devid(dp->d_name);
- devid.remove(0, fifoMatch.length()-1); // NB: leave "."
- if (Sys::isRegularFile(FAX_CONFIG | devid)) {
- devid.remove(0); // strip "."
- (void) Modem::getModemByID(devid); // adds to list
- }
- }
- }
- closedir(dir);
- }
- /*
- * Scan the spool directory for queue files and
- * enter them in the queues of outgoing jobs.
- */
- void
- faxQueueApp::scanQueueDirectory()
- {
- DIR* dir = Sys::opendir(sendDir);
- if (dir == NULL) {
- logError("Could not scan %s directory for outbound jobs",
- (const char*)sendDir);
- return;
- }
- for (dirent* dp = readdir(dir); dp; dp = readdir(dir)) {
- if (dp->d_name[0] == 'q')
- submitJob(&dp->d_name[1], true);
- }
- closedir(dir);
- }
- /*
- * Scan the client area for active client processes
- * and send a ``HELLO message'' to notify them the
- * queuer process has restarted. If no process is
- * listening on the FIFO, remove it; the associated
- * client state will be purged later.
- */
- void
- faxQueueApp::scanClientDirectory()
- {
- DIR* dir = Sys::opendir(clientDir);
- if (dir == NULL) {
- logError("Could not scan %s directory for clients",
- (const char*) clientDir);
- return;
- }
- for (dirent* dp = readdir(dir); dp; dp = readdir(dir)) {
- if (!isdigit(dp->d_name[0]))
- continue;
- fxStr fifo(clientDir | "/" | dp->d_name);
- if (Sys::isFIFOFile((const char*) fifo))
- if (!HylaClient::getClient(fifo).send("HELLO", 6))
- Sys::unlink(fifo);
- }
- closedir(dir);
- }
- /*
- * Process a job. Prepare it for transmission and
- * pass it on to the thread that does the actual
- * transmission work. The job is marked ``active to
- * this destination'' prior to preparing it because
- * preparation may involve asynchronous activities.
- * The job is placed on the active list so that it
- * can be located by filename if necessary.
- */
- void
- faxQueueApp::processJob(Job& job, FaxRequest* req, DestInfo& di)
- {
- JobStatus status;
- FaxMachineInfo& info = di.getInfo(job.dest);
- Job* bjob = job.bfirst(); // first job in batch
- Job* cjob = &job; // current job
- FaxRequest* creq = req; // current request
- Job* njob = NULL; // next job
-
- for (; cjob != NULL; cjob = njob) {
- creq = cjob->breq;
- njob = cjob->bnext;
- cjob->commid = ""; // set on return
- req->notice = ""; // Clear for new procssing
- di.active(*cjob);
- setActive(*cjob); // place job on active list
- updateRequest(*creq, *cjob);
- if (!prepareJobNeeded(*cjob, *creq, status)) {
- if (status != Job::done) {
- if (cjob->bprev == NULL)
- bjob = njob;
- cjob->state = FaxRequest::state_failed;
- deleteRequest(*cjob, creq, status, true);
- setDead(*cjob);
- }
- } else {
- if (prepareJobStart(*cjob, creq, info))
- return;
- else if (cjob->bprev == NULL)
- bjob = njob;
- }
- }
- if (bjob != NULL)
- sendJobStart(*bjob, bjob->breq);
- }
- /*
- * Check if the job requires preparation that should
- * done in a fork'd copy of the server. A sub-fork is
- * used if documents must be converted or a continuation
- * cover page must be crafted (i.e. the work may take
- * a while).
- */
- bool
- faxQueueApp::prepareJobNeeded(Job& job, FaxRequest& req, JobStatus& status)
- {
- if (!req.items.length()) {
- req.notice = "Job contains no documents {E323}";
- status = Job::rejected;
- jobError(job, "SEND REJECT: %s", (const char*) req.notice);
- return (false);
- }
- for (u_int i = 0, n = req.items.length(); i < n; i++)
- switch (req.items[i].op) {
- case FaxRequest::send_postscript: // convert PostScript
- case FaxRequest::send_pcl: // convert PCL
- case FaxRequest::send_tiff: // verify&possibly convert TIFF
- case FaxRequest::send_pdf: // convert PDF
- return (true);
- case FaxRequest::send_poll: // verify modem is capable
- if (!job.modem->supportsPolling()) {
- req.notice = "Modem does not support polling {E324}";
- status = Job::rejected;
- jobError(job, "SEND REJECT: %s", (const char*) req.notice);
- return (false);
- }
- break;
- }
- status = Job::done;
- return (req.cover != ""); // need continuation cover page
- }
- /*
- * Handler used by job preparation subprocess
- * to pass signal from parent queuer process.
- * We mark state so job preparation will be aborted
- * at the next safe point in the procedure.
- */
- void
- faxQueueApp::prepareCleanup(int s)
- {
- int old_errno = errno;
- signal(s, fxSIGHANDLER(faxQueueApp::prepareCleanup));
- logError("CAUGHT SIGNAL %d, ABORT JOB PREPARATION", s);
- faxQueueApp::instance().abortPrepare = true;
- errno = old_errno;
- }
- /*
- * Start job preparation in a sub-fork. The server process
- * forks and sets up a Dispatcher handler to reap the child
- * process. The exit status from the child is actually the
- * return value from the prepareJob method; this and a
- * reference to the original Job are passed back into the
- * server thread at which point the transmit work is actually
- * initiated.
- */
- bool
- faxQueueApp::prepareJobStart(Job& job, FaxRequest* req,
- FaxMachineInfo& info)
- {
- traceQueue(job, "PREPARE START");
- abortPrepare = false;
- pid_t pid = fork();
- switch (pid) {
- case 0: // child, do work
- /*
- * NB: There is a window here where the subprocess
- * doing the job preparation can have the old signal
- * handlers installed when a signal comes in. This
- * could be fixed by using the appropriate POSIX calls
- * to block and unblock signals, but signal usage is
- * quite tenuous (i.e. what is and is not supported
- * on a system), so rather than depend on this
- * functionality being supported, we'll just leave
- * the (small) window in until it shows itself to
- * be a problem.
- */
- signal(SIGTERM, fxSIGHANDLER(faxQueueApp::prepareCleanup));
- signal(SIGINT, fxSIGHANDLER(faxQueueApp::prepareCleanup));
- _exit(prepareJob(job, *req, info));
- /*NOTREACHED*/
- case -1: // fork failed, sleep and retry
- if (job.isOnList()) job.remove(); // Remove from active queue
- delayJob(job, *req, "Could not fork to prepare job for transmission {E340}",
- Sys::now() + random() % requeueInterval);
- delete req;
- return false;
- default: // parent, setup handler to wait
- job.startPrepare(pid);
- delete req; // must reread after preparation
- job.breq = NULL;
- Trigger::post(Trigger::JOB_PREP_BEGIN, job);
- return true;
- }
- }
- /*
- * Handle notification from the sub-fork that job preparation
- * is done. The exit status is checked and interpreted as the
- * return value from prepareJob if it was passed via _exit.
- */
- void
- faxQueueApp::prepareJobDone(Job& job, int status)
- {
- traceQueue(job, "PREPARE DONE");
- Trigger::post(Trigger::JOB_PREP_END, job);
- if (status&0xff) {
- logError("JOB %s: bad exit status %#x from sub-fork",
- (const char*) job.jobid, status);
- status = Job::failed;
- } else
- status >>= 8;
- if (job.suspendPending) { // co-thread waiting
- job.suspendPending = false;
- releaseModem(job);
- } else {
- FaxRequest* req = readRequest(job);
- if (!req) {
- // NB: no way to notify the user (XXX)
- logError("JOB %s: qfile vanished during preparation",
- (const char*) job.jobid);
- setDead(job);
- } else {
- bool processnext = false;
- bool startsendjob = false;
- Job* targetjob = &job;
- if (status == Job::done) { // preparation completed successfully
- job.breq = req;
- startsendjob = (job.bnext == NULL);
- processnext = !startsendjob;
- if (processnext) {
- targetjob = job.bnext;
- }
- } else {
- /*
- * Job preparation did not complete successfully.
- *
- * If there is more than one job in this batch, then remove this job
- * and adjust the batch accordingly.
- */
- if (job.bnext == NULL) { // no jobs left in batch
- targetjob = job.bprev;
- startsendjob = (targetjob != NULL);
- } else { // more jobs in batch
- targetjob = job.bnext;
- processnext = true;
- }
- /*
- * If there are other jobs in the batch, we have to be
- * careful to *not* release the modem, otherwise faxq will
- * schedule new jobs on this modem while the rest of the jobs
- * in the batch are still using it.
- */
- if (startsendjob || processnext)
- job.modem = NULL;
- if (status == Job::requeued) {
- if (job.isOnList()) job.remove();
- delayJob(job, *req, "Could not fork to prepare job for transmission {E340}",
- Sys::now() + random() % requeueInterval);
- delete req;
- } else {
- deleteRequest(job, req, status, true);
- setDead(job);
- }
- }
- if (processnext)
- processJob(*targetjob, targetjob->breq, destJobs[targetjob->dest]);
- else if (startsendjob)
- sendJobStart(*targetjob->bfirst(), targetjob->bfirst()->breq);
- else {
- /*
- * This destination was marked as called, but all jobs to this
- * destination failed preparation, so we must undo the call marking.
- */
- DestInfo& di = destJobs[job.dest];
- di.hangup(); // do before unblockDestJobs
- unblockDestJobs(di); // release any blocked jobs
- removeDestInfoJob(job);
- pokeScheduler();
- }
- }
- }
- }
- /*
- * Document Use Database.
- *
- * The server minimizes imaging operations by checking for the
- * existence of compatible, previously imaged, versions of documents.
- * This is done by using a file naming convention that includes the
- * source document name and the remote machine capabilities that are
- * used for imaging. The work done here (and in other HylaFAX apps)
- * also assumes certain naming convention used by hfaxd when creating
- * document files. Source documents are named:
- *
- * doc<docnum>.<type>
- *
- * where <docnum> is a unique document number that is assigned by
- * hfaxd at the time the document is stored on the server. Document
- * references by a job are then done using filenames (i.e. hard
- * links) of the form:
- *
- * doc<docnum>.<type>.<jobid>
- *
- * where <jobid> is the unique identifier assigned to each outbound
- * job. Then, each imaged document is named:
- *
- * doc<docnum>.<type>;<encoded-capabilities>
- *
- * where <encoded-capabilities> is a string that encodes the remote
- * machine's capabilities.
- *
- * Before imaging a document the scheduler checks to see if there is
- * an existing file with the appropriate name. If so then the file
- * is used and no preparation work is needed for sending the document.
- * Otherwise the document must be converted for transmission; this
- * result is written to a file with the appropriate name. After an
- * imaged document has been transmitted it is not immediately removed,
- * but rather the scheduler is informed that the job no longer holds
- * (needs) a reference to the document and the scheduler records this
- * information so that when no jobs reference the original source
- * document, all imaged forms may be expunged. As documents are
- * transmitted the job references to the original source documents are
- * converted to references to the ``base document name'' (the form
- * without the <jobid>) so that the link count on the inode for this
- * file reflects the number of references from jobs that are still
- * pending transmission. This means that the scheduler can use the
- * link count to decide when to expunge imaged versions of a document.
- *
- * Note that the reference counts used here do not necessarily
- * *guarantee* that a pre-imaged version of a document will be available.
- * There are race conditions under which a document may be re-imaged
- * because a previously imaged version was removed.
- *
- * A separate document scavenger program should be run periodically
- * to expunge any document files that might be left in the docq for
- * unexpected reasons. This program should read the set of jobs in
- * the sendq to build a onetime table of uses and then remove any
- * files found in the docq that are not referenced by a job.
- */
- /*
- * Remove a reference to an imaged document and if no
- * references exist for the corresponding source document,
- * expunge all imaged versions of the document.
- */
- void
- faxQueueApp::unrefDoc(const fxStr& file)
- {
- /*
- * Convert imaged document name to the base
- * (source) document name by removing the
- * encoded session parameters used for imaging.
- */
- u_int l = file.nextR(file.length(), ';');
- if (l == 0) {
- logError("Bogus document handed to unrefDoc: %s", (const char*) file);
- return;
- }
- fxStr doc = file.head(l-1);
- /*
- * Add file to the list of pending removals. We
- * do this before checking below so that the list
- * of files will always have something on it.
- */
- fxStr& files = pendingDocs[doc];
- if (files.find(0, file) == files.length()) // suppress duplicates
- files.append(file | " ");
- if (tracingLevel & FAXTRACE_DOCREFS)
- logInfo("DOC UNREF: %s files %s",
- (const char*) file, (const char*) files);
- /*
- * The following assumes that any source document has
- * been renamed to the base document name *before* this
- * routine is invoked (either directly or via a msg
- * received on a FIFO). Specifically, if the stat
- * call fails we assume the file does not exist and
- * that it is safe to remove the imaged documents.
- * This is conservative and if wrong will not break
- * anything; just potentially cause extra imaging
- * work to be done.
- */
- struct stat sb;
- if (Sys::stat(doc, sb) < 0 || sb.st_nlink == 1) {
- if (tracingLevel & FAXTRACE_DOCREFS)
- logInfo("DOC UNREF: expunge imaged files");
- /*
- * There are no additional references to the
- * original source document (all references
- * should be from completed jobs that reference
- * the original source document by its basename).
- * Expunge imaged documents that were waiting for
- * all potential uses to complete.
- */
- l = 0;
- do {
- (void) Sys::unlink(files.token(l, ' '));
- } while (l < files.length());
- pendingDocs.remove(doc);
- }
- }
- #include "class2.h"
- /*
- * Prepare a job by converting any user-submitted documents
- * to a format suitable for transmission.
- */
- JobStatus
- faxQueueApp::prepareJob(Job& job, FaxRequest& req,
- const FaxMachineInfo& info)
- {
- /*
- * Select imaging parameters according to requested
- * values, client capabilities, and modem capabilities.
- * Note that by this time we believe the modem is capable
- * of certain requirements needed to transmit the document
- * (based on the capabilities passed to us by faxgetty).
- */
- Class2Params params;
-
- /*
- * User requested vres (98 or 196) and usexvres (1=true or 0=false)
- */
- int vres = req.resolution;
- int usexvres = req.usexvres;
- /*
- * System overrides in JobControl:
- * VRes: we check for vres = 98 or vres = 196 in JobControl;
- * if vres is not set getVRes returns 0.
- * UseXVres: we check for usexvres = 0 or usexvres = 1 in JobControl;
- * if usexvres is not set getUseXVRes retuns -1.
- */
- if (job.getJCI().getVRes() == 98)
- vres = 98;
- else if (job.getJCI().getVRes() == 196)
- vres = 196;
- if (job.getJCI().getUseXVRes() == 0)
- usexvres = 0;
- else if (job.getJCI().getUseXVRes() == 1)
- usexvres = 1;
- // use the highest resolution the client supports
- params.vr = VR_NORMAL;
- if (usexvres) {
- if (info.getSupportsVRes() & VR_200X100 && job.modem->supportsVR(VR_200X100))
- params.vr = VR_200X100;
- if (info.getSupportsVRes() & VR_FINE && job.modem->supportsVR(VR_FINE))
- params.vr = VR_FINE;
- if (info.getSupportsVRes() & VR_200X200 && job.modem->supportsVR(VR_200X200))
- params.vr = VR_200X200;
- if (info.getSupportsVRes() & VR_R8 && job.modem->supportsVR(VR_R8))
- params.vr = VR_R8;
- if (info.getSupportsVRes() & VR_200X400 && job.modem->supportsVR(VR_200X400))
- params.vr = VR_200X400;
- if (info.getSupportsVRes() & VR_300X300 && job.modem->supportsVR(VR_300X300))
- params.vr = VR_300X300;
- if (info.getSupportsVRes() & VR_R16 && job.modem->supportsVR(VR_R16))
- params.vr = VR_R16;
- } else { // limit ourselves to normal and fine
- if (vres > 150) {
- if (info.getSupportsVRes() & VR_FINE && job.modem->supportsVR(VR_FINE))
- params.vr = VR_FINE;
- }
- }
- params.setPageWidthInMM(
- fxmin((u_int) req.pagewidth, (u_int) info.getMaxPageWidthInMM()));
- /*
- * Follow faxsend and use unlimited page length whenever possible.
- */
- useUnlimitedLN = (info.getMaxPageLengthInMM() == (u_short) -1);
- params.setPageLengthInMM(
- fxmin((u_int) req.pagelength, (u_int) info.getMaxPageLengthInMM()));
- /*
- * Generate MMR or 2D-encoded facsimile if:
- * o the server is permitted to generate it,
- * o the modem is capable of sending it,
- * o the remote side is known to be capable of it, and
- * o the user hasn't specified a desire to send 1D data.
- */
- int jcdf = job.getJCI().getDesiredDF();
- if (jcdf != -1) req.desireddf = jcdf;
- if (req.desireddf == DF_2DMMR && (req.desiredec != EC_DISABLE) &&
- use2D && job.modem->supportsMMR() &&
- (! info.getCalledBefore() || info.getSupportsMMR()) )
- params.df = DF_2DMMR;
- else if (req.desireddf > DF_1DMH) {
- params.df = (use2D && job.modem->supports2D() &&
- (! info.getCalledBefore() || info.getSupports2DEncoding()) ) ?
- DF_2DMR : DF_1DMH;
- } else
- params.df = DF_1DMH;
- /*
- * Check and process the documents to be sent
- * using the parameter selected above.
- */
- JobStatus status = Job::done;
- bool updateQFile = false;
- fxStr tmp; // NB: here to avoid compiler complaint
- u_int i = 0;
- while (i < req.items.length() && status == Job::done && !abortPrepare) {
- FaxItem& fitem = req.items[i];
- switch (fitem.op) {
- case FaxRequest::send_postscript: // convert PostScript
- case FaxRequest::send_pcl: // convert PCL
- case FaxRequest::send_tiff: // verify&possibly convert TIFF
- case FaxRequest::send_pdf: // convert PDF
- tmp = FaxRequest::mkbasedoc(fitem.item) | ";" | params.encodePage();
- status = convertDocument(job, fitem, tmp, params, req.notice);
- if (status == Job::done) {
- /*
- * Insert converted file into list and mark the
- * original document so that it's saved, but
- * not processed again. The converted file
- * is sent, while the saved file is kept around
- * in case it needs to be returned to the sender.
- */
- fitem.op++; // NB: assumes order of enum
- req.insertFax(i+1, tmp);
- } else
- Sys::unlink(tmp); // bail out
- updateQFile = true;
- break;
- }
- i++;
- }
- if (status == Job::done && !abortPrepare) {
- if (req.pagehandling == "" && !abortPrepare) {
- /*
- * Calculate/recalculate the per-page session parameters
- * and check the page count against the max pages. We
- * do this before generating continuation any cover page
- * to prevent any skippages setting from trying to skip
- * the continuation cover page.
- */
- if (!preparePageHandling(job, req, info, req.notice)) {
- status = Job::rejected; // XXX
- req.notice.insert("Document preparation failed: ");
- }
- updateQFile = true;
- }
- if (req.cover != "" && !abortPrepare) {
- /*
- * Generate a continuation cover page if necessary.
- * Note that a failure in doing this is not considered
- * fatal; perhaps this should be configurable?
- */
- if (updateQFile)
- updateRequest(req, job); // cover-page generation may look at the file
- if (makeCoverPage(job, req, params))
- req.nocountcover++;
- updateQFile = true;
- /*
- * Recalculate the per-page session parameters.
- */
- if (!preparePageHandling(job, req, info, req.notice)) {
- status = Job::rejected; // XXX
- req.notice.insert("Document preparation failed: ");
- }
- }
- }
- if (updateQFile)
- updateRequest(req, job);
- return (status);
- }
- /*
- * Prepare the job for transmission by analysing
- * the page characteristics and determining whether
- * or not the page transfer parameters will have
- * to be renegotiated after the page is sent. This
- * is done before the call is placed because it can
- * be slow and there can be timing problems if this
- * is done during transmission.
- */
- bool
- faxQueueApp::preparePageHandling(Job& job, FaxRequest& req,
- const FaxMachineInfo& info, fxStr& emsg)
- {
- /*
- * Figure out whether to try chopping off white space
- * from the bottom of pages. This can only be done
- * if the remote device is thought to be capable of
- * accepting variable-length pages.
- */
- u_int pagechop;
- if (info.getMaxPageLengthInMM() == (u_short)-1) {
- pagechop = req.pagechop;
- if (pagechop == FaxRequest::chop_default)
- pagechop = pageChop;
- } else
- pagechop = FaxRequest::chop_none;
- u_int maxPages = job.getJCI().getMaxSendPages();
- /*
- * Scan the pages and figure out where session parameters
- * will need to be renegotiated. Construct a string of
- * indicators to use when doing the actual transmission.
- *
- * NB: all files are coalesced into a single fax document
- * if possible
- */
- Class2Params params; // current parameters
- Class2Params next; // parameters for ``next'' page
- TIFF* tif = NULL; // current open TIFF image
- req.totpages = req.npages; // count pages previously transmitted
- if (req.skippages) { // job instructs us to skip pages...
- u_int i = req.findItem(FaxRequest::send_fax, 0);
- if (i) req.items[0].dirnum = req.skippages; // mark original
- req.items[i].dirnum = req.skippages; // mark prepared image
- req.skippedpages += req.skippages; // update tagline indicators
- req.skippages = 0;
- }
- for (u_int i = 0;;) {
- if (!tif || TIFFLastDirectory(tif)) {
- /*
- * Locate the next file to be sent.
- */
- if (tif) // close previous file
- TIFFClose(tif), tif = NULL;
- if (i >= req.items.length()) {
- req.pagehandling.append('P'); // EOP
- return (true);
- }
- i = req.findItem(FaxRequest::send_fax, i);
- if (i == fx_invalidArrayIndex) {
- req.pagehandling.append('P'); // EOP
- return (true);
- }
- const FaxItem& fitem = req.items[i];
- tif = TIFFOpen(fitem.item, "r");
- if (tif == NULL) {
- emsg = "Can not open document file " | fitem.item | " {E314}";
- if (tif)
- TIFFClose(tif);
- return (false);
- }
- if (fitem.dirnum != 0 && !TIFFSetDirectory(tif, fitem.dirnum)) {
- emsg = fxStr::format(
- "Can not set directory %u in document file %s {E315}"
- , fitem.dirnum
- , (const char*) fitem.item
- );
- if (tif)
- TIFFClose(tif);
- return (false);
- }
- i++; // advance for next find
- } else {
- /*
- * Read the next TIFF directory.
- */
- if (!TIFFReadDirectory(tif)) {
- emsg = fxStr::format(
- "Error reading directory %u in document file %s {E316}"
- , TIFFCurrentDirectory(tif)
- , TIFFFileName(tif)
- );
- if (tif)
- TIFFClose(tif);
- return (false);
- }
- }
- if (++req.totpages > maxPages) {
- emsg = fxStr::format("Too many pages in submission; max %u {E317}",
- maxPages);
- if (tif)
- TIFFClose(tif);
- return (false);
- }
- next = params;
- setupParams(tif, next, info);
- if (params.df != (u_int) -1) {
- /*
- * The pagehandling string has:
- * 'M' = EOM, for when parameters must be renegotiated
- * 'S' = MPS, for when next page uses the same parameters
- * 'P' = EOP, for the last page to be transmitted
- */
- req.pagehandling.append(next == params ? 'S' : 'M');
- }
- /*
- * Record the session parameters needed by each page
- * so that we can set the initial session parameters
- * as needed *before* dialing the telephone. This is
- * to cope with Class 2 modems that do not properly
- * implement the +FDIS command.
- */
- req.pagehandling.append(next.encodePage());
- /*
- * If page is to be chopped (i.e. sent with trailing white
- * space removed so the received page uses minimal paper),
- * scan the data and, if possible, record the amount of data
- * that should not be sent. The modem drivers will use this
- * information during transmission if it's actually possible
- * to do the chop (based on the negotiated session parameters).
- */
- if (pagechop == FaxRequest::chop_all ||
- (pagechop == FaxRequest::chop_last && TIFFLastDirectory(tif)))
- preparePageChop(req, tif, next, req.pagehandling);
- params = next;
- }
- }
- /*
- * Select session parameters according to the info
- * in the TIFF file. We setup the encoding scheme,
- * page width & length, and vertical-resolution
- * parameters.
- */
- void
- faxQueueApp::setupParams(TIFF* tif, Class2Params& params, const FaxMachineInfo& info)
- {
- uint16 compression = 0;
- (void) TIFFGetField(tif, TIFFTAG_COMPRESSION, &compression);
- if (compression == COMPRESSION_CCITTFAX4) {
- params.df = DF_2DMMR;
- } else {
- uint32 g3opts = 0;
- TIFFGetField(tif, TIFFTAG_GROUP3OPTIONS, &g3opts);
- params.df = (g3opts&GROUP3OPT_2DENCODING ? DF_2DMR : DF_1DMH);
- }
- uint32 w;
- TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &w);
- params.setPageWidthInPixels((u_int) w);
- /*
- * Try to deduce the vertical resolution of the image
- * image. This can be problematical for arbitrary TIFF
- * images 'cuz vendors sometimes don't give the units.
- * We, however, can depend on the info in images that
- * we generate 'cuz we're careful to include valid info.
- */
- float yres, xres;
- if (TIFFGetField(tif, TIFFTAG_YRESOLUTION, &yres) && TIFFGetField(tif, TIFFTAG_XRESOLUTION, &xres)) {
- uint16 resunit;
- TIFFGetFieldDefaulted(tif, TIFFTAG_RESOLUTIONUNIT, &resunit);
- if (resunit == RESUNIT_CENTIMETER)
- yres *= 25.4;
- xres *= 25.4;
- params.setRes((u_int) xres, (u_int) yres);
- } else {
- /*
- * No resolution is specified, try
- * to deduce one from the image length.
- */
- uint32 l;
- TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &l);
- // B4 at 98 lpi is ~1400 lines
- params.setRes(204, (l < 1450 ? 98 : 196));
- }
- /*
- * Select page length according to the image size and
- * vertical resolution. Note that if the resolution
- * info is bogus, we may select the wrong page size.
- */
- if (info.getMaxPageLengthInMM() != (u_short)-1) {
- uint32 h;
- TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &h);
- params.setPageLengthInMM((u_int)(h / yres));
- } else
- params.ln = LN_INF;
- }
- void
- faxQueueApp::preparePageChop(const FaxRequest& req,
- TIFF* tif, const Class2Params& params, fxStr& pagehandling)
- {
- tstrip_t s = TIFFNumberOfStrips(tif)-1;
- uint32* stripbytecount;
- (void) TIFFGetField(tif, TIFFTAG_STRIPBYTECOUNTS, &stripbytecount);
- u_int stripSize = (u_int) stripbytecount[s];
- if (stripSize == 0)
- return;
- u_char* data = new u_char[stripSize];
- if (TIFFReadRawStrip(tif, s, data, stripSize) >= 0) {
- uint16 fillorder;
- TIFFGetFieldDefaulted(tif, TIFFTAG_FILLORDER, &fillorder);
- MemoryDecoder dec(data, stripSize);
- dec.scanPageForBlanks(fillorder, params);
- float threshold = req.chopthreshold;
- if (threshold == -1)
- threshold = pageChopThreshold;
- u_int minRows = 0;
- switch(params.vr) {
- case VR_NORMAL:
- case VR_200X100:
- minRows = (u_int) (98. * threshold);
- break;
- case VR_FINE:
- case VR_200X200:
- minRows = (u_int) (196. * threshold);
- break;
- case VR_300X300:
- minRows = (u_int) (300. * threshold);
- break;
- case VR_R8:
- case VR_R16:
- case VR_200X400:
- minRows = (u_int) (391. * threshold);
- break;
- }
- if (dec.getLastBlanks() > minRows)
- {
- pagehandling.append(fxStr::format("Z%04x",
- fxmin((unsigned)0xFFFF, stripSize - (dec.getEndOfPage() - data))));
- }
- }
- delete [] data;
- }
- /*
- * Convert a document into a form suitable
- * for transmission to the remote fax machine.
- */
- JobStatus
- faxQueueApp::convertDocument(Job& job,
- const FaxItem& req,
- const fxStr& outFile,
- const Class2Params& params,
- fxStr& emsg)
- {
- JobStatus status;
- /*
- * Open/create the target file and lock it to guard against
- * concurrent jobs imaging the same document with the same
- * parameters. The parent will hold the open file descriptor
- * for the duration of the imaging job. Concurrent jobs will
- * block on flock and wait for the imaging to be completed.
- * Previously imaged documents will be flock'd immediately
- * and reused without delays after verifying that they were
- * last modified *after* the source image.
- *
- * NB: There is a race condition here. One process may create
- * the file but another may get the shared lock above before
- * the exclusive lock below is captured. If this happens
- * then the exclusive lock will block temporarily, but the
- * process with the shared lock may attempt to send a document
- * before it's preparation is completed. We could add a delay
- * before the shared lock but that would slow down the normal
- * case and the window is small--so let's leave it there for now.
- */
- struct stat sin;
- struct stat sout;
- if (Sys::stat(outFile, sout) == 0 && Sys::stat(req.item, sin) == 0) {
- if (sout.st_mtime < sin.st_mtime) {
- /*
- * It appears that the target file exists and is
- * older than the source image. (Thus the old target is an image
- * file from a previous job.) This can happen, for example,
- * if faxqclean isn't being run frequently-enough and faxq
- * for some reason did not delete the old target file after its job
- * completion. Thus, we delete the old file before continuing.
- */
- jobError(job, "Removing old image file: %s (run faxqclean more often)", (const char*) outFile);
- (void) Sys::unlink(outFile);
- }
- }
- int fd = Sys::open(outFile, O_RDWR|O_CREAT|O_EXCL, 0600);
- if (fd == -1) {
- if (errno == EEXIST) {
- /*
- * The file already exist, flock it in case it's
- * being created (we'll block until the imaging
- * is completed). Otherwise, the document imaging
- * has already been completed and we can just use it.
- */
- fd = Sys::open(outFile, O_RDWR); // NB: RDWR for flock emulations
- if (fd != -1) {
- if (flock(fd, LOCK_SH) == -1) {
- status = Job::format_failed;
- emsg = "Unable to lock shared document file {E318}";
- } else
- status = Job::done;
- (void) Sys::close(fd); // NB: implicit unlock
- } else {
- /*
- * This *can* happen if document preparation done
- * by another job fails (e.g. because of a time
- * limit or a malformed PostScript submission).
- */
- status = Job::format_failed;
- emsg = "Unable to open shared document file {E319}";
- }
- } else {
- status = Job::format_failed;
- emsg = "Unable to create document file {E320}";
- }
- /*
- * We were unable to open, create, or flock
- * the file. This should not happen.
- */
- if (status != Job::done)
- jobError(job, "CONVERT DOCUMENT: %s: %m", (const char*) emsg);
- } else {
- (void) flock(fd, LOCK_EX); // XXX check for errors?
- /*
- * Imaged document does not exist, run the document converter
- * to generate it. The converter is invoked according to:
- * -i jobid jobid number
- * -o file output (temp) file
- * -r <res> output resolution (dpi)
- * -w <pagewidth> output page width (pixels)
- * -l <pagelength> output page length (mm)
- * -m <maxpages> max pages to generate
- * -1|-2|-3 1d, 2d, or 2d-mmr encoding
- */
- fxStr rbuf = fxStr::format("%u", params.verticalRes());
- fxStr wbuf = fxStr::format("%u", params.pageWidth());
- fxStr lbuf = fxStr::format("%d", params.pageLength());
- fxStr mbuf = fxStr::format("%u", job.getJCI().getMaxSendPages());
- const char* argv[30];
- int ac = 0;
- switch (req.op) {
- case FaxRequest::send_postscript: argv[ac++] = ps2faxCmd; break;
- case FaxRequest::send_pdf: argv[ac++] = pdf2faxCmd; break;
- case FaxRequest::send_pcl: argv[ac++] = pcl2faxCmd; break;
- case FaxRequest::send_tiff: argv[ac++] = tiff2faxCmd; break;
- }
- argv[ac++] = "-i"; argv[ac++] = (const char*)job.jobid;
- argv[ac++] = "-o"; argv[ac++] = outFile;
- argv[ac++] = "-r"; argv[ac++] = (const char*)rbuf;
- argv[ac++] = "-w"; argv[ac++] = (const char*)wbuf;
- argv[ac++] = "-l"; argv[ac++] = (const char*)lbuf;
- argv[ac++] = "-m"; argv[ac++] = (const char*)mbuf;
- if (useUnlimitedLN) argv[ac++] = "-U";
- if (params.df == DF_2DMMR)
- argv[ac++] = "-3";
- else
- argv[ac++] = params.df == DF_1DMH ? "-1" : "-2";
- argv[ac++] = req.item;
- argv[ac] = NULL;
- // XXX the (char* const*) is a hack to force type compatibility
- status = runConverter(job, argv[0], (char* const*) argv, emsg);
- if (status == Job::done) {
- /*
- * Many converters exit with zero status even when
- * there are problems so scan the the generated TIFF
- * to verify the integrity of the converted data.
- *
- * NB: We must reopen the file instead of using the
- * previously opened file descriptor in case the
- * converter creates a new file with the given
- * output filename instead of just overwriting the
- * file created above. This can easily happen if,
- * for example, the converter creates a link from
- * the input file to the target (e.g. tiff2fax
- * does this when no conversion is required).
- */
- TIFF* tif = TIFFOpen(outFile, "r");
- if (tif) {
- while (!TIFFLastDirectory(tif))
- if (!TIFFReadDirectory(tif)) {
- status = Job::format_failed;
- emsg = "Converted document is not valid TIFF {E321}";
- break;
- }
- TIFFClose(tif);
- } else {
- status = Job::format_failed;
- emsg = "Could not reopen converted document to verify format {E322}";
- }
- if (status == Job::done) // discard any debugging output
- emsg = "";
- else
- jobError(job, "CONVERT DOCUMENT: %s", (const char*) emsg);
- } else if (status == Job::rejected)
- jobError(job, "SEND REJECT: %s", (const char*) emsg);
- (void) Sys::close(fd); // NB: implicit unlock
- }
- return (status);
- }
- static void
- closeAllBut(int fd)
- {
- for (int f = Sys::getOpenMax()-1; f >= 0; f--)
- if (f != fd)
- Sys::close(f);
- }
- /*
- * Startup a document converter program in a subprocess
- * with the output returned through a pipe. We could just use
- * popen or similar here, but we want to detect fork failure
- * separately from others so that jobs can be requeued instead
- * of rejected.
- */
- JobStatus
- faxQueueApp::runConverter(Job& job, const char* app, char* const* argv, fxStr& emsg)
- {
- fxStr cmdline(argv[0]);
- for (u_int i = 1; argv[i] != NULL; i++)
- cmdline.append(fxStr::format(" %s", argv[i]));
- traceQueue(job, "CONVERT DOCUMENT: %s", (const char*)cmdline);
- JobStatus status;
- int pfd[2];
- if (pipe(pfd) >= 0) {
- int fd;
- pid_t pid = fork();
- switch (pid) {
- case -1: // error
- jobError(job, "CONVERT DOCUMENT: fork: %m");
- status = Job::requeued; // job should be retried
- Sys::close(pfd[1]);
- break;
- case 0: // child, exec command
- if (pfd[1] != STDOUT_FILENO)
- dup2(pfd[1], STDOUT_FILENO);
- closeAllBut(STDOUT_FILENO);
- dup2(STDOUT_FILENO, STDERR_FILENO);
- fd = Sys::open(_PATH_DEVNULL, O_RDWR);
- if (fd != STDIN_FILENO)
- {
- dup2(fd, STDIN_FILENO);
- Sys::close(fd);
- }
- Sys::execv(app, argv);
- sleep(3); // XXX give parent time to catch signal
- _exit(255);
- /*NOTREACHED*/
- default: // parent, read from pipe and wait
- Sys::close(pfd[1]);
- if (runConverter1(job, pfd[0], emsg)) {
- int estat = -1;
- (void) Sys::waitpid(pid, estat);
- if (estat)
- jobError(job, "CONVERT DOCUMENT: exit status %#x", estat);
- switch (estat) {
- case 0: status = Job::done; break;
- case (254<<8): status = Job::rejected; break;
- case (255<<8): case 255: status = Job::no_formatter; break;
- default: status = Job::format_failed; break;
- }
- } else {
- kill(pid, SIGTERM);
- (void) Sys::waitpid(pid);
- status = Job::format_failed;
- }
- break;
- }
- Sys::close(pfd[0]);
- } else {
- jobError(job, "CONVERT DOCUMENT: pipe: %m");
- status = Job::format_failed;
- }
- return (status);
- }
- /*
- * Replace unprintable characters with ``?''s.
- */
- static void
- cleanse(char buf[], int n)
- {
- while (--n >= 0)
- if (!isprint(buf[n]) && !isspace(buf[n]))
- buf[n] = '?';
- }
- /*
- * Run the interpreter with the configured timeout and
- * collect the output from the interpreter in case there
- * is an error -- this is sent back to the user that
- * submitted the job.
- */
- bool
- faxQueueApp::runConverter1(Job& job, int fd, fxStr& output)
- {
- int n;
- Timeout timer;
- timer.startTimeout(postscriptTimeout*1000);
- char buf[1024];
- while ((n = Sys::read(fd, buf, sizeof (buf))) > 0 && !timer.wasTimeout()) {
- cleanse(buf, n);
- output.append(buf, n);
- }
- timer.stopTimeout();
- if (timer.wasTimeout()) {
- jobError(job, "CONVERT DOCUMENT: job time limit exceeded");
- if (output.length()) output.append("n");
- output.append("[Job time limit exceeded]");
- return (false);
- } else
- return (true);
- }
- /*
- * Generate a continuation cover page and insert it in
- * the array of files to be sent. Note that we assume
- * the cover page command generates PostScript which we
- * immediately image, discarding the PostScript. We
- * could have the cover page command script do this, but
- * then it would need to know how to invoke the PostScript
- * imager per the job characteristics. Note that we could
- * optimize things here by updating the pagehandling and
- * page counts for the job instead of resetting pagehandling
- * so that everything just gets recalculated from scratch.
- */
- bool
- faxQueueApp::makeCoverPage(Job& job, FaxRequest& req, const Class2Params& params)
- {
- bool ok = true;
- FaxItem fitem(FaxRequest::send_postscript, 0, fxStr::null, req.cover);
- fxStr cmd(coverCmd
- | quote | quoted(req.qfile) | enquote
- | quote | quoted(contCoverPageTemplate) | enquote
- | quote | fitem.item | enquote
- );
- traceQueue(job, "COVER PAGE: %s", (const char*)cmd);
- if (runCmd(cmd, true)) {
- fxStr emsg;
- fxStr tmp = fitem.item | ";" | params.encodePage();
- if (convertDocument(job, fitem, tmp, params, emsg)) {
- req.insertFax(0, tmp);
- req.cover = tmp; // needed in sendJobDone
- req.pagehandling = ""; // XXX force recalculation
- } else {
- jobError(job, "SEND: No continuation cover page, "
- " document conversion failed: %s", (const char*) emsg);
- ok = false;
- }
- Sys::unlink(fitem.item);
- } else {
- jobError(job,
- "SEND: No continuation cover page, generation cmd failed");
- ok = false;
- }
- return (ok);
- }
- const fxStr&
- faxQueueApp::pickCmd(const FaxRequest& req)
- {
- if (req.jobtype == "pager")
- return (sendPageCmd);
- if (req.jobtype == "uucp")
- return (sendUUCPCmd);
- return (sendFaxCmd); // XXX gotta return something
- }
- /*
- * Setup the argument vector and exec a subprocess.
- * This code assumes the command and dargs strings have
- * previously been processed to insert