////////////////////////////////////////////////////////////////////////////////
//
//	MacPrint - Windows NT Print Server for Macintosh Clients
//		Copyright (c) Microsoft Corp., 1991, 1992, 1993
//
//	macpsq.c - Macintosh Print Service queue service routines
//
//	Author: Frank D. Byrum
//		adapted from MacPrint from LAN Manager Services for Macintosh
//
//	DESCRIPTION:
//		This module provides the routines to manage an NT Printer Object
//		on an AppleTalk network.  A QueueServiceThread is started for
//		each NT Printer Object that is to be shared on the AppleTalk
//		network.  This thread publishes an NBP name for the printer,
//		listens for connection requests from Macintosh clients, and
//		handles the communication between the Macintosh and the NT
//		Print Spooler.
//
////////////////////////////////////////////////////////////////////////////////

#include <stdio.h>
#include <stdlib.h>

#include <windows.h>
#include <winsvc.h>
#include <macps.h>
#include <macpsmsg.h>
#include <debug.h>

extern HANDLE DbgSpoolFile;
extern	PQR	 pqrHead;

////////////////////////////////////////////////////////////////////////////////
//
//	QueueServiceThread() - Thread routine to service an NT Printer Object
//
//	DESCRIPTION:
//		This routine fields all AppleTalk PAP requests and service all
//		events associtated with each job.
//
//		pqr ===> points to the Print Queue record for the printer to
//		be serviced.
//
//		On exit from this routine, the queue is shut down and all resources
//		associated with the queue are freed.
//
////////////////////////////////////////////////////////////////////////////////
void
QueueServiceThread(
	PQR		pqr
)
{
	PQR 	*	ppQr;
	PJR			pjr;


	DBGPRINT(("Enter QueueServiceThread for %ws\n", pqr->pPrinterName));
	if (CreateListenerSocket(pqr) != NO_ERROR)
	{
    	DBGPRINT(("ERROR: failed to create session listener.\n"));
        pqr->ExitThread = TRUE;
	}
    else
    {
	    ReportEvent(hEventLog,
		    		EVENTLOG_INFORMATION_TYPE,
			    	EVENT_CATEGORY_ADMIN,
				    EVENT_PRINTER_REGISTERED,
				    NULL,
				    1,
				    0,
				    &(pqr->pPrinterName),
				    NULL);
    }

	//	service jobs until told to exit
	while (!pqr->ExitThread)
	{
		//
		// service PAP events.	HandleNextPAPEvent will wait for up to 2
		// seconds for a read or open to occur on this queue.  If one
		// happens, pjr is the job record the event happened on.  If
		// pjr is NULL, then no event was found.
		//
		HandleNextPAPEvent(pqr);

		//
		// check for service stop
		//
		if (WaitForSingleObject(hevStopRequested, 0) == WAIT_OBJECT_0)
		{
			DBGPRINT(("%ws thread gets service stop request\n", pqr->pPrinterName));
			pqr->ExitThread = TRUE;
			break;
		}
	} // end while !ExitThread

	DBGPRINT(("%ws received signal to die\n", pqr->pPrinterName));

	// Remove all outstanding pending jobs
	DBGPRINT(("%ws removing pending jobs\n", pqr->pPrinterName));

	while ((pjr = pqr->PendingJobs) != NULL)
    {
		RemoveJob(pjr);
    }


	// close the listener
	DBGPRINT(("%ws closing listener socket\n", pqr->pPrinterName));
    if (pqr->sListener != INVALID_SOCKET)
    {
	    closesocket(pqr->sListener);

	    // report printer removed
	    DBGPRINT(("%ws reporting printer removed\n", pqr->pPrinterName));
	    ReportEvent(hEventLog,
		    		EVENTLOG_INFORMATION_TYPE,
				    EVENT_CATEGORY_ADMIN,
				    EVENT_PRINTER_DEREGISTERED,
				    NULL,
				    1,
				    0,
				    &(pqr->pPrinterName),
				    NULL);
    }


	// remove ourselves from the queue list
	DBGPRINT(("queue thread waiting for the queue list mutex\n"));
	WaitForSingleObject(mutexQueueList, INFINITE);
	DBGPRINT(("queue thread removing self from queue\n"));

	for (ppQr = &pqrHead; ; ppQr = &(*ppQr)->pNext)
	{
		if (*ppQr == pqr)
		{
			*ppQr = pqr->pNext;
			break;
		}
	}

	DBGPRINT(("queue thread releasing list mutex\n"));
	ReleaseMutex(mutexQueueList);


	// close the handle to the thread that was opened on create
	CloseHandle(pqr->hThread);


	DBGPRINT(("closed thread for %ws\n", pqr->pPrinterName));


    //	all of this memory allocated in PScriptQInit()
	DBGPRINT(("%ws freeing memory\n", pqr->pPrinterName));

	if (pqr->pPrinterName != NULL)
    {
	    LocalFree(pqr->pPrinterName);
    }

	if (pqr->pMacPrinterName != NULL)
    {
	    LocalFree(pqr->pMacPrinterName);
    }

	if (pqr->pDriverName != NULL)
    {
	    LocalFree(pqr->pDriverName);
    }

	if (pqr->IdleStatus != NULL)
    {
	    LocalFree(pqr->IdleStatus);
    }

	if (pqr->SpoolingStatus != NULL)
    {
	    LocalFree(pqr->SpoolingStatus);
    }

	if (pqr->pPortName != NULL)
	{
		LocalFree(pqr->pPortName);
	}

	if (pqr->pDataType != NULL)
	{
		LocalFree(pqr->pDataType);
	}

	if (pqr->fonts != NULL)
	{
		LocalFree(pqr->fonts);
	}

	LocalFree(pqr);

	DBGPRINT(("leaving QueueServiceThread\n"));
}



