| 1 |
/* |
| 2 |
* PrintQueue.c |
| 3 |
* |
| 4 |
* Routines for processing and printing spool files. |
| 5 |
* |
| 6 |
* Written by: Ken McLeod, 2026-01-16 |
| 7 |
* Last update: 2026-02-03 |
| 8 |
* |
| 9 |
* This software is provided as-is, with no warranties expressed or implied, |
| 10 |
* under the terms of the BSD 2-Clause License. See separate "LICENSE" file. |
| 11 |
*/ |
| 12 |
|
| 13 |
#include <Types.h> |
| 14 |
#include <Devices.h> |
| 15 |
#include <Errors.h> |
| 16 |
#include <Events.h> |
| 17 |
#include <Files.h> |
| 18 |
#include <Folders.h> |
| 19 |
#include <SysEqu.h> |
| 20 |
#include <GestaltEqu.h> |
| 21 |
#include <Memory.h> |
| 22 |
#include <OSUtils.h> |
| 23 |
#include <Packages.h> |
| 24 |
#include <Resources.h> |
| 25 |
#include <String.h> |
| 26 |
#include "Logging.h" |
| 27 |
#include "PAP.h" |
| 28 |
#include "SpoolFiles.h" |
| 29 |
#include "PrintQueue.h" |
| 30 |
|
| 31 |
#define kCheckQueueInterval (60 * 10) /* how often to check for incoming jobs (10s) */ |
| 32 |
#define kSerialBreakInterval (60 * 0.25) /* how long to wait before sending more bytes */ |
| 33 |
|
| 34 |
long int gLastCheckedQueue = 0L; |
| 35 |
long int gCurIndex = 0L; |
| 36 |
char *gPrefixStr = 0L; |
| 37 |
char *gPortStr = 0L; |
| 38 |
Str63 gPrintFileName; |
| 39 |
short gPrintFileRefNum = 0; |
| 40 |
short gSerialRefNum = 0; |
| 41 |
long gBytesWritten = 0L; |
| 42 |
long gJobsPrinted = 0L; |
| 43 |
Boolean gProcessingEscape = false; |
| 44 |
short gBytesToRemove = 0; |
| 45 |
|
| 46 |
|
| 47 |
void FilterData(long *count, void *data) |
| 48 |
{ |
| 49 |
/* The AppleTalk ImageWriter driver assumes it is formatting output |
| 50 |
* for the ImageWriter II. Some of the printer control sequences |
| 51 |
* it can emit are not understood by an ImageWriter I. We can filter |
| 52 |
* these out so the unused parameters of the commands are not printed. |
| 53 |
* |
| 54 |
* In IW1 mode, we filter the following IW II-only control sequences: |
| 55 |
* 1B48xxxxxxxx (page length command, plus 4 parameter bytes) |
| 56 |
* 1BF8xxxxxxxx (same as 1B48, but for testing the filter on IW II) |
| 57 |
* 1B4Cxxxxxx (page left margin command, plus 3 parameter bytes) |
| 58 |
* 1B4Bxx (select color command, plus 1 parameter byte) |
| 59 |
* 1B61xx (print quality command, plus 1 parameter byte) |
| 60 |
* |
| 61 |
* We can also optionally filter the command that backs up the paper: |
| 62 |
* 1B72 (set reverse line feeding) |
| 63 |
* |
| 64 |
* Since control sequences can span data blocks, the 1B might have been |
| 65 |
* sent out before we get the command code. Our filter changes the |
| 66 |
* command code to FF which will be ignored, and removes the parameter |
| 67 |
* bytes which follow. (There is no need to filter IW II-only control |
| 68 |
* sequences that have no parameters.) |
| 69 |
*/ |
| 70 |
char *p; |
| 71 |
long length, index; |
| 72 |
char nopCmd = 0xFF; |
| 73 |
Boolean iw1Mode, noReverse; |
| 74 |
GetPrinterOptions(NULL, NULL, &iw1Mode, &noReverse); |
| 75 |
|
| 76 |
p = (char*)data; |
| 77 |
length = *count; |
| 78 |
for (index=0; index<length; index++) { |
| 79 |
if (p[index] == 0x1B) { |
| 80 |
gProcessingEscape = true; |
| 81 |
gBytesToRemove = 0; |
| 82 |
continue; |
| 83 |
} |
| 84 |
if (gProcessingEscape) { |
| 85 |
if (iw1Mode) { |
| 86 |
switch (p[index]) { |
| 87 |
case 0xF8: |
| 88 |
case 0x48: |
| 89 |
p[index] = nopCmd; gBytesToRemove = 4; break; |
| 90 |
case 0x4C: |
| 91 |
p[index] = nopCmd; gBytesToRemove = 3; break; |
| 92 |
case 0x4B: |
| 93 |
case 0x61: |
| 94 |
p[index] = nopCmd; gBytesToRemove = 1; break; |
| 95 |
default: |
| 96 |
break; |
| 97 |
} |
| 98 |
} |
| 99 |
if (noReverse && p[index] == 0x72) { |
| 100 |
p[index] = nopCmd; |
| 101 |
} |
| 102 |
gProcessingEscape = false; |
| 103 |
continue; |
| 104 |
} |
| 105 |
if (gBytesToRemove > 0 && length > (index+1)) { |
| 106 |
/* remove one byte by shifting remaining bytes left */ |
| 107 |
BlockMove(&p[index+1],&p[index],(length-index)-1); |
| 108 |
index -= 1; |
| 109 |
length -= 1; |
| 110 |
gBytesToRemove -= 1; |
| 111 |
} |
| 112 |
} |
| 113 |
*count = length; |
| 114 |
} |
| 115 |
|
| 116 |
long JobsPrinted(void) |
| 117 |
{ |
| 118 |
return gJobsPrinted; |
| 119 |
} |
| 120 |
|
| 121 |
void DeleteCurrentJob(void) |
| 122 |
{ |
| 123 |
/* finished with this print job, so we can delete the spool file */ |
| 124 |
/* %%% a full-featured program could move it to a "Completed" folder */ |
| 125 |
/* %%% whose oldest entries get deleted after free disk space falls */ |
| 126 |
/* %%% below some threshold, but we aren't that sophisticated yet. */ |
| 127 |
|
| 128 |
HParamBlockRec hpb; |
| 129 |
short spoolVRefNum; |
| 130 |
long spoolDirID; |
| 131 |
OSErr err; |
| 132 |
|
| 133 |
err = GetSpoolFolder(&spoolVRefNum, &spoolDirID); |
| 134 |
if (err != noErr) { |
| 135 |
MessageWithIntValue(LOG_ERR_X,"\pUnable to get spool folder:",err); |
| 136 |
return; |
| 137 |
} |
| 138 |
hpb.fileParam.ioCompletion = nil; |
| 139 |
hpb.fileParam.ioNamePtr = gPrintFileName; |
| 140 |
hpb.fileParam.ioVRefNum = spoolVRefNum; |
| 141 |
hpb.fileParam.ioDirID = spoolDirID; |
| 142 |
hpb.fileParam.ioFDirIndex = 0; |
| 143 |
err = PBHDelete(&hpb,false); |
| 144 |
if (err != noErr) { |
| 145 |
MessageWithIntValue(LOG_ERR_X,"\pUnable to delete spool file:",err); |
| 146 |
} else { |
| 147 |
MessageWithQuotedString(LOG_WARN,"\pDeleted spool file",gPrintFileName); |
| 148 |
} |
| 149 |
} |
| 150 |
|
| 151 |
void PrintCurrentJob(void) |
| 152 |
{ |
| 153 |
OSErr err = noErr; |
| 154 |
long count, readCount; |
| 155 |
/* open serial port if not already open */ |
| 156 |
if (gSerialRefNum == 0) { |
| 157 |
if (!gPortStr) { return; } |
| 158 |
err = OpenDriver((gPortStr[1]=='M') ? "\p.AOut":"\p.BOut",&gSerialRefNum); |
| 159 |
if (err != noErr) { |
| 160 |
MessageWithIntValue(LOG_ERR_X,"\pUnable to open serial port:",err); |
| 161 |
Message(LOG_ERR,"\pStopping print queue"); |
| 162 |
StopPrintQueue(); |
| 163 |
return; |
| 164 |
} |
| 165 |
gProcessingEscape = false; |
| 166 |
gBytesToRemove = 0; |
| 167 |
} |
| 168 |
/* keep reading more of the current file and sending |
| 169 |
* to serial port until done, then close the file. |
| 170 |
*/ |
| 171 |
if (gPrintFileRefNum != 0) { |
| 172 |
char buf[256]; |
| 173 |
Boolean readEOF = false; |
| 174 |
while (!err) { |
| 175 |
count = sizeof(buf); |
| 176 |
err = FSRead(gPrintFileRefNum,&count,buf); |
| 177 |
readCount = count; |
| 178 |
if (err == eofErr) { readEOF = true; } |
| 179 |
if (err == noErr || (err == eofErr && readCount > 0)) { |
| 180 |
/* filter unwanted commands before writing data out to printer */ |
| 181 |
FilterData(&count,buf); /* can reduce count */ |
| 182 |
err = FSWrite(gSerialRefNum,&count,buf); |
| 183 |
if (err != noErr) { |
| 184 |
MessageWithIntValue(LOG_ERR_X,"\pUnable to send print data:",err); |
| 185 |
readEOF = true; /* cannot continue */ |
| 186 |
} else if (count > 0) { |
| 187 |
gBytesWritten += count; |
| 188 |
} |
| 189 |
/* The IW II normally has a 2KB input buffer (optionally 32KB). |
| 190 |
* We break out of this loop and return to the main event loop |
| 191 |
* after each 256 bytes written. The goal of using a small buffer |
| 192 |
* is to avoid blocking too long on FSWrite, so the app remains |
| 193 |
* mostly responsive. We'll resume printing after CheckPrintQueue |
| 194 |
* enforces a short break (kSerialBreakInterval) so the main loop |
| 195 |
* can run a few times and pending events are processed, but the |
| 196 |
* break must be short so we can keep streaming bytes and don't |
| 197 |
* starve the printer's input buffer. |
| 198 |
*/ |
| 199 |
err = breakRecd; |
| 200 |
} |
| 201 |
} |
| 202 |
if (readEOF) { |
| 203 |
MessageWithIntValue(LOG_ERR,"\pSent bytes to printer:",gBytesWritten); |
| 204 |
err = FSClose(gPrintFileRefNum); |
| 205 |
if (err != noErr) { |
| 206 |
MessageWithIntValue(LOG_ERR_X,"\pUnable to close spool file:",err); |
| 207 |
} else { |
| 208 |
DeleteCurrentJob(); /* all done with this job */ |
| 209 |
} |
| 210 |
err = CloseDriver(gSerialRefNum); |
| 211 |
if (err != noErr) { |
| 212 |
MessageWithIntValue(LOG_WARN_X,"\pCould not close serial driver:",err); |
| 213 |
} |
| 214 |
gSerialRefNum = 0; |
| 215 |
gPrintFileRefNum = 0; |
| 216 |
gPrintFileName[0] = 0; |
| 217 |
gBytesWritten = 0L; |
| 218 |
gJobsPrinted++; |
| 219 |
gLastCheckedQueue = TickCount(); |
| 220 |
} |
| 221 |
} |
| 222 |
} |
| 223 |
|
| 224 |
void PrintNextJobInQueue(void) |
| 225 |
{ |
| 226 |
/* get first spooled job (iterating by index until we have one or error occurs) */ |
| 227 |
short spoolVRefNum; |
| 228 |
long spoolDirID; |
| 229 |
CInfoPBRec pb; |
| 230 |
Str63 itemName; |
| 231 |
short index; |
| 232 |
OSErr err; |
| 233 |
|
| 234 |
err = GetSpoolFolder(&spoolVRefNum, &spoolDirID); |
| 235 |
if (err != noErr) { |
| 236 |
MessageWithIntValue(LOG_ERR_X,"\pUnable to get spool folder:",err); |
| 237 |
return; |
| 238 |
} |
| 239 |
index = ++gCurIndex; |
| 240 |
|
| 241 |
pb.dirInfo.ioNamePtr = itemName; |
| 242 |
pb.dirInfo.ioFDirIndex = index; |
| 243 |
pb.dirInfo.ioDrDirID = spoolDirID; |
| 244 |
pb.dirInfo.ioVRefNum = spoolVRefNum; |
| 245 |
err = PBGetCatInfoSync((CInfoPBPtr)&pb); |
| 246 |
if (err == noErr) { |
| 247 |
/* We must have a file whose name has a prefix matching |
| 248 |
* our current printer type prefix, not have a .ps suffix, |
| 249 |
* and have our file type, to be considered for printing. |
| 250 |
*/ |
| 251 |
if (gPrefixStr && itemName[0]>3 && |
| 252 |
((pb.dirInfo.ioFlAttrib & ioDirMask)==0) && |
| 253 |
(memcmp(&itemName[1],&gPrefixStr[1],3)==0) && |
| 254 |
(memcmp(&itemName[itemName[0]-2],".ps",3)!=0)) |
| 255 |
{ |
| 256 |
HParamBlockRec hpb; |
| 257 |
hpb.fileParam.ioCompletion = nil; |
| 258 |
hpb.fileParam.ioNamePtr = itemName; |
| 259 |
hpb.fileParam.ioVRefNum = spoolVRefNum; |
| 260 |
hpb.fileParam.ioDirID = spoolDirID; |
| 261 |
hpb.fileParam.ioFDirIndex = 0; |
| 262 |
err = PBHGetFInfo(&hpb,false); |
| 263 |
if (err == noErr && hpb.fileParam.ioFlFndrInfo.fdType == 'pjob') |
| 264 |
{ |
| 265 |
hpb.fileParam.ioNamePtr = itemName; |
| 266 |
hpb.fileParam.ioVRefNum = spoolVRefNum; |
| 267 |
hpb.fileParam.ioDirID = spoolDirID; |
| 268 |
hpb.fileParam.ioFDirIndex = 0; |
| 269 |
hpb.ioParam.ioPermssn = fsRdWrPerm; /* want exclusive read-write access */ |
| 270 |
hpb.ioParam.ioMisc = nil; |
| 271 |
err = PBHOpen(&hpb,false); |
| 272 |
if (err == noErr) { |
| 273 |
gPrintFileRefNum = hpb.fileParam.ioFRefNum; |
| 274 |
BlockMove(itemName, gPrintFileName, sizeof(Str63)); |
| 275 |
MessageWithQuotedString(LOG_ERR,"\pPrinting",itemName); |
| 276 |
} |
| 277 |
} |
| 278 |
} |
| 279 |
} else { |
| 280 |
gCurIndex = 0; |
| 281 |
} |
| 282 |
} |
| 283 |
|
| 284 |
void StartPrintQueue(char *prefixToAccept, char *serialPortName) |
| 285 |
{ |
| 286 |
gLastCheckedQueue = TickCount(); /* non-zero value means the queue is running */ |
| 287 |
gPrefixStr = prefixToAccept; |
| 288 |
gPortStr = serialPortName; |
| 289 |
} |
| 290 |
|
| 291 |
void StopPrintQueue(void) |
| 292 |
{ |
| 293 |
OSErr err; |
| 294 |
gLastCheckedQueue = 0L; /* zero value means the queue is paused */ |
| 295 |
gBytesWritten = 0L; |
| 296 |
/* close any file currently being sent to printer */ |
| 297 |
if (gPrintFileRefNum) { |
| 298 |
err = FSClose(gPrintFileRefNum); |
| 299 |
if (err != noErr) { |
| 300 |
MessageWithIntValue(LOG_WARN_X,"\pCould not close spool file:",err); |
| 301 |
} |
| 302 |
gPrintFileRefNum = 0; |
| 303 |
} |
| 304 |
/* close serial port if opened */ |
| 305 |
if (gSerialRefNum) { |
| 306 |
err = CloseDriver(gSerialRefNum); |
| 307 |
if (err != noErr) { |
| 308 |
MessageWithIntValue(LOG_WARN_X,"\pCould not close serial driver:",err); |
| 309 |
} |
| 310 |
gSerialRefNum = 0; |
| 311 |
} |
| 312 |
} |
| 313 |
|
| 314 |
Boolean AcceptableTime(short startHour, short endHour) |
| 315 |
{ |
| 316 |
/* Given a start and end hour that define a subset of the |
| 317 |
* 24 hour period from 12 AM (0) to 12 AM (24), determine |
| 318 |
* if we are within acceptable printing hours. |
| 319 |
*/ |
| 320 |
DateTimeRec dtRec; |
| 321 |
unsigned long seconds; |
| 322 |
GetDateTime(&seconds); |
| 323 |
Secs2Date(seconds,&dtRec); |
| 324 |
if (dtRec.hour < startHour || dtRec.hour >= endHour) { |
| 325 |
return false; |
| 326 |
} |
| 327 |
return true; |
| 328 |
} |
| 329 |
|
| 330 |
void CheckPrintQueue(Boolean printingEnabled, short startHour, short endHour) |
| 331 |
{ |
| 332 |
long int curTicks; |
| 333 |
if (!printingEnabled || !gLastCheckedQueue) { |
| 334 |
return; /* printing not enabled, or queue is paused */ |
| 335 |
} |
| 336 |
curTicks = TickCount(); |
| 337 |
if ((gPrintFileRefNum && (gLastCheckedQueue + kSerialBreakInterval < curTicks)) || |
| 338 |
(gLastCheckedQueue + kCheckQueueInterval < curTicks)) { |
| 339 |
/* we're currently printing, or it's time to check if a new job has arrived */ |
| 340 |
gLastCheckedQueue = curTicks; |
| 341 |
if (gPrintFileRefNum) { |
| 342 |
PrintCurrentJob(); |
| 343 |
} else if (0 == GetActiveClientConnections() && AcceptableTime(startHour,endHour)) { |
| 344 |
PrintNextJobInQueue(); |
| 345 |
} else { |
| 346 |
return; /* busy spooling a print job, or waiting for printing hours to start */ |
| 347 |
} |
| 348 |
} |
| 349 |
} |