* Factor out some code for parsing the txnID out of a command into |-transactionIDFro...
[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 <sys/socket.h>
18 #import <netinet/in.h>
19
20 #import "DebuggerConnection.h"
21
22 #import "AppDelegate.h"
23 #import "LoggingController.h"
24
25 // GDBpConnection (Private) ////////////////////////////////////////////////////
26
27 @interface DebuggerConnection ()
28 @property (readwrite, copy) NSString* status;
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)connect;
38 - (void)close;
39 - (void)socketDidAccept;
40 - (void)socketDisconnected;
41 - (void)readStreamHasData;
42 - (void)send:(NSString*)command;
43 - (void)performSend:(NSString*)command;
44 - (void)errorEncountered:(NSString*)error;
45
46 - (void)handleResponse:(NSXMLDocument*)response;
47 - (void)initReceived:(NSXMLDocument*)response;
48 - (void)updateStatus:(NSXMLDocument*)response;
49 - (void)debuggerStep:(NSXMLDocument*)response;
50 - (void)rebuildStack:(NSXMLDocument*)response;
51 - (void)getStackFrame:(NSXMLDocument*)response;
52 - (void)setSource:(NSXMLDocument*)response;
53 - (void)contextsReceived:(NSXMLDocument*)response;
54 - (void)variablesReceived:(NSXMLDocument*)response;
55 - (void)propertiesReceived:(NSXMLDocument*)response;
56
57 - (NSNumber*)sendCommandWithCallback:(SEL)callback format:(NSString*)format, ...;
58
59 - (void)sendQueuedWrites;
60
61 - (NSString*)escapedURIPath:(NSString*)path;
62 - (NSInteger)transactionIDFromResponse:(NSXMLDocument*)response;
63 - (NSInteger)transactionIDFromCommand:(NSString*)command;
64 @end
65
66 // CFNetwork Callbacks /////////////////////////////////////////////////////////
67
68 void ReadStreamCallback(CFReadStreamRef stream, CFStreamEventType eventType, void* connectionRaw)
69 {
70 DebuggerConnection* connection = (DebuggerConnection*)connectionRaw;
71 switch (eventType)
72 {
73 case kCFStreamEventHasBytesAvailable:
74 [connection readStreamHasData];
75 break;
76
77 case kCFStreamEventErrorOccurred:
78 {
79 CFErrorRef error = CFReadStreamCopyError(stream);
80 CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
81 CFReadStreamClose(stream);
82 CFRelease(stream);
83 [connection errorEncountered:[[(NSError*)error autorelease] description]];
84 break;
85 }
86
87 case kCFStreamEventEndEncountered:
88 CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
89 CFReadStreamClose(stream);
90 CFRelease(stream);
91 [connection socketDisconnected];
92 break;
93 };
94 }
95
96 void WriteStreamCallback(CFWriteStreamRef stream, CFStreamEventType eventType, void* connectionRaw)
97 {
98 DebuggerConnection* connection = (DebuggerConnection*)connectionRaw;
99 switch (eventType)
100 {
101 case kCFStreamEventCanAcceptBytes:
102 [connection sendQueuedWrites];
103 break;
104
105 case kCFStreamEventErrorOccurred:
106 {
107 CFErrorRef error = CFWriteStreamCopyError(stream);
108 CFWriteStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
109 CFWriteStreamClose(stream);
110 CFRelease(stream);
111 [connection errorEncountered:[[(NSError*)error autorelease] description]];
112 break;
113 }
114
115 case kCFStreamEventEndEncountered:
116 CFWriteStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
117 CFWriteStreamClose(stream);
118 CFRelease(stream);
119 [connection socketDisconnected];
120 break;
121 }
122 }
123
124 void SocketAcceptCallback(CFSocketRef socket,
125 CFSocketCallBackType callbackType,
126 CFDataRef address,
127 const void* data,
128 void* connectionRaw)
129 {
130 assert(callbackType == kCFSocketAcceptCallBack);
131 NSLog(@"SocketAcceptCallback()");
132
133 DebuggerConnection* connection = (DebuggerConnection*)connectionRaw;
134
135 CFReadStreamRef readStream;
136 CFWriteStreamRef writeStream;
137
138 // Create the streams on the socket.
139 CFStreamCreatePairWithSocket(kCFAllocatorDefault,
140 *(CFSocketNativeHandle*)data, // Socket handle.
141 &readStream, // Read stream in-pointer.
142 &writeStream); // Write stream in-pointer.
143
144 // Create struct to register callbacks for the stream.
145 CFStreamClientContext context;
146 context.version = 0;
147 context.info = connection;
148 context.retain = NULL;
149 context.release = NULL;
150 context.copyDescription = NULL;
151
152 // Set the client of the read stream.
153 CFOptionFlags readFlags =
154 kCFStreamEventOpenCompleted |
155 kCFStreamEventHasBytesAvailable |
156 kCFStreamEventErrorOccurred |
157 kCFStreamEventEndEncountered;
158 if (CFReadStreamSetClient(readStream, readFlags, ReadStreamCallback, &context))
159 // Schedule in run loop to do asynchronous communication with the engine.
160 CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
161 else
162 return;
163
164 // Open the stream now that it's scheduled on the run loop.
165 if (!CFReadStreamOpen(readStream))
166 {
167 CFStreamError error = CFReadStreamGetError(readStream);
168 NSLog(@"error! %@", error);
169 return;
170 }
171
172 // Set the client of the write stream.
173 CFOptionFlags writeFlags =
174 kCFStreamEventOpenCompleted |
175 kCFStreamEventCanAcceptBytes |
176 kCFStreamEventErrorOccurred |
177 kCFStreamEventEndEncountered;
178 if (CFWriteStreamSetClient(writeStream, writeFlags, WriteStreamCallback, &context))
179 // Schedule it in the run loop to receive error information.
180 CFWriteStreamScheduleWithRunLoop(writeStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
181 else
182 return;
183
184 // Open the write stream.
185 if (!CFWriteStreamOpen(writeStream))
186 {
187 CFStreamError error = CFWriteStreamGetError(writeStream);
188 NSLog(@"error! %@", error);
189 return;
190 }
191
192 connection.readStream = readStream;
193 connection.writeStream = writeStream;
194 [connection socketDidAccept];
195 }
196
197 // GDBpConnection //////////////////////////////////////////////////////////////
198
199 @implementation DebuggerConnection
200 @synthesize socket = socket_;
201 @synthesize readStream = readStream_;
202 @synthesize lastReadTransaction = lastReadTransaction_;
203 @synthesize currentPacket = currentPacket_;
204 @synthesize writeStream = writeStream_;
205 @synthesize lastWrittenTransaction = lastWrittenTransaction_;
206 @synthesize queuedWrites = queuedWrites_;
207 @synthesize status;
208 @synthesize delegate;
209
210 /**
211 * Creates a new DebuggerConnection and initializes the socket from the given connection
212 * paramters.
213 */
214 - (id)initWithPort:(NSUInteger)aPort
215 {
216 if (self = [super init])
217 {
218 port = aPort;
219 connected = NO;
220
221 [[BreakpointManager sharedManager] setConnection:self];
222 [self connect];
223 }
224 return self;
225 }
226
227 /**
228 * Deallocates the object
229 */
230 - (void)dealloc
231 {
232 [self close];
233 self.currentPacket = nil;
234
235 [super dealloc];
236 }
237
238
239 // Getters /////////////////////////////////////////////////////////////////////
240 #pragma mark Getters
241
242 /**
243 * Gets the port number
244 */
245 - (NSUInteger)port
246 {
247 return port;
248 }
249
250 /**
251 * Returns the name of the remote host
252 */
253 - (NSString*)remoteHost
254 {
255 if (!connected)
256 {
257 return @"(DISCONNECTED)";
258 }
259 // TODO: Either impl or remove.
260 return @"";
261 }
262
263 /**
264 * Returns whether or not we have an active connection
265 */
266 - (BOOL)isConnected
267 {
268 return connected;
269 }
270
271 // Commands ////////////////////////////////////////////////////////////////////
272 #pragma mark Commands
273
274 /**
275 * Reestablishes communication with the remote debugger so that a new connection doesn't have to be
276 * created every time you want to debug a page
277 */
278 - (void)reconnect
279 {
280 [self close];
281 self.status = @"Connecting";
282 [self connect];
283 }
284
285 /**
286 * Tells the debugger to continue running the script. Returns the current stack frame.
287 */
288 - (void)run
289 {
290 [self sendCommandWithCallback:@selector(debuggerStep:) format:@"run"];
291 }
292
293 /**
294 * Tells the debugger to step into the current command.
295 */
296 - (void)stepIn
297 {
298 [self sendCommandWithCallback:@selector(debuggerStep:) format:@"step_into"];
299 }
300
301 /**
302 * Tells the debugger to step out of the current context
303 */
304 - (void)stepOut
305 {
306 [self sendCommandWithCallback:@selector(debuggerStep:) format:@"step_out"];
307 }
308
309 /**
310 * Tells the debugger to step over the current function
311 */
312 - (void)stepOver
313 {
314 [self sendCommandWithCallback:@selector(debuggerStep:) format:@"step_over"];
315 }
316
317 /**
318 * Tells the debugger engine to get a specifc property. This also takes in the NSXMLElement
319 * that requested it so that the child can be attached.
320 */
321 - (NSInteger)getProperty:(NSString*)property
322 {
323 [self sendCommandWithCallback:@selector(propertiesReceived:) format:@"property_get -n \"%@\"", property];
324 }
325
326 // Breakpoint Management ///////////////////////////////////////////////////////
327 #pragma mark Breakpoints
328
329 /**
330 * Send an add breakpoint command
331 */
332 - (void)addBreakpoint:(Breakpoint*)bp
333 {
334 if (!connected)
335 return;
336
337 NSString* file = [self escapedURIPath:[bp transformedPath]];
338 NSNumber* transaction = [self sendCommandWithCallback:@selector(breakpointReceived:)
339 format:@"breakpoint_set -t line -f %@ -n %i", file, [bp line]];
340 [callbackContext_ setObject:bp forKey:transaction];
341 }
342
343 /**
344 * Removes a breakpoint
345 */
346 - (void)removeBreakpoint:(Breakpoint*)bp
347 {
348 if (!connected)
349 {
350 return;
351 }
352
353 [self sendCommandWithCallback:nil format:@"breakpoint_remove -d %i", [bp debuggerId]];
354 }
355
356
357 // Socket and Stream Callbacks /////////////////////////////////////////////////
358 #pragma mark Callbacks
359
360 /**
361 * Called by SocketWrapper after the connection is successful. This immediately calls
362 * -[SocketWrapper receive] to clear the way for communication, though the information
363 * could be useful server information that we don't use right now.
364 */
365 - (void)socketDidAccept
366 {
367 connected = YES;
368 transactionID = 1;
369 stackFrames_ = [[NSMutableDictionary alloc] init];
370 self.queuedWrites = [NSMutableArray array];
371 writeQueueLock_ = [NSRecursiveLock new];
372 callTable_ = [NSMutableDictionary new];
373 callbackContext_ = [NSMutableDictionary new];
374 }
375
376 /**
377 * Receives errors from the SocketWrapper and updates the display
378 */
379 - (void)errorEncountered:(NSString*)error
380 {
381 [delegate errorEncountered:error];
382 }
383
384 /**
385 * Creates, connects to, and schedules a CFSocket.
386 */
387 - (void)connect
388 {
389 // Pass ourselves to the callback so we don't have to use ugly globals.
390 CFSocketContext context;
391 context.version = 0;
392 context.info = self;
393 context.retain = NULL;
394 context.release = NULL;
395 context.copyDescription = NULL;
396
397 // Create the address structure.
398 struct sockaddr_in address;
399 memset(&address, 0, sizeof(address));
400 address.sin_len = sizeof(address);
401 address.sin_family = AF_INET;
402 address.sin_port = htons(port);
403 address.sin_addr.s_addr = htonl(INADDR_ANY);
404
405 // Create the socket signature.
406 CFSocketSignature signature;
407 signature.protocolFamily = PF_INET;
408 signature.socketType = SOCK_STREAM;
409 signature.protocol = IPPROTO_TCP;
410 signature.address = (CFDataRef)[NSData dataWithBytes:&address length:sizeof(address)];
411
412 socket_ = CFSocketCreateWithSocketSignature(kCFAllocatorDefault,
413 &signature, // Socket signature.
414 kCFSocketAcceptCallBack, // Callback types.
415 SocketAcceptCallback, // Callout function pointer.
416 &context); // Context to pass to callout.
417 if (!socket_)
418 {
419 [self errorEncountered:@"Could not open socket."];
420 return;
421 }
422
423 // Allow old, yet-to-be recycled sockets to be reused.
424 BOOL yes = YES;
425 setsockopt(CFSocketGetNative(socket_), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(BOOL));
426
427 // Schedule the socket on the run loop.
428 CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket_, 0);
429 CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
430 CFRelease(source);
431
432 self.status = @"Connecting";
433 }
434
435 /**
436 * Closes a socket and releases the ref.
437 */
438 - (void)close
439 {
440 // The socket goes down, so do the streams, which clean themselves up.
441 CFSocketInvalidate(socket_);
442 CFRelease(socket_);
443 [stackFrames_ release];
444 self.queuedWrites = nil;
445 [writeQueueLock_ release];
446 [callTable_ release];
447 [callbackContext_ release];
448 }
449
450 /**
451 * Notification that the socket disconnected.
452 */
453 - (void)socketDisconnected
454 {
455 [self close];
456 [delegate debuggerDisconnected];
457 }
458
459 /**
460 * Callback from the CFReadStream that there is data waiting to be read.
461 */
462 - (void)readStreamHasData
463 {
464 UInt8 buffer[1024];
465 CFIndex bytesRead = CFReadStreamRead(readStream_, buffer, 1024);
466 const char* charBuffer = (const char*)buffer;
467
468 // We haven't finished reading a packet, so just read more data in.
469 if (currentPacketIndex_ < packetSize_)
470 {
471 currentPacketIndex_ += bytesRead;
472 CFStringRef bufferString = CFStringCreateWithBytes(kCFAllocatorDefault,
473 buffer,
474 bytesRead,
475 kCFStringEncodingUTF8,
476 true);
477 [self.currentPacket appendString:(NSString*)bufferString];
478 CFRelease(bufferString);
479 }
480 // Time to read a new packet.
481 else
482 {
483 // Read the message header: the size.
484 packetSize_ = atoi(charBuffer);
485 currentPacketIndex_ = bytesRead - strlen(charBuffer);
486 CFStringRef bufferString = CFStringCreateWithBytes(kCFAllocatorDefault,
487 buffer + strlen(charBuffer) + 1,
488 bytesRead - strlen(charBuffer) - 1,
489 kCFStringEncodingUTF8,
490 true);
491 self.currentPacket = [NSMutableString stringWithString:(NSString*)bufferString];
492 CFRelease(bufferString);
493 }
494
495 // We have finished reading the packet.
496 if (currentPacketIndex_ >= packetSize_)
497 {
498 packetSize_ = 0;
499 currentPacketIndex_ = 0;
500
501 // Test if we can convert it into an NSXMLDocument.
502 NSError* error = nil;
503 NSXMLDocument* xmlTest = [[NSXMLDocument alloc] initWithXMLString:currentPacket_ options:NSXMLDocumentTidyXML error:&error];
504
505 // Try to recover if we encountered an error.
506 if (!xmlTest)
507 {
508 // We do not want to starve the write queue, so manually parse out the
509 // transaction ID.
510 NSRange location = [currentPacket_ rangeOfString:@"transaction_id"];
511 if (location.location != NSNotFound)
512 {
513 NSUInteger start = location.location + location.length;
514 NSUInteger end = start;
515
516 NSCharacterSet* numericSet = [NSCharacterSet decimalDigitCharacterSet];
517
518 // Loop over the characters after the attribute name to extract the ID.
519 while (end < [currentPacket_ length])
520 {
521 unichar c = [currentPacket_ characterAtIndex:end];
522 if ([numericSet characterIsMember:c])
523 {
524 // If this character is numeric, extend the range to substring.
525 ++end;
526 }
527 else
528 {
529 if (start == end)
530 {
531 // If this character is nonnumeric and we have nothing in the
532 // range, skip this character.
533 ++start;
534 ++end;
535 }
536 else
537 {
538 // We've moved past the numeric ID so we should stop searching.
539 break;
540 }
541 }
542 }
543
544 // If we were able to extract the transaction ID, update the last read.
545 NSRange substringRange = NSMakeRange(start, end - start);
546 NSString* transaction = [currentPacket_ substringWithRange:substringRange];
547 if ([transaction length])
548 lastReadTransaction_ = [transaction intValue];
549 }
550
551 // Otherwise, assume +1 and hope it works.
552 ++lastReadTransaction_;
553 }
554 else
555 {
556 // See if the transaction can be parsed out.
557 NSInteger transaction = [self transactionIDFromResponse:xmlTest];
558 if (transaction < lastReadTransaction_)
559 {
560 NSLog(@"tx = %d vs %d", transaction, lastReadTransaction_);
561 NSLog(@"out of date transaction %@", xmlTest);
562 return;
563 }
564
565 if (transaction != lastWrittenTransaction_)
566 NSLog(@"txn %d(%d) <> %d doesn't match last written", transaction, lastReadTransaction_, lastWrittenTransaction_);
567
568 lastReadTransaction_ = transaction;
569 }
570
571 // Log this receive event.
572 LoggingController* logger = [(AppDelegate*)[NSApp delegate] loggingController];
573 LogEntry* log = [logger recordReceive:currentPacket_];
574 log.error = error;
575 log.lastWrittenTransactionID = lastWrittenTransaction_;
576 log.lastReadTransactionID = lastReadTransaction_;
577
578 // Finally, dispatch the handler for this response.
579 [self handleResponse:[xmlTest autorelease]];
580 }
581 }
582
583 /**
584 * Writes a command into the write stream. If the stream is ready for writing,
585 * we do so immediately. If not, the command is queued and will be written
586 * when the stream is ready.
587 */
588 - (void)send:(NSString*)command
589 {
590 if (CFWriteStreamCanAcceptBytes(writeStream_))
591 [self performSend:command];
592 else
593 [queuedWrites_ addObject:command];
594 [self sendQueuedWrites];
595 }
596
597 /**
598 * This performs a blocking send. This should ONLY be called when we know we
599 * have write access to the stream. We will busy wait in case we don't do a full
600 * send.
601 */
602 - (void)performSend:(NSString*)command
603 {
604 // If this is an out-of-date transaction, do not bother sending it.
605 NSInteger transaction = [self transactionIDFromCommand:command];
606 if (transaction != NSNotFound && transaction < lastWrittenTransaction_)
607 return;
608
609 BOOL done = NO;
610
611 char* string = (char*)[command UTF8String];
612 int stringLength = strlen(string);
613
614 // Busy wait while writing. BAADD. Should background this operation.
615 while (!done)
616 {
617 if (CFWriteStreamCanAcceptBytes(writeStream_))
618 {
619 // Include the NULL byte in the string when we write.
620 int bytesWritten = CFWriteStreamWrite(writeStream_, (UInt8*)string, stringLength + 1);
621 if (bytesWritten < 0)
622 {
623 NSLog(@"write error");
624 }
625 // Incomplete write.
626 else if (bytesWritten < strlen(string))
627 {
628 // Adjust the buffer and wait for another chance to write.
629 stringLength -= bytesWritten;
630 memmove(string, string + bytesWritten, stringLength);
631 }
632 else
633 {
634 done = YES;
635
636 // We need to scan the string to find the transactionID.
637 if (transaction == NSNotFound)
638 {
639 NSLog(@"sent %@ without a transaction ID", command);
640 continue;
641 }
642 lastWrittenTransaction_ = transaction;
643 }
644 }
645 }
646
647 // Log this trancation.
648 LoggingController* logger = [(AppDelegate*)[NSApp delegate] loggingController];
649 LogEntry* log = [logger recordSend:command];
650 log.lastWrittenTransactionID = lastWrittenTransaction_;
651 log.lastReadTransactionID = lastReadTransaction_;
652 }
653
654 - (void)handleResponse:(NSXMLDocument*)response
655 {
656 // Check and see if there's an error.
657 NSArray* error = [[response rootElement] elementsForName:@"error"];
658 if ([error count] > 0)
659 {
660 NSLog(@"Xdebug error: %@", error);
661 [delegate errorEncountered:[[[[error objectAtIndex:0] children] objectAtIndex:0] stringValue]];
662 }
663
664 if ([[[response rootElement] name] isEqualToString:@"init"])
665 {
666 [self initReceived:response];
667 return;
668 }
669
670 NSString* callbackStr = [callTable_ objectForKey:[NSNumber numberWithInt:lastReadTransaction_]];
671 if (callbackStr)
672 {
673 SEL callback = NSSelectorFromString(callbackStr);
674 [self performSelector:callback withObject:response];
675 }
676
677 [self sendQueuedWrites];
678 }
679
680 // Specific Response Handlers //////////////////////////////////////////////////
681 #pragma mark Response Handlers
682
683 /**
684 * Initial packet received. We've started a brand-new connection to the engine.
685 */
686 - (void)initReceived:(NSXMLDocument*)response
687 {
688 // Register any breakpoints that exist offline.
689 for (Breakpoint* bp in [[BreakpointManager sharedManager] breakpoints])
690 [self addBreakpoint:bp];
691
692 // Load the debugger to make it look active.
693 [delegate debuggerConnected];
694
695 // TODO: update the status.
696 }
697
698 /**
699 * Receiver for status updates. This just freshens up the UI.
700 */
701 - (void)updateStatus:(NSXMLDocument*)response
702 {
703 self.status = [[[[response rootElement] attributeForName:@"status"] stringValue] capitalizedString];
704 if (status == nil || [status isEqualToString:@"Stopped"] || [status isEqualToString:@"Stopping"])
705 {
706 connected = NO;
707 [self close];
708 [delegate debuggerDisconnected];
709
710 self.status = @"Stopped";
711 }
712 }
713
714 /**
715 * Step in/out/over and run all take this path. We first get the status of the
716 * debugger and then request fresh stack information.
717 */
718 - (void)debuggerStep:(NSXMLDocument*)response
719 {
720 [self updateStatus:response];
721 if (!connected)
722 return;
723
724 // If this is the run command, tell the delegate that a bunch of updates
725 // are coming. Also remove all existing stack routes and request a new stack.
726 // TODO: figure out if we can not clobber the stack every time.
727 NSString* command = [[[response rootElement] attributeForName:@"command"] stringValue];
728 if (YES || [command isEqualToString:@"run"])
729 {
730 if ([delegate respondsToSelector:@selector(clobberStack)])
731 [delegate clobberStack];
732 [stackFrames_ removeAllObjects];
733 stackFirstTransactionID_ = [[self sendCommandWithCallback:@selector(rebuildStack:) format:@"stack_depth"] intValue];
734 }
735 }
736
737 /**
738 * We ask for the stack_depth and now we clobber the stack and start rebuilding
739 * it.
740 */
741 - (void)rebuildStack:(NSXMLDocument*)response
742 {
743 NSInteger depth = [[[[response rootElement] attributeForName:@"depth"] stringValue] intValue];
744
745 if (stackFirstTransactionID_ == [self transactionIDFromResponse:response])
746 stackDepth_ = depth;
747
748 // We now need to alloc a bunch of stack frames and get the basic information
749 // for them.
750 for (NSInteger i = 0; i < depth; i++)
751 {
752 // Use the transaction ID to create a routing path.
753 NSNumber* routingID = [self sendCommandWithCallback:@selector(getStackFrame:) format:@"stack_get -d %d", i];
754 [stackFrames_ setObject:[StackFrame alloc] forKey:routingID];
755 }
756 }
757
758 /**
759 * The initial rebuild of the stack frame. We now have enough to initialize
760 * a StackFrame object.
761 */
762 - (void)getStackFrame:(NSXMLDocument*)response
763 {
764 // Get the routing information.
765 NSInteger routingID = [self transactionIDFromResponse:response];
766 if (routingID < stackFirstTransactionID_)
767 return;
768 NSNumber* routingNumber = [NSNumber numberWithInt:routingID];
769
770 // Make sure we initialized this frame in our last |-rebuildStack:|.
771 StackFrame* frame = [stackFrames_ objectForKey:routingNumber];
772 if (!frame)
773 return;
774
775 NSXMLElement* xmlframe = [[[response rootElement] children] objectAtIndex:0];
776
777 // Initialize the stack frame.
778 [frame initWithIndex:[[[xmlframe attributeForName:@"level"] stringValue] intValue]
779 withFilename:[[xmlframe attributeForName:@"filename"] stringValue]
780 withSource:nil
781 atLine:[[[xmlframe attributeForName:@"lineno"] stringValue] intValue]
782 inFunction:[[xmlframe attributeForName:@"where"] stringValue]
783 withVariables:nil];
784
785 // Get the source code of the file. Escape % in URL chars.
786 NSString* escapedFilename = [frame.filename stringByReplacingOccurrencesOfString:@"%" withString:@"%%"];
787 NSNumber* transaction = [self sendCommandWithCallback:@selector(setSource:) format:@"source -f %@", escapedFilename];
788 [callbackContext_ setObject:routingNumber forKey:transaction];
789
790 // Get the names of all the contexts.
791 transaction = [self sendCommandWithCallback:@selector(contextsReceived:) format:@"context_names -d %d", frame.index];
792 [callbackContext_ setObject:routingNumber forKey:transaction];
793
794 if ([delegate respondsToSelector:@selector(newStackFrame:)])
795 [delegate newStackFrame:frame];
796 }
797
798 /**
799 * Callback for setting the source of a file while rebuilding a specific stack
800 * frame.
801 */
802 - (void)setSource:(NSXMLDocument*)response
803 {
804 NSNumber* transaction = [NSNumber numberWithInt:[self transactionIDFromResponse:response]];
805 if ([transaction intValue] < stackFirstTransactionID_)
806 return;
807 NSNumber* routingNumber = [callbackContext_ objectForKey:transaction];
808 if (!routingNumber)
809 return;
810
811 [callbackContext_ removeObjectForKey:transaction];
812 StackFrame* frame = [stackFrames_ objectForKey:routingNumber];
813 if (!frame)
814 return;
815
816 frame.source = [[response rootElement] value];
817
818 if ([delegate respondsToSelector:@selector(sourceUpdated:)])
819 [delegate sourceUpdated:frame];
820 }
821
822 /**
823 * Enumerates all the contexts of a given stack frame. We then in turn get the
824 * contents of each one of these contexts.
825 */
826 - (void)contextsReceived:(NSXMLDocument*)response
827 {
828 // Get the stack frame's routing ID and use it again.
829 NSNumber* receivedTransaction = [NSNumber numberWithInt:[self transactionIDFromResponse:response]];
830 if ([receivedTransaction intValue] < stackFirstTransactionID_)
831 return;
832 NSNumber* routingID = [callbackContext_ objectForKey:receivedTransaction];
833 if (!routingID)
834 return;
835
836 // Get the stack frame by the |routingID|.
837 StackFrame* frame = [stackFrames_ objectForKey:routingID];
838
839 NSXMLElement* contextNames = [response rootElement];
840 for (NSXMLElement* context in [contextNames children])
841 {
842 NSInteger cid = [[[context attributeForName:@"id"] stringValue] intValue];
843
844 // Fetch each context's variables.
845 NSNumber* transaction = [self sendCommandWithCallback:@selector(variablesReceived:)
846 format:@"context_get -d %d -c %d", frame.index, cid];
847 [callbackContext_ setObject:routingID forKey:transaction];
848 }
849 }
850
851 /**
852 * Receives the variables from the context and attaches them to the stack frame.
853 */
854 - (void)variablesReceived:(NSXMLDocument*)response
855 {
856 // Get the stack frame's routing ID and use it again.
857 NSInteger transaction = [self transactionIDFromResponse:response];
858 if (transaction < stackFirstTransactionID_)
859 return;
860 NSNumber* receivedTransaction = [NSNumber numberWithInt:transaction];
861 NSNumber* routingID = [callbackContext_ objectForKey:receivedTransaction];
862 if (!routingID)
863 return;
864
865 // Get the stack frame by the |routingID|.
866 StackFrame* frame = [stackFrames_ objectForKey:routingID];
867
868 NSMutableArray* variables = [NSMutableArray array];
869
870 // Merge the frame's existing variables.
871 if (frame.variables)
872 [variables addObjectsFromArray:frame.variables];
873
874 // Add these new variables.
875 NSArray* addVariables = [[response rootElement] children];
876 if (addVariables)
877 [variables addObjectsFromArray:addVariables];
878
879 frame.variables = variables;
880 }
881
882 /**
883 * Callback from a |-getProperty:| request.
884 */
885 - (void)propertiesReceived:(NSXMLDocument*)response
886 {
887 NSInteger transaction = [self transactionIDFromResponse:response];
888
889 /*
890 <response>
891 <property> <!-- this is the one we requested -->
892 <property ... /> <!-- these are what we want -->
893 </property>
894 </repsonse>
895 */
896
897 // Detach all the children so we can insert them into another document.
898 NSXMLElement* parent = (NSXMLElement*)[[response rootElement] childAtIndex:0];
899 NSArray* children = [parent children];
900 [parent setChildren:nil];
901
902 [delegate receivedProperties:children forTransaction:transaction];
903 }
904
905 /**
906 * Callback for setting a breakpoint.
907 */
908 - (void)breakpointReceived:(NSXMLDocument*)response
909 {
910 NSNumber* transaction = [NSNumber numberWithInt:[self transactionIDFromResponse:response]];
911 Breakpoint* bp = [callbackContext_ objectForKey:transaction];
912 if (!bp)
913 return;
914
915 [callbackContext_ removeObjectForKey:callbackContext_];
916 [bp setDebuggerId:[[[[response rootElement] attributeForName:@"id"] stringValue] intValue]];
917 }
918
919 #pragma mark Private
920
921 /**
922 * This will send a command to the debugger engine. It will append the
923 * transaction ID automatically. It accepts a NSString command along with a
924 * a variable number of arguments to substitute into the command, a la
925 * +[NSString stringWithFormat:]. Returns the transaction ID as a NSNumber.
926 */
927 - (NSNumber*)sendCommandWithCallback:(SEL)callback format:(NSString*)format, ...
928 {
929 // Collect varargs and format command.
930 va_list args;
931 va_start(args, format);
932 NSString* command = [[NSString alloc] initWithFormat:format arguments:args];
933 va_end(args);
934
935 NSNumber* callbackKey = [NSNumber numberWithInt:transactionID++];
936 if (callback)
937 [callTable_ setObject:NSStringFromSelector(callback) forKey:callbackKey];
938
939 [self send:[NSString stringWithFormat:@"%@ -i %@", [command autorelease], callbackKey]];
940
941 return callbackKey;
942 }
943
944 /**
945 * Checks if there are unsent commands in the |queuedWrites_| queue and sends
946 * them if it's OK to do so. This will not block.
947 */
948 - (void)sendQueuedWrites
949 {
950 if (!connected)
951 return;
952
953 [writeQueueLock_ lock];
954 if (lastReadTransaction_ >= lastWrittenTransaction_ && [queuedWrites_ count] > 0)
955 {
956 NSString* command = [queuedWrites_ objectAtIndex:0];
957
958 // We don't want to block because this is called from the main thread.
959 // |-performSend:| busy waits when the stream is not ready. Bail out
960 // before we do that becuase busy waiting is BAD.
961 if (CFWriteStreamCanAcceptBytes(writeStream_))
962 {
963 [self performSend:command];
964 [queuedWrites_ removeObjectAtIndex:0];
965 }
966 }
967 [writeQueueLock_ unlock];
968 }
969
970 /**
971 * Given a file path, this returns a file:// URI and escapes any spaces for the
972 * debugger engine.
973 */
974 - (NSString*)escapedURIPath:(NSString*)path
975 {
976 // Custon GDBp paths are fine.
977 if ([[path substringToIndex:4] isEqualToString:@"gdbp"])
978 return path;
979
980 // Create a temporary URL that will escape all the nasty characters.
981 NSURL* url = [NSURL fileURLWithPath:path];
982 NSString* urlString = [url absoluteString];
983
984 // Remove the host because this is a file:// URL;
985 urlString = [urlString stringByReplacingOccurrencesOfString:[url host] withString:@""];
986
987 // Escape % for use in printf-style NSString formatters.
988 urlString = [urlString stringByReplacingOccurrencesOfString:@"%" withString:@"%%"];
989 return urlString;
990 }
991
992 /**
993 * Returns the transaction_id from an NSXMLDocument.
994 */
995 - (NSInteger)transactionIDFromResponse:(NSXMLDocument*)response
996 {
997 return [[[[response rootElement] attributeForName:@"transaction_id"] stringValue] intValue];
998 }
999
1000 /**
1001 * Scans a command string for the transaction ID component. If it is not found,
1002 * returns NSNotFound.
1003 */
1004 - (NSInteger)transactionIDFromCommand:(NSString*)command
1005 {
1006 NSRange occurrence = [command rangeOfString:@"-i "];
1007 if (occurrence.location == NSNotFound)
1008 return NSNotFound;
1009 NSString* transaction = [command substringFromIndex:occurrence.location + occurrence.length];
1010 return [transaction intValue];
1011 }
1012
1013 @end