thecloud
/SimpleSpooler
/amendments
/1
Simple Spooler 0.1 release
thecloud made amendment 1 2 days ago
--- LICENSE Wed Jan 28 13:03:22 2026
+++ LICENSE Wed Jan 28 13:03:22 2026
@@ -0,0 +1,28 @@
+BSD 2-Clause License
+
+Copyright (c) 2026, Ken McLeod.
+Some code was adapted from the Neighborhood Watch sample application
+by Ricardo Batista, found on Apple Developer CD Volume IX, and from the
+MoreFiles utility routines by Jim Luther, found on the March 1994 Dev CD.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
--- Logging.c Tue Feb 17 07:40:05 2026
+++ Logging.c Tue Feb 17 07:40:05 2026
@@ -0,0 +1,476 @@
+/*
+ * Logging.c
+ *
+ * Utility functions for managing a log window
+ * and writing formatted messages to the log.
+ *
+ * Written by: Ken McLeod, 2026-01-16
+ * Last update: 2026-01-31
+ *
+ * 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 <Types.h>
+#include <Memory.h>
+#include <CursorCtl.h>
+#include <AppleTalk.h>
+#include <Packages.h>
+#include <Events.h>
+#include <SysEqu.h>
+#include <Controls.h>
+#include <Desk.h>
+#include <Devices.h>
+#include <Errors.h>
+#include <Fonts.h>
+#include <Menus.h>
+#include <ToolUtils.h>
+#include <Events.h>
+#include <Quickdraw.h>
+#include <Resources.h>
+#include <OSEvents.h>
+#include <Scrap.h>
+#include <StdIO.h>
+#include <String.h>
+#include "Logging.h"
+
+extern char *PAPFunctionNames[];
+
+WindowPtr TextWindow = 0L;
+TEHandle TextTE = 0L;
+ControlHandle TextScrollBar = 0L;
+WindowRecord WRec;
+Boolean WindowActive = false;
+int gLogLevel = 0;
+
+
+int LogLevel(void)
+{
+ return gLogLevel;
+}
+
+void SetLogLevel(int newLevel)
+{
+ gLogLevel = newLevel;
+}
+
+Boolean InitLogWindow(int logWindowID, WindowPtr *window,
+ TEHandle *textTE, ControlHandle *scrollBar)
+{
+ Rect box, bounds;
+ TextWindow = GetNewWindow(logWindowID, &WRec, (WindowPtr) -1L);
+ *window = TextWindow;
+ if (!TextWindow) { return false; }
+ SetPort(TextWindow);
+ TextSize(9);
+ TextFont(1);
+ box = TextWindow->portRect;
+ box.left = 5;
+ box.bottom -= 16;
+ box.right -= 16;
+ bounds.top = box.top;
+ bounds.left = 5;
+ bounds.right = box.right;
+ bounds.bottom = 2000;
+ TextTE = TENew(&bounds,&box);
+ *textTE = TextTE;
+ if (!TextTE) { return(false); }
+ box = TextWindow->portRect;
+ box.top--;
+ box.right -= 15;
+ bounds.top = box.top - 1;
+ bounds.left = box.right;
+ bounds.right = bounds.left + 16;
+ bounds.bottom = box.bottom - 14;
+ TextScrollBar = NewControl(TextWindow,&bounds,"\p",1,1,1,1,scrollBarProc,(long)TextTE);
+ *scrollBar = TextScrollBar;
+ if (!TextScrollBar) { return false; }
+ BeginUpdate(TextWindow);
+ EndUpdate(TextWindow);
+ DrawControls(TextWindow);
+ DrawGrowIcon(TextWindow);
+ TEActivate(TextTE);
+ WindowActive = true;
+ AdjustTextScrollBar();
+
+ return true;
+}
+
+void UnloadLogWindow(void)
+{
+ if (TextWindow) {
+ CloseWindow(TextWindow);
+ }
+}
+
+void UpdateLogWindow(void)
+{
+ GrafPtr savePort;
+ Rect box;
+
+ GetPort(&savePort);
+ SetPort(TextWindow);
+ BeginUpdate(TextWindow);
+ box = TextWindow->portRect;
+ EraseRect(&box);
+ TEUpdate(&box,TextTE);
+ DrawControls(TextWindow);
+ EndUpdate(TextWindow);
+ DrawGrowIcon(TextWindow);
+ SetPort(savePort);
+}
+
+void ActivateLogWindow(Boolean active)
+{
+ WindowActive = active;
+ if (WindowActive) {
+ TEActivate(TextTE);
+ } else {
+ TEDeactivate(TextTE);
+ }
+ HiliteControl(TextScrollBar,(WindowActive) ? 0 : 255);
+}
+
+void GrowLogWindow(short h, short v)
+{
+ MoveControl(TextScrollBar,h - 15,0);
+ SizeControl(TextScrollBar,16,v - 15);
+ HLock((Handle)TextTE);
+ (**TextTE).viewRect.right = h - 15;
+ (**TextTE).viewRect.bottom = v - 15;
+ (**TextTE).destRect.right = h - 15;
+ HUnlock((Handle)TextTE);
+ TECalText(TextTE);
+ AdjustTextScrollBar();
+}
+
+void ShowLogWindow(Rect *r)
+{
+ Rect newRect;
+ //%%% should constrain r to be within qd.screenBits.bounds here
+ MoveWindow(TextWindow, r->left, r->top, true);
+ SizeWindow(TextWindow, r->right - r->left, r->bottom - r->top, true);
+ newRect = ((GrafPtr)TextWindow)->portRect;
+ GrowLogWindow(newRect.right,newRect.bottom);
+ ShowWindow(TextWindow);
+}
+
+pascal void CtlAction(ControlHandle theControl, short part)
+{
+ TEHandle TE;
+ short newValue, value, v, max;
+
+ TE = (TEHandle) GetCRefCon(theControl);
+ max = GetCtlMax(theControl);
+ HLock((Handle)TE);
+ v = (**TE).lineHeight;
+ HUnlock((Handle)TE);
+ newValue = value = GetCtlValue(theControl);
+ if (part == inPageUp) {
+ newValue -= 12;
+ if (newValue < 1)
+ newValue = 1;
+ SetCtlValue(theControl,newValue);
+ }
+ if (part == inPageDown) {
+ newValue += 12;
+ if (newValue > max)
+ newValue = max;
+ SetCtlValue(theControl,newValue);
+ }
+ if (part == inUpButton) {
+ newValue--;
+ if (newValue < 1)
+ newValue = 1;
+ SetCtlValue(theControl,newValue);
+ }
+ if (part == inDownButton) {
+ newValue++;
+ if (newValue > max)
+ newValue = max;
+ SetCtlValue(theControl,newValue);
+ }
+ if (value != newValue)
+ TEScroll(0,(value - newValue) * v,TE);
+}
+
+void HandleMouseInText(EventRecord *myEvent)
+{
+ short where;
+ ControlHandle whichControl;
+ short value, oldValue;
+ short v;
+ Rect box;
+
+ SetPort(TextWindow);
+ GlobalToLocal(&myEvent->where);
+ where = FindControl(myEvent->where,TextWindow,&whichControl);
+ switch (where) {
+ case inUpButton:
+ case inDownButton:
+ case inPageUp:
+ case inPageDown:
+ TrackControl(whichControl,myEvent->where,(ProcPtr)CtlAction);
+ break;
+ case inThumb:
+ HLock((Handle)TextTE);
+ v = (**(TextTE)).lineHeight;
+ HUnlock((Handle)TextTE);
+ oldValue = GetCtlValue(whichControl);
+ if (TrackControl(whichControl,myEvent->where,0L)) {
+ value = GetCtlValue(whichControl);
+ if (value != oldValue) {
+ TEScroll(0,(oldValue - value) * v,TextTE);
+ }
+ }
+ break;
+ case 0:
+ HLock((Handle)TextTE);
+ box = (**(TextTE)).viewRect;
+ HUnlock((Handle)TextTE);
+ if (PtInRect(myEvent->where,&box))
+ TEClick(myEvent->where,(myEvent->modifiers & shiftKey),TextTE);
+ break;
+ default:
+ break;
+ }
+}
+
+void AdjustTextScrollBar(void)
+{
+ short lines;
+ short v;
+ short top, bottom;
+
+ HLock((Handle)TextTE);
+ lines = (**TextTE).nLines;
+ v = (**TextTE).lineHeight;
+ top = (**TextTE).viewRect.top;
+ bottom = (**TextTE).viewRect.bottom;
+ HUnlock((Handle)TextTE);
+ bottom -= top + 10;
+ lines -= bottom / v;
+ if (lines < 1)
+ lines = 1;
+ if (lines > 1)
+ lines++;
+ SetCtlMax(TextScrollBar,lines);
+ if (lines > 1 && WindowActive)
+ HiliteControl(TextScrollBar,0);
+ else
+ HiliteControl(TextScrollBar,255);
+}
+
+void Message(int level, char *mess)
+{
+ long len;
+ GrafPtr savePort;
+ char st[40];
+ unsigned long t;
+ int index, line;
+
+ if ((level & 0x7FFF) > gLogLevel) {
+ return;
+ }
+ /* always insert new text at end */
+ HLock((Handle)TextTE);
+ index = (**TextTE).teLength;
+ HUnlock((Handle)TextTE);
+ TESetSelect(index,index,TextTE);
+
+ GetDateTime(&t);
+ GetPort(&savePort);
+ SetPort(TextWindow);
+ IUTimeString((long) t, true,st);
+ while (st[0] < 14) {
+ st[0]++;
+ st[st[0]] = ' ';
+ }
+ len = st[0];
+ TEInsert(&st[1],len,TextTE);
+ IUDateString((long) t, shortDate ,st);
+ while (st[0] < 14) {
+ st[0]++;
+ st[st[0]] = ' ';
+ }
+ len = st[0];
+ TEInsert(&st[1],len,TextTE);
+ /* if the high bit of level is set, add a log level prefix string */
+ if (level & 0x8000) {
+ switch (level & 0x7FFF) {
+ case 0: BlockMove("\pERROR: ",st,8); break;
+ case 1: BlockMove("\pWARNING: ",st,10); break;
+ case 2: BlockMove("\pINFO: ",st,7); break;
+ case 3: BlockMove("\pDEBUG: ",st,8); break;
+ default: st[0] = 0; break;
+ }
+ len = st[0];
+ TEInsert(&st[1],len,TextTE);
+ }
+ len = (long)mess[0] & 0x000000FF;
+ TEInsert(&mess[1],len,TextTE);
+ TEKey(0x0D,TextTE); /* add a carriage return */
+ TECalText(TextTE);
+
+ /* Eventually we will run out of memory if we keep adding text,
+ * so after we reach a hardcoded limit, remove a chunk of lines
+ * from the top of the text edit record to make room for new text.
+ * %%% TBD: limit should be calculated dynamically based on free memory
+ */
+ index = 0;
+ HLock((Handle)TextTE);
+ len = (**TextTE).teLength;
+ if (len > 16384) {
+ line = (**TextTE).nLines / 4; /* cut first 25% of the lines */
+ index = (**TextTE).lineStarts[line-1];
+ TESetSelect(0,index,TextTE);
+ TEDelete(TextTE);
+ index = (**TextTE).teLength;
+ TESetSelect(index,index,TextTE);
+ TEScroll(0,(**TextTE).lineHeight * (line-2),TextTE);
+ }
+ HUnlock((Handle)TextTE);
+ AdjustTextScrollBar();
+ CtlAction(TextScrollBar,inDownButton); /* adjust scroll down one line */
+ SetPort(savePort);
+}
+
+void MessageWithIntValue(int level, char *mess, long int value)
+{
+ if ((level & 0x7FFF) > gLogLevel) {
+ return;
+ } else {
+ int len, vlen;
+ char val[16];
+ char tmp[256];
+
+ BlockMove(mess, tmp, mess[0]+1);
+ tmp[tmp[0]+1] = ' ';
+ tmp[0] = tmp[0]+1;
+ sprintf(val, "%ld", value);
+ vlen = (int)(strlen(val) & 0xFF);
+ len = 255 - (1 + mess[0] + 1);
+ if (vlen < len) { len = vlen; }
+ BlockMove(val, &tmp[tmp[0]+1], len);
+ tmp[0] = tmp[0]+len;
+ Message(level,tmp);
+ }
+}
+
+void MessageWithQuotedString(int level, char *mess, char *str)
+{
+ if ((level & 0x7FFF) > gLogLevel) {
+ return;
+ } else {
+ int len;
+ char tmp[256];
+
+ BlockMove(mess, tmp, mess[0]+1);
+ tmp[0] = mess[0];
+ len = 255 - (1 + mess[0] + 1 + 1 + 1);
+ if (str[0] < len) { len = str[0]; }
+ BlockMove(" “",&tmp[tmp[0]+1],2);
+ tmp[0] += 2;
+ BlockMove(&str[1], &tmp[tmp[0]+1], len);
+ tmp[0] += len;
+ BlockMove("”", &tmp[tmp[0]+1], 1);
+ tmp[0] += 1;
+ Message(level,tmp);
+ }
+}
+
+void MessageWithDataPointer(int level, char *mess, char *data, int dataLen)
+{
+ if ((level & 0x7FFF) > gLogLevel) {
+ return;
+ } else {
+ int index, len;
+ char tmp[256];
+
+ BlockMove(mess, tmp, mess[0]+1);
+ tmp[tmp[0]+1] = ' ';
+ tmp[0] = tmp[0]+1;
+ len = 255 - (1 + mess[0] + 1);
+ if (dataLen < len) { len = dataLen; }
+ BlockMove(data, &tmp[tmp[0]+1], len);
+ tmp[0] = tmp[0]+len;
+ for (index = mess[0]+2; index < tmp[0]+1; index++) {
+ if (tmp[index] < 0x20 || tmp[index] > 0x7E) {
+ tmp[index] = '.'; /* change non-printing ascii chars to a dot */
+ }
+ }
+ Message(level,tmp);
+ }
+}
+
+void MessageWithNetAddr(int level, char *mess, short net, char node, char socket)
+{
+ if ((level & 0x7FFF) > gLogLevel) {
+ return;
+ } else {
+ int len;
+ char val[10];
+ char tmp[256];
+
+ len = 255 - (1 + 1 + 3 + 1 + 3 + 1 + 3);
+ if (mess[0] < len) { len = mess[0]; }
+ BlockMove(mess, tmp, len+1);
+ tmp[tmp[0]+1] = ' ';
+ tmp[0] = tmp[0]+1;
+ sprintf(val, "%u", (unsigned int)net);
+ len = (int)(strlen(val) & 0xFFFF);
+ BlockMove(val, &tmp[tmp[0]+1], len);
+ tmp[0] = tmp[0]+len;
+ tmp[tmp[0]+1] = '.';
+ tmp[0] = tmp[0]+1;
+ sprintf(val, "%d", (int)node & 0xFF);
+ len = (int)(strlen(val) & 0xFF);
+ BlockMove(val, &tmp[tmp[0]+1], len);
+ tmp[0] = tmp[0]+len;
+ tmp[tmp[0]+1] = ':';
+ tmp[0] = tmp[0]+1;
+ sprintf(val, "%d", (int)socket & 0xFF);
+ len = (int)(strlen(val) & 0xFF);
+ BlockMove(val, &tmp[tmp[0]+1], len);
+ tmp[0] = tmp[0]+len;
+ Message(level,tmp);
+ }
+}
+
+void MessageReceivedFromNetAddr(int level, int funcID, Boolean isResp, short net, char node, char socket)
+{
+ if ((level & 0x7FFF) > gLogLevel) {
+ return;
+ } else {
+ int len;
+ char tmp[256];
+
+ BlockMove("\pGot ", tmp, 10L);
+ len = (int)(strlen(PAPFunctionNames[funcID]) & 0xFFFF);
+ BlockMove(&PAPFunctionNames[funcID][0], &tmp[tmp[0]+1], len);
+ tmp[0] = tmp[0]+len;
+ len = (isResp) ? 14 : 13;
+ BlockMove((isResp) ? " response from" : " request from", &tmp[tmp[0]+1], len);
+ tmp[0] = tmp[0]+len;
+ MessageWithNetAddr(level,tmp, net, node, socket);
+ }
+}
+
+void MessageSentToNetAddr(int level, int funcID, Boolean isResp, short net, char node, char socket)
+{
+ if ((level & 0x7FFF) > gLogLevel) {
+ return;
+ } else {
+ int len;
+ char tmp[256];
+
+ BlockMove("\pSent ", tmp, 6L);
+ len = (int)(strlen(PAPFunctionNames[funcID]) & 0xFFFF);
+ BlockMove(&PAPFunctionNames[funcID][0], &tmp[tmp[0]+1], len);
+ tmp[0] = tmp[0]+len;
+ len = (isResp) ? 12 : 11;
+ BlockMove((isResp) ? " response to" : " request to", &tmp[tmp[0]+1], len);
+ tmp[0] = tmp[0]+len;
+ MessageWithNetAddr(level,tmp, net, node, socket);
+ }
+}
--- Logging.h Mon Feb 16 21:56:17 2026
+++ Logging.h Mon Feb 16 21:56:17 2026
@@ -0,0 +1,61 @@
+/*
+ * Logging.h
+ * C Interface to log window functions
+ *
+ * Written by: Ken McLeod, 2026-01-16
+ * Last update: 2026-01-31
+ *
+ * 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.
+ */
+
+#ifndef __LOGGING_UTILS__
+#define __LOGGING_UTILS__
+
+#include <Types.h>
+#include <TextEdit.h>
+#include <Controls.h>
+#include <Events.h>
+
+/* log levels */
+#define LOG_ERR 0 /* errors and minimal operation notifications */
+#define LOG_WARN 1 /* all of the above, plus warning messages */
+#define LOG_INFO 2 /* all of the above, plus full protocol messages */
+#define LOG_DEBUG 3 /* print everything, including data received */
+
+#define LOG_ERR_X (LOG_ERR|0x8000) /* add ERROR: prefix to message */
+#define LOG_WARN_X (LOG_WARN|0x8000) /* add WARNING: prefix to message */
+#define LOG_INFO_X (LOG_INFO|0x8000) /* add INFO: prefix to message */
+#define LOG_DEBUG_X (LOG_DEBUG|0x8000) /* add DEBUG: prefix to message */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+int LogLevel(void);
+void SetLogLevel(int newLevel);
+
+Boolean InitLogWindow(int logWindowID, WindowPtr *window,
+ TEHandle *textTE, ControlHandle *scrollBar);
+
+void UnloadLogWindow(void);
+void UpdateLogWindow(void);
+void ActivateLogWindow(Boolean active);
+void GrowLogWindow(short h, short v);
+void ShowLogWindow(Rect *r);
+void HandleMouseInText(EventRecord *myEvent);
+void AdjustTextScrollBar(void);
+
+void Message(int level, char *mess);
+void MessageWithIntValue(int level, char *mess, long int value);
+void MessageWithQuotedString(int level, char *mess, char *str);
+void MessageWithDataPointer(int level, char *mess, char *data, int dataLen);
+void MessageWithNetAddr(int level, char *mess, short net, char node, char socket);
+void MessageReceivedFromNetAddr(int level, int funcID, Boolean isResp, short net, char node, char socket);
+void MessageSentToNetAddr(int level, int funcID, Boolean isResp, short net, char node, char socket);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __LOGGING_UTILS__ */
--- PAP.c Tue Feb 17 09:58:06 2026
+++ PAP.c Tue Feb 17 09:58:06 2026
@@ -0,0 +1,1324 @@
+/*
+ * PAP.c
+ *
+ * Implementation of PAP (Printer Access Protocol)
+ * as described in Inside AppleTalk, Second Edition, 1990.
+ *
+ * Written by: Ken McLeod, 2026-01-07
+ * Last update: 2026-02-16
+ *
+ * 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 <Types.h>
+#include <Limits.h>
+#include <SysEqu.h>
+#include <AppleTalk.h>
+#include <Events.h>
+#include <ToolUtils.h>
+#include <Errors.h>
+#include <Devices.h>
+#include <Memory.h>
+#include <String.h>
+#include "Logging.h"
+#include "QueryParser.h"
+#include "SpoolFiles.h"
+#include "PAP.h"
+
+
+/* we can't use PSendRequest since we can't get the response's user bytes */
+#define USING_PSENDREQUEST_API 0
+
+/* don't wait for a status request to be sent before asking for more data */
+#define WAIT_FOR_POST_DATA_STATUS 0
+
+/* function prototypes */
+
+void HandleRequest(void);
+void HandleResponse(void);
+void CancelRequest(void);
+int SendRequest(char connID, char funcID);
+int SendResponse(char connID, char funcID);
+int SetUpRequestData(char funcID, Ptr reqBuf);
+int SetUpResponseData(char funcID, Ptr respBuf);
+void SendTickle(void);
+void StartTickling(void);
+void StopTickling(void);
+
+/* globals */
+
+/* %%% these should all go into a PAPServer struct,
+ * so we could potentially have multiple instances */
+
+int requestsPending = 0; /* how many calls to PGetRequest are pending */
+int responsesPending = 0; /* how many calls to PSendRequest are pending */
+long jobsSpooled = 0L; /* how many print jobs we have received and spooled */
+Boolean listening = false; /* are we actively listening for incoming requests? */
+Boolean respondedToFirstPostConnectionStatusReq = false; /* can we call SendData yet? */
+short serverRefNum = 0; /* reference number of server instance */
+char gConnID = 0; /* currently open connection ID, or 0 if no connection */
+char gConnSocket = 0; /* destination socket to use for this connection */
+char gFlowQuantum = 0; /* flow quantum to use for this connection */
+short gNet = 0;
+char gNode = 0;
+char gSocket = 0;
+unsigned short gWaitTime = 0;
+unsigned short gDataSeq = 0; /* PAP data sequence number, increments from 1 to 65,535 */
+long int gLastTickleRecd = 0L; /* last time we received any packet for connection */
+long int gLastTickleSent = 0L; /* last time we sent a tickle for connection */
+short gMPPRef = 0;
+short gMyNet = 0, gMyNode = 0;
+short gMyRecvSocket = 0, gMySendSocket = 0;
+short gATPRequestResult = 0;
+NamesTableEntry *NTPtr = 0L;
+EntityName name;
+MPPParamBlock p;
+
+ATPPBPtr gReqPBPtr;
+ATPPBPtr gTickleReqPBPtr;
+ATPPBPtr gSendReqPBPtr;
+ATPPBPtr gSendRespPBPtr;
+ATATPRecHandle gSendReqHdl; /* outgoing request for ATPRequest */
+ATATPRecPtr gSendReqPtr; /* so we only need to dereference locked gSendReqHdl once */
+ATATPRecHandle gSendRespHdl; /* outgoing response for ATPResponse */
+ATATPRecPtr gSendRespPtr; /* so we only need to dereference locked gSendReqHdl once */
+Ptr gRReqBuf; /* buffer for received request data */
+Ptr gSReqBuf; /* buffer for our sent request data */
+Ptr gRRespBuf; /* buffer for response we will get back from ATPRequest */
+Ptr gSRespBuf; /* buffer for outgoing response data from ATPResponse */
+BDSPtr gSReqBdsPtr; /* BDS for request data that we send */
+BDSPtr gRRespBdsPtr; /* BDS for the received response to our request */
+BDSPtr gSRespBdsPtr; /* BDS for response data that we send */
+long int gDataLen; /* total data received on current connection */
+int gPrinterType; /* {0..3}, see kPrinterType definitions */
+Boolean gColorRibbon;
+Boolean gSheetFeeder;
+Boolean gFinishedQuery = false;
+Handle gQueryHdl = nil; /* query sent to us by the LW driver */
+int gQueryLength; /* length of query */
+short gQueryReqSeq = 0; /* sequence ID of query (last SendData we received) */
+short gQueryReqTID = 0; /* transaction ID of query */
+
+
+/* constants */
+
+#define kMaxAddrBlocks 100
+#define kMaxPacketSize 578 /* maximum ATP packet size we can receive */
+#define kMaxResponses 8 /* maximum number of responses to expect */
+#define kRespBufSize (kMaxPacketSize * kMaxResponses)
+#define kTickleTicks (60 * 30) /* 30 seconds in ticks */
+#define kIdleTimeoutTicks (60 * 60 * 2) /* 2 minutes in ticks */
+
+#define kPrinterTypeUnknown 0
+#define kPrinterTypeLaserWriter 1
+#define kPrinterTypeLQ 2
+#define kPrinterTypeImageWriter 3
+
+#define COLOR_RIBBON_INSTALLED 0x80 /* 0b10000000 */
+#define SHEET_FEEDER_INSTALLED 0x40 /* 0b01000000 */
+#define PAPER_OUT_ERROR 0x20 /* 0b00100000 */
+#define COVER_OPEN_ERROR 0x10 /* 0b00010000 */
+#define PRINTER_OFF_LINE 0x08 /* 0b00001000 */
+#define PAPER_JAM_ERROR 0x04 /* 0b00000100 */
+#define PRINTER_FAULT 0x02 /* 0b00000010 */
+#define PRINTER_ACTIVE 0x01 /* 0b00000001 */
+
+char *PAPFunctionNames[] = {
+ "<unknown>", /* 0 */
+ "OpenConn", /* 1 */
+ "OpenConnReply", /* 2 */
+ "SendData", /* 3 */
+ "Data", /* 4 */
+ "Tickle", /* 5 */
+ "CloseConn", /* 6 */
+ "CloseConnReply", /* 7 */
+ "SendStatus", /* 8 */
+ "Status", /* 9 */
+};
+
+void SetPrinterOptions(Boolean hasColorRibbon, Boolean hasSheetFeeder)
+{
+ gColorRibbon = hasColorRibbon;
+ gSheetFeeder = hasSheetFeeder;
+}
+
+void GetPrinterOptions(Boolean *hasColorRibbon, Boolean *hasSheetFeeder)
+{
+ if (hasColorRibbon) { *hasColorRibbon = gColorRibbon; }
+ if (hasSheetFeeder) { *hasSheetFeeder = gSheetFeeder; }
+}
+
+int GetActiveClientConnections(void)
+{
+ return (gConnID) ? 1 : 0;
+}
+
+long JobsSpooled(void)
+{
+ return jobsSpooled;
+}
+
+/* ---------------------------------------------------------------------------*/
+/* server routines */
+
+pascal int SLInit (EntityPtr printerName, /* AppleTalk entity name to serve */
+ int flowQuantum, /* # of 512-byte buffers for read */
+ PAPStatusPtr statusBuf, /* initial status is returned here */
+ int *refNum) /* server refNum for this instance */
+{
+ OSErr err;
+ int len;
+ if (serverRefNum != 0) {
+ /* error: we already have a server up */
+ return aspNoMoreSess;
+ }
+ /* allocate buffers */
+ gReqPBPtr = (ATPPBPtr)(NewPtrClear(sizeof(ATPParamBlock)));
+ gSendReqPBPtr = (ATPPBPtr)(NewPtrClear(sizeof(ATPParamBlock)));
+ gSendRespPBPtr = (ATPPBPtr)(NewPtrClear(sizeof(ATPParamBlock)));
+ gTickleReqPBPtr = (ATPPBPtr)(NewPtrClear(sizeof(ATPParamBlock)));
+ gRReqBuf = NewPtrClear(kMaxPacketSize);
+ gSReqBuf = NewPtrClear(kMaxPacketSize);
+ gRRespBuf = NewPtrClear(kRespBufSize); /* 4624 bytes */
+ gSRespBuf = NewPtrClear(kMaxPacketSize); /* 578 bytes */
+ gSReqBdsPtr = (BDSPtr)(NewPtrClear(sizeof(BDSElement)*8));
+ gRRespBdsPtr = (BDSPtr)(NewPtrClear(sizeof(BDSElement)*8));
+ gSRespBdsPtr = (BDSPtr)(NewPtrClear(sizeof(BDSElement)*8));
+
+ gSendReqHdl = (ATATPRecHandle)NewHandleClear(atpSize);
+ MoveHHi((Handle)gSendReqHdl);
+ HLock((Handle)gSendReqHdl);
+ gSendReqPtr = *gSendReqHdl;
+
+ gSendRespHdl = (ATATPRecHandle)NewHandleClear(atpSize);
+ MoveHHi((Handle)gSendRespHdl);
+ HLock((Handle)gSendRespHdl);
+ gSendRespPtr = *gSendRespHdl;
+
+ gPrinterType = kPrinterTypeUnknown;
+ len = printerName->typeStr[0];
+ if (len == 11 && !memcmp(&printerName->typeStr[1],"LaserWriter",len)) {
+ gPrinterType = kPrinterTypeLaserWriter;
+ } else if (len == 2 && !memcmp(&printerName->typeStr[1],"LQ",len)) {
+ gPrinterType = kPrinterTypeLQ;
+ } else if (len == 11 && !memcmp(&printerName->typeStr[1],"ImageWriter",len)) {
+ gPrinterType = kPrinterTypeImageWriter;
+ }
+ if (statusBuf) {
+ /* set initial status (same as a Status response) */
+ (void)SetUpResponseData(kPAPStatus,(Ptr)&statusBuf);
+ }
+
+ Message(LOG_ERR,"\pStarting network setup");
+ err = OpenDriver("\p.MPP",&gMPPRef);
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pAppleTalk could not be opened:", err);
+ return err;
+ }
+ err = GetNodeAddress(&gMyNode, &gMyNet);
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pCould not obtain our address:", err);
+ return err;
+ }
+ gReqPBPtr->ATP.atpSocket = 0; /* dynamically allocate a socket */
+ gReqPBPtr->ATP.addrBlock.aNet = 0; /* accept requests from anyone */
+ gReqPBPtr->ATP.addrBlock.aNode = 0;
+ gReqPBPtr->ATP.addrBlock.aSocket = 0;
+ err = POpenATPSkt(gReqPBPtr, false); /* socket is returned in atpSocket */
+ gMyRecvSocket = (short)gReqPBPtr->ATP.atpSocket;
+ if (gMyRecvSocket == 0 || err != noErr || gReqPBPtr->ATP.ioResult != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pReceive socket could not be opened:", err);
+ return err;
+ } else {
+ MessageWithNetAddr(LOG_WARN,"\pListening on",gMyNet,gMyNode,gMyRecvSocket);
+ }
+
+ gSendReqPBPtr->ATP.atpSocket = 0; /* dynamically allocate a socket */
+ gSendReqPBPtr->ATP.addrBlock.aNet = 0; /* accept responses from anyone */
+ gSendReqPBPtr->ATP.addrBlock.aNode = 0;
+ gSendReqPBPtr->ATP.addrBlock.aSocket = 0;
+ err = POpenATPSkt(gSendReqPBPtr, false); /* socket is returned in atpSocket */
+ gMySendSocket = (short)gSendReqPBPtr->ATP.atpSocket;
+ if (gMySendSocket == 0 || err != noErr || gSendReqPBPtr->ATP.ioResult != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pSend socket could not be opened:",err);
+ return err;
+ } else {
+ MessageWithNetAddr(LOG_WARN,"\pSending on",gMyNet,gMyNode,gMySendSocket);
+ }
+
+ serverRefNum++;
+ if (refNum) {
+ *refNum = serverRefNum;
+ }
+ gFlowQuantum = flowQuantum;
+
+ err = PAPRegName(serverRefNum, printerName);
+ if (err == noErr) {
+ listening = 1;
+ }
+ return err;
+}
+
+pascal int PAPRegName (int refNum, /* server refNum for this name */
+ EntityPtr printerName) /* entity name to register */
+{
+ OSErr err;
+ short index;
+ if (!refNum) {
+ return paramErr;
+ }
+ NTPtr = (NamesTableEntry*) NewPtrClear(sizeof(NamesTableEntry));
+ if (!NTPtr) {
+ return(MemError());
+ }
+ NTPtr->nt.nteAddress.aSocket = (char)gMyRecvSocket & 0x00FF;
+ p.NBPinterval = 3;
+ p.NBPcount = 3;
+ p.NBPverifyFlag = true;
+ p.NBPntQElPtr = (Ptr) NTPtr;
+ BlockMove(printerName->objStr,&(NTPtr->nt.entityData[0]),33L);
+ index = printerName->objStr[0] + 1;
+ BlockMove(printerName->typeStr,&(NTPtr->nt.entityData[index]),33L);
+ index += printerName->typeStr[0] + 1;
+ BlockMove(printerName->zoneStr,&(NTPtr->nt.entityData[index]),33L);
+ err = PRegisterName(&p,true);
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pPRegisterName returned error", err);
+ }
+ while (p.MPPioResult == 1) ; /* poll until PRegisterName completes */
+ err = p.MPPioResult;
+ while (err == nbpDuplicate) {
+ printerName->objStr[0]++;
+ printerName->objStr[printerName->objStr[0]] = '1';
+ BlockMove(printerName->objStr,&(NTPtr->nt.entityData[0]),33L);
+ index = printerName->objStr[0] + 1;
+ BlockMove(printerName->typeStr,&(NTPtr->nt.entityData[index]),33L);
+ index += printerName->typeStr[0] + 1;
+ BlockMove(printerName->zoneStr,&(NTPtr->nt.entityData[index]),33L);
+ err = PRegisterName(&p,true);
+ while (p.MPPioResult == 1) ; /* try again once more */
+ err = p.MPPioResult;
+ }
+ /* save a copy of the name */
+ BlockMove(printerName, &name, sizeof(EntityName));
+ BlockMove(printerName->objStr,&name.objStr[0],33L);
+ /* we want to be able to print from our own node to the spooler */
+ if (err == noErr) {
+ p.SETSELF.newSelfFlag = 1;
+ err = PSetSelfSend(&p, false);
+ if (err == controlErr) { /* running on system 6? */
+ MessageWithIntValue(LOG_WARN_X,"\pCould not enable self-send mode:",err);
+ err = noErr; /* don't really need this to continue running */
+ }
+ }
+ return err;
+}
+
+pascal int PAPRemName (int refNum, /* server refNum for this name */
+ EntityPtr printerName) /* entity name to remove */
+{
+#pragma unused (printerName)
+ OSErr err;
+ if (gMyRecvSocket && NTPtr && refNum) {
+ p.NBPntQElPtr = (Ptr) &(NTPtr->nt.entityData[0]);
+ err = PRemoveName(&p,false);
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pCould not unregister spooler:",err);
+ } else {
+ MessageWithQuotedString(LOG_ERR,"\pUnregistered",(char*)&NTPtr->nt.entityData[0]);
+ }
+ DisposPtr((Ptr) NTPtr);
+
+ gReqPBPtr->ATP.atpSocket = (char)gMyRecvSocket & 0x00FF;
+ err = PCloseATPSkt(gReqPBPtr,false);
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pCould not close receive socket:",err);
+ } else {
+ Message(LOG_WARN,"\pReceive socket closed");
+ }
+ gSendReqPBPtr->ATP.atpSocket = (char)gMySendSocket & 0x00FF;
+ err = PCloseATPSkt(gSendReqPBPtr,false);
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pCould not close send socket:",err);
+ } else {
+ Message(LOG_WARN,"\pSend socket closed");
+ }
+ } else {
+ err = badATPSkt;
+ }
+ return err;
+}
+
+pascal int HeresStatus (int refNum, /* server refNum for updated status */
+ PAPStatusPtr statusBuf) /* new status string */
+{
+#pragma unused (refNum, statusBuf)
+ return unimpErr; /* TBI */
+}
+
+pascal int SLClose (int refNum) /* server refNum from SLInit */
+{
+ OSErr err;
+
+ /* Cancel any request which may be in flight */
+ CancelRequest();
+
+ /* Unregister from NBP and close our sockets */
+ err = PAPRemName(refNum, &name);
+
+ requestsPending = 0;
+ responsesPending = 0;
+ listening = 0;
+ serverRefNum = 0;
+ gMyRecvSocket = 0;
+ gMySendSocket = 0;
+ gPrinterType = kPrinterTypeUnknown;
+ respondedToFirstPostConnectionStatusReq = false;
+ gConnID = 0;
+ gDataSeq = 0;
+ gDataLen = 0;
+
+ /* Clean up allocations. */
+ if (gReqPBPtr) { DisposePtr((Ptr)(gReqPBPtr)); }
+ if (gSendReqPBPtr) { DisposePtr((Ptr)(gSendReqPBPtr)); }
+ if (gSendRespPBPtr) { DisposePtr((Ptr)(gSendRespPBPtr)); }
+ if (gTickleReqPBPtr) { DisposePtr((Ptr)(gTickleReqPBPtr)); }
+ if (gRReqBuf) { DisposePtr(gRReqBuf); }
+ if (gSReqBuf) { DisposePtr(gSReqBuf); }
+ if (gRRespBuf) { DisposePtr(gRRespBuf); }
+ if (gSRespBuf) { DisposePtr(gSRespBuf); }
+ if (gSReqBdsPtr) { DisposePtr((Ptr)(gSReqBdsPtr)); }
+ if (gRRespBdsPtr) { DisposePtr((Ptr)(gRRespBdsPtr)); }
+ if (gSRespBdsPtr) { DisposePtr((Ptr)(gSRespBdsPtr)); }
+ if (gSendReqHdl) { HUnlock((Handle)gSendReqHdl); DisposeHandle((Handle)gSendReqHdl); }
+ if (gSendRespHdl) { HUnlock((Handle)gSendRespHdl); DisposeHandle((Handle)gSendRespHdl); }
+
+ return err;
+}
+
+/* ---------------------------------------------------------------------------*/
+/* client routines */
+
+/* %%% These are all unimplemented at the moment, since we're a server only.
+ * Our implementation wants to make asynchronous ATP calls to avoid blocking
+ * on the network. We poll request and response status by calling CheckATP
+ * from the main runloop, but could alternatively provide ioCompletion routines.
+ * We would need to make synchronous ATP calls instead if this synchronous
+ * interface were to be implemented, or else spin up an inner runloop inside
+ * each API while we wait. There doesn't seem to be a good reason to do that.
+ */
+
+pascal int PAPOpen (int *refNum, /* returned: connection ID ref */
+ EntityPtr printerName, /* AppleTalk entity name */
+ int flowQuantum, /* # of 512-byte buffers for read */
+ PAPStatusPtr statusBuf, /* returned: status string */
+ int *compState) /* returned: status, 0=success */
+{
+#pragma unused (refNum, printerName, flowQuantum, statusBuf, compState)
+ return unimpErr;
+}
+
+pascal int PAPStatus (EntityPtr printerName, /* AppleTalk entity name */
+ PAPStatusPtr statusBuf, /* returned: status string */
+ long int *nodeAddr) /* returned: node address */
+{
+#pragma unused (printerName, statusBuf, nodeAddr)
+ return unimpErr;
+}
+
+pascal int PAPRead (int refNum, /* connection ID from PAPOpen */
+ char *rxBuf, /* buffer in which to read the data */
+ int *rxBufLen, /* size of the data read on output */
+ int *eofByte, /* set to positive value when EOF */
+ int *compState) /* returned: status, 0=success */
+{
+#pragma unused (refNum, rxBuf, rxBufLen, eofByte, compState)
+ return unimpErr;
+}
+
+pascal int PAPWrite (int refNum, /* connection ID from PAPOpen */
+ char *txBuf, /* buffer with data to be written */
+ int txBufLen, /* size of the data to be written */
+ int eofByte, /* set to positive on last buffer */
+ int *compState) /* returned: status, 0=success */
+{
+#pragma unused (refNum, txBuf, txBufLen, eofByte, compState)
+ return unimpErr;
+}
+
+pascal int PAPClose (int refNum) /* connection ID from PAPOpen */
+{
+#pragma unused (refNum)
+ return unimpErr;
+}
+
+pascal int PAPUnload (void) /* close connections, free storage */
+{
+ return unimpErr;
+}
+
+/* ---------------------------------------------------------------------------*/
+
+void HandleRequest(void)
+{
+ ATPPBPtr atp = gReqPBPtr;
+ char connID = *((char*)&atp->ATP.userData + 0);
+ char funcID = *((char*)&atp->ATP.userData + 1);
+ short seqID = atp->ATP.userData & 0x0000FFFF;
+ short net = atp->ATP.addrBlock.aNet;
+ char node = atp->ATP.addrBlock.aNode;
+ char socket = atp->ATP.addrBlock.aSocket;
+
+ if (funcID < kPAPOpenConn || funcID > kPAPStatus) {
+ funcID = 0; /* map unhandled functions to <unknown> string */
+ }
+ MessageReceivedFromNetAddr(LOG_INFO, funcID, false, net, node, socket);
+
+ if (gConnID == connID && connID != 0) {
+ gLastTickleRecd = TickCount(); /* update last activity on connection */
+ }
+ if (gConnID && connID && funcID == kPAPOpenConn) {
+ /* We only handle one connection at a time, and we already have one open.
+ Generally, a client trying to open a connection will keep retrying until
+ it succeeds or they decide to not wait anymore. If this is a duplicate
+ retry attempt for the already-open connection, then we'll drop it.
+ Otherwise, we reply with OpenConnReply "server busy" status, per IA:10-7.
+ */
+ if (gConnID == connID) {
+ return;
+ }
+ }
+
+ switch (funcID) {
+ case kPAPOpenConn:
+ if (gConnID) {
+ MessageWithIntValue(LOG_WARN_X,"\pIgnoring new connection",(int)connID & 0xFF);
+ SendResponse(connID, kPAPOpenConnReply);
+ return;
+ }
+ MessageWithNetAddr(LOG_ERR,"\pReceiving print job from", net, node, socket);
+ MessageWithIntValue(LOG_ERR,"\pOpening connection",(int)connID & 0xFF);
+ (void) OpenSpoolFile(gPrinterType);
+ gNet = net;
+ gNode = node;
+ gSocket = socket;
+ gConnSocket = gRReqBuf[0];
+ gFlowQuantum = gRReqBuf[1]; /* set before sending OpenConnReply */
+ if (gFlowQuantum < kPAPMinQuantum) { gFlowQuantum = kPAPMinQuantum; }
+ if (gFlowQuantum > kPAPMaxQuantum) { gFlowQuantum = kPAPMaxQuantum; }
+ BlockMove(&gRReqBuf[2], &gWaitTime, 2);
+ /* %%% gWaitTime is supposed to tell us which OpenConn to handle first.
+ * But we ignore it and just accept the first connection immediately. */
+ SendResponse(connID, kPAPOpenConnReply);
+ gConnID = connID; /* connection is now established */
+ StartTickling(); /* send periodic tickles */
+ break;
+ case kPAPOpenConnReply:
+ /* this is never sent as a request, only as a response */
+ break;
+ case kPAPSendData:
+ /* client is asking us to send data */
+ #if 0
+ /* %%% while this is correct behavior, if we miss the first SendData,
+ we will drop subsequent retries as dupes. Need to keep track
+ of what we processed successfully.
+ */
+ if (gQueryReqSeq >= seqID) {
+ MessageWithIntValue(LOG_INFO_X,"\pIgnoring dup SendData seq:",seqID);
+ break; /* avoid processing a duplicate request, per IA:10-11 */
+ }
+ #endif
+ /* remember transaction ID and new sequence ID */
+ gQueryReqSeq = seqID;
+ if (!gQueryReqTID) {
+ gQueryReqTID = atp->OTH2.transID;
+ }
+ if ((gPrinterType != kPrinterTypeLaserWriter) ||
+ (!gFinishedQuery && gQueryHdl && gQueryLength)) {
+ /* we can respond right away */
+ SendResponse(connID, kPAPData);
+ gFinishedQuery = true;
+ gQueryReqTID = 0;
+ } else {
+ /* need to pend the response until we've read Data with the query */
+ MessageWithIntValue(LOG_INFO,"\pPending response to TID:",gQueryReqTID);
+ /* %%% DEBUG
+ * It would seem logical to set gFinishedQuery=false here, so the
+ * next Data would be put into gQueryHdl instead of the spool file,
+ * but unfortunately actual data comes in before query data, and
+ * we quickly run out of memory. For now, gFinishedQuery cannot be
+ * set back to false again once the initial query has been processed.
+ * Need to better understand how to handle multiple SendData requests
+ * from the client which have different TIDs.
+ */
+ }
+ break;
+ case kPAPData:
+ /* this is never sent as a request, only as a response */
+ break;
+ case kPAPTickle:
+ /* client sent us a tickle so we don't time out, handled above */
+ break;
+ case kPAPCloseConn:
+ /* this is the normal sequence on success: client closes the connection */
+ if (gDataLen > 0) {
+ MessageWithIntValue(LOG_ERR,"\pReceived data bytes:", gDataLen);
+ jobsSpooled++;
+ }
+ CloseSpoolFile(gDataLen);
+ gDataLen = 0;
+ if (gConnID) {
+ StopTickling(); /* stop periodic tickles before connection closes */
+ CancelRequest(); /* cancel any outstanding requests so we don't wait */
+ MessageWithIntValue(LOG_ERR,"\pClosing connection",(int)connID & 0xFF);
+ SendResponse(connID, kPAPCloseConnReply);
+ gConnID = 0; /* connection is now closed */
+ respondedToFirstPostConnectionStatusReq = false;
+ gLastTickleRecd = 0L;
+ }
+ break;
+ case kPAPCloseConnReply:
+ /* this is never sent as a request, only as a response */
+ break;
+ case kPAPSendStatus:
+ SendResponse(connID, kPAPStatus);
+ if (gConnID && !respondedToFirstPostConnectionStatusReq) {
+ respondedToFirstPostConnectionStatusReq = true;
+ gDataSeq = 1;
+ gDataLen = 0;
+ SendRequest(gConnID, kPAPSendData); /* ask for data segment #1 */
+ }
+ break;
+ case kPAPStatus:
+ /* this is never sent as a request, only as a response */
+ break;
+ default:
+ MessageWithNetAddr(LOG_WARN,"\pGot unknown request from", net, node, socket);
+ break;
+ }
+}
+
+void CancelRequest(void)
+{
+ OSErr err = noErr;
+#if USING_PSENDREQUEST_API
+ if (gSendReqPBPtr->ATP.ioResult > noErr) {
+ err = PKillSendReq(gSendReqPBPtr, true);
+ gSendReqPBPtr->ATP.ioResult = reqAborted;
+ }
+#else
+ if (gSendReqPtr->abResult > noErr) {
+ err = ATPReqCancel(gSendReqHdl, true);
+ gSendReqPtr->abResult = reqAborted;
+ }
+#endif
+ if (responsesPending > 0) {
+ responsesPending--;
+ }
+ if (err != noErr && err != cbNotFound) {
+ MessageWithIntValue(LOG_WARN_X,"\pUnable to cancel request:",err);
+ }
+}
+
+#if USING_PSENDREQUEST_API
+void HandleResponse_from_PSendRequest(void)
+{
+ /* %%% PROBLEM %%%
+ * The ATPPBPtr (to ATPParamBlock) does not give us access to the
+ * user bytes for the response, separate from the request.
+ * Request user bytes are in MPPATPHeader's userData field, but
+ * where is the info we would find in ATATPRec's atpRspUData field?
+ */
+ ATPPBPtr atp = gSendReqPBPtr;
+ char connID = *((char*)&atp->ATP.userData + 0);
+ char funcID = *((char*)&atp->ATP.userData + 1);
+ char eofByte = *((char*)&atp->ATP.userData + 2);
+ short net = atp->ATP.addrBlock.aNet;
+ char node = atp->ATP.addrBlock.aNode;
+ char socket = atp->ATP.addrBlock.aSocket;
+
+ if (funcID < kPAPOpenConn || funcID > kPAPStatus) {
+ funcID = 0; /* map unhandled functions to <unknown> string */
+ }
+ MessageReceivedFromNetAddr(LOG_INFO,funcID, true, net, node, socket);
+
+ if (gConnID) {
+ if (gConnID == connID) {
+ gLastTickleRecd = TickCount(); /* update last activity for connection */
+ }
+ }
+
+ switch (funcID) {
+ case kPAPData:
+ MessageWithIntValue(LOG_INFO,"\pReceived data of length",
+ ((BDSPtr)atp->ATP.bdsPointer)->dataSize);
+ gDataLen += ((BDSPtr)atp->ATP.bdsPointer)->dataSize;
+ if (!eofByte) {
+ /* as long as we haven't hit EOF, keep asking for more data */
+ if (++gDataSeq == 0) {
+ gDataSeq = 1; /* reset to 1 if we wrap around, per IA 10-11 */
+ }
+ SendRequest(gConnID, kPAPSendData); /* ask for next data block */
+ }
+ break;
+ case kPAPCloseConnReply:
+ break;
+ default:
+ MessageWithNetAddr(LOG_WARN,"\pGot unknown response from", net, node, socket);
+ break;
+ }
+}
+#endif
+
+void HandleResponse_from_ATPRequest(void)
+{
+ ATATPRecPtr abp = gSendReqPtr;
+ char connID, funcID;
+ char eofByte;
+ short net = abp->atpAddress.aNet;
+ char node = abp->atpAddress.aNode;
+ char socket = abp->atpAddress.aSocket;
+ char *data = (char*)abp->atpRspBuf;
+ BDSPtr bds = abp->atpRspBDSPtr;
+ int numRsp = abp->atpNumRsp;
+ int index, dataLen, length = 0;
+
+ for (index=0; index < abp->atpNumRsp; index++) {
+ dataLen = ((BDSElement*)bds)[index].dataSize;
+ /* align response blocks so their data is contiguous */
+ BlockMove(((BDSElement*)bds)[index].buffPtr,data+length,dataLen);
+ length += dataLen;
+ }
+ abp->atpRspUData = ((BDSElement*)bds)[numRsp-1].userBytes;
+ connID = *((char*)&abp->atpRspUData + 0);
+ funcID = *((char*)&abp->atpRspUData + 1);
+ eofByte = *((char*)&abp->atpRspUData + 2);
+
+ if (funcID < kPAPOpenConn || funcID > kPAPStatus) {
+ funcID = 0; /* map unhandled functions to <unknown> string */
+ }
+ MessageReceivedFromNetAddr(LOG_INFO,funcID, true, net, node, socket);
+
+ if (gConnID) {
+ if (gConnID == connID) {
+ gLastTickleRecd = TickCount(); /* update last activity for connection */
+ }
+ }
+
+ switch (funcID) {
+ case kPAPData:
+ MessageWithIntValue(LOG_INFO,"\pReceived data with length",length);
+ MessageWithIntValue(LOG_DEBUG,"\p-- eofByte:",(short)eofByte);
+ MessageWithIntValue(LOG_DEBUG,"\p-- dataSeq:",(short)gDataSeq);
+ MessageWithDataPointer(LOG_DEBUG,"\p-- data:",data,length);
+
+ /* The LQ driver will send us a 2-byte Data response as the first
+ * and last packets of the data sequence (at minimum). If we haven't
+ * responded to its SendData request, this will be a self-id command
+ * (esc-?, or 0x1B3F). If we have responded to its SendData with the
+ * 'LQ1\r' identity string, then the data we get here will be simply 'LQ'.
+ * Either way, we need to filter this out as it isn't meant to be printed.
+ */
+ if ((length == 2) && (gPrinterType == kPrinterTypeLQ) &&
+ ((*(data+0) == 0x1B && *(data+1) == 0x3F) ||
+ (*(data+0) == 0x4C && *(data+1) == 0x51))) {
+ gDataSeq++;
+ Message(LOG_INFO,"\pFiltering 2 data bytes (LQ self-id)");
+
+ } else if (!gFinishedQuery && gPrinterType == kPrinterTypeLaserWriter) {
+ /* data sent to us after their SendData is a query, until '%%EOF' */
+ OSErr err;
+ gDataSeq++;
+ /* save this query; we will need it to compose a response */
+ if (!gQueryHdl) {
+ /* first part of query */
+ gQueryHdl = NewHandle((Size)length);
+ /* handle error if this call fails, no pun intended */
+ if ((err=MemError()) == noErr) {
+ HLock(gQueryHdl);
+ BlockMove(data,*gQueryHdl,length);
+ HUnlock(gQueryHdl);
+ gQueryLength = length;
+ }
+ } else {
+ /* continuation of query */
+ SetHandleSize(gQueryHdl,(Size)gQueryLength+length);
+ /* handle error if this call fails, no pun intended */
+ if ((err=MemError()) == noErr) {
+ HLock(gQueryHdl);
+ BlockMove(data,*gQueryHdl+(Size)gQueryLength,length);
+ HUnlock(gQueryHdl);
+ gQueryLength += length;
+ }
+ }
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pCould not save query:",err);
+ return;
+ }
+ MessageWithIntValue(LOG_INFO,"\pFiltering query bytes:",length);
+ MessageWithDataPointer(LOG_DEBUG,"\p-- end: ",data+(length-6),5);
+ if ((length>5 && !memcmp("%%EOF",data+(length-6),5)) ||
+ (length>9 && !memcmp("unknown\r\r",data+(length-10),9))) {
+ /* got EOF or unknown at end of PostScript query;
+ * now we can reply to it
+ */
+ MessageWithIntValue(LOG_INFO,"\pGot EOF; total query:",gQueryLength);
+ if (gQueryReqTID) {
+ SendResponse(gConnID, kPAPData);
+ gFinishedQuery = true;
+ gQueryReqTID = 0;
+ } /* else we have to wait for the SendData request to be retried */
+ }
+ eofByte = 0; /* clear so we ask for more data */
+
+ } else {
+ WriteDataToSpoolFile(length,data);
+ gDataLen += length;
+ if (++gDataSeq == 0) {
+ gDataSeq = 1; /* reset to 1 if we wrap around, per IA 10-11 */
+ }
+ }
+ if (!eofByte) {
+ /* as long as we haven't seen EOF, keep asking for more data */
+ SendRequest(gConnID, kPAPSendData); /* ask for next data block */
+ } else {
+ /* this is EOF */
+ /* NOTE: we usually don't get here with ImageWriter or LQ drivers.
+ * The driver is supposed to set eof in the last data packet, but
+ * more frequently, it just sends CloseConn to indicate that it's
+ * done with us.
+ */
+ MessageWithIntValue(LOG_ERR,"\pReceived data bytes:", gDataLen);
+ CloseSpoolFile(gDataLen);
+ if (gDataLen != 0) {
+ gDataLen = 0;
+ jobsSpooled++;
+ }
+ /* we don't send CloseConn on EOF; workstation does, per IA:10-11 */
+ }
+ break;
+ case kPAPCloseConnReply:
+ break;
+ default:
+ MessageWithNetAddr(LOG_WARN,"\pGot unknown response from", net, node, socket);
+ break;
+ }
+}
+
+void HandleResponse(void)
+{
+#if USING_PSENDREQUEST_API
+ HandleResponse_from_PSendRequest();
+#else
+ HandleResponse_from_ATPRequest();
+#endif
+}
+
+int SendRequest_with_ATPRequest(char connID, char funcID)
+{
+ ATATPRecPtr abp = gSendReqPtr;
+ short destSocket = (gConnSocket) ? gConnSocket : gSocket;
+ short retryInterval, retryCount;
+ int nBufs, dataLen;
+ OSErr err;
+
+ if (abp->abResult > noErr) {
+ /* %%% should we allocate a new ATATPRec and free in ioCompletion? */
+ /* %%% or does this just mean the connection has closed abnormally? */
+ Message(LOG_WARN_X,"\pRequest still in flight, cannot send yet");
+ return tooManyReqs;
+ }
+ retryInterval = 5; /* default timeout for our requests */
+ retryCount = 1;
+ if (funcID == kPAPSendData) {
+ /*retryInterval = 5; /* should be 15 seconds, per IA 10-10, but use 5 for now */
+ retryCount = SHRT_MAX; /* keep retrying until success or connection timeout */
+ }
+ dataLen = SetUpRequestData(funcID, gSReqBuf);
+
+ /* set up BDS to buffer a response that comes in across multiple packets */
+ nBufs = BuildBDS(gRRespBuf,(Ptr)(gRRespBdsPtr),kRespBufSize);
+
+ abp->atpUserData = 0L;
+ BlockMove(&connID,(char*)&abp->atpUserData + 0, 1);
+ BlockMove(&funcID,(char*)&abp->atpUserData + 1, 1);
+ if (funcID == kPAPSendData) {
+ BlockMove(&gDataSeq,(char*)&abp->atpUserData + 2, 2);
+ }
+ abp->abOpcode = tATPRequest;
+ abp->abResult = 0;
+ abp->abUserReference = 0;
+ abp->atpAddress.aNet = gNet;
+ abp->atpAddress.aNode = gNode;
+ abp->atpAddress.aSocket = destSocket;
+ abp->atpSocket = (char)gMySendSocket & 0x00FF;
+ abp->atpReqCount = dataLen;
+ abp->atpDataPtr = gSReqBuf;
+ abp->atpRspBDSPtr = gRRespBdsPtr;
+ abp->atpRspBuf = gRRespBuf;
+ abp->atpRspSize = kRespBufSize;
+ abp->atpNumBufs = nBufs;
+ abp->atpXO = true;
+ abp->atpTimeOut = retryInterval;
+ abp->atpRetries = retryCount;
+
+ err = ATPSndRequest(gSendReqHdl,true); /* send this async so we aren't blocked */
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pATPSndRequest returned", err);
+ } else {
+ responsesPending++;
+ MessageSentToNetAddr(LOG_INFO,funcID, false, gNet, gNode, (short)destSocket & 0xFF);
+ }
+ return err;
+}
+
+#if USING_PSENDREQUEST_API
+int SendRequest_with_PSendRequest(char connID, char funcID)
+{
+ ATPPBPtr atp = gSendReqPBPtr;
+ short destSocket = (gConnSocket) ? gConnSocket : gSocket;
+ short retryInterval, retryCount;
+ int dataLen = 0;
+ int nBufs = 0;
+ OSErr err;
+
+ if (atp->ATP.ioResult > noErr) {
+ /* we can't use this ATPPBPtr since it's still waiting for a response */
+ /* %%% should we allocate a new ATPParamBlock and free in ioCompletion? */
+ Message(LOG_WARN_X,"\pRequest still in flight, cannot send yet");
+ return tooManyReqs;
+ }
+ retryInterval = 5; /* default timeout for our requests */
+ retryCount = 1;
+ if (funcID == kPAPSendData) {
+ /*retryInterval = 5; /* should be 15 seconds, per IA 10-10, but use 5 for now */
+ retryCount = SHRT_MAX; /* keep retrying until success or connection timeout */
+ }
+ dataLen = SetUpRequestData(funcID, gSReqBuf);
+/* %%% FIXME this is wrong; using request data buffer for response buffer */
+ nBufs = BuildBDS(gSReqBuf,(Ptr)(gSReqBdsPtr),kMaxPacketSize);
+
+ atp->ATP.userData = 0L;
+ BlockMove(&connID,(char*)&atp->ATP.userData + 0, 1);
+ BlockMove(&funcID,(char*)&atp->ATP.userData + 1, 1);
+ if (funcID == kPAPSendData) {
+ BlockMove(&gDataSeq,(char*)&atp->ATP.userData + 2, 2);
+ }
+ atp->ATP.ioCompletion = nil; /* we will poll for completion */
+ atp->ATP.atpSocket = (char)gMySendSocket & 0x00FF;
+ atp->ATP.atpFlags = atpXOvalue;
+ atp->ATP.addrBlock.aNet = gNet;
+ atp->ATP.addrBlock.aNode = gNode;
+ atp->ATP.addrBlock.aSocket = (char)destSocket & 0x00FF;
+ atp->ATP.reqLength = dataLen; /* length of request buffer */
+ atp->ATP.reqPointer = gSReqBuf; /* pointer to buffer holding request data */
+ atp->ATP.bdsPointer = (Ptr)gSReqBdsPtr; /* buffer to hold response BDS */
+ atp->OTH1.u0.numOfBuffs = kMaxResponses; /* number of response buffers expected */
+ atp->SREQ.timeOutVal = retryInterval;
+ atp->SREQ.retryCount = retryCount;
+ atp->SREQ.TRelTime = 0; /* minimum 30 second timeout for other end of connection */
+
+ err = PNSendRequest(atp,true); /* send this async so we aren't blocked */
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pPNSendRequest returned", err);
+ } else {
+ responsesPending++;
+ MessageSentToNetAddr(LOG_INFO,funcID, false, gNet, gNode, (short)destSocket & 0xFF);
+ }
+ return err;
+}
+#endif
+
+int SendRequest(char connID, char funcID)
+{
+#if USING_PSENDREQUEST_API
+ return SendRequest_with_PSendRequest(connID, funcID);
+#else
+ return SendRequest_with_ATPRequest(connID, funcID);
+#endif
+}
+
+int SendResponse(char connID, char funcID)
+{
+ OSErr err;
+ int dataLen = 0;
+ int nBufs = 0;
+ int NumOfBufs = 0;
+#if 0
+ long int thisBit;
+
+ /* Walk through the request bitmap and see how many response buffers you need. */
+ for (thisBit = 0; thisBit < 8; thisBit++) {
+ /* Each bit that is set corresponds to a buffer. */
+ if (BitTst(&gReqPBPtr->OTH1.u0.bitMap, thisBit) == true) {
+ /* Your routine to fill in the appropriate response data. */
+ dataLen += SetUpResponseData(funcID, gSRespBuf);
+ NumOfBufs = NumOfBufs + 1;
+ }
+ }
+#else
+ /* our responses always fit into a single packet, for now */
+ dataLen += SetUpResponseData(funcID, gSRespBuf);
+ NumOfBufs = 1;
+#endif
+ /* Put your response data into the BDS structure. */
+ nBufs = BuildBDS(gSRespBuf,(Ptr)(gSRespBdsPtr),dataLen);
+
+ gSendRespPBPtr->ATP.atpSocket = gReqPBPtr->ATP.atpSocket;
+ gSendRespPBPtr->ATP.atpFlags = atpEOMvalue; /* indicate end of message */
+
+ /* Send response to the machine that sent you the request. */
+ gSendRespPBPtr->ATP.addrBlock.aNet = gReqPBPtr->ATP.addrBlock.aNet;
+ gSendRespPBPtr->ATP.addrBlock.aNode = gReqPBPtr->ATP.addrBlock.aNode;
+ gSendRespPBPtr->ATP.addrBlock.aSocket = gReqPBPtr->ATP.addrBlock.aSocket;
+ gSendRespPBPtr->ATP.bdsPointer = (Ptr)(gSRespBdsPtr);
+ gSendRespPBPtr->OTH1.u0.numOfBuffs = NumOfBufs; /* send all of the responses back now */
+ gSendRespPBPtr->OTH2.bdsSize = nBufs; /* indicate how many responses you are sending */
+ gSendRespPBPtr->OTH2.transID = gReqPBPtr->OTH2.transID; /* use transID returned from the PGetRequest */
+
+ /* Set up ATP user data in response */
+ gSendRespPBPtr->ATP.userData = 0L;
+ BlockMove(&connID,(char*)&gSendRespPBPtr->ATP.userData + 0, 1);
+ BlockMove(&funcID,(char*)&gSendRespPBPtr->ATP.userData + 1, 1);
+ if (funcID == kPAPData) {
+ char eofMarker = (nBufs > 1) ? 0 : 1; /* %%% how to handle last buf? */
+ BlockMove(&eofMarker,(char*)&gSendRespPBPtr->ATP.userData + 2, 1);
+ }
+
+/* err = PSendResponse(gSendRespPBPtr,false);*/
+ /* %%% PROBLEM %%%
+ /* PSendResponse is overwriting the 4-byte userData portion of the ATP header. */
+ /* The ATPPBPtr does not give us a way to set the response user bytes! */
+ /* Is this a misunderstanding and they need to go into a different field? */
+ /* Instead, set up the old ATATPRec response struct and use ATPResponse. */
+ {
+ ATATPRecPtr abp = (ATATPRecPtr)gSendRespPtr;
+ if (abp->abResult > noErr) {
+ /* we can't use this ATATPRec, since a response is still being sent */
+ /* %%% should we allocate a new ATATPRec and free in ioCompletion? */
+ /* %%% or does this just mean the connection has closed abnormally? */
+ Message(LOG_WARN_X,"\pResponse still being sent, cannot send yet");
+ return tooManyReqs;
+ }
+ abp->abOpcode = tATPResponse;
+ abp->abResult = 0;
+ abp->atpSocket = gReqPBPtr->ATP.atpSocket;
+ abp->atpEOM = true;
+ abp->atpAddress.aNet = gReqPBPtr->ATP.addrBlock.aNet;
+ abp->atpAddress.aNode = gReqPBPtr->ATP.addrBlock.aNode;
+ abp->atpAddress.aSocket = gReqPBPtr->ATP.addrBlock.aSocket;
+ abp->atpRspUData = gSendRespPBPtr->ATP.userData;
+ abp->atpRspBuf = gSRespBuf;
+ abp->atpRspSize = dataLen;
+ abp->atpTransID = gReqPBPtr->OTH2.transID;
+ /* if this is a Data response from us, it needs to go to the right TID */
+ if (funcID == kPAPData && gQueryReqTID) {
+ abp->atpTransID = gQueryReqTID;
+ }
+ err = ATPResponse(gSendRespHdl,true); /* send async so we aren't blocked */
+ }
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pATPResponse error", err);
+ } else {
+ short net = gSendRespPBPtr->ATP.addrBlock.aNet;
+ char node = gSendRespPBPtr->ATP.addrBlock.aNode;
+ char socket = gSendRespPBPtr->ATP.addrBlock.aSocket;
+ MessageSentToNetAddr(LOG_INFO,funcID, true, net, node, socket);
+ }
+ return err;
+}
+
+int SetUpRequestData(char funcID, Ptr reqBuf)
+{
+#pragma unused (reqBuf)
+ int dataLen = 0;
+ if (funcID == kPAPSendData ||
+ funcID == kPAPTickle ||
+ funcID == kPAPCloseConn ||
+ funcID == kPAPCloseConnReply ||
+ funcID == kPAPSendStatus) {
+ return dataLen; /* these requests have no additional data to send */
+ }
+ /* %%% the only other request type that needs data is kPAPOpenConn,
+ * but we don't make those requests yet.
+ */
+ return dataLen;
+}
+
+int SetUpResponseData(char funcID, Ptr respBuf)
+{
+ int index, dataLen = 0;
+ unsigned char status = 0;
+ respBuf[0] = 0;
+ respBuf[1] = 0;
+ respBuf[2] = 0;
+ respBuf[3] = 0;
+
+ if (gColorRibbon) { status |= COLOR_RIBBON_INSTALLED; }
+ if (gSheetFeeder) { status |= SHEET_FEEDER_INSTALLED; }
+
+ if (funcID == kPAPOpenConnReply) {
+ respBuf[0] = (char)gMyRecvSocket & 0x00FF; /* our responding socket */
+ respBuf[1] = gFlowQuantum; /* flow quantum (passed to us in OpenConn) */
+ /* respBuf[2] = 0; /* result */
+ /* respBuf[3] = 0; /* result */
+ if (gConnID) {
+ /* error result: not opening this connection request since we're busy */
+ *((short*)&respBuf[2]) = aspServerBusy;
+ }
+ }
+ if (funcID == kPAPOpenConnReply || funcID == kPAPStatus) {
+ /* status string contents vary depending on printer type */
+ if (gPrinterType == kPrinterTypeLaserWriter) {
+
+ /* The format of LaserWriter status strings is documented in
+ * the first edition of the PostScript Language Reference Manual
+ * (the red book), Appendix D, p.283. It can include up to 3
+ * key:value pairs, is bracketed by %%[ and ]%%, with fields
+ * separated by semicolons: job (omitted if none), status, and
+ * source (omitted if idle). Inside AppleTalk 10-12 confirms
+ * that this is a Pascal format string with a length byte.
+ *
+ * Possible values for the status key:
+ * 'idle' (no job in progress)
+ * 'busy' (executing PostScript)
+ * 'waiting' (for I/O mid-job)
+ * 'printing' (paper in motion)
+ * 'PrinterError: <reason>' (no paper, jam, etc.)
+ * 'initializing' (printer is starting up)
+ * 'printing test page' (during startup)
+ *
+ * Possible values for the source key:
+ * 'serial 9' (the 9-pin RS-422 serial port)
+ * 'serial 25' (the 25-pin RS-232 serial port)
+ * 'AppleTalk' (the network)
+ *
+ * Example status strings:
+ * %%[ status: idle ]%%
+ * %%[ job: Fred's Memo; status: busy; source: AppleTalk ]%%
+ */
+ index = 4;
+ BlockMove("\p%%[ status: ",&respBuf[index],13);
+ index += 13;
+ if (!gFinishedQuery || !gDataLen) {
+ BlockMove("waiting",&respBuf[index],7);
+ index += 7;
+ } else {
+ BlockMove((gConnID) ? "busy":"idle",&respBuf[index],4);
+ index += 4;
+ }
+ BlockMove(" ]%%",&respBuf[index],4);
+ index += 4;
+ respBuf[4] = index-5;
+
+ } else { /* ImageWriter or LQ */
+ /* documented in AppleTalk Technical Note #9 "The PAP Status Buffer" */
+ respBuf[4] = 2; /* length of status "string" (just 2 bytes of flags) */
+ respBuf[5] = (char)status;
+ respBuf[6] = (gConnID) ? 0x80 : 0x00; /* high bit set if printer busy */
+ }
+ dataLen = 5 + respBuf[4];
+ }
+ if (funcID == kPAPData) {
+ if (gPrinterType == kPrinterTypeLaserWriter) {
+ /* build response to current PostScript query */
+ HLock(gQueryHdl);
+ dataLen = BuildResponseToPSQuery((char*)*gQueryHdl, gQueryLength,
+ (char*)respBuf, kMaxPacketSize);
+ HUnlock(gQueryHdl);
+ /* we have consumed the current query and can release it now */
+ DisposeHandle(gQueryHdl);
+ gQueryHdl = nil;
+ gQueryLength = 0;
+ MessageWithIntValue(LOG_INFO,"\pSending data with length",dataLen);
+ MessageWithDataPointer(LOG_DEBUG,"\p-- data:",respBuf,dataLen);
+
+ } else if (gPrinterType == kPrinterTypeLQ) {
+ /* possible options:
+ * '1' = 15-inch carriage (always the case for the LQ)
+ * 'C' = color ribbon installed
+ * 'F' = sheet feeder installed
+ * 'E' = envelope feeder installed
+ * F and E are mutually exclusive, and are followed
+ * by a numeral indicating the number of feeder bins.
+ * 'P' = tractor drive in pull tractor position
+ * examples:
+ * 'LQ1\r' 'LQ1P\r' 'LQ1CF1\r' 'LQ1CE3\r'
+ */
+ index = 0;
+ dataLen = 4; /* min length of self ID string bytes */
+ respBuf[index++] = 'L'; /* first two bytes are 'LQ' */
+ respBuf[index++] = 'Q';
+ respBuf[index++] = '1'; /* 15-inch carriage (only option) */
+ if (gColorRibbon) {
+ respBuf[index++] = 'C'; /* color ribbon installed */
+ dataLen += 1;
+ }
+ if (gSheetFeeder) {
+ respBuf[index++] = 'F'; /* sheet feeder installed */
+ respBuf[index++] = '1'; /* number of feeder bins */
+ dataLen += 2;
+ }
+ respBuf[index++] = '\r'; /* carriage return (^M, aka 0x0D) */
+
+ } else if (gPrinterType == kPrinterTypeImageWriter) {
+ index = 0;
+ dataLen = 4; /* min length of self ID string bytes */
+ respBuf[index++] = 'I'; /* first two bytes are 'IW' */
+ respBuf[index++] = 'W';
+ respBuf[index++] = '1'; /* 10-inch carriage, first digit */
+ respBuf[index++] = '0'; /* 10-inch carriage, second digit */
+ if (gColorRibbon) {
+ respBuf[index++] = 'C'; /* color ribbon installed */
+ dataLen += 1;
+ }
+ if (gSheetFeeder) {
+ respBuf[index++] = 'F'; /* sheet feeder installed */
+ dataLen += 1;
+ }
+ }
+ }
+ return dataLen;
+}
+
+void SendTickle(void)
+{
+ OSErr err;
+ ATPPBPtr atp = gTickleReqPBPtr;
+ char connID = gConnID;
+ char funcID = kPAPTickle;
+ short destSocket = (gConnSocket) ? gConnSocket : gSocket;
+
+ BlockMove(&connID,(char*)&atp->ATP.userData + 0, 1);
+ BlockMove(&funcID,(char*)&atp->ATP.userData + 1, 1);
+ atp->ATP.ioCompletion = nil;
+ atp->ATP.atpSocket = (char)gMySendSocket & 0x00FF;
+ atp->ATP.atpFlags = 0; /* must be sent in ALO mode, not XO mode (IA:10-9) */
+ atp->ATP.addrBlock.aNet = gNet;
+ atp->ATP.addrBlock.aNode = gNode;
+ atp->ATP.addrBlock.aSocket = (char)destSocket & 0x00FF;
+ atp->ATP.reqLength = 0; /* length of request buffer */
+ atp->ATP.reqPointer = nil; /* pointer to buffer holding request data */
+ atp->ATP.bdsPointer = (Ptr)nil; /* buffer to hold response BDS */
+ atp->OTH1.u0.numOfBuffs = 0; /* number of response buffers expected */
+ atp->SREQ.timeOutVal = 1; /* timeout in seconds before retrying */
+ atp->SREQ.retryCount = 0; /* how many times to retry the request */
+ atp->SREQ.TRelTime = 0; /* minimum 30 second timeout for other end of connection */
+
+ err = PNSendRequest(atp,true); /* send this async so we aren't blocked */
+ if (err != noErr) {
+ MessageWithIntValue(LOG_WARN_X,"\pCould not start tickle:", err);
+ } else {
+ MessageSentToNetAddr(LOG_INFO,funcID, false, gNet, gNode, (short)destSocket & 0xFF);
+ }
+}
+
+void StartTickling(void)
+{
+ gLastTickleSent = 1L;
+ gLastTickleRecd = TickCount();
+}
+
+void StopTickling(void)
+{
+ OSErr err = PKillSendReq(gTickleReqPBPtr,false);
+ if (err != noErr && err != cbNotFound) {
+ MessageWithIntValue(LOG_WARN_X,"\pCould not stop tickle:", err);
+ } else {
+ MessageWithIntValue(LOG_WARN,"\pStopped tickle for connection",(int)gConnID & 0xFF);
+ }
+ gLastTickleSent = 0L;
+}
+
+void CloseConnection(void)
+{
+ OSErr err;
+ StopTickling();
+ CancelRequest(); /* cancel outstanding request (if any) so we don't wait */
+ err = SendRequest(gConnID, kPAPCloseConn);
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pCloseConn request error", err);
+ } else {
+ MessageWithIntValue(LOG_ERR,"\pClosing connection",(int)gConnID & 0xFF);
+ }
+ gConnID = 0; /* connection is now closed */
+ gDataLen = 0;
+ gLastTickleRecd = 0L;
+ respondedToFirstPostConnectionStatusReq = false;
+}
+
+void CheckATP(void)
+{
+ OSErr err;
+ if (!listening || !gReqPBPtr || !gSendReqPBPtr || !gSendReqPtr) {
+ return; /* no server started, or no buffers allocated */
+ }
+ if (gConnID != 0) {
+ long curTicks = TickCount();
+ if (gLastTickleRecd && (gLastTickleRecd + kIdleTimeoutTicks < curTicks)) {
+ /* time to close this connection due to inactivity */
+ MessageWithIntValue(LOG_ERR,"\pReceived data bytes:", gDataLen);
+ CloseSpoolFile(gDataLen);
+ CloseConnection();
+ } else if (gLastTickleSent && (gLastTickleSent + kTickleTicks < curTicks)) {
+ /* time to send a tickle */
+ SendTickle();
+ gLastTickleSent = curTicks;
+ }
+ }
+#if USING_PSENDREQUEST_API
+ if (gSendReqPBPtr->ATP.ioResult <= noErr)
+#else
+ if (gSendReqPtr->abResult <= noErr)
+#endif
+ {
+ /* either a sent request completed, or we haven't made any request yet */
+ if (responsesPending > 0) {
+ /* got an incoming response */
+ responsesPending--;
+ #if USING_PSENDREQUEST_API
+ err = gSendReqPBPtr->ATP.ioResult;
+ #else
+ err = gSendReqPtr->abResult;
+ #endif
+ if (err != noErr) {
+ /* don't report failure if connection is already closed */
+ if (!(gConnID == 0 && err == reqFailed)) {
+ MessageWithIntValue(LOG_ERR_X,"\pSendRequest returned", err);
+ }
+ if (gConnID) {
+ CloseConnection();
+ }
+ } else {
+ /* handle this response to our request */
+ HandleResponse();
+ }
+ }
+ }
+ if (gReqPBPtr->ATP.ioResult <= noErr) {
+ /* either an incoming request completed, or we haven't asked for one yet */
+ if (requestsPending > 0) {
+ /* got an incoming request */
+ requestsPending--;
+ err = gReqPBPtr->ATP.ioResult;
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pGetRequest returned", err);
+ } else {
+ /* handle the request and send response */
+ HandleRequest();
+ }
+ }
+ /* start a new PGetRequest */
+ gReqPBPtr->ATP.ioCompletion = nil; /* we will poll for completion */
+ gReqPBPtr->ATP.atpSocket = gMyRecvSocket & 0x00FF; /* use our socket */
+ gReqPBPtr->ATP.reqLength = kMaxPacketSize; /* on input, max length of request buffer */
+ gReqPBPtr->ATP.reqPointer = gRReqBuf; /* pointer to buffer for incoming request data */
+ err = PGetRequest(gReqPBPtr,true); /* issue asynchronous PGetRequest */
+ if (err == noErr) {
+ requestsPending++;
+ } else {
+ MessageWithIntValue(LOG_ERR_X,"\pPGetRequest error", err);
+ }
+ }
+}
--- PAP.h Sun Feb 1 00:41:01 2026
+++ PAP.h Sun Feb 1 00:41:01 2026
@@ -0,0 +1,110 @@
+/*
+ * PAP.h
+ * C Interface to PAP (Printer Access Protocol) routines
+ *
+ * Written by: Ken McLeod, 2026-01-07
+ * Last update: 2026-01-28
+ *
+ * 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.
+ */
+
+#ifndef __PRINTER_ACCESS_PROTOCOL__
+#define __PRINTER_ACCESS_PROTOCOL__
+
+#include <Types.h>
+#include <AppleTalk.h>
+
+typedef struct {
+ long int unused;
+ char statusStr[256];
+} PAPStatusRec, *PAPStatusPtr;
+
+/* PAP function IDs */
+#define kPAPOpenConn 1
+#define kPAPOpenConnReply 2
+#define kPAPSendData 3
+#define kPAPData 4
+#define kPAPTickle 5
+#define kPAPCloseConn 6
+#define kPAPCloseConnReply 7
+#define kPAPSendStatus 8
+#define kPAPStatus 9
+
+/* other constants */
+#define kPAPMinQuantum 1
+#define kPAPMaxQuantum 8
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* call this in main event loop to process incoming requests */
+void CheckATP(void);
+
+/* call this prior to SLInit to specify emulated printer options */
+void SetPrinterOptions(Boolean hasColorRibbon, Boolean hasSheetFeeder);
+
+/* accessor for options */
+void GetPrinterOptions(Boolean *hasColorRibbon, Boolean *hasSheetFeeder);
+
+/* number of active connections (i.e. not idle) */
+int GetActiveClientConnections(void);
+
+/* number of print jobs we have successfully received and spooled */
+long JobsSpooled(void);
+
+
+/* server routines */
+
+pascal int SLInit (EntityPtr printerName, /* AppleTalk entity name to serve */
+ int flowQuantum, /* # of 512-byte buffers for read */
+ PAPStatusPtr statusBuf, /* initial status is returned here */
+ int *refNum); /* server refNum for this instance */
+
+pascal int PAPRegName (int refNum, /* server refNum for this name */
+ EntityPtr printerName); /* AppleTalk entity name to register */
+
+pascal int PAPRemName (int refNum, /* server refNum for this name */
+ EntityPtr printerName); /* AppleTalk entity name to remove */
+
+pascal int HeresStatus (int refNum, /* server refNum for updated status */
+ PAPStatusPtr statusBuf);/* new status string for this server */
+
+pascal int SLClose (int refNum); /* server refNum from SLInit */
+
+
+/* client routines */
+
+pascal int PAPOpen (int *refNum, /* returned: connection ID ref */
+ EntityPtr printerName, /* AppleTalk entity name */
+ int flowQuantum, /* # of 512-byte buffers for read */
+ PAPStatusPtr statusBuf, /* returned: status string */
+ int *compState); /* returned: status, 0=success */
+
+pascal int PAPStatus (EntityPtr printerName, /* AppleTalk entity name */
+ PAPStatusPtr statusBuf, /* returned: status string */
+ long int *nodeAddr); /* returned: node address */
+
+pascal int PAPRead (int refNum, /* connection ID from PAPOpen */
+ char *rxBuf, /* buffer in which to read the data */
+ int *rxBufLen, /* size of the data read on output */
+ int *eofByte, /* set to positive value when EOF */
+ int *compState); /* returned: status, 0=success */
+
+pascal int PAPWrite (int refNum, /* connection ID from PAPOpen */
+ char *txBuf, /* buffer with data to be written */
+ int txBufLen, /* size of the data to be written */
+ int eofByte, /* set to positive on last buffer */
+ int *compState); /* returned: status, 0=success */
+
+pascal int PAPClose (int refNum); /* connection ID from PAPOpen */
+
+pascal int PAPUnload (void); /* close connections, free storage */
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __PRINTER_ACCESS_PROTOCOL__ */
--- PrintQueue.c Fri Feb 13 10:18:33 2026
+++ PrintQueue.c Fri Feb 13 10:18:33 2026
@@ -0,0 +1,287 @@
+/*
+ * 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 <Types.h>
+#include <Devices.h>
+#include <Errors.h>
+#include <Events.h>
+#include <Files.h>
+#include <Folders.h>
+#include <SysEqu.h>
+#include <GestaltEqu.h>
+#include <Memory.h>
+#include <OSUtils.h>
+#include <Packages.h>
+#include <Resources.h>
+#include <String.h>
+#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 gRetryAttempts = 0;
+short gPrintFileRefNum = 0;
+short gSerialRefNum = 0;
+long gBytesWritten = 0L;
+long gJobsPrinted = 0L;
+
+
+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;
+ }
+ gRetryAttempts = 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)) {
+ err = FSWrite(gSerialRefNum,&count,buf);
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pUnable to send print data:",err);
+ /* go away and retry later if we couldn't write to the port */
+ if (count > 0 && count < readCount) {
+ /* rewind read position, since we didn't write all bytes we read */
+ long delta = readCount - count;
+ err = SetFPos(gPrintFileRefNum, fsFromMark, -delta);
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pUnable to rewind spool file:",err);
+ }
+ }
+ if (++gRetryAttempts > 3) {
+ 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 */
+ }
+ }
+}
--- PrintQueue.h Sun Feb 1 11:25:28 2026
+++ PrintQueue.h Sun Feb 1 11:25:28 2026
@@ -0,0 +1,35 @@
+/*
+ * PrintQueue.h
+ * C interface to routines for processing and printing spool files.
+ *
+ * Written by: Ken McLeod, 2026-01-16
+ * Last update: 2026-01-28
+ *
+ * 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.
+ */
+
+#ifndef __PRINT_QUEUE__
+#define __PRINT_QUEUE__
+
+#include <Types.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* call this in main event loop to check and process spooled print jobs. */
+/* startHour and endHour values define a time window when we can actually print. */
+void CheckPrintQueue(Boolean printingEnabled, short startHour, short endHour);
+
+void StartPrintQueue(char *prefixToAccept, char *serialPortName);
+void StopPrintQueue(void);
+
+long JobsPrinted(void);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __PRINT_QUEUE__ */
--- QueryParser.c Tue Feb 17 10:01:19 2026
+++ QueryParser.c Tue Feb 17 10:01:19 2026
@@ -0,0 +1,146 @@
+/*
+ * QueryParser.c
+ *
+ * Routines for parsing and responding to PostScript queries.
+ *
+ * Written by: Ken McLeod, 2026-02-10
+ * Last update: 2026-02-16
+ *
+ * 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 <Types.h>
+#include <Memory.h>
+#include <OSUtils.h>
+#include <Packages.h>
+#include <String.h>
+#include "QueryParser.h"
+
+
+int BuildResponseToPSQuery(char *data, int dataLen, char *resp, int respLen)
+{
+ /* quick and dirty routine to parse features being requested
+ * by the driver so we respond in the correct order. Input data
+ * points to the query (i.e. the response to our last SendData)
+ * with length dataLen. resp is a pointer to a buffer preallocated
+ * for our response, which can be no larger than respLen bytes.
+ */
+ int length = dataLen;
+ int index, rspIdx = 0;
+
+ if (!data || !dataLen || !resp || !respLen) {
+ return 0;
+ }
+ for (index=0; index<length; index++) {
+ if ((length>(index+3)) &&
+ (!memcmp("%%?",&data[index],3))) {
+ index += 3;
+ if ((length>(index+19+21)) && /* query prefix plus longest strlen */
+ (!memcmp("BeginFeatureQuery: ",&data[index],19))) {
+ index += 19;
+ /* feature query */
+ if (!memcmp("*LanguageLevel",&data[index],14)) {
+ BlockMove("\"3\"\n",&resp[rspIdx],4);
+ index += 14;
+ rspIdx += 4;
+ } else if (!memcmp("*PSVersion",&data[index],10)) {
+ BlockMove("\"(3010.10 6) 5\"\n",&resp[rspIdx],16);
+ index += 10;
+ rspIdx += 16;
+ } else if (!memcmp("*?Resolution",&data[index],12)) {
+ BlockMove("600dpi\n",&resp[rspIdx],7);
+ index += 12;
+ rspIdx += 7;
+ } else if (!memcmp("*TTRasterizer",&data[index],13)) {
+ BlockMove("Type42\n",&resp[rspIdx],7);
+ index += 13;
+ rspIdx += 7;
+ } else if (!memcmp("*DefaultColorSpace",&data[index],18)) {
+ BlockMove("Gray\n",&resp[rspIdx],5);
+ index += 18;
+ rspIdx += 5;
+ } else if (!memcmp("*LandscapeOrientation",&data[index],21)) {
+ BlockMove("Minus90\n",&resp[rspIdx],8);
+ index += 21;
+ rspIdx += 8;
+ } else if (!memcmp("*Product",&data[index],8)) {
+ BlockMove("\"(Simple Spooler)\"\n",&resp[rspIdx],19);
+ index += 8;
+ rspIdx += 19;
+ }
+
+ } else if ((length>(index+16)) &&
+ (!memcmp("BeginFontQuery: ",&data[index],16))) {
+ char *tokenEnd;
+ int tokenLen;
+ index += 16;
+ /* font query */
+ tokenEnd = &data[index];
+ tokenLen = 0;
+ while (index<length) {
+ tokenEnd++;
+ tokenLen++;
+ if (*tokenEnd == 0x20 || *tokenEnd == 0x0D || *tokenEnd == 0x0A) {
+ resp[rspIdx++] = '/';
+ BlockMove(&data[index],&resp[rspIdx],tokenLen);
+ rspIdx += tokenLen;
+ /* we want all fonts used to be supplied with the
+ * document, so we reply 'No' for each one specified.
+ */
+ BlockMove(":No\n\r",&resp[rspIdx],5);
+ rspIdx += 5;
+ if (*tokenEnd == 0x20) {
+ /* move forward to start of next token */
+ index += tokenLen+1;
+ tokenEnd = &data[index];
+ tokenLen = 0;
+ } else {
+ /* at end of font query line */
+ break;
+ }
+ }
+ }
+ BlockMove("*\n\r",&resp[rspIdx],3);
+ rspIdx += 3;
+
+ } else if ((length>(index+12)) &&
+ (!memcmp("BeginQuery: ",&data[index],12))) {
+ index += 12;
+ /* query */
+ if (!memcmp("ADOSpooler",&data[index],10)) {
+ /* this can return 0 or spooler */
+ BlockMove("0\n",&resp[rspIdx],2);
+ index += 10;
+ rspIdx += 2;
+ } else if (!memcmp("ADOIsBinaryOK?",&data[index],14)) {
+ BlockMove("False\n",&resp[rspIdx],6);
+ index += 14;
+ rspIdx += 6;
+ } else if (!memcmp("RBIUAMListQuery",&data[index],15)) {
+ /* LaserWriter 8.6.1 or later can use ASIP security protocol */
+ BlockMove("*\n",&resp[rspIdx],2);
+ index += 15;
+ rspIdx += 2;
+ }
+
+ } else if ((length>(index+19)) &&
+ (!memcmp("BeginProcSetQuery: ",&data[index],19))) {
+ index += 19;
+ /* proc set query */
+ BlockMove("unknown\n\r",&resp[rspIdx],9);
+ rspIdx += 9;
+
+ } else if ((length>(index+5)) &&
+ (!memcmp("Begin",&data[index],5))) {
+ index += 5;
+ /* everything else is unknown */
+ BlockMove("unknown\n\r",&resp[rspIdx],9);
+ rspIdx += 9;
+ }
+
+ } /* found a query prefix '%%?' */
+ } /* end parse loop */
+
+ return rspIdx;
+}
--- QueryParser.h Wed Feb 11 14:02:17 2026
+++ QueryParser.h Wed Feb 11 14:02:17 2026
@@ -0,0 +1,28 @@
+/*
+ * QueryParser.h
+ * C interface to routines for parsing PostScript queries.
+ *
+ * Written by: Ken McLeod, 2026-02-10
+ * Last update: 2026-02-10
+ *
+ * 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.
+ */
+
+#ifndef __QUERY_PARSER__
+#define __QUERY_PARSER__
+
+#include <Types.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+int BuildResponseToPSQuery(char *data, int dataLen, char *resp, int respLen);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __QUERY_PARSER__ */
--- README Tue Feb 17 09:53:04 2026
+++ README Tue Feb 17 09:53:04 2026
@@ -0,0 +1,202 @@
+## Simple Spooler 0.1 ##
+Copyright (c) 2026, Ken McLeod.
+
+This application acts as a virtual printer on an AppleTalk network, receiving
+print jobs using PAP (Printer Access Protocol) and spooling them to files. It
+can either print immediately or at a later scheduled time, sending the print
+job to a locally-connected ImageWriter printer which would otherwise not be
+visible on the network.
+
+Simple Spooler is freeware. You may use and distribute the software at no
+charge. See the LICENSE file for conditions on your use of the source code.
+
+
+**System requirements**
+
+System 6.0 or later (up to Mac OS 9.2.2) is currently required to run Simple
+Spooler. This software has been tested on System 6.0.5, 7.0.1, and 7.5.3, as
+well as Mac OS 8.6 and 9.2.2.
+
+AppleTalk must be set to Active in the Chooser. You should have enough free
+disk space on your startup disk to accomodate the print jobs you expect to
+receive. ImageWriter spool files occupy less space than LaserWriter spool
+files, but all can add up if they are not being processed immediately.
+
+
+**Starting the spooler**
+
+After opening the application, select "Start Spooler…" from the Control menu.
+A dialog will present the following options:
+
+* Spooler name
+ Type a name for the print spooler, which you can think of as a virtual
+ printer. This name will appear on the network and can be selected by other
+ users as an AppleTalk printer in the Chooser.
+
+* Virtual printer type
+ Select one of the icons representing LaserWriter, ImageWriter LQ, or
+ ImageWriter to create a virtual printer of that type. For an ImageWriter or
+ LQ, you can additionally select whether the printer is seen as having a
+ color ribbon or a sheet feeder.
+
+* Print to this port
+ Select the icon for the local serial port where your ImageWriter printer
+ is connected. If the Enabled checkbox is selected, spooled print jobs will
+ automatically be printed to this port once they have been received, as long
+ as the current time is within the scheduled printing hours. The default
+ setting is 12 AM - 12 AM (or 0 - 24), an unrestricted span of 24 hours
+ starting at midnight.
+
+You can restrict printer use by using the up/down arrow control to change the
+start and end times; for example, you may only want printing to occur during
+business hours and not at night. If the spooler receives a print job outside
+the specified hours, it will be printed later once the start hour is reached.
+Uncheck the Enabled box if you don't have a local printer connected, or if you
+just want to hold all jobs for printing at a later date.
+
+Click Start to begin running the spooler. This registers the printer name you
+provided on the AppleTalk network, listens for incoming print jobs, and
+prepares to print any previously spooled jobs in its spool folder.
+
+
+**Stopping the spooler**
+
+The spooler can be stopped by selecting "Stop Spooler" from the Control menu,
+or by quitting the application.
+
+
+**Printing to the spooler**
+
+On a Mac connected to your AppleTalk network, open Chooser and select the
+printer driver for the printer type being emulated. For example, if Simple
+Spooler is emulating an AppleTalk ImageWriter, then select the "AppleTalk
+ImageWriter" icon in Chooser. You should see the spooler appear as a printer
+in the list. After selecting it and closing the Chooser window, you should be
+able to print to the spooler from any application.
+
+
+**About the log**
+
+Simple Spooler displays a single window with a log of its activity. This log is
+entirely in memory, and does not persist when the application quits. To avoid
+running out of memory, the earliest entries are deleted periodically as new
+ones are added to the end.
+
+By default, messages are logged when the spooler starts up or stops, when
+incoming connections are handled, when spooled jobs are printed, or if an error
+occurs. However, more details can be enabled to aid in debugging. There are four
+log levels, each printing its own messages as well as those of the levels below
+it. Level 0 is the default. At level 1, internal activity and warnings are
+logged. At level 2, all protocol messages are displayed, and at the maximum
+level 3, additional debug data is logged.
+
+Because changing the log level to 2 or higher will greatly diminish server
+performance, and the extra information is intended for development purposes
+only, there is no menu item for this hidden feature. Press the command-shift-L
+key combination to increase the log level by 1. After reaching 3, the log
+level will reset to 0 again. The spooler should normally be run only at the
+default log level 0.
+
+Pressing the command-shift-S key combination will output the server's current
+status to the log.
+
+
+**About the print queue**
+
+The first time Simple Spooler is run, it will create a spool folder (named
+Simple Spooler) in the Preferences folder within the System Folder, or in the
+System Folder itself if prior to System 7.0. Print jobs are saved as temporary
+files in the spool folder. Their file names start with a prefix that depends on
+the printer type selected: IW for ImageWriter, LQ for ImageWriter LQ, and LW for
+LaserWriter. When the spooler is running, it checks periodically for files in
+this folder which have the file type 'pjob', whose names start with the current
+prefix and a hyphen, and do not end in a .ps extension. For example, a file
+named "IW-12345" will be printed, but not "IWEm" or "IW12345" or "IW-12345.ps".
+
+Spool files are processed in the order they were added to the folder. If the
+Enabled checkbox was selected and printing is currently enabled, the file is
+sent to the selected serial port. Once all bytes in the spool file have been
+sent to that port without an error, that job is considered complete and the file
+is deleted.
+
+
+**About LaserWriter spooling**
+
+This application was primarily designed to emulate an ImageWriter printer, but
+it can also create a virtual LaserWriter which spools its print jobs to
+PostScript files. This feature is somewhat redundant, as you could just select
+"print to file" in the LaserWriter print dialog to obtain the same result. An
+ImageWriter cannot render PostScript code, but older LaserWriter models with a
+serial port connection can theoretically be used to print spooled files; this
+has not been tested.
+
+Note: you can use software such as Ghostscript, Adobe Illustrator, or even
+Preview.app (on older versions of Mac OS X) to view PostScript files, or convert
+them to another format for viewing on a modern computer.
+
+
+**Generating PostScript output from ImageWriter print jobs**
+
+If you would like to have print jobs for an ImageWriter automatically generate
+PostScript output files, simply copy the "IWEm" file from this distribution into
+the spool folder. "IWEm" (ImageWriter Emulator) is PostScript code from Apple
+that can interpret ImageWriter print sequences. When a print job is received
+successfully, and the file "IWEm" is found in the spool folder, a second file
+with a .ps extension will also be created and appear in the folder. This .ps
+file can be viewed in applications which support PostScript.
+
+Note: IWEm is not compatible with color printing, or with the ImageWriter LQ's
+Best and Faster modes. A better option for converting ImageWriter spool files
+into a format that can be viewed on a modern computer is the 'imagewriter'
+command line tool, available from <https://github.com/greg-kennedy/ImageWriter>.
+
+
+**Using Simple Spooler in a Mac emulator**
+
+If you don't want to keep a real Mac and ImageWriter turned on for hours, you
+can run Simple Spooler in a virtual Mac with emulation software such as QEMU,
+Mini vMac, or Snow that supports AppleTalk networking. Be aware that printing to
+a serial port where nothing is actually connected sends those bytes to nowhere.
+You may want to uncheck the Enabled box for printing to a serial port when
+running Simple Spooler in an emulator, and instead copy the spool files over to
+your real Mac's spool folder when you are ready to print them.
+
+
+**Known issues**
+
+Printing to the spooler from the same Mac which is running the spooler does not
+work well yet. (There's a bug with self-send mode. Don't try it unless you like
+rebooting.)
+
+On System 6, there are some errors being logged with the tickle mechanism which
+is part of PAP. This should be fixable and does not seem to affect printing.
+
+There is currently no handshaking with the real printer before attempting to
+print, so if no printer is connected to the selected port, data is sent into the
+void without any error and the spool file is deleted.
+
+In this release, LaserWriter spooling is only known to work with version 8.3.4
+of the LaserWriter 8 driver. This is the version which shipped with system
+software 7.5.3. Version 8.6.1 and later send an additional query prior to normal
+setup, as does 7.1.1 and likely others. In general, we can't yet handle more
+than one query transaction (meaning the driver is asking us to answer its
+questions, rather than giving us data) in a given print job. A fix is being
+investigated.
+
+
+**Bug reports, feedback, etc.**
+
+Please let me know about your experience with Simple Spooler, especially if you
+run into any problems. This prerelease software is still under development and
+not yet considered stable. Bug reports and suggestions for improvement are
+always welcome. (When reporting a problem, please mention the system version,
+spooler configuration, and what software you are using to print, so I can try
+to reproduce it.) Thanks!
+
+<https://mastodon.social/@_the_cloud>
+<thecloudexpanse @gmail.com>
+
+
+**Modification history**
+
+0.1 2026-02-16: first release for testing
--- Spooler.c Mon Feb 16 21:55:58 2026
+++ Spooler.c Mon Feb 16 21:55:58 2026
@@ -0,0 +1,1027 @@
+/*
+ * Simple Spooler
+ * Copyright (C) 2026, Ken McLeod.
+ *
+ * This application registers itself as a virtual printer on the AppleTalk
+ * network and receives print jobs using PAP (Printer Access Protocol).
+ * It can optionally send the print job to a locally-connected ImageWriter
+ * printer which otherwise would not be visible on the network. It can also
+ * generate a PostScript file from the print job automatically, if the IWEm
+ * file is present in the spool directory.
+ *
+ * Written by: Ken McLeod, 2026-01-07
+ * Last update: 2026-01-31
+ *
+ * Some code was adapted from the Neighborhood Watch sample application
+ * by Ricardo Batista, found on Apple Developer CD Volume IX, and from the
+ * MoreFiles utility routines by Jim Luther, found on the March 1994 Dev CD.
+ *
+ * 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 <StdIO.h>
+#include <String.h>
+#include <Memory.h>
+#include <Types.h>
+#include <CursorCtl.h>
+#include <AppleTalk.h>
+#include <Packages.h>
+#include <Events.h>
+#include <SysEqu.h>
+#include <GestaltEqu.h>
+#include <Controls.h>
+#include <Desk.h>
+#include <Devices.h>
+#include <Errors.h>
+#include <Fonts.h>
+#include <Menus.h>
+#include <ToolUtils.h>
+#include <Events.h>
+#include <Quickdraw.h>
+#include <Resources.h>
+#include <OSEvents.h>
+#include <Scrap.h>
+#include "Logging.h"
+#include "SpoolFiles.h"
+#include "PrintQueue.h"
+#include "PAP.h"
+
+/* resource IDs */
+
+#define appleMenu 128
+#define fileMenu 129
+#define editMenu 130
+#define controlMenu 131
+
+#define quitItem 1
+
+#define undoItem 1
+#define cutItem 3
+#define copyItem 4
+#define pasteItem 5
+#define clearItem 6
+#define selectAllItem 8
+
+#define startSpoolerItem 1
+#define stopSpoolerItem 2
+
+#define okItem 1
+#define cancelItem 2
+#define spoolerNameItem 3
+#define iconLWItem 6
+#define iconLQItem 7
+#define iconIWItem 8
+#define modemPortItem 10
+#define printerPortItem 11
+#define colorRibbonCBoxItem 13
+#define sheetFeederCBoxItem 14
+#define printingEnabledItem 15
+#define scheduleUserItem 16
+#define printerTypeUserItem 17
+#define localPortUserItem 18
+
+#define logWindowID 128
+#define spoolerDialogID 128
+#define upDownArrowsPictID 128
+#define typeStringsID 128
+#define nameStringsID 129
+#define portStringsID 130
+
+#define kIconFrameInset -5
+#define kIconFrameSize 2
+#define kButtonFrameInset -4
+#define kButtonFrameSize 3
+#define kButtonFrameRadius 18
+#define kMinWindowHeight 100
+#define kMinWindowWidth 416
+#define kPrefsVersion 3
+
+
+/* function prototypes */
+
+void DoAbout(void);
+void DoStatus(void);
+OSErr StartSpooler(void);
+OSErr StopSpooler(void);
+void SetMenus(void);
+Boolean Setup(void);
+Boolean RunEventLoop(void);
+Boolean DoCommand(long mResult);
+Boolean HandleMouseDowns(void);
+void LoadPreferences();
+void SavePreferences();
+
+char ourName[256];
+char ourType[34];
+char ourPort[34];
+char ourPrefix[4];
+int serverRef = 0;
+Boolean inFront;
+EventRecord myEvent;
+MenuHandle myMenu[5];
+WindowPtr textWindow = 0L;
+TEHandle textTE = 0L;
+Rect windowRect = { 40, 10, 320, 490 }; /* default, global coordinates */
+ControlHandle textScrollBar = 0L;
+short defaultPrinterItem = iconIWItem;
+short defaultPortItem = modemPortItem;
+Boolean defaultColorRibbon = false;
+Boolean defaultSheetFeeder = false;
+Boolean defaultPrintEnabled = true;
+short curSelectedIndex = 0; /* selected segment in date scheduler */
+short schedStartHour = 0; /* default start to 12 AM */
+short schedEndHour = 24; /* default end to next 12 AM */
+char sAMStr[4]; /* pascal string with first 2 chars of AM string from itl0 */
+char sPMStr[4]; /* pascal string with first 2 chars of PM string from itl0 */
+char s12HrCycle; /* values: 0=24hr, 1=12hr */
+
+char *sepStr = "\p----------------------------------------------------";
+
+
+void main()
+{
+ if (!Setup()) {
+ return;
+ }
+ Message(LOG_ERR,"\pSelect Control > Start Spooler… to start the server");
+
+ while (RunEventLoop()) ;
+
+ StopSpooler();
+ SavePreferences();
+ UnloadLogWindow();
+}
+
+Boolean Setup(void)
+{
+ short count;
+ for (count = 0; count < 6; count++) {
+ MoreMasters();
+ }
+ InitGraf(&qd.thePort);
+ InitFonts();
+ InitWindows();
+ TEInit();
+ InitMenus();
+ InitDialogs(0L);
+ DrawMenuBar();
+ FlushEvents(everyEvent,0L);
+ InitCursor();
+ MaxApplZone();
+ SetCursor(*GetCursor(watchCursor));
+ InitAllPacks();
+ SetMenus();
+ if (!InitLogWindow(logWindowID, &textWindow, &textTE, &textScrollBar)) {
+ return(false);
+ }
+ if (InitSpoolFolder() != noErr) {
+ Message(LOG_ERR_X,"\pCould not find or create spool folder!");
+ }
+ LoadPreferences();
+ ShowLogWindow(&windowRect);
+ inFront = true;
+ return(true);
+}
+
+void SetMenus()
+{
+ /* load menus from resource file */
+ int i;
+ for (i=appleMenu-appleMenu; i<=controlMenu-appleMenu; i++) {
+ myMenu[i] = GetMenu(appleMenu+i);
+ }
+ AddResMenu(myMenu[appleMenu-appleMenu],'DRVR');
+ /* install menus in menu bar */
+ for (i=appleMenu-appleMenu; i<=controlMenu-appleMenu; i++) {
+ InsertMenu(myMenu[i], 0);
+ }
+ EnableItem(myMenu[controlMenu-appleMenu],startSpoolerItem);
+ EnableItem(myMenu[editMenu-appleMenu],copyItem);
+ EnableItem(myMenu[editMenu-appleMenu],selectAllItem);
+ DrawMenuBar();
+}
+
+Boolean RunEventLoop()
+{
+ register char theChar;
+ register Boolean event;
+ WindowPtr w;
+ GrafPtr savePort;
+ Rect box;
+ Point mouse;
+
+ CheckATP(); /* check incoming requests and responses */
+ CheckPrintQueue(defaultPrintEnabled, schedStartHour, schedEndHour);
+
+ event = GetNextEvent(everyEvent,&myEvent);
+ w = FrontWindow();
+ if ((w == textWindow) && inFront && textTE) {
+ GetPort(&savePort);
+ SetPort(textWindow);
+ /* %%% don't need to blink insertion point; text isn't editable */
+ /*TEIdle(textTE);*/
+ box = w->portRect;
+ GetMouse(&mouse);
+ box.right -= 16;
+ box.bottom -= 16;
+ if (PtInRect(mouse,&box)) {
+ SetCursor(*(GetCursor(iBeamCursor)));
+ } else {
+ InitCursor();
+ }
+ SetPort(savePort);
+ }
+ if (event) {
+ switch (myEvent.what) {
+ case app4Evt:
+ inFront = (myEvent.modifiers & 128) ? true : false;
+ break;
+ case activateEvt:
+ w = (WindowPtr) myEvent.message;
+ if (w == textWindow) {
+ ActivateLogWindow(myEvent.modifiers & activeFlag);
+ }
+ break;
+ case keyDown:
+ case autoKey:
+ theChar = myEvent.message & charCodeMask;
+ if (myEvent.modifiers & cmdKey) {
+ if (myEvent.modifiers & shiftKey) {
+ if (theChar == 'l') {
+ SetLogLevel(LogLevel()+1);
+ if (LogLevel() > LOG_DEBUG) {
+ SetLogLevel(LOG_ERR);
+ }
+ MessageWithIntValue(LOG_ERR,"\pSetting log level:", LogLevel());
+ } else if (theChar == 's') {
+ DoStatus();
+ }
+ }
+ if (!DoCommand(MenuKey(theChar))) {
+ return(false);
+ }
+ } else {
+ w = FrontWindow();
+ /* %%% don't accept keyboard input into log text */
+ #if 0
+ if (w == textWindow) {
+ short lines;
+ HLock((Handle)textTE);
+ lines = (**textTE).nLines;
+ TEKey(theChar,textTE);
+ if (lines != (**textTE).nLines)
+ AdjustTextScrollBar();
+ HUnlock((Handle)textTE);
+ }
+ #endif
+ }
+ break;
+ case updateEvt:
+ w = (WindowPtr) myEvent.message;
+ if (w == textWindow)
+ UpdateLogWindow();
+ break;
+ case mouseDown:
+ if (!HandleMouseDowns())
+ return(false);
+ break;
+ default:
+ break;
+ }
+ }
+ return(true);
+}
+
+Boolean DoCommand(long mResult)
+{
+ OSErr err;
+ register short theItem;
+ char st[256];
+
+ theItem = LoWord(mResult);
+ switch (HiWord(mResult)) {
+ case appleMenu:
+ GetItem(myMenu[0],theItem,st);
+ if (theItem > 2)
+ OpenDeskAcc(st);
+ else
+ DoAbout();
+ break;
+ case fileMenu:
+ switch (theItem) {
+ case quitItem:
+ return(false);
+ break;
+ default:
+ break;
+ }
+ break;
+ case editMenu:
+ if (!SystemEdit(theItem-1)) {
+ switch (theItem) {
+ case undoItem:
+ break;
+ case cutItem:
+ if (textTE) {
+ TECut(textTE);
+ AdjustTextScrollBar();
+ }
+ ZeroScrap();
+ TEToScrap();
+ break;
+ case copyItem:
+ if (textTE) {
+ TECopy(textTE);
+ }
+ ZeroScrap();
+ TEToScrap();
+ break;
+ case pasteItem:
+ TEFromScrap();
+ if (textTE) {
+ TEPaste(textTE);
+ AdjustTextScrollBar();
+ }
+ break;
+ case clearItem:
+ if (textTE) {
+ TEDelete(textTE);
+ AdjustTextScrollBar();
+ }
+ break;
+ case selectAllItem:
+ if (textTE) {
+ int index;
+ HLock((Handle)textTE);
+ index = (**textTE).teLength;
+ HUnlock((Handle)textTE);
+ TESetSelect(0,index,textTE);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ break;
+ case controlMenu:
+ switch (theItem) {
+ case startSpoolerItem:
+ err = StartSpooler();
+ if (err == noErr) {
+ StartPrintQueue(ourPrefix,ourPort);
+ Message(LOG_ERR,"\pSpooler started");
+ EnableItem(myMenu[controlMenu-appleMenu],stopSpoolerItem);
+ DisableItem(myMenu[controlMenu-appleMenu],startSpoolerItem);
+ } else {
+ EnableItem(myMenu[controlMenu-appleMenu],startSpoolerItem);
+ DisableItem(myMenu[controlMenu-appleMenu],stopSpoolerItem);
+ }
+ break;
+ case stopSpoolerItem:
+ err = StopSpooler();
+ if (err == noErr) {
+ StopPrintQueue();
+ Message(LOG_ERR,"\pSpooler stopped");
+ } else {
+ MessageWithIntValue(LOG_ERR,"\pERROR: Could not stop spooler:",err);
+ }
+ EnableItem(myMenu[controlMenu-appleMenu],startSpoolerItem);
+ DisableItem(myMenu[controlMenu-appleMenu],stopSpoolerItem);
+ break;
+ default:
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+ HiliteMenu(0);
+ return(true);
+}
+
+Boolean HandleMouseDowns()
+{
+ WindowPtr whichWindow;
+ Rect box;
+ long new;
+ short v, h;
+
+ switch (FindWindow(myEvent.where,&whichWindow)) {
+ case inSysWindow:
+ SystemClick(&myEvent,whichWindow);
+ break;
+ case inMenuBar:
+ return(DoCommand(MenuSelect(myEvent.where)));
+ break;
+ case inGrow:
+ box = qd.screenBits.bounds;
+ box.top = kMinWindowHeight;
+ box.left = kMinWindowWidth;
+ new = GrowWindow(whichWindow,myEvent.where,&box);
+ if (new) {
+ v = HiWord(new);
+ h = LoWord(new);
+ SetPort(whichWindow);
+ SizeWindow(whichWindow,h,v,true);
+ EraseRect(&(whichWindow->portRect));
+ InvalRect(&(whichWindow->portRect));
+ if (whichWindow == textWindow) {
+ GrowLogWindow(h,v);
+ }
+ }
+ break;
+ case inGoAway:
+ if (!TrackGoAway(whichWindow,myEvent.where))
+ break;
+ break;
+ case inDrag:
+ if ((myEvent.modifiers & cmdKey) || (FrontWindow() == whichWindow))
+ DragWindow(whichWindow,myEvent.where,&qd.screenBits.bounds);
+ else {
+ SelectWindow(whichWindow);
+ SetPort(whichWindow);
+ }
+ break;
+ case inContent:
+ if (whichWindow != FrontWindow()) {
+ SelectWindow(whichWindow);
+ SetPort(whichWindow);
+ }
+ else {
+ if (whichWindow == textWindow) {
+ HandleMouseInText(&myEvent);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ return(true);
+}
+
+void CenterRect(Rect *r)
+{
+ /* centers the rectangle horizontally on main screen,
+ * one-third of the way down vertically from the top.
+ */
+ OffsetRect(r,
+ - r->left
+ + ((qd.screenBits.bounds.right - qd.screenBits.bounds.left)
+ - (r->right - r->left)) / 2,
+ - r->top
+ + (((qd.screenBits.bounds.bottom - qd.screenBits.bounds.top - GetMBarHeight())
+ - (r->bottom - r->top)) / 3) + GetMBarHeight());
+}
+
+DialogTHndl CenterDialog(short whichID)
+{
+ DialogTHndl d = (DialogTHndl) GetResource('DLOG', whichID);
+ if ((d != 0L) && (ResError() == noErr)) {
+ HLock((Handle)d);
+ CenterRect(&(*d)->boundsRect);
+ HUnlock((Handle)d);
+ }
+ return(d);
+}
+
+void LoadDateFormatStrings(void)
+{
+ /*Intl0Hndl h = (Intl0Hndl)GetResource('itl0',16383); /* explicit user overrides? */
+ Intl0Hndl h = (Intl0Hndl)IUGetIntl(0); /* get current date & time formats */
+ if (h) {
+ HLock((Handle)h);
+ s12HrCycle = ((**h).timeCycle == 0xFF) ? true:false; /* 0x00,0x01:24h, 0xFF:12hr */
+ if (s12HrCycle) {
+ /* these are C strings with a leading space character that we skip */
+ BlockMove(&(**h).mornStr[1],&sAMStr[1],2);
+ BlockMove(&(**h).eveStr[1],&sPMStr[1],2);
+ sAMStr[0] = sPMStr[0] = 2;
+ } else {
+ BlockMove("\p00",sAMStr,3);
+ BlockMove(sAMStr,sPMStr,3);
+ }
+ HUnlock((Handle)h);
+ ReleaseResource((Handle)h);
+ } else {
+ s12HrCycle = true;
+ BlockMove("\pAM",sAMStr,3);
+ BlockMove("\pPM",sPMStr,3);
+ }
+}
+
+void GetScheduleStrings(short scheduledHour, char *theHourStr, char *theAmPmStr)
+{
+ /* fill in the provided Pascal string buffers, given scheduledHour as 0..24 */
+ /* note: each buffer must be able to hold at least 2 bytes + 1 length byte. */
+
+ short hour = scheduledHour;
+ BlockMove(sAMStr,theAmPmStr,3);
+ if (hour >= 12) {
+ hour = (hour == 12) ? 12 : hour - 12;
+ if (scheduledHour < 24) {
+ BlockMove(sPMStr,theAmPmStr,3);
+ }
+ }
+ theHourStr[0] = 2;
+ if (!s12HrCycle) {
+ theHourStr[1] = (scheduledHour < 10) ? '0':(scheduledHour < 20) ? '1':'2';
+ theHourStr[2] = '0' + (scheduledHour % 10);
+ } else if (hour > 9) {
+ theHourStr[1] = '1';
+ theHourStr[2] = '0' + (hour - 10);
+ } else if (hour < 1) {
+ theHourStr[1] = '1';
+ theHourStr[2] = '2';
+ } else {
+ theHourStr[1] = ' ';
+ theHourStr[2] = '1' + (hour - 1);
+ }
+}
+
+void HandleClickInScheduler(Point pt, Rect *r, Boolean redrawAll)
+{
+ Boolean redraw = redrawAll;
+ short index;
+ Rect tmp = *r;
+ Rect parts[7] = {
+ { tmp.top, tmp.left+0, tmp.bottom, tmp.left+0 },
+ { tmp.top, tmp.left+2, tmp.bottom, tmp.left+21 },
+ { tmp.top, tmp.left+22, tmp.bottom, tmp.left+43 },
+ { tmp.top, tmp.left+44, tmp.bottom, tmp.left+54 },
+ { tmp.top, tmp.left+54, tmp.bottom, tmp.left+73 },
+ { tmp.top, tmp.left+74, tmp.bottom, tmp.left+95 },
+ { tmp.top, tmp.left+98, tmp.bottom, tmp.right },
+ };
+
+ for (index = 1; index < 6; index++) {
+ if (index == 3 || (!s12HrCycle && (index == 2 || index == 5))) {
+ continue; /* not selectable */
+ }
+ if (PtInRect(pt, &parts[index])) {
+ InvertRect(&parts[curSelectedIndex]); /* deselect the current selection first */
+ if (curSelectedIndex == index) {
+ curSelectedIndex = 0; /* we deselected this, so now there is no selection */
+ } else {
+ InvertRect(&parts[index]);
+ curSelectedIndex = index; /* this index is now the current selection */
+ }
+ break;
+ }
+ }
+ if (PtInRect(pt, &parts[6])) {
+ /* click in arrow control */
+ Boolean increase = false;
+ redraw = true;
+ if (pt.v < parts[6].top + ((parts[6].bottom-parts[6].top)/2)) {
+ /* down arrow, increase or toggle value */
+ increase = true;
+ }
+ switch (curSelectedIndex) {
+ case 0: /* nothing was selected, so select first item as a hint */
+ curSelectedIndex = 1;
+ InvertRect(&parts[curSelectedIndex]);
+ break;
+ case 1: /* start hour (0..24) */
+ if (increase) {
+ if (schedStartHour < 24) { schedStartHour++; }
+ } else {
+ if (schedStartHour > 0) { schedStartHour--; }
+ }
+ if (schedStartHour > schedEndHour) { schedEndHour++; /* push it forward */ }
+ break;
+ case 2: /* start am/pm */
+ if (schedStartHour == 24) {
+ /* toggle start from AM to PM (24 to 12) */
+ schedStartHour -= 12;
+ if (schedEndHour < schedStartHour) { schedEndHour = schedStartHour; }
+ } else if (schedStartHour < 12) {
+ /* toggle start from AM to PM (0..11 to 12..23) */
+ schedStartHour += 12;
+ if (schedEndHour < schedStartHour) { schedEndHour = schedStartHour; }
+ } else {
+ /* toggle from PM to AM (12..23 to 0..11) */
+ schedStartHour -= 12;
+ }
+ break;
+ case 4: /* end hour (0..24) */
+ if (increase) {
+ if (schedEndHour < 24) { schedEndHour++; }
+ } else {
+ if (schedEndHour > 0) { schedEndHour--; }
+ }
+ if (schedEndHour < schedStartHour) { schedStartHour = schedEndHour; }
+ break;
+ case 5: /* end am/pm */
+ if (schedEndHour == 24) {
+ /* toggle start from AM to PM (24 to 12) */
+ schedEndHour -= 12;
+ if (schedStartHour > schedEndHour) { schedStartHour = schedEndHour; }
+ } else if (schedEndHour < 12) {
+ /* toggle start from AM to PM (0..11 to 12..23) */
+ schedEndHour += 12;
+ if (schedEndHour < schedStartHour) { schedEndHour = schedStartHour; }
+ } else {
+ /* toggle from PM to AM (12..23 to 0..11) */
+ schedEndHour -= 12;
+ if (schedEndHour < schedStartHour) { schedStartHour = schedEndHour; }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (redraw) {
+ char startHourStr[4];
+ char startAmPmStr[4];
+ char endHourStr[4];
+ char endAmPmStr[4];
+ GetScheduleStrings(schedStartHour, startHourStr, startAmPmStr);
+ GetScheduleStrings(schedEndHour, endHourStr, endAmPmStr);
+ TextFont(1);
+ for (index = 1; index < 6; index++) {
+ EraseRect(&parts[index]);
+ MoveTo(parts[index].left,tmp.bottom-5);
+ switch (index) {
+ case 1: DrawString(startHourStr); break;
+ case 2: DrawString(startAmPmStr); break;
+ case 3: DrawString("\p-"); break;
+ case 4: DrawString(endHourStr); break;
+ case 5: DrawString(endAmPmStr); break;
+ default:
+ break;
+ }
+ if (index == curSelectedIndex) {
+ InvertRect(&parts[index]); /* re-select current selection */
+ }
+ }
+ TextFont(0);
+ }
+}
+
+void ShowButtonPress(DialogPtr theDialog, short theItem)
+{
+ long finalCount;
+ short tmpInt;
+ Handle tmpHandle;
+ Rect tmpRect;
+
+ GetDItem(theDialog, theItem, &tmpInt, &tmpHandle, &tmpRect);
+ HiliteControl((ControlHandle)tmpHandle, true); /* "push" */
+ Delay(10L, &finalCount);
+ HiliteControl((ControlHandle)tmpHandle, false); /* "release" */
+}
+
+pascal Boolean MyDialogFilter(DialogPtr theDialog, EventRecord *theEvent, short *itemHit)
+{
+ char tmpChar;
+ short tmpInt;
+ Handle tmpHandle;
+ Rect tmpRect;
+ Point tmpPt;
+
+ switch(theEvent->what) {
+ case keyDown:
+ case autoKey:
+ /* convert the key to char */
+ tmpChar = (unsigned char)(theEvent->message & charCodeMask);
+ if (tmpChar == '\r' || tmpChar == '\n' || tmpChar == '\3') {
+ /* return or enter pressed */
+ *itemHit = okItem;
+ ShowButtonPress(theDialog, okItem);
+ return true;
+ }
+ else if (((theEvent->modifiers & cmdKey) && (tmpChar == '.')) ||
+ (tmpChar == 0x1B)) {
+ /* command-period or escape pressed */
+ *itemHit = cancelItem;
+ ShowButtonPress(theDialog, cancelItem);
+ return true;
+ }
+ return false;
+ case mouseDown:
+ GetDItem(theDialog, scheduleUserItem, &tmpInt, &tmpHandle, &tmpRect);
+ tmpPt = theEvent->where;
+ GlobalToLocal(&tmpPt);
+ if (PtInRect(tmpPt, &tmpRect)) {
+ HandleClickInScheduler(tmpPt, &tmpRect, false);
+ }
+ return false;
+ default:
+ return false;
+ }
+ return false;
+}
+
+OSErr StartSpooler(void)
+{
+ OSErr err = noErr;
+ char portStr[256];
+ char typeStr[256];
+ char nameStr[256];
+ GrafPtr savePort;
+ DialogTHndl dtHdl;
+ DialogPtr aDialog;
+ short item, index;
+ Handle H;
+ PicHandle arrowsPictHdl;
+ Rect box, picBox, tmpBox;
+ DialogRecord d;
+ Point pt = { 0,0 };
+
+ /* preload stuff */
+ LoadPreferences();
+ LoadDateFormatStrings();
+ arrowsPictHdl = (PicHandle)GetResource('PICT',upDownArrowsPictID);
+
+ GetPort(&savePort);
+ /* get handle to dialog template and center it before displaying */
+ dtHdl = CenterDialog(spoolerDialogID);
+ aDialog = GetNewDialog(spoolerDialogID, (Ptr) &d, (WindowPtr) -1L);
+ if (aDialog) {
+ SetPort(aDialog);
+ /* draw default button indicator */
+ GetDItem(aDialog, okItem, &item, &H, &box);
+ PenSize(kButtonFrameSize,kButtonFrameSize);
+ InsetRect(&box,kButtonFrameInset,kButtonFrameInset);
+ FrameRoundRect(&box,kButtonFrameRadius,kButtonFrameRadius);
+ PenNormal();
+
+ /* draw group boxes */
+ GetDItem(aDialog, printerTypeUserItem, &item, &H, &box);
+ PenSize(1,1);
+ PenPat(&qd.gray);
+ FrameRect(&box);
+ PenNormal();
+ GetDItem(aDialog, localPortUserItem, &item, &H, &box);
+ PenSize(1,1);
+ PenPat(&qd.gray);
+ FrameRect(&box);
+ PenNormal();
+
+ /* arrow control */
+ GetDItem(aDialog, scheduleUserItem, &item, &H, &box);
+ HLock((Handle)arrowsPictHdl);
+ picBox = (**arrowsPictHdl).picFrame;
+ HUnlock((Handle)arrowsPictHdl);
+ tmpBox = box;
+ tmpBox.left = tmpBox.right - (picBox.right-picBox.left);
+ tmpBox.bottom = tmpBox.top + (picBox.bottom-picBox.top);
+ DrawPicture(arrowsPictHdl,&tmpBox);
+ HandleClickInScheduler(pt,&box,true); /* draw initial values */
+
+ /* set up default name, printer type, and printer options */
+ GetIndString(typeStr,typeStringsID,defaultPrinterItem-(iconLWItem-1));
+ GetIndString(nameStr,nameStringsID,defaultPrinterItem-(iconLWItem-1));
+ GetDItem(aDialog, spoolerNameItem, &item, &H, &box);
+ SetIText(H, (ourName[0] > 0) ? ourName : nameStr);
+ GetDItem(aDialog, defaultPrinterItem, &item, &H, &box);
+ PenSize(kIconFrameSize,kIconFrameSize);
+ InsetRect(&box,kIconFrameInset,kIconFrameInset);
+ FrameRect(&box);
+ PenNormal();
+ GetDItem(aDialog, colorRibbonCBoxItem, &index, &H, &box);
+ SetCtlValue((ControlHandle)H,defaultColorRibbon);
+ HiliteControl((ControlHandle)H,(defaultPrinterItem==iconLWItem)?255:0);
+ GetDItem(aDialog, sheetFeederCBoxItem, &index, &H, &box);
+ SetCtlValue((ControlHandle)H,defaultSheetFeeder);
+ HiliteControl((ControlHandle)H,(defaultPrinterItem==iconLWItem)?255:0);
+ GetDItem(aDialog, printingEnabledItem, &index, &H, &box);
+ SetCtlValue((ControlHandle)H,defaultPrintEnabled);
+
+ /* set up default port */
+ GetIndString(portStr,portStringsID,defaultPortItem-(modemPortItem-1));
+ GetDItem(aDialog, defaultPortItem, &item, &H, &box);
+ PenSize(kIconFrameSize,kIconFrameSize);
+ InsetRect(&box,kIconFrameInset,kIconFrameInset);
+ FrameRect(&box);
+ PenNormal();
+
+ item = 0;
+ while ((item != okItem) && (item != cancelItem)) {
+ if (item >= iconLWItem && item <= iconIWItem) {
+ short tmpItem;
+ for (index = iconLWItem; index <= iconIWItem; index++) {
+ GetDItem(aDialog, index, &tmpItem, &H, &box);
+ InsetRect(&box,kIconFrameInset,kIconFrameInset);
+ PenSize(kIconFrameSize,kIconFrameSize);
+ PenPat(&qd.white);
+ FrameRect(&box);
+ PenNormal();
+ }
+ GetDItem(aDialog, item, &tmpItem, &H, &box);
+ InsetRect(&box,kIconFrameInset,kIconFrameInset);
+ PenSize(kIconFrameSize,kIconFrameSize);
+ PenPat(&qd.black);
+ FrameRect(&box);
+ PenNormal();
+ GetIndString(typeStr, 128, item-(iconLWItem-1));
+ GetIndString(nameStr, 129, item-(iconLWItem-1));
+ GetDItem(aDialog, spoolerNameItem, &tmpItem, &H, &box);
+ SetIText(H, nameStr);
+ GetDItem(aDialog, colorRibbonCBoxItem, &tmpItem, &H, &box);
+ HiliteControl((ControlHandle)H,(item==iconLWItem)?255:0);
+ GetDItem(aDialog, sheetFeederCBoxItem, &tmpItem, &H, &box);
+ HiliteControl((ControlHandle)H,(item==iconLWItem)?255:0);
+ }
+ else if (item >= modemPortItem && item <= printerPortItem) {
+ short tmpItem;
+ for (index = modemPortItem; index <= printerPortItem; index++) {
+ GetDItem(aDialog, index, &tmpItem, &H, &box);
+ InsetRect(&box,kIconFrameInset,kIconFrameInset);
+ PenSize(kIconFrameSize,kIconFrameSize);
+ PenPat(&qd.white);
+ FrameRect(&box);
+ PenNormal();
+ }
+ GetDItem(aDialog, item, &tmpItem, &H, &box);
+ InsetRect(&box,kIconFrameInset,kIconFrameInset);
+ PenSize(kIconFrameSize,kIconFrameSize);
+ PenPat(&qd.black);
+ FrameRect(&box);
+ PenNormal();
+ GetIndString(portStr, 130, item-(modemPortItem-1));
+ }
+ else if (item >= colorRibbonCBoxItem && item <= printingEnabledItem) {
+ short tmpItem, curValue;
+ GetDItem(aDialog, item, &tmpItem, &H, &box);
+ curValue = GetCtlValue((ControlHandle)H);
+ SetCtlValue((ControlHandle)H,!curValue);
+ }
+ ModalDialog((ModalFilterProcPtr)MyDialogFilter, &item);
+ }
+ if (item == okItem) {
+ short tmpItem;
+ Boolean hasColorRibbon, hasSheetFeeder, printEnabled;
+ GetDItem(aDialog, spoolerNameItem, &tmpItem, &H, &box);
+ GetIText(H, nameStr);
+ GetDItem(aDialog, colorRibbonCBoxItem, &tmpItem, &H, &box);
+ hasColorRibbon = (GetCtlValue((ControlHandle)H) != 0);
+ GetDItem(aDialog, sheetFeederCBoxItem, &tmpItem, &H, &box);
+ hasSheetFeeder = (GetCtlValue((ControlHandle)H) != 0);
+ GetDItem(aDialog, printingEnabledItem, &tmpItem, &H, &box);
+ printEnabled = (GetCtlValue((ControlHandle)H) != 0);
+ SetPrinterOptions(hasColorRibbon, hasSheetFeeder);
+ defaultColorRibbon = hasColorRibbon;
+ defaultSheetFeeder = hasSheetFeeder;
+ defaultPrintEnabled = printEnabled;
+ }
+ curSelectedIndex = 0;
+ ReleaseResource((Handle)arrowsPictHdl);
+ CloseDialog(aDialog);
+ }
+ SetPort(savePort);
+ UpdateLogWindow();
+
+ if (item == okItem) {
+ /* save our name, type, and local printer port */
+ BlockMove(&nameStr[1],&ourName[1],nameStr[0]);
+ ourName[0] = nameStr[0];
+ BlockMove(&typeStr[1],&ourType[1],typeStr[0]);
+ ourType[0] = typeStr[0];
+ BlockMove(&portStr[1],&ourPort[1],portStr[0]);
+ ourPort[0] = portStr[0];
+ /* prefix is based on selected printer type */
+ ourPrefix[0] = 3;
+ ourPrefix[1] = ourType[1]; /* 'L' or 'I' */
+ ourPrefix[2] = (ourType[2] == 'Q') ? 'Q' : 'W';
+ ourPrefix[3] = '-';
+ /* save for next time (after spool folder is set up) */
+ SavePreferences();
+ {
+ EntityName name;
+ PAPStatusRec stRec;
+ name.zoneStr[0] = 1;
+ name.zoneStr[1] = '*';
+ BlockMove(ourType, name.typeStr, 33L);
+ BlockMove(ourName, name.objStr, 33L);
+ BlockMove("\pRegistering", stRec.statusStr, 12L);
+
+ err = SLInit(&name, kPAPMaxQuantum, &stRec, &serverRef);
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pRegistering name:",err);
+ (void) SLClose(serverRef);
+ } else {
+ BlockMove("Registered “", &nameStr[1],12L);
+ nameStr[0] = 12;
+ BlockMove(&ourName[1],&nameStr[nameStr[0]+1],(long)((short)ourName[0]));
+ nameStr[0] += ourName[0];
+ BlockMove("” in local zone", &nameStr[nameStr[0]+1],15L);
+ nameStr[0] += 15;
+ Message(LOG_ERR,nameStr);
+ }
+ }
+ }
+ return (item == okItem && !err) ? noErr : userCanceledErr;
+}
+
+OSErr StopSpooler(void)
+{
+ return SLClose(serverRef);
+}
+
+void DoAbout()
+{
+ Message(0,sepStr);
+ Message(0,"\pSimple Spooler is a basic print spooler application.");
+ Message(0,"\pIt presents itself as an AppleTalk printer and will");
+ Message(0,"\prespond to Printer Access Protocol (PAP) messages.");
+ Message(0,"\pCopyright © 2026 Ken McLeod.");
+ Message(0,sepStr);
+}
+
+void DoStatus()
+{
+ Message(0,sepStr);
+ MessageWithIntValue(0,"\pSpooler active:",serverRef);
+ MessageWithQuotedString(0,"\pSpooler name:",ourName);
+ MessageWithQuotedString(0,"\pPrinter type:",ourType);
+ MessageWithIntValue(0,"\pColor ribbon:",(defaultColorRibbon)?1:0);
+ MessageWithIntValue(0,"\pSheet feeder:",(defaultSheetFeeder)?1:0);
+ MessageWithQuotedString(0,"\pPrint to port:",ourPort);
+ MessageWithIntValue(0,"\pPrinting enabled:",(defaultPrintEnabled)?1:0);
+ MessageWithIntValue(0,"\pScheduled start hour:",schedStartHour);
+ MessageWithIntValue(0,"\pScheduled end hour:",schedEndHour);
+ MessageWithIntValue(0,"\pJobs spooled:",JobsSpooled());
+ MessageWithIntValue(0,"\pJobs printed:",JobsPrinted());
+ Message(0,sepStr);
+}
+
+void LoadPreferences()
+{
+ char *p;
+ Ptr prefData = 0L;
+ long count = 4L+2L+8+18+256+34+34;
+ OSErr err = CopyDataFromPrefsFile(&count,&prefData);
+ if (err != noErr || !prefData) { return; }
+ if (*((long*)&prefData[0]) != 'pref' || *((short*)&prefData[4]) != kPrefsVersion) {
+ DisposPtr(prefData);
+ return;
+ }
+ defaultPrinterItem = *((char*)&prefData[6]);
+ defaultPortItem = *((char*)&prefData[7]);
+ defaultColorRibbon = *((char*)&prefData[8]);
+ defaultSheetFeeder = *((char*)&prefData[9]);
+ defaultPrintEnabled = *((char*)&prefData[10]);
+ /* reserved char at prefsData[11] */
+ schedStartHour = *((char*)&prefData[12]);
+ schedEndHour = *((char*)&prefData[13]);
+ /* window rect in global coordinates */
+ windowRect = *((Rect*)&prefData[14]);
+ /* reserved chars at prefsData[22..31] */
+ p = (char*)&prefData[32];
+ BlockMove(p,ourName,p[0]+1);
+ p += p[0]+1;
+ BlockMove(p,ourType,p[0]+1);
+ p += p[0]+1;
+ BlockMove(p,ourPort,p[0]+1);
+ DisposPtr(prefData);
+}
+
+void SavePreferences()
+{
+ char *p, val;
+ Boolean hasColorRibbon, hasSheetFeeder, printEnabled;
+ long length = 4L+2L+8+18+ourName[0]+1+ourType[0]+1+ourPort[0]+1;
+ Ptr prefData = NewPtrClear(length);
+ if (!prefData) { return; }
+
+ /* offset 0: magic 'pref' tag (4 bytes) */
+ *((long*)&prefData[0]) = 'pref';
+ /* offset 4: prefs version (2 bytes) */
+ *((short*)&prefData[4]) = kPrefsVersion;
+ /* offset 6: selected printer item */
+ val = iconIWItem;
+ if (ourType[1] == 'L') { val = (ourType[2] == 'Q') ? iconLQItem : iconLWItem; }
+ *((char*)&prefData[6]) = val;
+ /* offset 7: selected printer port item */
+ val = (ourPort[1] == 'M') ? modemPortItem : printerPortItem;
+ *((char*)&prefData[7]) = val;
+ /* offset 8, 9: boolean values of Color Ribbon and Sheet Feeder */
+ GetPrinterOptions(&hasColorRibbon,&hasSheetFeeder);
+ *((char*)&prefData[8]) = (hasColorRibbon) ? -1 : 0;
+ *((char*)&prefData[9]) = (hasSheetFeeder) ? -1 : 0;
+ /* offset 10, 11: boolean values of Print Enabled and TBD */
+ printEnabled = defaultPrintEnabled;
+ *((char*)&prefData[10]) = (printEnabled) ? -1 : 0;
+ /* offset 12: scheduled start hour for printing (1 byte) */
+ *((char*)&prefData[12]) = (char)(schedStartHour & 0xFF);
+ /* offset 13: scheduled end hour for printing (1 byte) */
+ *((char*)&prefData[13]) = (char)(schedEndHour & 0xFF);
+ /* offset 14-15:top, 16-17:left, 18-19:bottom, 20-21:right (window rect) */
+ *((Rect*)&prefData[14]) = ((GrafPtr)textWindow)->portRect;
+ LocalToGlobal((Point*)&prefData[14]);
+ LocalToGlobal((Point*)&prefData[18]);
+ /* reserved chars from [22..31] */
+
+ p = (char*)&prefData[32];
+ BlockMove(ourName,p,ourName[0]+1);
+ p += ourName[0]+1;
+ BlockMove(ourType,p,ourType[0]+1);
+ p += ourType[0]+1;
+ BlockMove(ourPort,p,ourPort[0]+1);
+
+ (void)SaveDataToPrefsFile(length,prefData);
+ DisposPtr(prefData);
+}
--- Spooler.make Wed Feb 11 22:24:01 2026
+++ Spooler.make Wed Feb 11 22:24:01 2026
@@ -0,0 +1,35 @@
+SPOOLER_OBJECTS = Spooler.c.o ∂
+ SpoolFiles.c.o ∂
+ PrintQueue.c.o ∂
+ QueryParser.c.o ∂
+ Logging.c.o ∂
+ PAP.c.o
+
+SPOOLER_LIBRARIES = "{Libraries}"Runtime.o ∂
+ "{Libraries}"Interface.o ∂
+ "{CLibraries}"StdCLib.o
+
+SPOOLER_TARGET = 'Simple Spooler'
+
+Spooler ƒƒ Spooler.make {SPOOLER_OBJECTS} {SPOOLER_LIBRARIES}
+ Link -o {SPOOLER_TARGET} {SPOOLER_OBJECTS} {SPOOLER_LIBRARIES}
+ Rez -rd -append -o {SPOOLER_TARGET} Spooler.r
+ SetFile -a BIM -t APPL -c spoo {SPOOLER_TARGET}
+
+Spooler.c.o ƒ Spooler.make Spooler.c
+ C -r -mbg off Spooler.c
+
+SpoolFiles.c.o ƒ Spooler.make SpoolFiles.h SpoolFiles.c
+ C -r -mbg off SpoolFiles.c
+
+PrintQueue.c.o ƒ Spooler.make PrintQueue.h PrintQueue.c
+ C -r -mbg off PrintQueue.c
+
+QueryParser.c.o ƒ Spooler.make QueryParser.h QueryParser.c
+ C -r -mbg off QueryParser.c
+
+Logging.c.o ƒ Spooler.make Logging.h Logging.c
+ C -r -mbg off Logging.c
+
+PAP.c.o ƒ Spooler.make PAP.h PAP.c
+ C -r -mbg off PAP.c
--- Spooler.r Mon Feb 16 20:10:16 2026
+++ Spooler.r Mon Feb 16 20:10:16 2026
@@ -0,0 +1,755 @@
+/*
+ * Spooler.r
+ * Resource Definitions
+ *
+ */
+
+#include "Types.r"
+#include "SysTypes.r"
+
+#define VERSION_STR "0.1"
+
+/* resource type declarations */
+type 'spoo' as 'STR ';
+
+resource 'spoo' (0) {
+ VERSION_STR
+};
+
+resource 'BNDL' (128, purgeable) {
+ 'spoo',
+ 0,
+ { /* array TypeArray: 2 elements */
+ /* [1] */
+ 'FREF',
+ { /* array IDArray: 2 elements */
+ /* [1] */
+ 0, 128,
+ /* [2] */
+ 1, 129
+ },
+ /* [2] */
+ 'ICN#',
+ { /* array IDArray: 2 elements */
+ /* [1] */
+ 0, 128,
+ /* [2] */
+ 1, 129
+ }
+ }
+};
+
+resource 'FREF' (128, purgeable) {
+ 'APPL', 0, ""
+};
+
+resource 'FREF' (129, purgeable) {
+ 'pjob', 1, ""
+};
+
+/*
+ * 'vers' development=0x20, alpha=0x40, beta=0x60, release=0x80;
+ */
+resource 'vers' (1) {
+ 0x00, 0x10, development, 0x00, verUS,
+ VERSION_STR,
+ $$Format("Simple Spooler %s \nCopyright © 2026 Ken McLeod",
+ VERSION_STR)
+};
+
+resource 'DLOG' (128, "Printer Setup") {
+ {40, 40, 264, 464},
+ dBoxProc,
+ visible,
+ noGoAway,
+ 0x0,
+ 128,
+ ""
+};
+
+resource 'DITL' (128) {
+ { /* array DITLarray: 18 elements */
+ /* [1] */
+ {191, 344, 211, 410},
+ Button {
+ enabled,
+ "Start"
+ },
+ /* [2] */
+ {191, 266, 211, 332},
+ Button {
+ enabled,
+ "Cancel"
+ },
+ /* [3] */
+ {34, 81, 51, 409},
+ EditText {
+ enabled,
+ ""
+ },
+ /* [4] */
+ {13, 78, 29, 412},
+ StaticText {
+ disabled,
+ "Spooler name:"
+ },
+ /* [5] */
+ {11, 23, 43, 55},
+ Icon {
+ disabled,
+ 128
+ },
+ /* [6] */
+ {88, 96, 120, 128},
+ Icon {
+ enabled,
+ 129
+ },
+ /* [7] */
+ {88, 143, 120, 175},
+ Icon {
+ enabled,
+ 130
+ },
+ /* [8] */
+ {88, 190, 120, 222},
+ Icon {
+ enabled,
+ 131
+ },
+ /* [9] */
+ {61, 88, 77, 230},
+ StaticText {
+ disabled,
+ " Virtual printer type:"
+ },
+ /* [10] */
+ {88, 290, 120, 322},
+ Icon {
+ enabled,
+ 200
+ },
+ /* [11] */
+ {88, 339, 120, 371},
+ Icon {
+ enabled,
+ 201
+ },
+ /* [12] */
+ {61, 276, 77, 400},
+ StaticText {
+ disabled,
+ " Print to this port:"
+ },
+ /* [13] */
+ {133, 88, 151, 206},
+ CheckBox {
+ enabled,
+ "Color Ribbon"
+ },
+ /* [14] */
+ {152, 88, 170, 194},
+ CheckBox {
+ enabled,
+ "Sheet Feeder"
+ },
+ /* [15] */
+ {133, 276, 151, 358},
+ CheckBox {
+ enabled,
+ "Enabled"
+ },
+ /* [16] */
+ {153, 292, 171, 402},
+ UserItem {
+ enabled
+ },
+ /* [17] */
+ {68, 78, 178, 253},
+ UserItem {
+ disabled
+ },
+ /* [18] */
+ {68, 266, 178, 411},
+ UserItem {
+ disabled
+ },
+ }
+};
+
+resource 'MENU' (128) {
+ 128,
+ textMenuProc,
+ 0x7FFFFFFD,
+ enabled,
+ apple,
+ { /* array: 2 elements */
+ /* [1] */
+ "About Simple Spooler…", noIcon, noKey, noMark, plain,
+ /* [2] */
+ "-", noIcon, noKey, noMark, plain
+ }
+};
+
+resource 'MENU' (129) {
+ 129,
+ textMenuProc,
+ allEnabled,
+ enabled,
+ "File",
+ { /* array: 1 elements */
+ /* [1] */
+ "Quit", noIcon, "Q", noMark, plain
+ }
+};
+
+resource 'MENU' (130, preload) {
+ 130,
+ textMenuProc,
+ 0x0,
+ enabled,
+ "Edit",
+ { /* array: 8 elements */
+ /* [1] */
+ "Undo", noIcon, "Z", noMark, plain,
+ /* [2] */
+ "-", noIcon, noKey, noMark, plain,
+ /* [3] */
+ "Cut", noIcon, "X", noMark, plain,
+ /* [4] */
+ "Copy", noIcon, "C", noMark, plain,
+ /* [5] */
+ "Paste", noIcon, "V", noMark, plain,
+ /* [6] */
+ "Clear", noIcon, noKey, noMark, plain,
+ /* [7] */
+ "-", noIcon, noKey, noMark, plain,
+ /* [8] */
+ "Select All", noIcon, "A", noMark, plain
+ }
+};
+
+resource 'MENU' (131, preload) {
+ 131,
+ textMenuProc,
+ 0x0,
+ enabled,
+ "Control",
+ { /* array: 2 elements */
+ /* [1] */
+ "Start Spooler…", noIcon, noKey, noMark, plain,
+ /* [2] */
+ "Stop Spooler", noIcon, noKey, noMark, plain,
+ }
+};
+
+resource 'WIND' (128) {
+ {40, 10, 320, 490},
+ documentProc,
+ invisible,
+ noGoAway,
+ 0x0,
+ "Simple Spooler Log"
+};
+
+resource 'SIZE' (0) {
+ reserved,
+ acceptSuspendResumeEvents,
+ reserved,
+ canBackground,
+ notMultiFinderAware,
+ backgroundAndForeground,
+ dontGetFrontClicks,
+ ignoreChildDiedEvents,
+ is32BitCompatible,
+ notHighLevelEventAware,
+ onlyLocalHLEvents,
+ notStationeryAware,
+ dontUseTextEditServices,
+ reserved,
+ reserved,
+ reserved,
+ 98304,
+ 81920
+};
+
+resource 'SIZE' (-1) {
+ reserved,
+ acceptSuspendResumeEvents,
+ reserved,
+ canBackground,
+ notMultiFinderAware,
+ backgroundAndForeground,
+ dontGetFrontClicks,
+ ignoreChildDiedEvents,
+ is32BitCompatible,
+ notHighLevelEventAware,
+ onlyLocalHLEvents,
+ notStationeryAware,
+ dontUseTextEditServices,
+ reserved,
+ reserved,
+ reserved,
+ 81920,
+ 61440
+};
+
+resource 'STR#' (128, "Types") {
+ { /* array StringArray: 3 elements */
+ /* [1] */
+ "LaserWriter",
+ /* [2] */
+ "LQ",
+ /* [3] */
+ "ImageWriter"
+ }
+};
+
+resource 'STR#' (129, "Names") {
+ { /* array StringArray: 3 elements */
+ /* [1] */
+ "LaserWriter Spooler",
+ /* [2] */
+ "LQ Spooler",
+ /* [3] */
+ "ImageWriter Spooler"
+ }
+};
+
+resource 'STR#' (130, "Ports") {
+ { /* array StringArray: 2 elements */
+ /* [1] */
+ "Modem",
+ /* [2] */
+ "Printer"
+ }
+};
+
+resource 'ics8' (128) {
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"0000 0000 FFFF FF00 0000 00FF FF00 0000"
+ $"0000 00FF 00F5 F5FF FFFF FFF5 F5FF 0000"
+ $"0000 00FF 00FF F5FF F5F5 F5FC F5FF 0000"
+ $"0000 00FF 00FF F5FF F9F5 F5FC F5FF 0000"
+ $"0000 00FF 00FF F5FF FCF9 F9FC F5FF 0000"
+ $"0000 00FF 00F5 F5FF FFFC FFF5 F5FF 0000"
+ $"0000 0000 FFFF FF00 0000 00FF FF00 0000"
+ $"00FF FFFF FFFF FFFF FFFF FFFF FFFF FFFF"
+ $"0000 0000 FF08 FF08 FF00 00FF FF00 0000"
+ $"0000 0000 00FF 08FF 08FF FF08 FF00 0000"
+ $"0000 0000 0000 FF08 0808 08FF FF00 0000"
+ $"0000 0000 0000 00FF FF08 0808 FF00 0000"
+ $"0000 0000 0000 0000 00FF FF08 FF00 0000"
+ $"0000 0000 0000 0000 00FF FFFF FFFF 0000"
+ $"0000 0000 0000 0000 00FF FFFF FFFF"
+};
+
+resource 'ics8' (129) {
+ $"00FF FFFF FFFF FFFF FFFF FF00 0000 0000"
+ $"00FF F5F5 F5F5 F5F5 F5F5 FFFF 0000 0000"
+ $"00FF F5F5 F5F5 F5F5 F5F5 FFF6 FF00 0000"
+ $"00FF F5F5 F5F5 F5F5 F5F5 FFFF FFFF 0000"
+ $"00FF F5F5 FFFF F5F5 F5FF FFF5 F5FF 0000"
+ $"00FF F5FF F5F5 FFFF FFF9 00FF F5FF 0000"
+ $"00FF F5FF F5FF FFF5 F500 F9FF F5FF 0000"
+ $"00FF F5FF F5FF F9F7 F500 FFFF F5FF 0000"
+ $"00FF F5FF 0000 FFFF FFFF F5FF F5FF 0000"
+ $"00FF FFF5 FFFF F5F5 F5FF FFF5 FFFF 0000"
+ $"00FF 00FF FFFF FFFF FFFF FFFF FFFF 0000"
+ $"00FF F5F5 FF08 FF08 FF08 FFFF F5FF 0000"
+ $"00FF F5F5 F5FF 08FF 08FF 08FF F5FF 0000"
+ $"00FF F5F5 F5F5 FFFF 0808 08FF F5FF 0000"
+ $"00FF F5F5 F5F5 F5F5 FFFF FFFF FFFF 0000"
+ $"00FF FFFF FFFF FFFF FFFF FFFF FFFF"
+};
+
+resource 'ics4' (128) {
+ $"0000 0000 0000 0000 0000 FFF0 000F F000"
+ $"000F 0CCF FFFC 0F00 000F 0FCF CCCE 0F00"
+ $"000F 0FCF DCCE 0F00 000F 0FCF EDDE 0F00"
+ $"000F 0CCF FEF0 0F00 0000 FFF0 000F F000"
+ $"0FFF FFFF FFFF FFFF 0000 F2F0 F02F F000"
+ $"0000 0F0F 2FF0 F000 0000 00F2 020F F000"
+ $"0000 000F F020 F000 0000 0000 0FF2 F000"
+ $"0000 0000 0FFF FF00 0000 0000 0FFF FF"
+};
+
+resource 'ics4' (129) {
+ $"0FFF FFFF FFF0 0000 0F0C 0C0C 0CFF 0000"
+ $"0FC0 C0C0 C0FC F000 0F0C 0C0C 0CFF FF00"
+ $"0FC0 FFC0 CFF0 CF00 0F0F 00FF FD0F 0F00"
+ $"0FCF 0FF0 00DF CF00 0F0F 0FDC 00FF 0F00"
+ $"0FCF 00FF FF0F CF00 0FF0 FF00 0FF0 FF00"
+ $"0FCF FFFF FFFF FF00 0F0C F0F0 F2FF 0F00"
+ $"0FC0 CF2F 0F0F CF00 0F0C 0CFF 202F 0F00"
+ $"0FC0 C0C0 FFFF FF00 0FFF FFFF FFFF FF"
+};
+
+resource 'ics#' (128) {
+ { /* array: 2 elements */
+ /* [1] */
+ $"0000 0E18 11E4 1514 1594 15F4 11E4 0E18"
+ $"7FFF 0A98 0568 0218 0188 0068 007C 007C",
+ /* [2] */
+ $"0000 0E18 1FFC 1FFC 1FFC 1FFC 1FFC 0FF8"
+ $"7FFF 0FF8 07F8 03F8 01F8 0078 007C 007C"
+ }
+};
+
+resource 'ics#' (129) {
+ { /* array: 2 elements */
+ /* [1] */
+ $"7FE0 4030 4028 403C 4C64 5394 5654 5654"
+ $"53D4 6C6C 5FFC 4AB4 4554 4314 40FC 7FFC",
+ /* [2] */
+ $"7FE0 7FF0 7FF8 7FFC 7FFC 7FFC 7FFC 7FFC"
+ $"7FFC 7FFC 7FFC 7FFC 7FFC 7FFC 7FFC 7FFC"
+ }
+};
+
+resource 'ICON' (1000) {
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"00FF F800 0080 0800 00A0 2800 0080 0800"
+ $"00A0 2800 0080 0800 00A0 2800 0080 0800"
+ $"00A0 2800 0080 0800 0FFF FFF0 0900 0490"
+ $"0FFF FF90 0900 0490 0FFF FC90 0800 00F0"
+ $"0FFF FF80 0800 0080 0A00 0080 0800 0080"
+ $"0FFF FF80"
+};
+
+resource 'ICON' (129) {
+ $"1FFF FC00 1000 0600 1000 0500 1007 8480"
+ $"100C C440 1018 C420 1018 C7F0 1018 8010"
+ $"1019 8010 100E 3E10 101C 1C10 106E 1810"
+ $"10C6 1010 1187 3010 7183 E01C B181 C01E"
+ $"91C0 E11A 90E3 7E1A D03C 1C1A B000 001E"
+ $"7000 001C 7FFF FFFC AAAA AAAE 8000 0002"
+ $"8000 0002 D555 5552 AAAA AAAE 7FFF FFFC"
+ $"1FFF FFF0 1000 0010 1000 0010 1FFF FFF0"
+};
+
+resource 'ICON' (130) {
+ $"01FF FFF8 0200 0014 0400 0022 0510 70A2"
+ $"0410 883E 0510 88A0 0410 A820 0510 90A0"
+ $"041E 6820 0400 0020 0FFF FFF0 F800 001F"
+ $"8FFF FFF5 F800 001D 8800 0015 8FFF FFF5"
+ $"A000 0005 8000 0007 FFFF FFFC D555 5554"
+ $"FFFF FFFC 0000 0140 0000 0140 0000 0140"
+ $"0000 03E0 0000 0220 0000 0220 0000 03E0"
+ $"0000 02A0 BFFF FC9D 0000 0140 BFFF FE3D"
+};
+
+resource 'ICON' (131) {
+ $"07FF FF00 0400 0100 0500 0500 0400 0100"
+ $"051E C500 0400 0100 051B 4500 0400 0100"
+ $"051D C500 0400 0100 FFFF FFFF 9000 0049"
+ $"FFFF FFF9 9000 0049 9FFF FFC9 8000 0009"
+ $"FFFF FFF9 8000 000F 8000 0008 9000 0008"
+ $"8000 0008 FFFF FFF8 0000 0140 0000 0140"
+ $"0000 03E0 0000 0220 0000 0220 0000 03E0"
+ $"0000 02A0 BFFF FC9D 0000 0140 BFFF FE3D"
+};
+
+resource 'ICON' (128) {
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"0030 0C00 0048 1200 004F F200 0084 0900"
+ $"00B6 0500 00B7 0500 00B7 0500 00B7 0500"
+ $"0087 F900 004F FA00 0048 1200 0030 0C00"
+ $"FFFF FFFF 4000 0002 3FFF FFFC 00AA 0500"
+ $"00AB 0500 0065 D900 0022 3700 0019 0900"
+ $"000C 0500 0003 0300 0000 C100 0000 2100"
+ $"0000 7F80 0000 7F80 0000 7F80"
+};
+
+resource 'ICON' (200) {
+ $"FFFF FFFF 8000 0001 8000 0001 8000 0031"
+ $"8000 0049 81C0 0049 8220 0131 8410 0081"
+ $"8810 0641 8808 0901 9008 0901 9008 2601"
+ $"9010 1001 9060 C801 8881 2001 8881 2001"
+ $"8484 C001 8442 0001 8221 0001 8210 0E01"
+ $"8108 1181 8084 2041 8042 2021 8021 C021"
+ $"8010 0021 800C 0041 8003 0081 8000 C301"
+ $"8000 3C01 8000 0001 8000 0001 FFFF FFFF"
+};
+
+resource 'ICON' (201) {
+ $"FFFF FFFF 8000 0001 8000 0001 8000 0001"
+ $"80FF C001 8080 6001 8080 5001 8080 4801"
+ $"8080 4401 8080 7E01 8080 0201 8080 0201"
+ $"8080 0201 8080 0201 8080 0201 8080 0201"
+ $"8080 0201 8080 0201 8F80 03E1 8880 0221"
+ $"8880 0239 88FF FE29 8800 0029 8800 0029"
+ $"8800 0039 8800 0021 8800 0021 8FFF FFE1"
+ $"8000 0001 8000 0001 8000 0001 FFFF FFFF"
+};
+
+resource 'icl8' (128) {
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 FFFF 0000 0000"
+ $"0000 0000 FFFF 0000 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 00FF 00F6 FF00 0000"
+ $"0000 00FF F600 FF00 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 00FF 00F6 FFFF FFFF"
+ $"FFFF FFFF F600 FF00 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 FF00 00F6 F6FF F6F6"
+ $"F6F6 F6F6 FCF6 00FF 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 FF00 FFFF F6FF F92B"
+ $"F6F6 F6F6 F6FC 00FF 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 FF00 FFFF F6FF FCF9"
+ $"F6F6 F6F6 F6FC 00FF 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 FF00 FFFF F6FF FCF9"
+ $"2BF6 F6F6 F6FC 00FF 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 FF00 FFFF F6FF FCF9"
+ $"F9F6 F6F6 F6FC 00FF 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 FF00 00F6 F6FF FCFC"
+ $"FCFC FCFC FCF6 00FF 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 00FF 00F6 FFFF FFFF"
+ $"FFFF FFFF FFF6 FF00 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 00FF 00F6 FF00 0000"
+ $"0000 00FF F6F6 FF00 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 FFFF 0000 0000"
+ $"0000 0000 FFFF 0000 0000 0000 0000 0000"
+ $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF"
+ $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF"
+ $"00FF 0000 0000 0000 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 0000 0000 FF00"
+ $"0000 FFFF FFFF FFFF FFFF FFFF FFFF FFFF"
+ $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF 0000"
+ $"0000 0000 0000 0000 FF08 FF08 FF08 FF00"
+ $"0000 0000 00FF 08FF 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 FF08 FF08 FF08 FFFF"
+ $"0000 0000 00FF 08FF 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 00FF FF08 08FF 08FF"
+ $"FFFF 00FF FF08 08FF 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 FF08 0808 FF08"
+ $"0808 FFFF 08FF FFFF 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 00FF FF08 08FF"
+ $"0808 0808 FF08 08FF 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 0000 FFFF 0808"
+ $"0808 0808 08FF 08FF 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 0000 0000 FFFF"
+ $"0808 0808 0808 FFFF 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"FFFF 0808 0808 08FF 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"0000 FF08 0808 08FF 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"00FF FFFF FFFF FFFF FF00 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"00FF FFFF FFFF FFFF FF00 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"00FF FFFF FFFF FFFF FF"
+};
+
+resource 'icl8' (129) {
+ $"0000 FFFF FFFF FFFF FFFF FFFF FFFF FFFF"
+ $"FFFF FFFF FFFF FFFF 0000 0000 0000 0000"
+ $"0000 FFF5 F5F5 F5F5 F5F5 F5F5 F5F5 F5F5"
+ $"F5F5 F5F5 F5F5 F5FF FF00 0000 0000 0000"
+ $"0000 FFF5 F5F5 F5F5 F5F5 F5F5 F5F5 F5F5"
+ $"F5F5 F5F5 F5F5 F5FF F6FF 0000 0000 0000"
+ $"0000 FFF5 F5F5 F5F5 F5F5 F5F5 F5F5 F5F5"
+ $"F5F5 F5F5 F5F5 F5FF F6F6 FF00 0000 0000"
+ $"0000 FFF5 F5F5 F5F5 F5F5 F5F5 F5F5 F5F5"
+ $"F5F5 F5F5 F5F5 F5FF F6F6 F6FF 0000 0000"
+ $"0000 FFF5 F5F5 F5F5 F5F5 F5F5 F5F5 F5F5"
+ $"F5F5 F5F5 F5F5 F5FF FFFF FFFF FF00 0000"
+ $"0000 FFF5 F5F5 F5F5 F5FF FFF5 F5F5 F5F5"
+ $"F5F5 F5FF FFF5 F5F5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5F5 FF00 F6FF F5F5 F5F5"
+ $"F5F5 FFF6 00FF F5F5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5F5 FF00 F6FF FFFF FFFF"
+ $"FFFF FFF6 00FF F5F5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5FF 0000 F6F6 FFF6 F6F6"
+ $"F6F6 F6FC F600 FFF5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5FF 00FF FFF6 FFF9 2BF6"
+ $"F6F6 F6F6 FC00 FFF5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5FF 00FF FFF6 FFFC F9F6"
+ $"F6F6 F6F6 FC00 FFF5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5FF 00FF FFF6 FFFC F92B"
+ $"F6F6 F6F6 FC00 FFF5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5FF 00FF FFF6 FFFC F9F9"
+ $"F6F6 F6F6 FC00 FFF5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5FF 0000 F6F6 FFFC FCFC"
+ $"FCFC FCFC F600 FFF5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5F5 FF00 F6FF FFFF FFFF"
+ $"FFFF FFFF F6FF F5F5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5F5 FF00 F6FF F5F5 F5F5"
+ $"F5F5 FFF6 F6FF F5F5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5F5 F5FF FFF5 F5F5 F5F5"
+ $"F5F5 F5FF FFF5 F5F5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 FFFF FFFF FFFF FFFF FFFF FFFF"
+ $"FFFF FFFF FFFF FFFF FFFF F5F5 FF00 0000"
+ $"0000 FFF5 F5FF FFFF FFFF FFFF FFFF FFFF"
+ $"FFFF FFFF FFFF FFFF FFF5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5F5 FF08 FF08 FFF5 F5F5"
+ $"F5F5 FF08 FFF5 F5F5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5F5 F5FF 08FF 08FF F5F5"
+ $"F5F5 FF08 FFF5 F5F5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5F5 F5FF 08FF 0808 FFFF"
+ $"F5FF FF08 FFF5 F5F5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5F5 F5F5 FF08 FF08 0808"
+ $"FF08 08FF FFF5 F5F5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5F5 F5F5 F5FF FFFF 0808"
+ $"08FF 0808 FFF5 F5F5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5F5 F5F5 F5F5 F5FF FF08"
+ $"0808 FF08 FFF5 F5F5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5F5 F5F5 F5F5 F5F5 F5FF"
+ $"0808 0808 FFF5 F5F5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5F5 F5F5 F5F5 F5F5 F5F5"
+ $"FF08 0808 FFF5 F5F5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5F5 F5F5 F5F5 F5F5 F5FF"
+ $"FFFF FFFF FFFF F5F5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5F5 F5F5 F5F5 F5F5 F5FF"
+ $"FFFF FFFF FFFF F5F5 F5F5 F5F5 FF00 0000"
+ $"0000 FFF5 F5F5 F5F5 F5F5 F5F5 F5F5 F5F5"
+ $"F5F5 F5F5 F5F5 F5F5 F5F5 F5F5 FF00 0000"
+ $"0000 FFFF FFFF FFFF FFFF FFFF FFFF FFFF"
+ $"FFFF FFFF FFFF FFFF FFFF FFFF FF"
+};
+
+resource 'icl4' (128) {
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"0000 0000 00FF 0000 0000 FF00 0000 0000"
+ $"0000 0000 0F0C F000 000F C0F0 0000 0000"
+ $"0000 0000 0F0C FFFF FFFF C0F0 0000 0000"
+ $"0000 0000 F00C CFCC CCCC EC0F 0000 0000"
+ $"0000 0000 F0FF CFDC CCCC CE0F 0000 0000"
+ $"0000 0000 F0FF CFED CCCC CE0F 0000 0000"
+ $"0000 0000 F0FF CFED CCCC CE0F 0000 0000"
+ $"0000 0000 F0FF CFED DCCC CE0F 0000 0000"
+ $"0000 0000 F00C CFEE EEEE EC0F 0000 0000"
+ $"0000 0000 0F0C FFFF FFFF FCF0 0000 0000"
+ $"0000 0000 0F0C F000 000F CCF0 0000 0000"
+ $"0000 0000 00FF 0000 0000 FF00 0000 0000"
+ $"FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF"
+ $"0F00 0000 0000 0000 0000 0000 0000 00F0"
+ $"00FF FFFF FFFF FFFF FFFF FFFF FFFF FF00"
+ $"0000 0000 F0F2 F0F0 0000 0F2F 0000 0000"
+ $"0000 0000 F2F0 F2FF 0000 0F0F 0000 0000"
+ $"0000 0000 0FF0 2F0F FF0F F02F 0000 0000"
+ $"0000 0000 00F2 02F2 02FF 0FFF 0000 0000"
+ $"0000 0000 000F F02F 2020 F02F 0000 0000"
+ $"0000 0000 0000 FF02 0202 0F0F 0000 0000"
+ $"0000 0000 0000 00FF 2020 20FF 0000 0000"
+ $"0000 0000 0000 0000 FF02 020F 0000 0000"
+ $"0000 0000 0000 0000 00F0 202F 0000 0000"
+ $"0000 0000 0000 0000 0FFF FFFF F000 0000"
+ $"0000 0000 0000 0000 0FFF FFFF F000 0000"
+ $"0000 0000 0000 0000 0FFF FFFF F0"
+};
+
+resource 'icl4' (129) {
+ $"00FF FFFF FFFF FFFF FFFF FFFF 0000 0000"
+ $"00FC 0C0C 0C0C 0C0C 0C0C 0C0F F000 0000"
+ $"00F0 C0C0 C0C0 C0C0 C0C0 C0CF CF00 0000"
+ $"00FC 0C0C 0C0C 0C0C 0C0C 0C0F CCF0 0000"
+ $"00F0 C0C0 C0C0 C0C0 C0C0 C0CF CCCF 0000"
+ $"00FC 0C0C 0C0C 0C0C 0C0C 0C0F FFFF F000"
+ $"00F0 C0C0 0FF0 C0C0 C0CF F0C0 C0C0 F000"
+ $"00FC 0C0C F0CF 0C0C 0CFC 0F0C 0C0C F000"
+ $"00F0 C0C0 F0CF FFFF FFFC 0FC0 C0C0 F000"
+ $"00FC 0C0F 00CC FCCC CCCE C0FC 0C0C F000"
+ $"00F0 C0CF 0FFC FDCC CCCC E0F0 C0C0 F000"
+ $"00FC 0C0F 0FFC FEDC CCCC E0FC 0C0C F000"
+ $"00F0 C0CF 0FFC FEDC CCCC E0F0 C0C0 F000"
+ $"00FC 0C0F 0FFC FEDD CCCC E0FC 0C0C F000"
+ $"00F0 C0CF 00CC FEEE EEEE C0F0 C0C0 F000"
+ $"00FC 0C0C F0CF FFFF FFFF CF0C 0C0C F000"
+ $"00F0 C0C0 F0CF C0C0 C0FC CFC0 C0C0 F000"
+ $"00FC 0C0C 0FFC 0C0C 0C0F FC0C 0C0C F000"
+ $"00F0 FFFF FFFF FFFF FFFF FFFF FFC0 F000"
+ $"00FC 0FFF FFFF FFFF FFFF FFFF FC0C F000"
+ $"00F0 C0C0 F0F0 F0C0 C0F0 F0C0 C0C0 F000"
+ $"00FC 0C0C 0F2F 0F0C 0CF2 FC0C 0C0C F000"
+ $"00F0 C0C0 CF0F 20FF CFF0 F0C0 C0C0 F000"
+ $"00FC 0C0C 0CF2 F202 F02F FC0C 0C0C F000"
+ $"00F0 C0C0 C0CF FF20 2F02 F0C0 C0C0 F000"
+ $"00FC 0C0C 0C0C 0FF2 02F0 FC0C 0C0C F000"
+ $"00F0 C0C0 C0C0 C0CF 2020 F0C0 C0C0 F000"
+ $"00FC 0C0C 0C0C 0C0C F202 FC0C 0C0C F000"
+ $"00F0 C0C0 C0C0 C0CF FFFF FFC0 C0C0 F000"
+ $"00FC 0C0C 0C0C 0C0F FFFF FF0C 0C0C F000"
+ $"00F0 C0C0 C0C0 C0C0 C0C0 C0C0 C0C0 F000"
+ $"00FF FFFF FFFF FFFF FFFF FFFF FFFF F0"
+};
+
+resource 'ICN#' (128) {
+ { /* array: 2 elements */
+ /* [1] */
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"0030 0C00 0048 1200 004F F200 0084 0900"
+ $"00B6 0500 00B7 0500 00B7 0500 00B7 0500"
+ $"0087 F900 004F FA00 0048 1200 0030 0C00"
+ $"FFFF FFFF 4000 0002 3FFF FFFC 00AA 0500"
+ $"00AB 0500 0065 D900 0022 3700 0019 0900"
+ $"000C 0500 0003 0300 0000 C100 0000 2100"
+ $"0000 7F80 0000 7F80 0000 7F80",
+ /* [2] */
+ $"0000 0000 0000 0000 0000 0000 0000 0000"
+ $"0030 0C00 0078 1E00 007F FE00 00FF FF00"
+ $"00FF FF00 00FF FF00 00FF FF00 00FF FF00"
+ $"00FF FF00 007F FE00 0078 1E00 0030 0C00"
+ $"FFFF FFFF 7FFF FFFE 3FFF FFFC 00FE 0700"
+ $"00FF 0700 007F DF00 003F FF00 001F FF00"
+ $"000F FF00 0003 FF00 0000 FF00 0000 3F00"
+ $"0000 7F80 0000 7F80 0000 7F80"
+ }
+};
+
+resource 'ICN#' (129) {
+ { /* array: 2 elements */
+ /* [1] */
+ $"3FFF FF00 2000 0180 2000 0140 2000 0120"
+ $"2000 0110 2000 01F8 2060 1808 2090 2408"
+ $"209F E408 2108 1208 216C 0A08 216E 0A08"
+ $"216E 0A08 216E 0A08 210F F208 209F F408"
+ $"2090 2408 2060 1808 2FFF FFC8 27FF FF88"
+ $"20A8 2808 2054 2808 2053 6808 2028 9808"
+ $"201C 4808 2006 2808 2001 0808 2000 8808"
+ $"2001 FC08 2001 FC08 2000 0008 3FFF FFF8",
+ /* [2] */
+ $"3FFF FF00 3FFF FF80 3FFF FFC0 3FFF FFE0"
+ $"3FFF FFF0 3FFF FFF8 3FFF FFF8 3FFF FFF8"
+ $"3FFF FFF8 3FFF FFF8 3FFF FFF8 3FFF FFF8"
+ $"3FFF FFF8 3FFF FFF8 3FFF FFF8 3FFF FFF8"
+ $"3FFF FFF8 3FFF FFF8 3FFF FFF8 3FFF FFF8"
+ $"3FFF FFF8 3FFF FFF8 3FFF FFF8 3FFF FFF8"
+ $"3FFF FFF8 3FFF FFF8 3FFF FFF8 3FFF FFF8"
+ $"3FFF FFF8 3FFF FFF8 3FFF FFF8 3FFF FFF8"
+ }
+};
+
+resource 'PICT' (128, purgeable) {
+ 89,
+ {134, 272, 152, 283},
+ $"1101 0100 0A8A D08A D075 3075 3090 0002"
+ $"00FE 0160 0110 0170 00FE 0160 0110 016B"
+ $"0086 0110 0098 011B 0000 3F80 4040 8420"
+ $"8E20 9F20 BFA0 8E20 8E20 8020 8020 8E20"
+ $"8E20 BFA0 9F20 8E20 8420 4040 3F80 FF"
+};
+
+resource 'PICT' (129, purgeable) {
+ 133,
+ {183, 223, 201, 240},
+ $"1101 A030 3901 000A 00B7 00DF 00C9 00F0"
+ $"3200 C800 DF00 C900 F090 0004 00B7 00D8"
+ $"00C8 00F0 00B7 00DF 00C8 00F0 00B7 00DF"
+ $"00C8 00F0 0000 0007 C000 0018 3000 0021"
+ $"0800 0041 0400 0081 0200 0081 0200 0101"
+ $"0100 0101 0100 0101 0100 0100 8140 0100"
+ $"4100 3880 22DC A480 1252 A440 0452 2420"
+ $"0852 2418 3052 0007 C000 FF"
+};
--- SpoolFiles.c Fri Feb 13 10:23:34 2026
+++ SpoolFiles.c Fri Feb 13 10:23:34 2026
@@ -0,0 +1,555 @@
+/*
+ * SpoolFiles.c
+ *
+ * Routines for handling spool files and folders.
+ *
+ * Written by: Ken McLeod, 2026-01-14
+ * Last update: 2026-01-31
+ *
+ * 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 <Types.h>
+#include <Errors.h>
+#include <Events.h>
+#include <Files.h>
+#include <Folders.h>
+#include <SysEqu.h>
+#include <GestaltEqu.h>
+#include <Memory.h>
+#include <OSUtils.h>
+#include <Packages.h>
+#include <Resources.h>
+#include <StdIO.h>
+#include <String.h>
+#include "Logging.h"
+#include "SpoolFiles.h"
+
+/* set if we should save each incoming data packet length */
+#define USING_CHUNK_LENGTHS 0
+
+char *prefFileName = "\pSimple Spooler Prefs";
+char *spoolFolderName = "\pSimple Spooler";
+
+char spoolFileName[256];
+short spoolFileRefNum;
+short spoolVRefNum;
+long spoolDirID;
+
+char *spoolFilePrefixes[] = {
+ "??", /* 0 */
+ "LW", /* 1 */
+ "LQ", /* 2 */
+ "IW", /* 3 */
+};
+
+int NumToolboxTraps(void)
+{
+ if (NGetTrapAddress(0xA86E /* _InitGraf */, ToolTrap) ==
+ NGetTrapAddress(0xAA6E, ToolTrap)) {
+ return 0x200;
+ }
+ return 0x400;
+}
+
+TrapType GetTrapType(int theTrap)
+{
+ if ((theTrap & 0x0800) > 0) {
+ return ToolTrap;
+ }
+ return OSTrap;
+}
+
+Boolean TrapAvailable(int theTrap)
+{
+ int trap = theTrap;
+ TrapType tType = GetTrapType(theTrap);
+ if (tType == ToolTrap) {
+ trap = theTrap & 0x07FF;
+ if (trap >= NumToolboxTraps) {
+ trap = 0xA89F; /* _Unimplemented */
+ }
+ }
+ return (NGetTrapAddress(trap, tType) !=
+ NGetTrapAddress(0xA89F, ToolTrap)); /* _Unimplemented */
+}
+
+Boolean GestaltAvailable(void)
+{
+ return TrapAvailable(0xA1AD); /* _Gestalt */
+}
+
+/* MyFindFolder is an adaptation of the FindSysFolder function
+ * from DTS Utilities sample code on Dev CD Mar '94. It should
+ * work even if Gestalt is not available by returning the
+ * system folder for any folders normally in the system folder.
+ */
+OSErr MyFindFolder(OSType folderType,short *foundVRefNum,long *foundDirID)
+{
+ long gesResponse;
+ SysEnvRec envRec;
+ WDPBRec myWDPB;
+ unsigned char volName[34];
+ OSErr err;
+
+ *foundVRefNum = 0;
+ *foundDirID = 0;
+ if (GestaltAvailable() &&
+ !Gestalt(gestaltFindFolderAttr, &gesResponse) &&
+ (gesResponse & (1<<gestaltFindFolderPresent))) { /* Does Folder Manager exist? */
+ err = FindFolder(kOnSystemDisk, folderType, kDontCreateFolder,
+ foundVRefNum, foundDirID);
+ } else {
+ /* Gestalt can't give us the answer, so we resort to SysEnvirons */
+ if (!(err = SysEnvirons(curSysEnvVers, &envRec))) {
+ myWDPB.ioVRefNum = envRec.sysVRefNum;
+ volName[0] = '\000'; /* Zero volume name */
+ myWDPB.ioNamePtr = volName;
+ myWDPB.ioWDIndex = 0;
+ myWDPB.ioWDProcID = 0;
+ if (!(err = PBGetWDInfo(&myWDPB, 0))) {
+ *foundVRefNum = myWDPB.ioWDVRefNum;
+ *foundDirID = myWDPB.ioWDDirID;
+ }
+ }
+ }
+ return err;
+}
+
+/* GetDirID is an adaptation of the GetDirID function from the
+ * MoreFilesExtras sample code on Dev CD Mar '94. (Thanks, Jim!)
+ */
+OSErr GetDirID(short vRefNum,long dirID,char *name,long *theDirID,Boolean *isDir)
+{
+ OSErr err;
+ CInfoPBRec pb;
+ pb.hFileInfo.ioNamePtr = name;
+ pb.hFileInfo.ioVRefNum = vRefNum;
+ pb.hFileInfo.ioDirID = dirID;
+ pb.hFileInfo.ioFDirIndex = 0; /* use ioNamePtr and ioDirID */
+ err = PBGetCatInfoSync(&pb);
+ *theDirID = pb.hFileInfo.ioDirID;
+ *isDir = (pb.hFileInfo.ioFlAttrib & 0x10) != 0;
+ return err;
+}
+
+OSErr InitSpoolFolder(void)
+{
+ /* create our spool directory inside Preferences if we have it,
+ * otherwise in the System Folder.
+ */
+ short myVRefNum;
+ long myDirID;
+ OSErr err = MyFindFolder(kPreferencesFolderType,&myVRefNum,&myDirID);
+ if (err == noErr) {
+ HParamBlockRec pb;
+ char nameBuf[34];
+ BlockMove(spoolFolderName,nameBuf,15);
+ pb.fileParam.ioCompletion = nil;
+ pb.fileParam.ioNamePtr = nameBuf;
+ pb.fileParam.ioVRefNum = myVRefNum;
+ pb.fileParam.ioDirID = myDirID;
+ err = PBDirCreate(&pb,false);
+ if (err != noErr && err != dupFNErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pCould not create spool folder:",err);
+ return err;
+ } else {
+ Boolean isDir;
+ long theDirID;
+ BlockMove(spoolFolderName,nameBuf,15);
+ err = GetDirID(myVRefNum,myDirID,nameBuf,&theDirID,&isDir);
+ if (err != noErr || !isDir) {
+ MessageWithIntValue(LOG_ERR_X,"\pCould not get spool folder dirID:",err);
+ return false;
+ }
+ spoolVRefNum = myVRefNum;
+ spoolDirID = theDirID;
+ }
+ } else {
+ MessageWithIntValue(LOG_ERR_X,"\pCould not locate preference folder:",err);
+ }
+ return err;
+}
+
+OSErr GetSpoolFolder(short *vRefNum, long *dirID)
+{
+ if (vRefNum) { *vRefNum = spoolVRefNum; }
+ if (dirID) { *dirID = spoolDirID; }
+ return noErr;
+}
+
+#if FILENAME_USES_TICK_COUNT
+void NewSpoolFilename(int printerType, char *nameBuf)
+{
+ /* creates a new filename in the provided 34-byte buffer,
+ * in the form <typePrefix>-<curTicks>, e.g. "IW-6778001"
+ */
+ char tickString[256];
+ long curTicks = TickCount();
+ int len = (int)(strlen(spoolFilePrefixes[printerType]) & 0xFF);
+ BlockMove(&spoolFilePrefixes[printerType][0], &nameBuf[nameBuf[0]+1], len);
+ nameBuf[0] = len;
+ BlockMove("-",&nameBuf[nameBuf[0]+1],1);
+ nameBuf[0] += 1;
+ NumToString(curTicks,tickString);
+ len = 31 - (1 + nameBuf[0] + 1);
+ if ((short)tickString[0] > len) {
+ tickString[0] = len & 0xFF;
+ }
+ BlockMove(&tickString[1],&nameBuf[nameBuf[0]+1],len);
+ nameBuf[0] += tickString[0];
+}
+#else
+void NewSpoolFilename(int printerType, char *nameBuf)
+{
+ /* creates a new filename in the provided 34-byte buffer,
+ * in the form <typePrefix>-YYYYMMDD-HHMMSS, e.g. "IW-20260121-092756"
+ */
+ char tmpStr[34];
+ DateTimeRec dtRec;
+ int len;
+ unsigned long seconds;
+ GetDateTime(&seconds);
+ Secs2Date(seconds,&dtRec);
+
+ len = (int)(strlen(spoolFilePrefixes[printerType]) & 0xFF);
+ BlockMove(&spoolFilePrefixes[printerType][0], &nameBuf[nameBuf[0]+1], len);
+ nameBuf[0] = len;
+ sprintf(tmpStr,"-%d%02d%02d-%02d%02d%02d",
+ dtRec.year,dtRec.month,dtRec.day,
+ dtRec.hour,dtRec.minute,dtRec.second);
+ len = strlen(tmpStr);
+ BlockMove(tmpStr,&nameBuf[nameBuf[0]+1],len);
+ nameBuf[0] += len;
+}
+#endif
+
+OSErr OpenSpoolFile(int printerType)
+{
+ /* creates a new spool file in the spool directory,
+ * using printer type as the prefix for the filename,
+ * and opens it for writing.
+ * Note: InitSpoolFolder must have been called first.
+ * The file's fRefNum is stored in spoolFileRefNum.
+ */
+ HParamBlockRec pb;
+ short vRefNum = spoolVRefNum; /* set up by InitSpoolFolder */
+ long dirID = spoolDirID;
+ OSErr err;
+
+ NewSpoolFilename(printerType,spoolFileName);
+ MessageWithQuotedString(LOG_ERR,"\pSpooling to file",spoolFileName);
+
+ pb.fileParam.ioCompletion = nil;
+ pb.fileParam.ioFDirIndex = 0;
+ pb.fileParam.ioNamePtr = spoolFileName;
+ pb.fileParam.ioVRefNum = vRefNum;
+ pb.fileParam.ioDirID = dirID;
+ err = PBHCreate(&pb,false);
+ if (err != noErr && err != dupFNErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pCould not create spool file:",err);
+ return err;
+ }
+ pb.fileParam.ioNamePtr = spoolFileName;
+ pb.fileParam.ioFDirIndex = 0;
+ pb.fileParam.ioVRefNum = vRefNum;
+ pb.fileParam.ioDirID = dirID;
+ pb.ioParam.ioPermssn = fsRdWrPerm; /* want exclusive read-write access */
+ pb.ioParam.ioMisc = nil;
+ err = PBHOpen(&pb,false);
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pCould not open spool file:",err);
+ return err;
+ }
+ spoolFileRefNum = pb.ioParam.ioRefNum;
+ return err;
+}
+
+OSErr WriteDataToSpoolFile(long count, void *data)
+{
+ OSErr err = noErr;
+ if (!spoolFileRefNum) {
+ err = fnOpnErr; /* no spool file open! */
+ }
+#if USING_CHUNK_LENGTHS
+ if (err == noErr) {
+ /* the length of each data chunk comes from atpActCount,
+ * which is defined as a short integer (16 bits).
+ * ergo, we only have to store lengths as 2 bytes.
+ * Note this is in network byte order on 68k/PPC.
+ */
+ short chunkLength = (short)(count & 0x0000FFFF);
+ long chunkLengthCount = 2;
+ err = FSWrite(spoolFileRefNum,&chunkLengthCount,&chunkLength);
+ }
+#endif
+ if (err == noErr) {
+ long chunkCount = count;
+ err = FSWrite(spoolFileRefNum,&chunkCount,data);
+ }
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pCould not write data:", err);
+ }
+ return err;
+}
+
+OSErr SetFileInfo(char *fNameBuf, short vRefNum, long dirID,
+ long fType, long fCreator)
+{
+ OSErr err;
+ HParamBlockRec pb;
+ pb.fileParam.ioCompletion = nil;
+ pb.fileParam.ioNamePtr = fNameBuf;
+ pb.fileParam.ioVRefNum = vRefNum;
+ pb.fileParam.ioDirID = dirID;
+ pb.fileParam.ioFDirIndex = 0;
+ err = PBHGetFInfo(&pb,false);
+ if (err != noErr) {
+ MessageWithIntValue(LOG_WARN_X,"\pCould not get file's FInfo:",err);
+ return err;
+ }
+ pb.fileParam.ioCompletion = nil;
+ pb.fileParam.ioNamePtr = fNameBuf;
+ pb.fileParam.ioVRefNum = vRefNum;
+ pb.fileParam.ioDirID = dirID;
+ pb.fileParam.ioFDirIndex = 0;
+ pb.fileParam.ioFlFndrInfo.fdType = fType;
+ pb.fileParam.ioFlFndrInfo.fdCreator = fCreator;
+ pb.fileParam.ioFlFndrInfo.fdFlags &= ~(1<<8); /* clear "inited" bit */
+ err = PBHSetFInfo(&pb,false);
+ if (err != noErr) {
+ MessageWithIntValue(LOG_WARN_X,"\pCould not set file's FInfo:",err);
+ }
+ return err;
+}
+
+OSErr MakeIWEmPostscriptFile(void)
+{
+ OSErr err;
+ HParamBlockRec pb;
+ char psFileName[34];
+ char IWEmFileName[34];
+ short IWEmFileRefNum;
+ short psFileRefNum;
+ short vRefNum = spoolVRefNum; /* set up by InitSpoolFolder */
+ long dirID = spoolDirID;
+ long count;
+ char buf[1024];
+ err = noErr;
+ if (!(spoolFileName[0] > 2 && spoolFileName[3] == '-' &&
+ ((spoolFileName[1] == 'I' && spoolFileName[2] == 'W') ||
+ (spoolFileName[1] == 'L' && spoolFileName[2] == 'Q')))) {
+ return err; /* not a file starting with 'LQ-' or 'IW-' so ignore it */
+ }
+ BlockMove("\pIWEm",IWEmFileName,5);
+ BlockMove(spoolFileName,psFileName,29); /* copy 28 bytes max, plus length byte */
+ BlockMove(".ps",&psFileName[psFileName[0]+1],3); /* add .ps extension */
+ psFileName[0] += 3;
+
+ /* try to open a file named IWEm in the spool directory */
+ pb.fileParam.ioNamePtr = IWEmFileName;
+ pb.fileParam.ioFDirIndex = 0;
+ pb.fileParam.ioVRefNum = vRefNum;
+ pb.fileParam.ioDirID = dirID;
+ pb.ioParam.ioPermssn = fsRdPerm; /* read access */
+ pb.ioParam.ioMisc = nil;
+ err = PBHOpen(&pb,false);
+ if (err != noErr) {
+ return err; /* file doesn't exist or otherwise can't be used */
+ }
+ IWEmFileRefNum = pb.ioParam.ioRefNum;
+
+ /* create and open the output PS file */
+ pb.fileParam.ioCompletion = nil;
+ pb.fileParam.ioFDirIndex = 0;
+ pb.fileParam.ioNamePtr = psFileName;
+ pb.fileParam.ioVRefNum = vRefNum;
+ pb.fileParam.ioDirID = dirID;
+ err = PBHCreate(&pb,false);
+ if (err != noErr && err != dupFNErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pCould not create PostScript file:",err);
+ return err;
+ }
+ pb.fileParam.ioNamePtr = psFileName;
+ pb.fileParam.ioFDirIndex = 0;
+ pb.fileParam.ioVRefNum = vRefNum;
+ pb.fileParam.ioDirID = dirID;
+ pb.ioParam.ioPermssn = fsRdWrPerm; /* want exclusive read-write access */
+ pb.ioParam.ioMisc = nil;
+ err = PBHOpen(&pb,false);
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pCould not open PostScript file:",err);
+ return err;
+ }
+ psFileRefNum = pb.ioParam.ioRefNum;
+
+ MessageWithQuotedString(LOG_WARN,"\pWriting PostScript file",psFileName);
+
+ /* write IWEm file to the output file */
+ err = SetFPos(IWEmFileRefNum,fsFromStart,0L);
+ while (!err) {
+ count = sizeof(buf);
+ err = FSRead(IWEmFileRefNum,&count,buf);
+ if (err == noErr || (err == eofErr && count > 0)) {
+ err = FSWrite(psFileRefNum,&count,buf);
+ }
+ }
+ err = FSClose(IWEmFileRefNum);
+
+ /* read data chunks from the spool file and write to output file */
+ err = SetFPos(spoolFileRefNum,fsFromStart,0L);
+ while (!err) {
+#if USING_CHUNK_LENGTHS
+ short int chunkLengthData;
+ count = 2;
+ err = FSRead(spoolFileRefNum,&count,&chunkLengthData);
+ if (err == noErr) {
+ count = chunkLengthData;
+ err = FSRead(spoolFileRefNum,&count,buf);
+ if (err == noErr || (err == eofErr && count > 0)) {
+ err = FSWrite(psFileRefNum,&count,buf);
+ }
+ }
+#else
+ count = sizeof(buf);
+ err = FSRead(spoolFileRefNum,&count,buf);
+ if (err == noErr || (err == eofErr && count > 0)) {
+ err = FSWrite(psFileRefNum,&count,buf);
+ }
+#endif
+ }
+ err = FSClose(psFileRefNum);
+ (void) SetFileInfo(psFileName, vRefNum, dirID, 'TEXT', 'KAHL');
+ return err;
+}
+
+OSErr FinalizeSpoolFile(void)
+{
+ /* Once all the data that's going to be written has been written,
+ * we call this to set the file's FInfo. This is one way to tell
+ * later whether the spool file is "good" or incomplete.
+ */
+ OSErr err;
+ ParamBlockRec pb;
+ if (!spoolFileRefNum) {
+ return noErr; /* nothing to do */
+ }
+ pb.fileParam.ioCompletion = nil;
+ pb.fileParam.ioFRefNum = spoolFileRefNum;
+ err = PBFlushFile(&pb,false);
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pCould not flush spool file to disk:",err);
+ return err;
+ }
+ (void) SetFileInfo(spoolFileName, spoolVRefNum, spoolDirID, 'pjob', 'spoo');
+ return err;
+}
+
+OSErr CloseSpoolFile(long int numBytesWritten)
+{
+ OSErr err;
+ if (!spoolFileRefNum) {
+ return noErr; /* nothing to do */
+ }
+ if (numBytesWritten > 0) {
+ (void) FinalizeSpoolFile();
+ (void) MakeIWEmPostscriptFile();
+ }
+ err = FSClose(spoolFileRefNum);
+ spoolFileRefNum = 0;
+ spoolFileName[0] = 0;
+ return err;
+}
+
+OSErr SaveDataToPrefsFile(long count, void *data)
+{
+ OSErr err;
+ HParamBlockRec pb;
+ short vRefNum = spoolVRefNum; /* set up by InitSpoolFolder */
+ short prefFileRefNum;
+ long dirID = spoolDirID;
+ long actCount;
+
+ pb.fileParam.ioCompletion = nil;
+ pb.fileParam.ioFDirIndex = 0;
+ pb.fileParam.ioNamePtr = prefFileName;
+ pb.fileParam.ioVRefNum = vRefNum;
+ pb.fileParam.ioDirID = dirID;
+ err = PBHCreate(&pb,false);
+ if (err != noErr && err != dupFNErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pCould not create prefs file:",err);
+ return err;
+ }
+ pb.fileParam.ioNamePtr = prefFileName;
+ pb.fileParam.ioFDirIndex = 0;
+ pb.fileParam.ioVRefNum = vRefNum;
+ pb.fileParam.ioDirID = dirID;
+ pb.ioParam.ioPermssn = fsRdWrPerm; /* want exclusive read-write access */
+ pb.ioParam.ioMisc = nil;
+ err = PBHOpen(&pb,false);
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pCould not open prefs file:",err);
+ return err;
+ }
+ prefFileRefNum = pb.ioParam.ioRefNum;
+
+ /* write provided data to the prefs file */
+ err = SetFPos(prefFileRefNum,fsFromStart,0L);
+ actCount = count;
+ if (err == noErr) {
+ err = FSWrite(prefFileRefNum,&actCount,data);
+ }
+ if (err != noErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pCould not write prefs file:",err);
+ return err;
+ }
+ err = FSClose(prefFileRefNum);
+ (void) SetFileInfo(prefFileName, vRefNum, dirID, 'pref', 'spoo');
+ return err;
+}
+
+OSErr CopyDataFromPrefsFile(long *count, void **data)
+{
+ OSErr err;
+ HParamBlockRec pb;
+ short vRefNum = spoolVRefNum; /* set up by InitSpoolFolder */
+ short prefFileRefNum;
+ long dirID = spoolDirID;
+ long actCount;
+ Ptr prefsData;
+
+ pb.fileParam.ioCompletion = nil;
+ pb.fileParam.ioNamePtr = prefFileName;
+ pb.fileParam.ioFDirIndex = 0;
+ pb.fileParam.ioVRefNum = vRefNum;
+ pb.fileParam.ioDirID = dirID;
+ pb.ioParam.ioPermssn = fsRdWrPerm; /* want exclusive read-write access */
+ pb.ioParam.ioMisc = nil;
+ err = PBHOpen(&pb,false);
+ if (err != noErr) {
+ /* note: it's expected that the prefs file won't exist the first time we run */
+ if (err != fnfErr) {
+ MessageWithIntValue(LOG_ERR_X,"\pCould not open prefs file:",err);
+ }
+ return err;
+ }
+ prefFileRefNum = pb.ioParam.ioRefNum;
+
+ actCount = *count;
+ prefsData = NewPtrClear(actCount);
+ if (!prefsData) {
+ err = mFulErr;
+ }
+ if (err == noErr) {
+ err = SetFPos(prefFileRefNum,fsFromStart,0L);
+ }
+ if (err == noErr) {
+ err = FSRead(prefFileRefNum,&actCount,prefsData);
+ }
+ err = FSClose(prefFileRefNum);
+ *count = actCount;
+ *data = prefsData;
+
+ return err;
+}
--- SpoolFiles.h Wed Jan 28 13:37:25 2026
+++ SpoolFiles.h Wed Jan 28 13:37:25 2026
@@ -0,0 +1,34 @@
+/*
+ * SpoolFiles.h
+ * C interface to routines for handling spool files and folders.
+ *
+ * Written by: Ken McLeod, 2026-01-14
+ * Last update: 2026-01-28
+ *
+ * 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.
+ */
+
+#ifndef __SPOOL_FILES__
+#define __SPOOL_FILES__
+
+#include <Types.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+OSErr InitSpoolFolder(void);
+OSErr GetSpoolFolder(short *vRefNum, long *dirID);
+OSErr OpenSpoolFile(int printerType);
+OSErr WriteDataToSpoolFile(long count, void *data);
+OSErr CloseSpoolFile(long int numBytesWritten);
+OSErr SaveDataToPrefsFile(long count, void *data);
+OSErr CopyDataFromPrefsFile(long *count, void **data); /* caller must dispose data */
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __SPOOL_FILES__ */