////////////////////////////////////////////////////////////////////////////////
//
//	HandleNewJob() - Handle the open of a print job from a Macintosh
//
//	DESCRIPTION:
//		This routine does the necessary processing to handle the open
//		of a PAP connection from a Macintosh.
//
//		If this routine is unable to complete the processesing necessary
//		to open a job, the job is cancelled, the job data structures are
//		cleaned up.
//
////////////////////////////////////////////////////////////////////////////////
DWORD
HandleNewJob(
	PQR		pqr
)
{
	PJR			        pjr = NULL;
	DOC_INFO_1	        diJobInfo;
	PRINTER_DEFAULTS	pdDefaults;
	DWORD		        dwError = NO_ERROR;
	BOOL		        boolOK = TRUE;
	DWORD		        rc = NO_ERROR;
    PJOB_INFO_2         pji2GetJob=NULL;
    DWORD               dwNeeded;
	int			        fNonBlocking;

	DBGPRINT(("enter HandleNewJob()\n"));

	do
	{
		// allocate a job structure
		if ((rc = CreateNewJob(pqr)) != NO_ERROR)
		{
			DBGPRINT(("FAIL - cannot create a new job structure\n"));
			break;
		}

		pjr = pqr->PendingJobs;

		// accept the connection
		if ((pjr->sJob = accept(pqr->sListener, NULL, NULL)) == INVALID_SOCKET)
		{
			rc = GetLastError();
			DBGPRINT(("accept() fails with %d\n", rc));
			break;
		}

		// make the socket non-blocking
		fNonBlocking = 1;
		if (ioctlsocket(pjr->sJob, FIONBIO, &fNonBlocking) == SOCKET_ERROR)
		{
			rc = GetLastError();
			DBGPRINT(("ioctlsocket(FIONBIO) fails with %d\n", rc));
			break;
		}

		// initialize an NT print job
		pdDefaults.pDatatype = pqr->pDataType;
		pdDefaults.pDevMode = NULL;
		pdDefaults.DesiredAccess = PRINTER_ACCESS_USE;

		if (!OpenPrinter(pqr->pPrinterName, &pjr->hPrinter, &pdDefaults))
		{
			rc = GetLastError();
			DBGPRINT(("OpenPrinter() fails with %d\n"));
			pjr->hPrinter = INVALID_HANDLE_VALUE;
			break;
		}

		diJobInfo.pDocName = NULL;
		diJobInfo.pOutputFile = NULL;
		diJobInfo.pDatatype = pqr->pDataType;

		pjr->dwJobId = StartDocPrinter(pjr->hPrinter, 1, (LPBYTE) &diJobInfo);
		if (pjr->dwJobId == 0)
		{
			rc = GetLastError();
			DBGPRINT(("StartDocPrinter() fails with %d\n", rc));
			break;
		}

#if DBG_SPOOL_LOCALLY
    if (DbgSpoolFile == INVALID_HANDLE_VALUE)
    {
        DbgSpoolFile = CreateFile( L"e:\\tmp\\injob.ps",
                            GENERIC_READ|GENERIC_WRITE,
                            FILE_SHARE_READ,
                            NULL,
                            OPEN_ALWAYS,
                            FILE_ATTRIBUTE_NORMAL|FILE_ATTRIBUTE_TEMPORARY,
                            NULL );
    }
#endif


        //
        // set pParameters field of the jobinfo to a unique string that our
        // monitor can identify, so that it can know if the job came from a Mac.
        //

        dwNeeded = 1024;
        while (1)
        {
            pji2GetJob = LocalAlloc( LMEM_FIXED, dwNeeded );
            if (pji2GetJob == NULL)
            {
			    DBGPRINT(("HandleNewJob: alloc for %d bytes failed\n", dwNeeded));
			    rc = ERROR_INSUFFICIENT_BUFFER;
			    break;
            }

            rc = 0;
            if (!GetJob( pjr->hPrinter, pjr->dwJobId, 2,
                         (LPBYTE)pji2GetJob, dwNeeded, &dwNeeded ))
            {
                rc = GetLastError();
            }

            if ( rc == ERROR_INSUFFICIENT_BUFFER )
            {
                LocalFree(pji2GetJob);
            }
            else
            {
                break;
            }
        }

        if (rc != 0)
        {
		    DBGPRINT(("HandleNewJob: GetJob failed, rc=%d\n", rc));
			break;
        }

        pji2GetJob->pParameters = LFILTERCONTROL;
        pji2GetJob->Position = JOB_POSITION_UNSPECIFIED;

        SetJob( pjr->hPrinter,pjr->dwJobId, 2, (LPBYTE)pji2GetJob, 0 );

        LocalFree(pji2GetJob);

		pjr->FirstWrite = TRUE;

		// prime for a read
		if (setsockopt(pjr->sJob,
					   SOL_APPLETALK,
					   SO_PAP_PRIME_READ,
					   pjr->bufPool[pjr->bufIndx].Buffer,
					   PAP_DEFAULT_BUFFER) == SOCKET_ERROR)
		{
			DBGPRINT(("setsockopt(SO_PAP_PRIME_READ) fails with %d\n", GetLastError()));
			rc = GetLastError();
			break;
		}
	} while (FALSE);

	if ((rc != NO_ERROR) && (NULL != pjr))
	{
		RemoveJob(pjr);
	}

	return rc;
}




