/* * PrintQueue.c * * Routines for processing and printing spool files. * * Written by: Ken McLeod, 2026-01-16 * Last update: 2026-02-03 * * This software is provided as-is, with no warranties expressed or implied, * under the terms of the BSD 2-Clause License. See separate "LICENSE" file. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Logging.h" #include "PAP.h" #include "SpoolFiles.h" #include "PrintQueue.h" #define kCheckQueueInterval (60 * 10) /* how often to check for incoming jobs (10s) */ #define kSerialBreakInterval (60 * 0.25) /* how long to wait before sending more bytes */ long int gLastCheckedQueue = 0L; long int gCurIndex = 0L; char *gPrefixStr = 0L; char *gPortStr = 0L; Str63 gPrintFileName; short gPrintFileRefNum = 0; short gSerialRefNum = 0; long gBytesWritten = 0L; long gJobsPrinted = 0L; Boolean gProcessingEscape = false; short gBytesToRemove = 0; void FilterData(long *count, void *data) { /* The AppleTalk ImageWriter driver assumes it is formatting output * for the ImageWriter II. Some of the printer control sequences * it can emit are not understood by an ImageWriter I. We can filter * these out so the unused parameters of the commands are not printed. * * In IW1 mode, we filter the following IW II-only control sequences: * 1B48xxxxxxxx (page length command, plus 4 parameter bytes) * 1BF8xxxxxxxx (same as 1B48, but for testing the filter on IW II) * 1B4Cxxxxxx (page left margin command, plus 3 parameter bytes) * 1B4Bxx (select color command, plus 1 parameter byte) * 1B61xx (print quality command, plus 1 parameter byte) * * We can also optionally filter the command that backs up the paper: * 1B72 (set reverse line feeding) * * Since control sequences can span data blocks, the 1B might have been * sent out before we get the command code. Our filter changes the * command code to FF which will be ignored, and removes the parameter * bytes which follow. (There is no need to filter IW II-only control * sequences that have no parameters.) */ char *p; long length, index; char nopCmd = 0xFF; Boolean iw1Mode, noReverse; GetPrinterOptions(NULL, NULL, &iw1Mode, &noReverse); p = (char*)data; length = *count; for (index=0; index 0 && length > (index+1)) { /* remove one byte by shifting remaining bytes left */ BlockMove(&p[index+1],&p[index],(length-index)-1); index -= 1; length -= 1; gBytesToRemove -= 1; } } *count = length; } long JobsPrinted(void) { return gJobsPrinted; } void DeleteCurrentJob(void) { /* finished with this print job, so we can delete the spool file */ /* %%% a full-featured program could move it to a "Completed" folder */ /* %%% whose oldest entries get deleted after free disk space falls */ /* %%% below some threshold, but we aren't that sophisticated yet. */ HParamBlockRec hpb; short spoolVRefNum; long spoolDirID; OSErr err; err = GetSpoolFolder(&spoolVRefNum, &spoolDirID); if (err != noErr) { MessageWithIntValue(LOG_ERR_X,"\pUnable to get spool folder:",err); return; } hpb.fileParam.ioCompletion = nil; hpb.fileParam.ioNamePtr = gPrintFileName; hpb.fileParam.ioVRefNum = spoolVRefNum; hpb.fileParam.ioDirID = spoolDirID; hpb.fileParam.ioFDirIndex = 0; err = PBHDelete(&hpb,false); if (err != noErr) { MessageWithIntValue(LOG_ERR_X,"\pUnable to delete spool file:",err); } else { MessageWithQuotedString(LOG_WARN,"\pDeleted spool file",gPrintFileName); } } void PrintCurrentJob(void) { OSErr err = noErr; long count, readCount; /* open serial port if not already open */ if (gSerialRefNum == 0) { if (!gPortStr) { return; } err = OpenDriver((gPortStr[1]=='M') ? "\p.AOut":"\p.BOut",&gSerialRefNum); if (err != noErr) { MessageWithIntValue(LOG_ERR_X,"\pUnable to open serial port:",err); Message(LOG_ERR,"\pStopping print queue"); StopPrintQueue(); return; } gProcessingEscape = false; gBytesToRemove = 0; } /* keep reading more of the current file and sending * to serial port until done, then close the file. */ if (gPrintFileRefNum != 0) { char buf[256]; Boolean readEOF = false; while (!err) { count = sizeof(buf); err = FSRead(gPrintFileRefNum,&count,buf); readCount = count; if (err == eofErr) { readEOF = true; } if (err == noErr || (err == eofErr && readCount > 0)) { /* filter unwanted commands before writing data out to printer */ FilterData(&count,buf); /* can reduce count */ err = FSWrite(gSerialRefNum,&count,buf); if (err != noErr) { MessageWithIntValue(LOG_ERR_X,"\pUnable to send print data:",err); readEOF = true; /* cannot continue */ } else if (count > 0) { gBytesWritten += count; } /* The IW II normally has a 2KB input buffer (optionally 32KB). * We break out of this loop and return to the main event loop * after each 256 bytes written. The goal of using a small buffer * is to avoid blocking too long on FSWrite, so the app remains * mostly responsive. We'll resume printing after CheckPrintQueue * enforces a short break (kSerialBreakInterval) so the main loop * can run a few times and pending events are processed, but the * break must be short so we can keep streaming bytes and don't * starve the printer's input buffer. */ err = breakRecd; } } if (readEOF) { MessageWithIntValue(LOG_ERR,"\pSent bytes to printer:",gBytesWritten); err = FSClose(gPrintFileRefNum); if (err != noErr) { MessageWithIntValue(LOG_ERR_X,"\pUnable to close spool file:",err); } else { DeleteCurrentJob(); /* all done with this job */ } err = CloseDriver(gSerialRefNum); if (err != noErr) { MessageWithIntValue(LOG_WARN_X,"\pCould not close serial driver:",err); } gSerialRefNum = 0; gPrintFileRefNum = 0; gPrintFileName[0] = 0; gBytesWritten = 0L; gJobsPrinted++; gLastCheckedQueue = TickCount(); } } } void PrintNextJobInQueue(void) { /* get first spooled job (iterating by index until we have one or error occurs) */ short spoolVRefNum; long spoolDirID; CInfoPBRec pb; Str63 itemName; short index; OSErr err; err = GetSpoolFolder(&spoolVRefNum, &spoolDirID); if (err != noErr) { MessageWithIntValue(LOG_ERR_X,"\pUnable to get spool folder:",err); return; } index = ++gCurIndex; pb.dirInfo.ioNamePtr = itemName; pb.dirInfo.ioFDirIndex = index; pb.dirInfo.ioDrDirID = spoolDirID; pb.dirInfo.ioVRefNum = spoolVRefNum; err = PBGetCatInfoSync((CInfoPBPtr)&pb); if (err == noErr) { /* We must have a file whose name has a prefix matching * our current printer type prefix, not have a .ps suffix, * and have our file type, to be considered for printing. */ if (gPrefixStr && itemName[0]>3 && ((pb.dirInfo.ioFlAttrib & ioDirMask)==0) && (memcmp(&itemName[1],&gPrefixStr[1],3)==0) && (memcmp(&itemName[itemName[0]-2],".ps",3)!=0)) { HParamBlockRec hpb; hpb.fileParam.ioCompletion = nil; hpb.fileParam.ioNamePtr = itemName; hpb.fileParam.ioVRefNum = spoolVRefNum; hpb.fileParam.ioDirID = spoolDirID; hpb.fileParam.ioFDirIndex = 0; err = PBHGetFInfo(&hpb,false); if (err == noErr && hpb.fileParam.ioFlFndrInfo.fdType == 'pjob') { hpb.fileParam.ioNamePtr = itemName; hpb.fileParam.ioVRefNum = spoolVRefNum; hpb.fileParam.ioDirID = spoolDirID; hpb.fileParam.ioFDirIndex = 0; hpb.ioParam.ioPermssn = fsRdWrPerm; /* want exclusive read-write access */ hpb.ioParam.ioMisc = nil; err = PBHOpen(&hpb,false); if (err == noErr) { gPrintFileRefNum = hpb.fileParam.ioFRefNum; BlockMove(itemName, gPrintFileName, sizeof(Str63)); MessageWithQuotedString(LOG_ERR,"\pPrinting",itemName); } } } } else { gCurIndex = 0; } } void StartPrintQueue(char *prefixToAccept, char *serialPortName) { gLastCheckedQueue = TickCount(); /* non-zero value means the queue is running */ gPrefixStr = prefixToAccept; gPortStr = serialPortName; } void StopPrintQueue(void) { OSErr err; gLastCheckedQueue = 0L; /* zero value means the queue is paused */ gBytesWritten = 0L; /* close any file currently being sent to printer */ if (gPrintFileRefNum) { err = FSClose(gPrintFileRefNum); if (err != noErr) { MessageWithIntValue(LOG_WARN_X,"\pCould not close spool file:",err); } gPrintFileRefNum = 0; } /* close serial port if opened */ if (gSerialRefNum) { err = CloseDriver(gSerialRefNum); if (err != noErr) { MessageWithIntValue(LOG_WARN_X,"\pCould not close serial driver:",err); } gSerialRefNum = 0; } } Boolean AcceptableTime(short startHour, short endHour) { /* Given a start and end hour that define a subset of the * 24 hour period from 12 AM (0) to 12 AM (24), determine * if we are within acceptable printing hours. */ DateTimeRec dtRec; unsigned long seconds; GetDateTime(&seconds); Secs2Date(seconds,&dtRec); if (dtRec.hour < startHour || dtRec.hour >= endHour) { return false; } return true; } void CheckPrintQueue(Boolean printingEnabled, short startHour, short endHour) { long int curTicks; if (!printingEnabled || !gLastCheckedQueue) { return; /* printing not enabled, or queue is paused */ } curTicks = TickCount(); if ((gPrintFileRefNum && (gLastCheckedQueue + kSerialBreakInterval < curTicks)) || (gLastCheckedQueue + kCheckQueueInterval < curTicks)) { /* we're currently printing, or it's time to check if a new job has arrived */ gLastCheckedQueue = curTicks; if (gPrintFileRefNum) { PrintCurrentJob(); } else if (0 == GetActiveClientConnections() && AcceptableTime(startHour,endHour)) { PrintNextJobInQueue(); } else { return; /* busy spooling a print job, or waiting for printing hours to start */ } } }