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