AmendHub

Download

thecloud

/

SimpleSpooler

/

PrintQueue.c

 

(View History)

thecloud   FilterData tweaks Latest amendment: 5 on 2026-07-30

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 }