Fix reconnect feature by resetting the last txR and txW values.
[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 lastReadTransaction_ = 0;
337 lastWrittenTransaction_ = 0;
338 self.queuedWrites = [NSMutableArray array];
339 writeQueueLock_ = [NSRecursiveLock new];
340 }
341
342 /**
343 * Closes a socket and releases the ref.
344 */
345 - (void)close
346 {
347 if (thread_) {
348 [thread_ cancel];
349 }
350 if (runLoop_ && quitSource_) {
351 CFRunLoopSourceSignal(quitSource_);
352 CFRunLoopWakeUp([runLoop_ getCFRunLoop]);
353 }
354 }
355
356 /**
357 * Quits the run loop and stops the thread.
358 */
359 - (void)performQuitSignal
360 {
361 self.queuedWrites = nil;
362 connected_ = NO;
363 [writeQueueLock_ release];
364
365 if (runLoop_) {
366 CFRunLoopStop([runLoop_ getCFRunLoop]);
367 }
368
369 // The socket goes down, so do the streams, which clean themselves up.
370 if (socket_) {
371 NSLog(@"invalidating socket");
372 CFSocketInvalidate(socket_);
373 CFRelease(socket_);
374 socket_ = NULL;
375 }
376 }
377
378 /**
379 * Notification that the socket disconnected.
380 */
381 - (void)socketDisconnected
382 {
383 if (connected_) {
384 // The state still is connected, which means that we did not get here
385 // through normal disconnected procedure (a call to |-close|, followed by
386 // the downing of the socket and the stream, which also produces this
387 // messsage). Instead, the stream callbacks encountered EOF unexpectedly.
388 //[self close];
389 }
390 if ([delegate_ respondsToSelector:@selector(connectionDidClose:)])
391 [delegate_ connectionDidClose:self];
392 }
393
394 /**
395 * Writes a command into the write stream. If the stream is ready for writing,
396 * we do so immediately. If not, the command is queued and will be written
397 * when the stream is ready.
398 */
399 - (void)send:(NSString*)command
400 {
401 if (lastReadTransaction_ >= lastWrittenTransaction_ && CFWriteStreamCanAcceptBytes(writeStream_)) {
402 [self performSend:command];
403 } else {
404 [writeQueueLock_ lock];
405 [queuedWrites_ addObject:command];
406 [writeQueueLock_ unlock];
407 }
408 [self sendQueuedWrites];
409 }
410
411 /**
412 * This will send a command to the debugger engine. It will append the
413 * transaction ID automatically. It accepts a NSString command along with a
414 * a variable number of arguments to substitute into the command, a la
415 * +[NSString stringWithFormat:]. Returns the transaction ID as a NSNumber.
416 */
417 - (NSNumber*)sendCommandWithFormat:(NSString*)format, ...
418 {
419 // Collect varargs and format command.
420 va_list args;
421 va_start(args, format);
422 NSString* command = [[NSString alloc] initWithFormat:format arguments:args];
423 va_end(args);
424
425 NSNumber* callbackKey = [NSNumber numberWithInt:transactionID++];
426 NSString* taggedCommand = [NSString stringWithFormat:@"%@ -i %@", [command autorelease], callbackKey];
427 [self performSelector:@selector(send:)
428 onThread:thread_
429 withObject:taggedCommand
430 waitUntilDone:connected_];
431
432 return callbackKey;
433 }
434
435 /**
436 * Given a file path, this returns a file:// URI and escapes any spaces for the
437 * debugger engine.
438 */
439 - (NSString*)escapedURIPath:(NSString*)path
440 {
441 // Custon GDBp paths are fine.
442 if ([[path substringToIndex:4] isEqualToString:@"gdbp"])
443 return path;
444
445 // Create a temporary URL that will escape all the nasty characters.
446 NSURL* url = [NSURL fileURLWithPath:path];
447 NSString* urlString = [url absoluteString];
448
449 // Remove the host because this is a file:// URL;
450 urlString = [urlString stringByReplacingOccurrencesOfString:[url host] withString:@""];
451
452 // Escape % for use in printf-style NSString formatters.
453 urlString = [urlString stringByReplacingOccurrencesOfString:@"%" withString:@"%%"];
454 return urlString;
455 }
456
457 /**
458 * Returns the transaction_id from an NSXMLDocument.
459 */
460 - (NSInteger)transactionIDFromResponse:(NSXMLDocument*)response
461 {
462 return [[[[response rootElement] attributeForName:@"transaction_id"] stringValue] intValue];
463 }
464
465 /**
466 * Scans a command string for the transaction ID component. If it is not found,
467 * returns NSNotFound.
468 */
469 - (NSInteger)transactionIDFromCommand:(NSString*)command
470 {
471 NSRange occurrence = [command rangeOfString:@"-i "];
472 if (occurrence.location == NSNotFound)
473 return NSNotFound;
474 NSString* transaction = [command substringFromIndex:occurrence.location + occurrence.length];
475 return [transaction intValue];
476 }
477
478 // Private /////////////////////////////////////////////////////////////////////
479 #pragma mark Private
480
481 // Delegate Thread-Safe Wrappers ///////////////////////////////////////////////
482
483 /**
484 * Receives errors from the SocketWrapper and updates the display
485 */
486 - (void)errorEncountered:(NSString*)error
487 {
488 if (![delegate_ respondsToSelector:@selector(errorEncountered:)])
489 return;
490 [delegate_ performSelectorOnMainThread:@selector(errorEncountered:)
491 withObject:error
492 waitUntilDone:NO];
493 }
494
495 - (LogEntry*)recordSend:(NSString*)command
496 {
497 LoggingController* logger = [[AppDelegate instance] loggingController];
498 LogEntry* entry = [LogEntry newSendEntry:command];
499 entry.lastReadTransactionID = lastReadTransaction_;
500 entry.lastWrittenTransactionID = lastWrittenTransaction_;
501 [logger performSelectorOnMainThread:@selector(recordEntry:)
502 withObject:entry
503 waitUntilDone:NO];
504 return [entry autorelease];
505 }
506
507 - (LogEntry*)recordReceive:(NSString*)command
508 {
509 LoggingController* logger = [[AppDelegate instance] loggingController];
510 LogEntry* entry = [LogEntry newReceiveEntry:command];
511 entry.lastReadTransactionID = lastReadTransaction_;
512 entry.lastWrittenTransactionID = lastWrittenTransaction_;
513 [logger performSelectorOnMainThread:@selector(recordEntry:)
514 withObject:entry
515 waitUntilDone:NO];
516 return [entry autorelease];
517 }
518
519 // Stream Managers /////////////////////////////////////////////////////////////
520
521 /**
522 * Callback from the CFReadStream that there is data waiting to be read.
523 */
524 - (void)readStreamHasData
525 {
526 const NSUInteger kBufferSize = 1024;
527 UInt8 buffer[kBufferSize];
528 CFIndex bufferOffset = 0; // Starting point in |buffer| to work with.
529 CFIndex bytesRead = CFReadStreamRead(readStream_, buffer, kBufferSize);
530 const char* charBuffer = (const char*)buffer;
531
532 // The read loop works by going through the buffer until all the bytes have
533 // been processed.
534 while (bufferOffset < bytesRead)
535 {
536 // Find the NULL separator, or the end of the string.
537 NSUInteger partLength = 0;
538 for (NSUInteger i = bufferOffset; i < bytesRead && charBuffer[i] != '\0'; ++i, ++partLength) ;
539
540 // If there is not a current packet, set some state.
541 if (!self.currentPacket)
542 {
543 // Read the message header: the size. This will be |partLength| bytes.
544 packetSize_ = atoi(charBuffer + bufferOffset);
545 currentPacketIndex_ = 0;
546 self.currentPacket = [NSMutableString stringWithCapacity:packetSize_];
547 bufferOffset += partLength + 1; // Pass over the NULL byte.
548 continue; // Spin the loop to begin reading actual data.
549 }
550
551 // Substring the byte stream and append it to the packet string.
552 CFStringRef bufferString = CFStringCreateWithBytes(kCFAllocatorDefault,
553 buffer + bufferOffset, // Byte pointer, offset by start index.
554 partLength, // Length.
555 kCFStringEncodingUTF8,
556 true);
557 [self.currentPacket appendString:(NSString*)bufferString];
558 CFRelease(bufferString);
559
560 // Advance counters.
561 currentPacketIndex_ += partLength;
562 bufferOffset += partLength + 1;
563
564 // If this read finished the packet, handle it and reset.
565 if (currentPacketIndex_ >= packetSize_)
566 {
567 [self handlePacket:[[currentPacket_ retain] autorelease]];
568 self.currentPacket = nil;
569 packetSize_ = 0;
570 currentPacketIndex_ = 0;
571 }
572 }
573 }
574
575 /**
576 * Performs the packet handling of a raw string XML packet. From this point on,
577 * the packets are associated with a transaction and are then dispatched.
578 */
579 - (void)handlePacket:(NSString*)packet
580 {
581 // Test if we can convert it into an NSXMLDocument.
582 NSError* error = nil;
583 NSXMLDocument* xmlTest = [[NSXMLDocument alloc] initWithXMLString:currentPacket_ options:NSXMLDocumentTidyXML error:&error];
584
585 // Try to recover if we encountered an error.
586 if (!xmlTest)
587 {
588 // We do not want to starve the write queue, so manually parse out the
589 // transaction ID.
590 NSRange location = [currentPacket_ rangeOfString:@"transaction_id"];
591 if (location.location != NSNotFound)
592 {
593 NSUInteger start = location.location + location.length;
594 NSUInteger end = start;
595
596 NSCharacterSet* numericSet = [NSCharacterSet decimalDigitCharacterSet];
597
598 // Loop over the characters after the attribute name to extract the ID.
599 while (end < [currentPacket_ length])
600 {
601 unichar c = [currentPacket_ characterAtIndex:end];
602 if ([numericSet characterIsMember:c])
603 {
604 // If this character is numeric, extend the range to substring.
605 ++end;
606 }
607 else
608 {
609 if (start == end)
610 {
611 // If this character is nonnumeric and we have nothing in the
612 // range, skip this character.
613 ++start;
614 ++end;
615 }
616 else
617 {
618 // We've moved past the numeric ID so we should stop searching.
619 break;
620 }
621 }
622 }
623
624 // If we were able to extract the transaction ID, update the last read.
625 NSRange substringRange = NSMakeRange(start, end - start);
626 NSString* transactionStr = [currentPacket_ substringWithRange:substringRange];
627 if ([transactionStr length])
628 lastReadTransaction_ = [transactionStr intValue];
629 }
630
631 // Otherwise, assume +1 and hope it works.
632 ++lastReadTransaction_;
633 } else if (!reconnect_) {
634 // See if the transaction can be parsed out.
635 NSInteger transaction = [self transactionIDFromResponse:xmlTest];
636 if (transaction < lastReadTransaction_) {
637 NSLog(@"tx = %d vs %d", transaction, lastReadTransaction_);
638 NSLog(@"out of date transaction %@", packet);
639 return;
640 }
641
642 if (transaction != lastWrittenTransaction_)
643 NSLog(@"txn %d <> %d last written, %d last read", transaction, lastWrittenTransaction_, lastReadTransaction_);
644
645 lastReadTransaction_ = transaction;
646 }
647
648 // Log this receive event.
649 LogEntry* log = [self recordReceive:currentPacket_];
650 log.error = error;
651
652 // Finally, dispatch the handler for this response.
653 [self handleResponse:[xmlTest autorelease]];
654 }
655
656 - (void)handleResponse:(NSXMLDocument*)response
657 {
658 // Check and see if there's an error.
659 NSArray* error = [[response rootElement] elementsForName:@"error"];
660 if ([error count] > 0)
661 {
662 NSLog(@"Xdebug error: %@", error);
663 NSString* errorMessage = [[[[error objectAtIndex:0] children] objectAtIndex:0] stringValue];
664 [self errorEncountered:errorMessage];
665 }
666
667 if ([[[response rootElement] name] isEqualToString:@"init"]) {
668 connected_ = YES;
669 reconnect_ = NO;
670 [delegate_ performSelectorOnMainThread:@selector(handleInitialResponse:)
671 withObject:response
672 waitUntilDone:NO];
673 return;
674 }
675
676 if ([delegate_ respondsToSelector:@selector(handleResponse:)])
677 [delegate_ performSelectorOnMainThread:@selector(handleResponse:)
678 withObject:response
679 waitUntilDone:NO];
680
681 [self sendQueuedWrites];
682 }
683
684 /**
685 * This performs a blocking send. This should ONLY be called when we know we
686 * have write access to the stream. We will busy wait in case we don't do a full
687 * send.
688 */
689 - (void)performSend:(NSString*)command
690 {
691 // If this is an out-of-date transaction, do not bother sending it.
692 NSInteger transaction = [self transactionIDFromCommand:command];
693 if (transaction != NSNotFound && transaction < lastWrittenTransaction_)
694 return;
695
696 BOOL done = NO;
697
698 char* string = (char*)[command UTF8String];
699 int stringLength = strlen(string);
700
701 // Busy wait while writing. BAADD. Should background this operation.
702 while (!done)
703 {
704 if (CFWriteStreamCanAcceptBytes(writeStream_))
705 {
706 // Include the NULL byte in the string when we write.
707 int bytesWritten = CFWriteStreamWrite(writeStream_, (UInt8*)string, stringLength + 1);
708 if (bytesWritten < 0)
709 {
710 NSLog(@"write error");
711 }
712 // Incomplete write.
713 else if (bytesWritten < strlen(string))
714 {
715 // Adjust the buffer and wait for another chance to write.
716 stringLength -= bytesWritten;
717 memmove(string, string + bytesWritten, stringLength);
718 }
719 else
720 {
721 done = YES;
722
723 // We need to scan the string to find the transactionID.
724 if (transaction == NSNotFound)
725 {
726 NSLog(@"sent %@ without a transaction ID", command);
727 continue;
728 }
729 lastWrittenTransaction_ = transaction;
730 }
731 }
732 }
733
734 // Log this trancation.
735 [self recordSend:command];
736 }
737
738 /**
739 * Checks if there are unsent commands in the |queuedWrites_| queue and sends
740 * them if it's OK to do so. This will not block.
741 */
742 - (void)sendQueuedWrites
743 {
744 if (!connected_)
745 return;
746
747 [writeQueueLock_ lock];
748 if (lastReadTransaction_ >= lastWrittenTransaction_ && [queuedWrites_ count] > 0)
749 {
750 NSString* command = [queuedWrites_ objectAtIndex:0];
751
752 // We don't want to block because this is called from the main thread.
753 // |-performSend:| busy waits when the stream is not ready. Bail out
754 // before we do that becuase busy waiting is BAD.
755 if (CFWriteStreamCanAcceptBytes(writeStream_))
756 {
757 [self performSend:command];
758 [queuedWrites_ removeObjectAtIndex:0];
759 }
760 }
761 [writeQueueLock_ unlock];
762 }
763
764 @end