AmendHub

Download:

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__ */