////////////////////////////////////////////////////////////////////////////////
//
//	HandleRead() - Handle a read event from a Macintosh print job
//
//	DESCRIPTION:
//		This routine does the necessary processing to handle a read
//		on a PAP connection from a Macintosh.
//
////////////////////////////////////////////////////////////////////////////////
DWORD
HandleRead(
	PJR		pjr
)
{
	DWORD			rc = NO_ERROR;
	DWORD			dwParseError = NO_ERROR;
	PQR 			pqr = pjr->job_pQr;
    WSABUF          wsaBuf;
	int				iRecvFlags = 0;
    DWORD           dwBytesRead;
    BOOL            fRemoveJob = FALSE;
#if	DBG
	int				CheckPoint = 0;
#endif

	DBGPRINT(("enter HandleRead()\n"));

	do
	{
		// get the data.  recv() will return the negative count of
		// bytes read if EOM is not set.  SOCKET_ERROR is -1.

        wsaBuf.len = pjr->dwFlowQuantum * PAP_QUANTUM_SIZE;
        wsaBuf.buf = pjr->bufPool[pjr->bufIndx].Buffer;

		if (WSARecv(pjr->sJob,
			  	    &wsaBuf,
			  	    1,
                    &pjr->cbRead,
			  	    &iRecvFlags,
                    NULL,
                    NULL) == SOCKET_ERROR)
		{
			DBGPRINT(("CheckPoint = %d\n", CheckPoint = 1));
			rc = GetLastError();
			DBGPRINT(("recv() fails with %d, removing job\n", rc));
			if (rc == WSAEDISCON)
				rc = NO_ERROR;
			RemoveJob(pjr);
			break;
		}

		// if this is flagged EOM, echo the EOM and ignore any error
		// (disconnect will show when we try to prime for a read)

		pjr->EOFRecvd = FALSE;
		if (iRecvFlags != MSG_PARTIAL)
		{
			rc = TellClient(pjr, TRUE, NULL, 0);
			pjr->EOFRecvd = TRUE;
            pjr->EOFRecvdAt = GetTickCount();
		}

		DBGPRINT(("%ws: Read (%d%s)\n", pqr->pPrinterName,
				pjr->cbRead, pjr->EOFRecvd ? ", EOF" : ""));

		// deal with the pending buffer if there is one
		pjr->DataBuffer = pjr->bufPool[pjr->bufIndx].Buffer;
		pjr->XferLen = pjr->cbRead;
		if (pjr->PendingLen)
		{
			DBGPRINT(("USING PENDING BUFFER\n"));
			pjr->DataBuffer -= pjr->PendingLen;
			pjr->XferLen += pjr->PendingLen;
			pjr->PendingLen = 0;
		}

		// setup buffers for next read
		pjr->bufIndx ^= 1;

		// prime for the next read if we haven't disconnected
		if (rc == NO_ERROR)
		{
			DBGPRINT(("priming for another read\n"));
			if (setsockopt(pjr->sJob,
							SOL_APPLETALK,
							SO_PAP_PRIME_READ,
							pjr->bufPool[pjr->bufIndx].Buffer,
							PAP_DEFAULT_BUFFER) == SOCKET_ERROR)
			{
				rc = GetLastError();
				DBGPRINT(("setsockopt() fails with %d\n", rc));

				//
				// this call could fail if the client has disconnected.  Therefore,
				// we parse the data we have received first, then return this
				// error code.
				//
			}
		}

		// parse this data.
		switch (dwParseError = PSParse(pjr, pjr->DataBuffer, pjr->XferLen))
		{
			case NO_ERROR:
				break;

			case ERROR_NOT_SUPPORTED:
				//
				// job from a downlevel client
				//
				DBGPRINT(("aborting a downlevel driver job\n"));
				ReportEvent(hEventLog,
							EVENTLOG_WARNING_TYPE,
							EVENT_CATEGORY_ADMIN,
							EVENT_DOWNLEVEL_DRIVER,
							NULL,
							0,
							0,
							NULL,
							NULL);
				DBGPRINT(("CheckPoint = %d\n", CheckPoint = 2));
                fRemoveJob = TRUE;
				break;

			case ERROR_INVALID_PARAMETER:
				//
				// PostScript DSC error.
				//
				DBGPRINT(("ERROR on PSParse().  Aborting job\n"));
				ReportEvent(hEventLog,
							EVENTLOG_WARNING_TYPE,
							EVENT_CATEGORY_USAGE,
							EVENT_DSC_SYNTAX_ERROR,
							NULL,
							1,
							0,
							(LPCWSTR *)(&pjr->pszUser),
							NULL);
				DBGPRINT(("CheckPoint = %d\n", CheckPoint = 3));
                fRemoveJob = TRUE;
				break;

			case WSAEINVAL:
				//
				// TellClient got a disconnect
				//
				DBGPRINT(("CheckPoint = %d\n", CheckPoint = 4));
				DBGPRINT(("PSParse returns WSAEINVAL, RemoveJob for disconnect\n"));
                fRemoveJob = TRUE;
				break;

			default:
				//
				// some other error - report unknown error
				// and remove job
				//
				DBGPRINT(("CheckPoint = %d\n", CheckPoint = 5));
				DBGPRINT(("PSParse returns error %d\n", dwParseError));
				ReportWin32Error(dwParseError);
                fRemoveJob = TRUE;
		}

		// rc is the return code for TellClient.  If it is an error, we
		// have a disconnect and need to return it.  If it's not, psparse
		// could have gotten a disconnect and we need to return that
		if (rc != NO_ERROR || (fRemoveJob == TRUE))
		{
			DBGPRINT(("HandleRead: rc = %d, fRemoveJob = %d, so removejob\n",rc,fRemoveJob));
			RemoveJob(pjr);
			rc = NO_ERROR;
		}
	} while (FALSE);

	return rc;
}



