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