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