////////////////////////////////////////////////////////////////////////////////
//
//	CreateNewJob() - Initialize a job data structure
//
//	DESCRIPTION:
//		This routine allocates, initializes and links a job data structure to the
//		job chain for a queue.
//
//		if this fails (due to lack of memory), the returned value is NULL.
//		Otherwise, it is a pointer to a job structure.
//
////////////////////////////////////////////////////////////////////////////////
DWORD CreateNewJob(PQR pqr)
{

	PJR			pjr = NULL;
	DWORD		rc = NO_ERROR;

	DBGPRINT(("enter CreateNewJob(%ws)\n", pqr->pPrinterName));

	do
	{
		// allocate a job structure
		if ((pjr = (PJR)LocalAlloc(LPTR, sizeof(JOB_RECORD))) == NULL)
		{
			//
			// log an error and return
			//
			rc = GetLastError();
			DBGPRINT(("LocalAlloc(pjr) fails with %d\n", rc));
			break;
		}

		// initialize job structure
		pjr->job_pQr = pqr;
		pjr->NextJob = NULL;
		pjr->dwFlags = JOB_FLAG_NULL;
		pjr->hPrinter = INVALID_HANDLE_VALUE;
		pjr->dwJobId = 0;
		pjr->sJob = INVALID_SOCKET;
		pjr->hicFontFamily = INVALID_HANDLE_VALUE;
		pjr->hicFontFace = INVALID_HANDLE_VALUE;
		pjr->dwFlowQuantum = 8;
		pjr->XferLen = 0;
		pjr->DataBuffer = NULL;
		pjr->bufPool = (PBR)(pjr->buffer);
		pjr->bufIndx = 0;
		pjr->cbRead = 0;
		pjr->PendingLen = 0;
		pjr->psJobState = psStandardJob;
		pjr->JSState = JSWrite;
		pjr->SavedJSState = JSWrite;
		pjr->InProgress = NOTHING;
		pjr->InBinaryOp = 0;
#if DBG
		pjr->PapEventCount = 1;
#endif
		pjr->JSKeyWord[0] = 0;

		// get an information context for font family query
		if ((pjr->hicFontFamily = CreateIC(pqr->pDriverName,
											pqr->pPrinterName,
											pqr->pPortName,
											NULL)) == NULL)
		{
			rc = GetLastError();
			DBGPRINT(("CreateIC(hicFontFamily) fails with %d\n", rc));
			break;
		}

		// get an information context for font face query
		if ((pjr->hicFontFace = CreateIC(pqr->pDriverName,
										pqr->pPrinterName,
										pqr->pPortName,
										NULL)) == NULL)
		{
			rc = GetLastError();
			DBGPRINT(("CreateIC(hicFontFace) fails with %d\n", rc));
			break;
		}

		// if this is first job, bump thread priority and change our status
		if (pqr->PendingJobs == NULL)
		{
			DBGPRINT(("first job on queue, bumping thread priority\n"));
			SetThreadPriority(pqr->hThread, THREAD_PRIORITY_ABOVE_NORMAL);

			// Change our status from idle to spooling
			DBGPRINT(("setting status to %s\n", pqr->IdleStatus));
			if ((setsockopt(pqr->sListener,
							SOL_APPLETALK,
							SO_PAP_SET_SERVER_STATUS,
							pqr->SpoolingStatus,
							strlen(pqr->SpoolingStatus))) == SOCKET_ERROR)
			{
				rc = GetLastError();
				DBGPRINT(("setsockopt(status) fails with %d\n", rc));
				break;
			}
		}

		// Add the new job to the list of pending jobs for this print queue.
		pjr->NextJob = pqr->PendingJobs;
		pqr->PendingJobs = pjr;
	} while (FALSE);

	if (rc != NO_ERROR)
	{
		if (pjr != NULL)
		{
			if ((pjr->hicFontFamily != NULL) && (pjr->hicFontFamily != INVALID_HANDLE_VALUE))
			{
				DeleteDC(pjr->hicFontFamily);
			}

			if ((pjr->hicFontFace != NULL) && (pjr->hicFontFace != INVALID_HANDLE_VALUE))
			{
				DeleteDC(pjr->hicFontFace);
			}

			LocalFree(pjr);
		}
	}

	return rc;
}




