AmendHub

Download

thecloud

/

SimpleSpooler

/

PrintQueue.c

 

(View History)

thecloud   roll back FilterData change that removed 1B5A/1B44 commands Latest amendment: 8 on 2026-08-07

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