Use a C++ class to handle the CFNetwork events, rather than plain C functions.
[macgdbp.git] / Source / NetworkConnection.mm
1 /*
2 * MacGDBp
3 * Copyright (c) 2007 - 2010, Blue Static <http://www.bluestatic.org>
4 *
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
10 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
15 */
16
17 #import "NetworkConnection.h"
18
19 #import <sys/socket.h>
20 #import <netinet/in.h>
21
22 #import "AppDelegate.h"
23 #import "LoggingController.h"
24
25 // NetworkConnection (Private) /////////////////////////////////////////////////
26
27 @interface NetworkConnection ()
28
29 @property (assign) CFSocketRef socket;
30 @property (assign) CFReadStreamRef readStream;
31 @property NSUInteger lastReadTransaction;
32 @property (retain) NSMutableString* currentPacket;
33 @property (assign) CFWriteStreamRef writeStream;
34 @property NSUInteger lastWrittenTransaction;
35 @property (retain) NSMutableArray* queuedWrites;
36
37 - (void)runNetworkThread;
38
39 - (void)socketDidAccept;
40 - (void)socketDisconnected;
41 - (void)readStreamHasData;
42
43 // These methods MUST be called on the network thread as they are not threadsafe.
44 - (void)send:(NSString*)command;
45 - (void)performSend:(NSString*)command;
46 - (void)sendQueuedWrites;
47
48 - (void)performQuitSignal;
49
50 - (void)handleResponse:(NSXMLDocument*)response;
51 - (void)handlePacket:(NSString*)packet;
52
53 // Threadsafe wrappers for the delegate's methods.
54 - (void)errorEncountered:(NSString*)error;
55 - (LogEntry*)recordSend:(NSString*)command;
56 - (LogEntry*)recordReceive:(NSString*)command;
57
58 @end
59
60 // CFNetwork Callbacks /////////////////////////////////////////////////////////
61
62 class NetworkCallbackController
63 {
64 public:
65 NetworkCallbackController(NetworkConnection* connection)
66 : connection_(connection),
67 runLoop_(CFRunLoopGetCurrent())
68 {
69 }
70
71 static void SocketAcceptCallback(CFSocketRef socket,
72 CFSocketCallBackType callbackType,
73 CFDataRef address,
74 const void* data,
75 void* self)
76 {
77 assert(callbackType == kCFSocketAcceptCallBack);
78 static_cast<NetworkCallbackController*>(self)->OnSocketAccept(socket, address, data);
79 }
80
81 void OnSocketAccept(CFSocketRef socket,
82 CFDataRef address,
83 const void* data)
84 {
85 CFReadStreamRef readStream;
86 CFWriteStreamRef writeStream;
87
88 // Create the streams on the socket.
89 CFStreamCreatePairWithSocket(kCFAllocatorDefault,
90 *(CFSocketNativeHandle*)data, // Socket handle.
91 &readStream, // Read stream in-pointer.
92 &writeStream); // Write stream in-pointer.
93
94 // Create struct to register callbacks for the stream.
95 CFStreamClientContext context = { 0 };
96 context.info = this;
97
98 // Set the client of the read stream.
99 CFOptionFlags readFlags = kCFStreamEventOpenCompleted |
100 kCFStreamEventHasBytesAvailable |
101 kCFStreamEventErrorOccurred |
102 kCFStreamEventEndEncountered;
103 if (CFReadStreamSetClient(readStream, readFlags, &NetworkCallbackController::ReadStreamCallback, &context))
104 // Schedule in run loop to do asynchronous communication with the engine.
105 CFReadStreamScheduleWithRunLoop(readStream, runLoop_, kCFRunLoopCommonModes);
106 else
107 return;
108
109 // Open the stream now that it's scheduled on the run loop.
110 if (!CFReadStreamOpen(readStream)) {
111 ReportError(CFReadStreamCopyError(readStream));
112 return;
113 }
114
115 // Set the client of the write stream.
116 CFOptionFlags writeFlags = kCFStreamEventOpenCompleted |
117 kCFStreamEventCanAcceptBytes |
118 kCFStreamEventErrorOccurred |
119 kCFStreamEventEndEncountered;
120 if (CFWriteStreamSetClient(writeStream, writeFlags, &NetworkCallbackController::WriteStreamCallback, &context))
121 // Schedule it in the run loop to receive error information.
122 CFWriteStreamScheduleWithRunLoop(writeStream, runLoop_, kCFRunLoopCommonModes);
123 else
124 return;
125
126 // Open the write stream.
127 if (!CFWriteStreamOpen(writeStream)) {
128 ReportError(CFWriteStreamCopyError(writeStream));
129 return;
130 }
131
132 connection_.readStream = readStream;
133 connection_.writeStream = writeStream;
134 [connection_ socketDidAccept];
135 }
136
137 static void ReadStreamCallback(CFReadStreamRef stream,
138 CFStreamEventType eventType,
139 void* self)
140 {
141 static_cast<NetworkCallbackController*>(self)->OnReadStreamEvent(stream, eventType);
142 }
143
144 void OnReadStreamEvent(CFReadStreamRef stream, CFStreamEventType eventType)
145 {
146 switch (eventType)
147 {
148 case kCFStreamEventHasBytesAvailable:
149 [connection_ readStreamHasData];
150 break;
151
152 case kCFStreamEventErrorOccurred:
153 {
154 ReportError(CFReadStreamCopyError(stream));
155 UnscheduleReadStream();
156 break;
157 }
158
159 case kCFStreamEventEndEncountered:
160 UnscheduleReadStream();
161 [connection_ socketDisconnected];
162 break;
163 };
164 }
165
166 void UnscheduleReadStream()
167 {
168 CFReadStreamUnscheduleFromRunLoop(connection_.readStream, runLoop_, kCFRunLoopCommonModes);
169 CFReadStreamClose(connection_.readStream);
170 CFRelease(connection_.readStream);
171 connection_.readStream = NULL;
172 }
173
174 static void WriteStreamCallback(CFWriteStreamRef stream,
175 CFStreamEventType eventType,
176 void* self)
177 {
178 static_cast<NetworkCallbackController*>(self)->OnWriteStreamEvent(stream, eventType);
179 }
180
181 void OnWriteStreamEvent(CFWriteStreamRef stream, CFStreamEventType eventType)
182 {
183 switch (eventType)
184 {
185 case kCFStreamEventCanAcceptBytes:
186 [connection_ sendQueuedWrites];
187 break;
188
189 case kCFStreamEventErrorOccurred:
190 {
191 ReportError(CFWriteStreamCopyError(stream));
192 UnscheduleWriteStream();
193 break;
194 }
195
196 case kCFStreamEventEndEncountered:
197 UnscheduleReadStream();
198 [connection_ socketDisconnected];
199 break;
200 }
201 }
202 void UnscheduleWriteStream()
203 {
204 CFWriteStreamUnscheduleFromRunLoop(connection_.writeStream, runLoop_, kCFRunLoopCommonModes);
205 CFWriteStreamClose(connection_.writeStream);
206 CFRelease(connection_.writeStream);
207 connection_.writeStream = NULL;
208 }
209
210 private:
211 void ReportError(CFErrorRef error)
212 {
213 [connection_ errorEncountered:[(NSError*)error description]];
214 CFRelease(error);
215 }
216
217 NetworkConnection* connection_; // Weak, owns this.
218 CFRunLoopRef runLoop_; // Weak.
219 };
220
221 // Other Run Loop Callbacks ////////////////////////////////////////////////////
222
223 void PerformQuitSignal(void* info)
224 {
225 NetworkConnection* obj = (NetworkConnection*)info;
226 [obj performQuitSignal];
227 }
228
229 ////////////////////////////////////////////////////////////////////////////////
230
231 @implementation NetworkConnection
232
233 @synthesize port = port_;
234 @synthesize connected = connected_;
235 @synthesize delegate = delegate_;
236
237 @synthesize socket = socket_;
238 @synthesize readStream = readStream_;
239 @synthesize lastReadTransaction = lastReadTransaction_;
240 @synthesize currentPacket = currentPacket_;
241 @synthesize writeStream = writeStream_;
242 @synthesize lastWrittenTransaction = lastWrittenTransaction_;
243 @synthesize queuedWrites = queuedWrites_;
244
245 - (id)initWithPort:(NSUInteger)aPort
246 {
247 if (self = [super init])
248 {
249 port_ = aPort;
250 }
251 return self;
252 }
253
254 - (void)dealloc
255 {
256 self.currentPacket = nil;
257 [super dealloc];
258 }
259
260 /**
261 * Kicks off the socket on another thread.
262 */
263 - (void)connect
264 {
265 if (thread_ && !connected_) {
266 // A thread has been detached but the socket has yet to connect. Do not
267 // spawn a new thread otherwise multiple threads will be blocked on the same
268 // socket.
269 return;
270 }
271 [NSThread detachNewThreadSelector:@selector(runNetworkThread) toTarget:self withObject:nil];
272 }
273
274 /**
275 * Creates, connects to, and schedules a CFSocket.
276 */
277 - (void)runNetworkThread
278 {
279 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
280
281 thread_ = [NSThread currentThread];
282 runLoop_ = [NSRunLoop currentRunLoop];
283 callbackController_ = new NetworkCallbackController(self);
284
285 // Pass ourselves to the callback so we don't have to use ugly globals.
286 CFSocketContext context = { 0 };
287 context.info = callbackController_;
288
289 // Create the address structure.
290 struct sockaddr_in address;
291 memset(&address, 0, sizeof(address));
292 address.sin_len = sizeof(address);
293 address.sin_family = AF_INET;
294 address.sin_port = htons(port_);
295 address.sin_addr.s_addr = htonl(INADDR_ANY);
296
297 // Create the socket signature.
298 CFSocketSignature signature;
299 signature.protocolFamily = PF_INET;
300 signature.socketType = SOCK_STREAM;
301 signature.protocol = IPPROTO_TCP;
302 signature.address = (CFDataRef)[NSData dataWithBytes:&address length:sizeof(address)];
303
304 do {
305 socket_ = CFSocketCreateWithSocketSignature(kCFAllocatorDefault,
306 &signature, // Socket signature.
307 kCFSocketAcceptCallBack, // Callback types.
308 &NetworkCallbackController::SocketAcceptCallback, // Callout function pointer.
309 &context); // Context to pass to callout.
310 if (!socket_) {
311 [self errorEncountered:@"Could not open socket."];
312 sleep(1);
313 }
314 } while (!socket_);
315
316 // Allow old, yet-to-be recycled sockets to be reused.
317 BOOL yes = YES;
318 setsockopt(CFSocketGetNative(socket_), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(BOOL));
319 setsockopt(CFSocketGetNative(socket_), SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(BOOL));
320
321 // Schedule the socket on the run loop.
322 CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket_, 0);
323 CFRunLoopAddSource([runLoop_ getCFRunLoop], source, kCFRunLoopCommonModes);
324 CFRelease(source);
325
326 // Create a source that is used to quit.
327 CFRunLoopSourceContext quitContext = { 0 };
328 quitContext.info = self;
329 quitContext.perform = PerformQuitSignal;
330 quitSource_ = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &quitContext);
331 CFRunLoopAddSource([runLoop_ getCFRunLoop], quitSource_, kCFRunLoopCommonModes);
332
333 CFRunLoopRun();
334
335 thread_ = nil;
336 runLoop_ = nil;
337 delete callbackController_;
338 callbackController_ = NULL;
339
340 CFRunLoopSourceInvalidate(quitSource_);
341 CFRelease(quitSource_);
342 quitSource_ = NULL;
343
344 [pool release];
345 }
346
347 /**
348 * Called by SocketWrapper after the connection is successful. This immediately calls
349 * -[SocketWrapper receive] to clear the way for communication, though the information
350 * could be useful server information that we don't use right now.
351 */
352 - (void)socketDidAccept
353 {
354 connected_ = YES;
355 transactionID = 1;
356 lastReadTransaction_ = 0;
357 lastWrittenTransaction_ = 0;
358 self.queuedWrites = [NSMutableArray array];
359 writeQueueLock_ = [NSRecursiveLock new];
360 if ([delegate_ respondsToSelector:@selector(connectionDidAccept:)])
361 [delegate_ performSelectorOnMainThread:@selector(connectionDidAccept:)
362 withObject:self
363 waitUntilDone:NO];
364 }
365
366 /**
367 * Closes a socket and releases the ref.
368 */
369 - (void)close
370 {
371 if (thread_) {
372 [thread_ cancel];
373 }
374 if (runLoop_ && quitSource_) {
375 CFRunLoopSourceSignal(quitSource_);
376 CFRunLoopWakeUp([runLoop_ getCFRunLoop]);
377 }
378 }
379
380 /**
381 * Quits the run loop and stops the thread.
382 */
383 - (void)performQuitSignal
384 {
385 self.queuedWrites = nil;
386 connected_ = NO;
387 [writeQueueLock_ release];
388
389 if (runLoop_) {
390 CFRunLoopStop([runLoop_ getCFRunLoop]);
391 }
392
393 // The socket goes down, so do the streams, which clean themselves up.
394 if (socket_) {
395 NSLog(@"invalidating socket");
396 CFSocketInvalidate(socket_);
397 CFRelease(socket_);
398 socket_ = NULL;
399 }
400 }
401
402 /**
403 * Notification that the socket disconnected.
404 */
405 - (void)socketDisconnected
406 {
407 if (connected_) {
408 // The state still is connected, which means that we did not get here
409 // through normal disconnected procedure (a call to |-close|, followed by
410 // the downing of the socket and the stream, which also produces this
411 // messsage). Instead, the stream callbacks encountered EOF unexpectedly.
412 //[self close];
413 }
414 if ([delegate_ respondsToSelector:@selector(connectionDidClose:)])
415 [delegate_ connectionDidClose:self];
416 }
417
418 /**
419 * Writes a command into the write stream. If the stream is ready for writing,
420 * we do so immediately. If not, the command is queued and will be written
421 * when the stream is ready.
422 */
423 - (void)send:(NSString*)command
424 {
425 if (lastReadTransaction_ >= lastWrittenTransaction_ && CFWriteStreamCanAcceptBytes(writeStream_)) {
426 [self performSend:command];
427 } else {
428 [writeQueueLock_ lock];
429 [queuedWrites_ addObject:command];
430 [writeQueueLock_ unlock];
431 }
432 [self sendQueuedWrites];
433 }
434
435 /**
436 * This will send a command to the debugger engine. It will append the
437 * transaction ID automatically. It accepts a NSString command along with a
438 * a variable number of arguments to substitute into the command, a la
439 * +[NSString stringWithFormat:]. Returns the transaction ID as a NSNumber.
440 */
441 - (NSNumber*)sendCommandWithFormat:(NSString*)format, ...
442 {
443 // Collect varargs and format command.
444 va_list args;
445 va_start(args, format);
446 NSString* command = [[NSString alloc] initWithFormat:format arguments:args];
447 va_end(args);
448
449 NSNumber* callbackKey = [NSNumber numberWithInt:transactionID++];
450 NSString* taggedCommand = [NSString stringWithFormat:@"%@ -i %@", [command autorelease], callbackKey];
451 [self performSelector:@selector(send:)
452 onThread:thread_
453 withObject:taggedCommand
454 waitUntilDone:connected_];
455
456 return callbackKey;
457 }
458
459 /**
460 * Given a file path, this returns a file:// URI and escapes any spaces for the
461 * debugger engine.
462 */
463 - (NSString*)escapedURIPath:(NSString*)path
464 {
465 // Custon GDBp paths are fine.
466 if ([[path substringToIndex:4] isEqualToString:@"gdbp"])
467 return path;
468
469 // Create a temporary URL that will escape all the nasty characters.
470 NSURL* url = [NSURL fileURLWithPath:path];
471 NSString* urlString = [url absoluteString];
472
473 // Remove the host because this is a file:// URL;
474 urlString = [urlString stringByReplacingOccurrencesOfString:[url host] withString:@""];
475
476 // Escape % for use in printf-style NSString formatters.
477 urlString = [urlString stringByReplacingOccurrencesOfString:@"%" withString:@"%%"];
478 return urlString;
479 }
480
481 /**
482 * Returns the transaction_id from an NSXMLDocument.
483 */
484 - (NSInteger)transactionIDFromResponse:(NSXMLDocument*)response
485 {
486 return [[[[response rootElement] attributeForName:@"transaction_id"] stringValue] intValue];
487 }
488
489 /**
490 * Scans a command string for the transaction ID component. If it is not found,
491 * returns NSNotFound.
492 */
493 - (NSInteger)transactionIDFromCommand:(NSString*)command
494 {
495 NSRange occurrence = [command rangeOfString:@"-i "];
496 if (occurrence.location == NSNotFound)
497 return NSNotFound;
498 NSString* transaction = [command substringFromIndex:occurrence.location + occurrence.length];
499 return [transaction intValue];
500 }
501
502 // Private /////////////////////////////////////////////////////////////////////
503 #pragma mark Private
504
505 // Delegate Thread-Safe Wrappers ///////////////////////////////////////////////
506
507 /**
508 * Receives errors from the SocketWrapper and updates the display
509 */
510 - (void)errorEncountered:(NSString*)error
511 {
512 if (![delegate_ respondsToSelector:@selector(errorEncountered:)])
513 return;
514 [delegate_ performSelectorOnMainThread:@selector(errorEncountered:)
515 withObject:error
516 waitUntilDone:NO];
517 }
518
519 - (LogEntry*)recordSend:(NSString*)command
520 {
521 LoggingController* logger = [[AppDelegate instance] loggingController];
522 LogEntry* entry = [LogEntry newSendEntry:command];
523 entry.lastReadTransactionID = lastReadTransaction_;
524 entry.lastWrittenTransactionID = lastWrittenTransaction_;
525 [logger performSelectorOnMainThread:@selector(recordEntry:)
526 withObject:entry
527 waitUntilDone:NO];
528 return [entry autorelease];
529 }
530
531 - (LogEntry*)recordReceive:(NSString*)command
532 {
533 LoggingController* logger = [[AppDelegate instance] loggingController];
534 LogEntry* entry = [LogEntry newReceiveEntry:command];
535 entry.lastReadTransactionID = lastReadTransaction_;
536 entry.lastWrittenTransactionID = lastWrittenTransaction_;
537 [logger performSelectorOnMainThread:@selector(recordEntry:)
538 withObject:entry
539 waitUntilDone:NO];
540 return [entry autorelease];
541 }
542
543 // Stream Managers /////////////////////////////////////////////////////////////
544
545 /**
546 * Callback from the CFReadStream that there is data waiting to be read.
547 */
548 - (void)readStreamHasData
549 {
550 const NSUInteger kBufferSize = 1024;
551 UInt8 buffer[kBufferSize];
552 CFIndex bufferOffset = 0; // Starting point in |buffer| to work with.
553 CFIndex bytesRead = CFReadStreamRead(readStream_, buffer, kBufferSize);
554 const char* charBuffer = (const char*)buffer;
555
556 // The read loop works by going through the buffer until all the bytes have
557 // been processed.
558 while (bufferOffset < bytesRead)
559 {
560 // Find the NULL separator, or the end of the string.
561 NSUInteger partLength = 0;
562 for (NSUInteger i = bufferOffset; i < bytesRead && charBuffer[i] != '\0'; ++i, ++partLength) ;
563
564 // If there is not a current packet, set some state.
565 if (!self.currentPacket)
566 {
567 // Read the message header: the size. This will be |partLength| bytes.
568 packetSize_ = atoi(charBuffer + bufferOffset);
569 currentPacketIndex_ = 0;
570 self.currentPacket = [NSMutableString stringWithCapacity:packetSize_];
571 bufferOffset += partLength + 1; // Pass over the NULL byte.
572 continue; // Spin the loop to begin reading actual data.
573 }
574
575 // Substring the byte stream and append it to the packet string.
576 CFStringRef bufferString = CFStringCreateWithBytes(kCFAllocatorDefault,
577 buffer + bufferOffset, // Byte pointer, offset by start index.
578 partLength, // Length.
579 kCFStringEncodingUTF8,
580 true);
581 [self.currentPacket appendString:(NSString*)bufferString];
582 CFRelease(bufferString);
583
584 // Advance counters.
585 currentPacketIndex_ += partLength;
586 bufferOffset += partLength + 1;
587
588 // If this read finished the packet, handle it and reset.
589 if (currentPacketIndex_ >= packetSize_)
590 {
591 [self handlePacket:[[currentPacket_ retain] autorelease]];
592 self.currentPacket = nil;
593 packetSize_ = 0;
594 currentPacketIndex_ = 0;
595 }
596 }
597 }
598
599 /**
600 * Performs the packet handling of a raw string XML packet. From this point on,
601 * the packets are associated with a transaction and are then dispatched.
602 */
603 - (void)handlePacket:(NSString*)packet
604 {
605 // Test if we can convert it into an NSXMLDocument.
606 NSError* error = nil;
607 NSXMLDocument* xmlTest = [[NSXMLDocument alloc] initWithXMLString:currentPacket_ options:NSXMLDocumentTidyXML error:&error];
608
609 // Try to recover if we encountered an error.
610 if (!xmlTest)
611 {
612 // We do not want to starve the write queue, so manually parse out the
613 // transaction ID.
614 NSRange location = [currentPacket_ rangeOfString:@"transaction_id"];
615 if (location.location != NSNotFound)
616 {
617 NSUInteger start = location.location + location.length;
618 NSUInteger end = start;
619
620 NSCharacterSet* numericSet = [NSCharacterSet decimalDigitCharacterSet];
621
622 // Loop over the characters after the attribute name to extract the ID.
623 while (end < [currentPacket_ length])
624 {
625 unichar c = [currentPacket_ characterAtIndex:end];
626 if ([numericSet characterIsMember:c])
627 {
628 // If this character is numeric, extend the range to substring.
629 ++end;
630 }
631 else
632 {
633 if (start == end)
634 {
635 // If this character is nonnumeric and we have nothing in the
636 // range, skip this character.
637 ++start;
638 ++end;
639 }
640 else
641 {
642 // We've moved past the numeric ID so we should stop searching.
643 break;
644 }
645 }
646 }
647
648 // If we were able to extract the transaction ID, update the last read.
649 NSRange substringRange = NSMakeRange(start, end - start);
650 NSString* transactionStr = [currentPacket_ substringWithRange:substringRange];
651 if ([transactionStr length])
652 lastReadTransaction_ = [transactionStr intValue];
653 }
654
655 // Otherwise, assume +1 and hope it works.
656 ++lastReadTransaction_;
657 } else /*if (!reconnect_)*/ {
658 // See if the transaction can be parsed out.
659 NSInteger transaction = [self transactionIDFromResponse:xmlTest];
660 if (transaction < lastReadTransaction_) {
661 NSLog(@"tx = %d vs %d", transaction, lastReadTransaction_);
662 NSLog(@"out of date transaction %@", packet);
663 return;
664 }
665
666 if (transaction != lastWrittenTransaction_)
667 NSLog(@"txn %d <> %d last written, %d last read", transaction, lastWrittenTransaction_, lastReadTransaction_);
668
669 lastReadTransaction_ = transaction;
670 }
671
672 // Log this receive event.
673 LogEntry* log = [self recordReceive:currentPacket_];
674 log.error = error;
675
676 // Finally, dispatch the handler for this response.
677 [self handleResponse:[xmlTest autorelease]];
678 }
679
680 - (void)handleResponse:(NSXMLDocument*)response
681 {
682 // Check and see if there's an error.
683 NSArray* error = [[response rootElement] elementsForName:@"error"];
684 if ([error count] > 0)
685 {
686 NSLog(@"Xdebug error: %@", error);
687 NSString* errorMessage = [[[[error objectAtIndex:0] children] objectAtIndex:0] stringValue];
688 [self errorEncountered:errorMessage];
689 }
690
691 if ([[[response rootElement] name] isEqualToString:@"init"]) {
692 connected_ = YES;
693 [delegate_ performSelectorOnMainThread:@selector(handleInitialResponse:)
694 withObject:response
695 waitUntilDone:NO];
696 return;
697 }
698
699 if ([delegate_ respondsToSelector:@selector(handleResponse:)])
700 [delegate_ performSelectorOnMainThread:@selector(handleResponse:)
701 withObject:response
702 waitUntilDone:NO];
703
704 [self sendQueuedWrites];
705 }
706
707 /**
708 * This performs a blocking send. This should ONLY be called when we know we
709 * have write access to the stream. We will busy wait in case we don't do a full
710 * send.
711 */
712 - (void)performSend:(NSString*)command
713 {
714 // If this is an out-of-date transaction, do not bother sending it.
715 NSInteger transaction = [self transactionIDFromCommand:command];
716 if (transaction != NSNotFound && transaction < lastWrittenTransaction_)
717 return;
718
719 BOOL done = NO;
720
721 char* string = (char*)[command UTF8String];
722 int stringLength = strlen(string);
723
724 // Busy wait while writing. BAADD. Should background this operation.
725 while (!done)
726 {
727 if (CFWriteStreamCanAcceptBytes(writeStream_))
728 {
729 // Include the NULL byte in the string when we write.
730 int bytesWritten = CFWriteStreamWrite(writeStream_, (UInt8*)string, stringLength + 1);
731 if (bytesWritten < 0)
732 {
733 NSLog(@"write error");
734 }
735 // Incomplete write.
736 else if (bytesWritten < strlen(string))
737 {
738 // Adjust the buffer and wait for another chance to write.
739 stringLength -= bytesWritten;
740 memmove(string, string + bytesWritten, stringLength);
741 }
742 else
743 {
744 done = YES;
745
746 // We need to scan the string to find the transactionID.
747 if (transaction == NSNotFound)
748 {
749 NSLog(@"sent %@ without a transaction ID", command);
750 continue;
751 }
752 lastWrittenTransaction_ = transaction;
753 }
754 }
755 }
756
757 // Log this trancation.
758 [self recordSend:command];
759 }
760
761 /**
762 * Checks if there are unsent commands in the |queuedWrites_| queue and sends
763 * them if it's OK to do so. This will not block.
764 */
765 - (void)sendQueuedWrites
766 {
767 if (!connected_)
768 return;
769
770 [writeQueueLock_ lock];
771 if (lastReadTransaction_ >= lastWrittenTransaction_ && [queuedWrites_ count] > 0)
772 {
773 NSString* command = [queuedWrites_ objectAtIndex:0];
774
775 // We don't want to block because this is called from the main thread.
776 // |-performSend:| busy waits when the stream is not ready. Bail out
777 // before we do that becuase busy waiting is BAD.
778 if (CFWriteStreamCanAcceptBytes(writeStream_))
779 {
780 [self performSend:command];
781 [queuedWrites_ removeObjectAtIndex:0];
782 }
783 }
784 [writeQueueLock_ unlock];
785 }
786
787 @end