////////////////////////////////////////////////////////////////////////////////
//
//	RemoveJob() - Close a job and clean up the job list
//
//	DESCRIPTION:
//		This routine examines the state of a job and cleans up appropriately.
//		It then unlinks the job structure from the job list and frees it.
//
////////////////////////////////////////////////////////////////////////////////
void
RemoveJob(
	PJR		pjr
)
{
	PJR *	ppjob;
	char	psEOF = '\04';
	DWORD	cbWritten;
	PQR		pqr = pjr->job_pQr;

	DBGPRINT(("enter RemoveJob(%ws)\n", pqr->pPrinterName));

	// find the job in the pending list
	ppjob = &pqr->PendingJobs;
	while (*ppjob != NULL && *ppjob != pjr)
		ppjob = &(*ppjob)->NextJob;

	// remove it from the list
	*ppjob = pjr->NextJob;

	// clean up the socket
	if (pjr->sJob != INVALID_SOCKET)
	{
		DBGPRINT(("closing socket\n"));
		closesocket(pjr->sJob);
	}

	// clean up information contexts
	if (pjr->hicFontFamily != NULL)
	{
		DeleteDC(pjr->hicFontFamily);
	}

	if (pjr->hicFontFace != NULL)
	{
		DeleteDC(pjr->hicFontFace);
	}

	// end the NT print job and close the printer
	if (pjr->hPrinter != INVALID_HANDLE_VALUE)
	{
		if (pqr->ExitThread)
		{
			// we are aborting, so delete the job
			if (!SetJob(pjr->hPrinter, pjr->dwJobId, 0, NULL, JOB_CONTROL_CANCEL))
			{
				DBGPRINT(("ERROR: unable to cancel print job on service stop, rc=%d\n", GetLastError()));
			}
		}

		// Do not write anything if we have not written anything yet !!!
		if (!pjr->FirstWrite && !wcscmp(pqr->pDataType, MACPS_DATATYPE_RAW))
		{
			WritePrinter(pjr->hPrinter,
						 &psEOF,
						 1,
						 &cbWritten);
		}

		EndDocPrinter(pjr->hPrinter);

#if DBG_SPOOL_LOCALLY
        CloseHandle(DbgSpoolFile);
        DbgSpoolFile = INVALID_HANDLE_VALUE;
#endif

		ClosePrinter(pjr->hPrinter);
	}

	// if all the jobs in this queue handled, drop back to normal priority
	if (pqr->PendingJobs == NULL)
	{
		DBGPRINT(("last job removed, dropping thread priority\n"));
		SetThreadPriority(pqr->hThread, THREAD_PRIORITY_NORMAL);

		// change the status from spooling to idle
		DBGPRINT(("setting status to %s\n", pqr->IdleStatus));
		setsockopt(pqr->sListener,
				   SOL_APPLETALK,
				   SO_PAP_SET_SERVER_STATUS,
				   pqr->IdleStatus,
				   strlen(pqr->IdleStatus));
	}

	// free the job structure
	LocalFree(pjr);
}



