Retired Document
Important: This sample code may not represent best practices for current development. The project may use deprecated symbols and illustrate technologies and techniques that are no longer recommended.
OTSimpleServerHTTP.c
/* |
File: OTSimpleServerHTTP.c |
Contains: Implementation of the simple HTTP server sample. |
Written by: Quinn "The Eskimo!" |
Copyright: © Copyright 2001 Apple Computer, Inc. All rights reserved. |
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Computer, Inc. |
("Apple") in consideration of your agreement to the following terms, and your |
use, installation, modification or redistribution of this Apple software |
constitutes acceptance of these terms. If you do not agree with these terms, |
please do not use, install, modify or redistribute this Apple software. |
In consideration of your agreement to abide by the following terms, and subject |
to these terms, Apple grants you a personal, non-exclusive license, under AppleÕs |
copyrights in this original Apple software (the "Apple Software"), to use, |
reproduce, modify and redistribute the Apple Software, with or without |
modifications, in source and/or binary forms; provided that if you redistribute |
the Apple Software in its entirety and without modifications, you must retain |
this notice and the following text and disclaimers in all such redistributions of |
the Apple Software. Neither the name, trademarks, service marks or logos of |
Apple Computer, Inc. may be used to endorse or promote products derived from the |
Apple Software without specific prior written permission from Apple. Except as |
expressly stated in this notice, no other rights or licenses, express or implied, |
are granted by Apple herein, including but not limited to any patent rights that |
may be infringed by your derivative works or by other works in which the Apple |
Software may be incorporated. |
The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO |
WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED |
WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN |
COMBINATION WITH YOUR PRODUCTS. |
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR |
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE |
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION |
OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT |
(INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN |
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
Change History (most recent first): |
2/12/2001 MK Carbonized and updated to CodeWarrior 6.0 |
7/23/1999 Karl Groethe Updated for Metrowerks Codewarrior Pro 2.1 |
*/ |
///////////////////////////////////////////////////////////////////// |
// Carbon.h |
#include <Carbon.h> |
///////////////////////////////////////////////////////////////////// |
#include <stdio.h> |
///////////////////////////////////////////////////////////////////// |
// Pick up our own prototype. |
#include "OTSimpleServerHTTP.h" |
///////////////////////////////////////////////////////////////////// |
// When this boolean is to set true, this module assumes that the |
// host application is trying to quit and all the threads created by |
// this module start dying. See the associated comment in the |
// YieldingNotifier routine. |
Boolean gQuitNow = false; |
// RRK Comments added 8/27/97 |
// DoNegotiateIPReuseAddrOption is defined in the file |
// EnableIPReuseAddrSample.c. This call uses the OTOptionManagement function |
// to set the IP level ResuseAddr option so that an IP address can be |
// reused on immediate relaunch of the application. |
extern OSStatus DoNegotiateIPReuseAddrOption(EndpointRef ep, Boolean enableReuseIPMode); |
///////////////////////////////////////////////////////////////////// |
void MoreAssertQ(Boolean mustBeTrue) |
{ |
if ( ! mustBeTrue ) { |
DebugStr("\pMoreAssertQ: Assertion failure."); |
} |
} |
OTNotifyUPP gYieldingNotifierUPP = NULL; |
static pascal void YieldingNotifier(EndpointRef ep, OTEventCode code, |
OTResult result, void* cookie) |
// This simple notifier checks for kOTSyncIdleEvent and |
// when it gets one calls the Thread Manager routine |
// YieldToAnyThread. Open Transport sends kOTSyncIdleEvent |
// whenever it's waiting for something, eg data to arrive |
// inside a sync/blocking OTRcv call. In such cases, we |
// yield the processor to some other thread that might |
// be doing useful work. |
// |
// The routine also checks the gQuitNow boolean to see if the |
// the host application wants us to quit. This roundabout technique |
// avoids a number of problems including: |
// |
// 1. Threads stuck inside OT synchronous calls -- You can't just |
// call DisposeThread on a thread that's waiting for an OT |
// synchronous call to complete. Trust me, it would be bad! |
// Instead, this routine calls OTCancelSynchronousCalls to get |
// out of the call. The given error code (userCanceledErr) |
// propagates out to the caller, which causes the calling |
// thread to eventually terminate. |
// 2. Threads holding resources -- You can't just DisposeThread |
// a networking thread because it might be holding resouces, |
// like memory or endpoints, that need to be cleaned up properly. |
// Cancelling the thread in this way causes the thread's own |
// code to clean up those resources just like it would for any |
// any other error. |
// |
// I could have used a more sophisticated mechanism to support |
// quitting (such as a boolean per thread, or returning some |
// "thread object" to which the application can send a "cancel" |
// message, but this way is easy and works just fine for this |
// simple sample |
{ |
#pragma unused(result) |
#pragma unused(cookie) |
OSStatus junk; |
switch (code) { |
case kOTSyncIdleEvent: |
junk = YieldToAnyThread(); |
MoreAssertQ(junk == noErr); |
if (gQuitNow) { |
junk = OTCancelSynchronousCalls(ep, userCanceledErr); |
MoreAssertQ(junk == noErr); |
} |
break; |
default: |
// do nothing |
break; |
} |
} |
///////////////////////////////////////////////////////////////////// |
static void SetDefaultEndpointModes(EndpointRef ep) |
// This routine sets the supplied endpoint into the default |
// mode used in this application. The specifics are: |
// blocking, synchronous, and using synch idle events with |
// the standard YieldingNotifier. |
{ |
OSStatus junk; |
if (gYieldingNotifierUPP == NULL) |
{ |
gYieldingNotifierUPP = NewOTNotifyUPP((OTNotifyProcPtr)YieldingNotifier); |
} |
junk = OTSetBlocking(ep); |
MoreAssertQ(junk == noErr); |
junk = OTSetSynchronous(ep); |
MoreAssertQ(junk == noErr); |
junk = OTInstallNotifier(ep, gYieldingNotifierUPP, ep); |
MoreAssertQ(junk == noErr); |
junk = OTUseSyncIdleEvents(ep, true); |
MoreAssertQ(junk == noErr); |
} |
///////////////////////////////////////////////////////////////////// |
static OSStatus OTSndQ(EndpointRef ep, void *buf, size_t nbytes) |
// My own personal wrapper around the OTSnd routine that cleans |
// up the error result. |
{ |
OTResult bytesSent; |
bytesSent = OTSnd(ep, buf, nbytes, 0); |
if (bytesSent >= 0) { |
// Because we're running in synchronous blocking mode, OTSnd |
// should not return until it has sent all the bytes unless it |
// gets an error. If it does, we want to hear about it. |
MoreAssertQ(bytesSent == nbytes); |
return (noErr); |
} else { |
return (bytesSent); |
} |
} |
///////////////////////////////////////////////////////////////////// |
static OSErr FSReadQ(short refNum, long count, void *buffPtr) |
// My own wrapper for FSRead. Whose bright idea was it for |
// it to return the count anyway! |
{ |
OSStatus err; |
long tmpCount; |
tmpCount = count; |
err = FSRead(refNum, &count, buffPtr); |
MoreAssertQ((err != noErr) || (count == tmpCount)); |
return (err); |
} |
///////////////////////////////////////////////////////////////////// |
static Boolean StringHasSuffix(const char *str, const char *suffix) |
// Returns true if the end of str is suffix. |
{ |
Boolean result; |
result = false; |
if ( OTStrLength(str) >= OTStrLength(suffix) ) { |
if ( OTStrEqual(str + OTStrLength(str) - OTStrLength(suffix) , suffix) ) { |
result = true; |
} |
} |
return (result); |
} |
static OSStatus ExtractRequestedFileName(const char *buffer, |
char *fileName, char *mimeType) |
// Assuming that buffer is a C string contain an HTTP request, |
// extract the name of the file that's being requested. |
// Also check to see if the file has one of the common suffixes, |
// and set mimeType appropriately. |
// |
// Obviously this routine should use Internet Config to |
// map the file type/creator/extension to a MIME type, |
// but I don't want to complicate the sample with that code. |
{ |
OSStatus err; |
// Default the result to empty. |
fileName[0] = 0; |
// Scan the request looking for the fileName. Obviously this is not |
// a very good validation of the request, but this is an OT sample, |
// not an HTTP one. Also note that we require HTTP/1.0, but some |
// ancient clients might just generate "GET %s<cr><lf>" |
(void) sscanf(buffer, "GET %s HTTP/1.0", fileName); |
// If the file name is still blank, scanf must have failed. |
// Note that I don't rely on the result from scanf because in a |
// previous life I learnt to mistrust it. |
if (fileName[0] == 0) { |
err = -1; |
} else { |
// So the request is cool. Normalise the file name. |
// Requests for the root return "index.html". |
if ( OTStrEqual(fileName, "/") ) { |
OTStrCopy(fileName, "index.html"); |
} |
// Remove the prefix slash. Note that we don't deal with |
// "slashes" embedded in the fileName, so we don't handle |
// any directories other than the root. This would be |
// easy to do, but again this is not an HTTP sample. |
if ( fileName[0] == '/' ) { |
BlockMoveData(&fileName[1], &fileName[0], OTStrLength(fileName)); |
} |
// Set mimeType based on the file's suffix. |
if ( StringHasSuffix(fileName, ".html") ) { |
OTStrCopy(mimeType, "text/html"); |
} else if ( StringHasSuffix(fileName, ".gif") ) { |
OTStrCopy(mimeType, "image/gif"); |
} else if ( StringHasSuffix(fileName, ".jpg") ) { |
OTStrCopy(mimeType, "image/jpeg"); |
} else { |
OTStrCopy(mimeType, "text/plain"); |
} |
err = noErr; |
} |
#if qDebug |
printf("ExtractRequestedFileName: Returning %d, Ò%sÓ, Ò%sÓ\n", err, fileName, mimeType); |
#endif |
return (err); |
} |
///////////////////////////////////////////////////////////////////// |
// The worker thread reads characters one at a time from the endpoint |
// and uses the following state machine to determine when the request is |
// finished. For HTTP/1.0 requests, the request is terminated by |
// two consecutive CR LF pairs. Each time we read one of the appropriate |
// characters we increment the state until we get to kDone, at which |
// point we go off to process the request. |
enum { |
kWorkerWaitingForCR1, |
kWorkerWaitingForLF1, |
kWorkerWaitingForCR2, |
kWorkerWaitingForLF2, |
kWorkerDone |
}; |
// This is the size of the transfer buffer that each worker thread |
// allocates to read file system data and write network data. |
enum { |
kTransferBufferSize = 4096 |
}; |
// WorkerContext holds the information needed by a worker endpoint to |
// operate. A WorkerContext is created by the listener endpoint |
// and passed as the thread parameter to the worker thread. If the |
// listener successfully does this, it's assumed that the worker |
// thread has taken responsibility for freeing the context. |
struct WorkerContext { |
EndpointRef worker; |
short vRefNum; |
long dirID; |
}; |
typedef struct WorkerContext WorkerContext, *WorkerContextPtr; |
// The two buffers hold standard HTTP responses. The first is the |
// default text we spit out when we get an error. The second is |
// the header that we use when we successfully field a request. |
// Again note that this sample is not about HTTP, so these responses |
// are probably not particularly compliant to the HTTP protocol. |
char gDefaultOutputText[] = "HTTP/1.0 200 OK\15\12Content-Type: text/html\15\12\15\12<H1>Say what!</H1><P>\15\12Error Number (%d), Error Text (%s)"; |
char gHTTPHeader[] = "HTTP/1.0 200 OK\15\12Content-Type: %s\15\12\15\12"; |
///////////////////////////////////////////////////////////////////// |
static OSStatus ReadHTTPRequest(EndpointRef worker, char *buffer) |
// This routine reads the HTTP request from the worker endpoint, |
// using the state machine described above, and puts it into the |
// indicated buffer. The buffer must be at least kTransferBufferSize |
// bytes big. |
// |
// This is pretty feeble |
// code (reading data one byte at a time is bad for performance), |
// but it works and I'm not quite sure how to fix it. Perhaps |
// OTCountDataBytes? |
// |
// Also, the code does not support with requests bigger than |
// kTransferBufferSize. In practise, this isn't a problem. |
{ |
OSStatus err; |
long bufferIndex; |
int state; |
char ch; |
OTResult bytesReceived; |
OTFlags junkFlags; |
MoreAssertQ(worker != nil); |
MoreAssertQ(buffer != nil); |
bufferIndex = 0; |
state = kWorkerWaitingForCR1; |
do { |
bytesReceived = OTRcv(worker, &ch, sizeof(char), &junkFlags); |
if (bytesReceived >= 0) { |
MoreAssertQ(bytesReceived == sizeof(char)); |
err = noErr; |
// Put the character into the buffer. |
buffer[bufferIndex] = ch; |
bufferIndex += 1; |
// Check that we still have space to include our null terminator. |
if (bufferIndex >= kTransferBufferSize) { |
err = -1; |
} |
// Do the magic state machine. Note the use of |
// hardwired numbers for CR and LF. This is correct |
// because the Internet standards say that these |
// numbers can't change. I don't use \n and \r |
// because these values change between various C |
// compilers on the Mac. |
switch (ch) { |
case 13: |
switch (state) { |
case kWorkerWaitingForCR1: |
state = kWorkerWaitingForLF1; |
break; |
case kWorkerWaitingForCR2: |
state = kWorkerWaitingForLF2; |
break; |
default: |
state = kWorkerWaitingForCR1; |
break; |
} |
break; |
case 10: |
switch (state) { |
case kWorkerWaitingForLF1: |
state = kWorkerWaitingForCR2; |
break; |
case kWorkerWaitingForLF2: |
state = kWorkerDone; |
break; |
default: |
state = kWorkerWaitingForCR1; |
break; |
} |
break; |
default: |
state = kWorkerWaitingForCR1; |
break; |
} |
} else { |
err = bytesReceived; |
} |
} while ( err == noErr && state != kWorkerDone ); |
if (err == noErr) { |
// Append the null terminator that turns the HTTP request into a C string. |
buffer[bufferIndex] = 0; |
} |
return (err); |
} |
///////////////////////////////////////////////////////////////////// |
static OSStatus CopyFileToEndpoint(const FSSpec *fileSpec, char *buffer, EndpointRef worker) |
// Copy the file denoted by fileSpec to the endpoint. buffer is a |
// temporary buffer of size kTransferBufferSize. Initially buffer |
// contains a C string that is the HTTP header to output. After that, |
// the routine uses buffer as temporary storage. We do this because |
// we want any errors opening the file to be noticed before we send |
// the header saying that the request went through successfully. |
{ |
OSStatus err; |
OSStatus junk; |
long bytesToSend; |
long bytesThisTime; |
short fileRefNum; |
err = FSpOpenDF(fileSpec, fsRdPerm, &fileRefNum); |
if (err == noErr) { |
err = GetEOF(fileRefNum, &bytesToSend); |
// Write the HTTP header out to the endpoint. |
if (err == noErr) { |
err = OTSndQ(worker, buffer, OTStrLength(buffer)); |
} |
// Copy the file in kTransferBufferSize chunks to the endpoint. |
while (err == noErr && bytesToSend > 0) { |
if (bytesToSend > kTransferBufferSize) { |
bytesThisTime = kTransferBufferSize; |
} else { |
bytesThisTime = bytesToSend; |
} |
err = FSReadQ(fileRefNum, bytesThisTime, buffer); |
if (err == noErr) { |
err = OTSndQ(worker, buffer, bytesThisTime); |
} |
bytesToSend -= bytesThisTime; |
} |
// Clean up. |
junk = FSClose(fileRefNum); |
MoreAssertQ(junk == noErr); |
} |
return (err); |
} |
static OSStatus OrderlyDisconnect(EndpointRef ep) |
// Gosh XTI is lame. RcvOrderlyDisconnect is non-blocking |
// (it doesn't wait for the T_ORDREL event) so we have to |
// block in a Rcv call. This is standard XTI practice. |
{ |
OSStatus err; |
UInt8 tmp; |
OTFlags junkFlags; |
OTResult look; |
err = OTSndOrderlyDisconnect(ep); |
if (err == noErr) { |
err = OTRcv(ep, &tmp, sizeof(tmp), &junkFlags); |
if (err == kOTLookErr) { |
look = OTLook(ep); |
switch (look) { |
case T_ORDREL: |
err = OTRcvOrderlyDisconnect(ep); |
break; |
default: |
// leave err set to kOTLookErr |
break; |
} |
} else if (err == noErr) { |
err = kOTLookErr; // something is happening, but it's not a disconnect |
} |
} |
return err; |
} |
///////////////////////////////////////////////////////////////////// |
ThreadEntryUPP gWorkerThreadProcUPP = NULL; |
static pascal OSStatus WorkerThreadProc(WorkerContextPtr context) |
// This routine is the starting routine for the worker thread. |
// The thread is responsible for reading an HTTP request from |
// an endpoint, processing the requesting and writing the results |
// back to the endpoint. |
{ |
OSStatus err; |
OSStatus junk; |
char *buffer = nil; |
char *errStr; |
char fileName[256]; |
char mimeType[256]; |
FSSpec fileSpec; |
printf("WorkerThreadProc: Starting\n"); |
fflush(stdout); |
MoreAssertQ(context != nil); |
MoreAssertQ(context->worker != nil); |
// Allocate the transfer buffer in the heap. |
err = noErr; |
buffer = OTAllocMemInContext(kTransferBufferSize, NULL); |
if (buffer == nil) { |
err = kENOMEMErr; |
} |
// Read the request into the transfer buffer. |
if (err == noErr) { |
err = ReadHTTPRequest(context->worker, buffer); |
} |
if (err == noErr) { |
// Get the requested file name (and it's mimeType) from the |
// HTTP request in the transfer buffer. |
err = ExtractRequestedFileName(buffer, fileName, mimeType); |
if (err == noErr) { |
// Create the appropriate HTTP response in the buffer. |
sprintf(buffer, gHTTPHeader, mimeType); |
// Copy the file (with preceding HTTP header) to the endpoint. |
CopyCStringToPascal(fileName, fileName); |
(void) FSMakeFSSpec(context->vRefNum, context->dirID, fileName, &fileSpec); |
err = CopyFileToEndpoint(&fileSpec, buffer, context->worker); |
} |
// Handle any errors by sending back an appropriate error header. |
if (err != noErr) { |
switch (err) { |
case fnfErr: |
errStr = "File Not Found"; |
break; |
default: |
errStr = "Unknown Error"; |
break; |
} |
sprintf(buffer, gDefaultOutputText, err, errStr); |
err = OTSndQ(context->worker, buffer, OTStrLength(buffer)); |
} |
} |
// Shut down the endpoint and clean up the WorkerContext. |
if (err == noErr) { |
err = OrderlyDisconnect(context->worker); |
} |
junk = OTCloseProvider(context->worker); |
MoreAssertQ(junk == noErr); |
OTFreeMem(context); |
if (buffer != nil) { |
OTFreeMem(buffer); |
} |
printf("WorkerThreadProc: Stopping with final result %d.\n", (int)err); |
fflush(stdout); |
return (noErr); |
} |
///////////////////////////////////////////////////////////////////// |
OSStatus RunHTTPServer(InetHost ipAddr, short vRefNum, long dirID) |
// This routine runs an HTTP server. It doesn't return until |
// someone sets gQuitNow, so you should most probably call this |
// routine on its own thread. ipAddr is the IP address that |
// the server listens on. Specify kOTAnyInetAddress to listen |
// on all IP addresses on the machine; specify an IP address |
// to listen on a specific address. vRefNum and dirID point |
// to the root directory of the HTTP information to be served. |
// |
// The routine creates a listening endpoint and listens for connection |
// requests on that endpoint. When a connection request arrives, it creates |
// a new worker thread (with accompanying endpoint) and accepts the connection |
// on that thread. |
// |
// Note the use of the "tilisten" module which prevents multiple |
// simultaneous T_LISTEN events coming from the transport provider, |
// thereby greatly simplifying the listen/accept sequence. |
{ |
OSStatus err; |
EndpointRef listener; |
TBind bindReq; |
InetAddress ipAddress; |
InetAddress remoteIPAddress; |
TCall call; |
ThreadID workerThread; |
OSStatus junk; |
WorkerContextPtr workerContext; |
TEndpointInfo Info; |
char buf[128]; |
// display IP address in String |
OTInetHostToString(ipAddr, buf); |
printf("HTTP Server on <%s> Starting.\n", buf); |
fflush(stdout); |
// Create the listen endpoint. |
// RRK comments added 8/27/97 |
// In order for the IP address to be re-used after quitting this sample program and |
// restarting it, the "ReuseAddr" option must be set. |
// One should be able to set the "ReuseAddr" option as part of the configuration string |
// however, if you try to do this along with specifying the use of the tilisten |
// module, the following code hangs the system hard. As an alternative, the |
// endpoint is created with the tilisten module layered above tcp. After that the |
// OTOptionManagement call is made to set this option. |
// RRK Comments end |
//listener = OTOpenEndpoint(OTCreateConfiguration("tilisten,tcp(ReuseAddr=1)"), 0, nil, &err); |
listener = OTOpenEndpointInContext(OTCreateConfiguration("tilisten,tcp"), 0, &Info, &err, NULL); |
// RRK addition 8/27/97 |
// set the ReuseAddr option |
if (err == noErr) { |
junk = DoNegotiateIPReuseAddrOption(listener, true); |
MoreAssertQ(junk == noErr); |
} |
// end RRK addition 8/27/97 |
// Set the endpoint mode and bind it to the appropriate IP address. |
if (err == noErr) { |
SetDefaultEndpointModes(listener); |
OTInitInetAddress(&ipAddress, 80, ipAddr); // port & host ip |
bindReq.addr.buf = (UInt8 *) &ipAddress; |
bindReq.addr.len = sizeof(ipAddress); |
bindReq.qlen = 1; |
err = OTBind(listener, &bindReq, nil); |
} |
while (err == noErr) { |
// Listen for connection attempts... |
OTMemzero(&call, sizeof(TCall)); |
call.addr.buf = (UInt8 *) &remoteIPAddress; |
call.addr.maxlen = sizeof(remoteIPAddress); |
err = OTListen(listener, &call); |
// ... then spool a worker thread for this connection. |
if (err == noErr) { |
// Create the worker context. |
workerThread = kNoThreadID; |
workerContext = OTAllocMemInContext(sizeof(WorkerContext), NULL); |
if (workerContext == nil) { |
err = kENOMEMErr; |
} else { |
workerContext->worker = nil; |
workerContext->vRefNum = vRefNum; |
workerContext->dirID = dirID; |
} |
// Open the worker endpoint. |
if (err == noErr) { |
workerContext->worker = OTOpenEndpointInContext(OTCreateConfiguration("tcp"), 0, nil, &err, NULL); |
if (err == noErr) { |
SetDefaultEndpointModes(workerContext->worker); |
} |
} |
if (gWorkerThreadProcUPP == NULL) |
{ |
gWorkerThreadProcUPP = NewThreadEntryUPP((ThreadEntryProcPtr)WorkerThreadProc); |
} |
// Create the worker thread. |
if (err == noErr) { |
err = NewThread(kCooperativeThread, gWorkerThreadProcUPP, workerContext, |
0, kNewSuspend | kCreateIfNeeded, |
nil, |
&workerThread); |
} |
// Accept the connection on the thread. |
if (err == noErr) { |
err = OTAccept(listener, workerContext->worker, &call); |
} |
// Schedule the thread for execution. |
if (err == noErr) { |
err = SetThreadState(workerThread, kReadyThreadState, kNoThreadID); |
} |
// Clean up on error. |
if (err != noErr) { |
if (workerContext != nil) { |
if (workerContext->worker != nil) { |
junk = OTCloseProvider(workerContext->worker); |
MoreAssertQ(junk == noErr); |
} |
OTFreeMem(workerContext); |
} |
if (workerThread != kNoThreadID) { |
junk = DisposeThread(workerThread, nil, true); |
MoreAssertQ(junk == noErr); |
} |
printf("StartHTTPServer: Failed to spool worker, error %d.\n", (int)err); |
fflush(stdout); |
err = noErr; |
} |
} |
} |
// Clean up the listener endpoint. |
if (listener != nil) { |
junk = OTCloseProvider(listener); |
MoreAssertQ(junk == noErr); |
} |
printf("HTTP Server on %08x: Stopping.\n", (unsigned int)ipAddr); |
fflush(stdout); |
return (err); |
} |
Copyright © 2003 Apple Computer, Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2003-01-14