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