////////////////////////////////////////////////////////////////////////////////
//
//	HandleNextPAPEvent() - Wait for a PAP event
//
//	DESCRIPTION:
//		This routine waits for a service stop request or an Open or Read to
//		complete on an outstanding job.  In the event of an Open or Read
//		event, the routine finds the job that the event completed for and
//		returns a pointer to that job.
//
//		In the case of a service stop event, the return value is NULL
//
//	NOTES:
//
//		Finding the job that corresponds to the event is tricky.  In the
//		case of the open event it is simple as only one job ever has an
//		open pending.  However, for reads, most jobs will have reads
//		pending simultaneously.
//
//		To find a job with a completed read, we depend on three things.
//		First, all reads are done so that they will trigger a single
//		NT Event.  When this event is signalled, we start looking for
//		completed reads.  Second, when a read completes it changes a
//		status code that is stored on a per job basis, so it's possible
//		to walk a list to find reads that have completed.  Third, we
//		need to be careful about when we reset the event.  The race
//		condition to avoid is between walking the list and reseting
//		the event.  If there are reads outstanding, a read at the beginning
//		of the list could complete before we finish walking the list.
//		To avoid this, we only reset the event when no reads are outstanding
//
////////////////////////////////////////////////////////////////////////////////
void
HandleNextPAPEvent(
	PQR		pqr
)
{
	DWORD	rc = NO_ERROR;
	DWORD	dwIndex;
	PJR		pjr, pjrNext, pjrOrgFirst;
	fd_set	readfds;
	fd_set	exceptfds;
	struct	timeval	timeout;
	int		cEvents;

	do
	{
        //
        // check to see if any OTI-jobs need to be timed out
        // this is a hack to work-around the Apple's OTI bug where the Mac client fails to
        // send the ConnectionClose to us after it has sent EOF (because it crashes!).  To
        // avoid the job staying in our spooler forever, we force the connection closed if
        // we haven't heard from the mac for 60 seconds after it sends an EOF
        //
        pjr = pqr->PendingJobs;
        while(pjr != NULL)
    	{
		    pjrNext = pjr->NextJob;

            if (pjr->EOFRecvd && EXECUTE_OTI_HACK(pjr->EOFRecvdAt))
            {
				DBGPRINT(("%ws must be OTI user ! closing the connection on behalf of client!\n",pjr->pszUser));
		    	RemoveJob(pjr);
            }

		    pjr = pjrNext;
    	}

		// setup socket list with all pending jobs and listener socket
		FD_ZERO(&readfds);
		FD_ZERO(&exceptfds);
		FD_SET(pqr->sListener, &readfds);

		for (dwIndex = 1, pjr = pqr->PendingJobs;
			 (dwIndex < FD_SETSIZE) && (pjr != NULL);
			 dwIndex++, pjr = pjr->NextJob)
		{
			FD_SET(pjr->sJob, &readfds);
			FD_SET(pjr->sJob, &exceptfds);
		}

		// wait for up to 2 seconds for a set of sockets to be ready
		timeout.tv_sec = 2;
		timeout.tv_usec = 0;


		if ((cEvents = select(0, &readfds, NULL, &exceptfds, &timeout)) == SOCKET_ERROR)
		{
			rc = GetLastError();
			DBGPRINT(("select() fails with %d: CLOSING DOWN QUEUE\n", rc));
			pqr->ExitThread = TRUE;
			break;
		}

		if (cEvents == 0)
		{
            // timeout, done
			break;
		}

		// handle a new connection if there is one
		if (FD_ISSET(pqr->sListener, &readfds))
		{
			if ((rc = HandleNewJob(pqr)) != NO_ERROR)
			{
				DBGPRINT(("ERROR - could not open new job - CLOSING DOWN QUEUE\n"));
				pqr->ExitThread = TRUE;
				break;
			}
		}

		pjr = pqr->PendingJobs;
        pjrOrgFirst = NULL;

        // since every pjr that succeeds on select goes to the tail of the list, make
        // sure we have a way of getting out of this loop!  pjrOrgFirst is the way
        while(pjr != NULL && pjr != pjrOrgFirst)
		{
			pjrNext = pjr->NextJob;

			if (FD_ISSET(pjr->sJob, &exceptfds))
			{
				DBGPRINT(("job for user %ws ends\n", pjr->pszUser));
				RemoveJob(pjr);
			}

			else if (FD_ISSET(pjr->sJob, &readfds))
			{
                // mark the first pjr that's going to be moved to the tail
                if (pjrOrgFirst == NULL)
                {
                    pjrOrgFirst = pjr;
                }

				// Move this job to the end of the queue
				MoveJobAtEnd(pqr, pjr);

				// HandleRead() will remove pjr if a disconnect happens
				HandleRead(pjr);
			}

		    pjr = pjrNext;
		}

		rc = NO_ERROR;
	} while (FALSE);

	if (rc != NO_ERROR)
	{
		ReportWin32Error(rc);
	}
}




