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