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