/*
** MoveJobAtEnd - Move this job to end of queue.
**
** This is to ensure TRUE round robin scheduling of jobs within a queue.
** Since we always start at head of queue at GetNextPAPEvent, we need to
** do this for any job which got service. The way we achieve this is as
** follows: Ji will be pushed to the end of the queue.
**
** Before the change:
**
** Q -> J1 -> J2 -> ... -> Ji -> Jj -> ... -> Jn -> NULL
**
** After the change:
**
** Q -> J1 -> J2 -> ... -> Jj -> ... -> Jn -> Ji -> NULL
**
** Note that in the boundary conditions of n = 1 OR i = n, it is a NOP i.e.
** its unlinked and linked back - BIG DEAL !!
*/
void
MoveJobAtEnd(PQR pqr, PJR pjr)
{
	PJR *	ppjob = &pqr->PendingJobs;
	BOOL	found = FALSE;

	for (ppjob = &pqr->PendingJobs;
		 *ppjob != NULL;
		 ppjob = &(*ppjob)->NextJob)
	{
		if (*ppjob == pjr)
		{
			/* Unlink it from its current position */
			*ppjob = pjr->NextJob;
			break;
		}
	}

	for (NOTHING;
		 *ppjob != NULL;
		 ppjob = &(*ppjob)->NextJob)
	{
		NOTHING;
	}

	/* Link job at tail */
	*ppjob = pjr;

        // and terminate the tail
        pjr->NextJob = NULL;
}


void
ReportWin32Error (
	DWORD	dwError
)
{
	LPWSTR  pszError = NULL;
	DWORD   rc = NO_ERROR;

	DBGPRINT(("enter ReportWin32Error(%d)\n", dwError));

	do
	{
		if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
							FORMAT_MESSAGE_IGNORE_INSERTS |
							FORMAT_MESSAGE_FROM_SYSTEM,
						  NULL,
						  dwError,
						  0,
						  (LPWSTR)(&pszError),
						  128,
						  NULL) == 0)
		{
			// Report unknown error
			ReportEvent(
				hEventLog,
				EVENTLOG_WARNING_TYPE,
				EVENT_CATEGORY_INTERNAL,
				EVENT_MESSAGE_NOT_FOUND,
				NULL,
				0,
				sizeof(DWORD),
				NULL,
				&dwError);

		}
		else
		{
			// report known error
			ReportEvent(hEventLog,
						EVENTLOG_WARNING_TYPE,
						EVENT_CATEGORY_INTERNAL,
						EVENT_SYSTEM_ERROR,
						NULL,
						1,
						0,
						&pszError,
						NULL);
		}
	} while (FALSE);

	if (NULL != pszError)
	{
		LocalFree(pszError);
	}
}