Rewrite |-[DebuggerConnection getProperty:]| to be asynchronous.
[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
24 // GDBpConnection (Private) ////////////////////////////////////////////////////
25
26 @interface DebuggerConnection ()
27 @property (readwrite, copy) NSString* status;
28 @property (assign) CFSocketRef socket;
29 @property (assign) CFReadStreamRef readStream;
30 @property NSUInteger lastReadTransaction;
31 @property (retain) NSMutableString* currentPacket;
32 @property (assign) CFWriteStreamRef writeStream;
33 @property NSUInteger lastWrittenTransaction;
34 @property (retain) NSMutableArray* queuedWrites;
35
36 - (void)connect;
37 - (void)close;
38 - (void)socketDidAccept;
39 - (void)socketDisconnected;
40 - (void)readStreamHasData;
41 - (void)send:(NSString*)command;
42 - (void)performSend:(NSString*)command;
43 - (void)errorEncountered:(NSString*)error;
44
45 - (void)handleResponse:(NSXMLDocument*)response;
46 - (void)initReceived:(NSXMLDocument*)response;
47 - (void)updateStatus:(NSXMLDocument*)response;
48 - (void)debuggerStep:(NSXMLDocument*)response;
49 - (void)rebuildStack:(NSXMLDocument*)response;
50 - (void)getStackFrame:(NSXMLDocument*)response;
51 - (void)setSource:(NSXMLDocument*)response;
52 - (void)contextsReceived:(NSXMLDocument*)response;
53 - (void)variablesReceived:(NSXMLDocument*)response;
54 - (void)propertiesReceived:(NSXMLDocument*)response;
55
56 - (NSNumber*)sendCommandWithCallback:(SEL)callback format:(NSString*)format, ...;
57
58 - (void)sendQueuedWrites;
59
60 - (NSString*)escapedURIPath:(NSString*)path;
61 - (NSInteger)transactionIDFromResponse:(NSXMLDocument*)response;
62 @end
63
64 // CFNetwork Callbacks /////////////////////////////////////////////////////////
65
66 void ReadStreamCallback(CFReadStreamRef stream, CFStreamEventType eventType, void* connectionRaw)
67 {
68 DebuggerConnection* connection = (DebuggerConnection*)connectionRaw;
69 switch (eventType)
70 {
71 case kCFStreamEventHasBytesAvailable:
72 NSLog(@"About to read.");
73 [connection readStreamHasData];
74 break;
75
76 case kCFStreamEventErrorOccurred:
77 {
78 CFErrorRef error = CFReadStreamCopyError(stream);
79 CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
80 CFReadStreamClose(stream);
81 CFRelease(stream);
82 [connection errorEncountered:[[(NSError*)error autorelease] description]];
83 break;
84 }
85
86 case kCFStreamEventEndEncountered:
87 CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
88 CFReadStreamClose(stream);
89 CFRelease(stream);
90 [connection socketDisconnected];
91 break;
92 };
93 }
94
95 void WriteStreamCallback(CFWriteStreamRef stream, CFStreamEventType eventType, void* connectionRaw)
96 {
97 DebuggerConnection* connection = (DebuggerConnection*)connectionRaw;
98 switch (eventType)
99 {
100 case kCFStreamEventCanAcceptBytes:
101 [connection sendQueuedWrites];
102 break;
103
104 case kCFStreamEventErrorOccurred:
105 {
106 CFErrorRef error = CFWriteStreamCopyError(stream);
107 CFWriteStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
108 CFWriteStreamClose(stream);
109 CFRelease(stream);
110 [connection errorEncountered:[[(NSError*)error autorelease] description]];
111 break;
112 }
113
114 case kCFStreamEventEndEncountered:
115 CFWriteStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
116 CFWriteStreamClose(stream);
117 CFRelease(stream);
118 [connection socketDisconnected];
119 break;
120 }
121 }
122
123 void SocketAcceptCallback(CFSocketRef socket,
124 CFSocketCallBackType callbackType,
125 CFDataRef address,
126 const void* data,
127 void* connectionRaw)
128 {
129 assert(callbackType == kCFSocketAcceptCallBack);
130 NSLog(@"SocketAcceptCallback()");
131
132 DebuggerConnection* connection = (DebuggerConnection*)connectionRaw;
133
134 CFReadStreamRef readStream;
135 CFWriteStreamRef writeStream;
136
137 // Create the streams on the socket.
138 CFStreamCreatePairWithSocket(kCFAllocatorDefault,
139 *(CFSocketNativeHandle*)data, // Socket handle.
140 &readStream, // Read stream in-pointer.
141 &writeStream); // Write stream in-pointer.
142
143 // Create struct to register callbacks for the stream.
144 CFStreamClientContext context;
145 context.version = 0;
146 context.info = connection;
147 context.retain = NULL;
148 context.release = NULL;
149 context.copyDescription = NULL;
150
151 // Set the client of the read stream.
152 CFOptionFlags readFlags =
153 kCFStreamEventOpenCompleted |
154 kCFStreamEventHasBytesAvailable |
155 kCFStreamEventErrorOccurred |
156 kCFStreamEventEndEncountered;
157 if (CFReadStreamSetClient(readStream, readFlags, ReadStreamCallback, &context))
158 // Schedule in run loop to do asynchronous communication with the engine.
159 CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
160 else
161 return;
162
163 // Open the stream now that it's scheduled on the run loop.
164 if (!CFReadStreamOpen(readStream))
165 {
166 CFStreamError error = CFReadStreamGetError(readStream);
167 NSLog(@"error! %@", error);
168 return;
169 }
170
171 // Set the client of the write stream.
172 CFOptionFlags writeFlags =
173 kCFStreamEventOpenCompleted |
174 kCFStreamEventCanAcceptBytes |
175 kCFStreamEventErrorOccurred |
176 kCFStreamEventEndEncountered;
177 if (CFWriteStreamSetClient(writeStream, writeFlags, WriteStreamCallback, &context))
178 // Schedule it in the run loop to receive error information.
179 CFWriteStreamScheduleWithRunLoop(writeStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
180 else
181 return;
182
183 // Open the write stream.
184 if (!CFWriteStreamOpen(writeStream))
185 {
186 CFStreamError error = CFWriteStreamGetError(writeStream);
187 NSLog(@"error! %@", error);
188 return;
189 }
190
191 connection.readStream = readStream;
192 connection.writeStream = writeStream;
193 [connection socketDidAccept];
194 }
195
196 // GDBpConnection //////////////////////////////////////////////////////////////
197
198 @implementation DebuggerConnection
199 @synthesize socket = socket_;
200 @synthesize readStream = readStream_;
201 @synthesize lastReadTransaction = lastReadTransaction_;
202 @synthesize currentPacket = currentPacket_;
203 @synthesize writeStream = writeStream_;
204 @synthesize lastWrittenTransaction = lastWrittenTransaction_;
205 @synthesize queuedWrites = queuedWrites_;
206 @synthesize status;
207 @synthesize delegate;
208
209 /**
210 * Creates a new DebuggerConnection and initializes the socket from the given connection
211 * paramters.
212 */
213 - (id)initWithPort:(NSUInteger)aPort
214 {
215 if (self = [super init])
216 {
217 port = aPort;
218 connected = NO;
219
220 [[BreakpointManager sharedManager] setConnection:self];
221
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 * Creates an entirely new stack and returns it as an array of StackFrame objects.
287 */
288 - (NSArray*)getCurrentStack
289 {
290 NSMutableArray* stack = [NSMutableArray array];
291 NSLog(@"NOTIMPLEMENTED(): %s", _cmd);
292 return stack;
293 }
294
295 /**
296 * Tells the debugger to continue running the script. Returns the current stack frame.
297 */
298 - (void)run
299 {
300 [self sendCommandWithCallback:@selector(debuggerStep:) format:@"run"];
301 }
302
303 /**
304 * Tells the debugger to step into the current command.
305 */
306 - (void)stepIn
307 {
308 [self sendCommandWithCallback:@selector(debuggerStep:) format:@"step_into"];
309 }
310
311 /**
312 * Tells the debugger to step out of the current context
313 */
314 - (void)stepOut
315 {
316 [self sendCommandWithCallback:@selector(debuggerStep:) format:@"step_out"];
317 }
318
319 /**
320 * Tells the debugger to step over the current function
321 */
322 - (void)stepOver
323 {
324 [self sendCommandWithCallback:@selector(debuggerStep:) format:@"step_over"];
325 }
326
327 /**
328 * Tells the debugger engine to get a specifc property. This also takes in the NSXMLElement
329 * that requested it so that the child can be attached.
330 */
331 - (NSInteger)getProperty:(NSString*)property
332 {
333 [self sendCommandWithCallback:@selector(propertiesReceived:) format:@"property_get -n \"%@\"", property];
334 }
335
336 // Breakpoint Management ///////////////////////////////////////////////////////
337 #pragma mark Breakpoints
338
339 /**
340 * Send an add breakpoint command
341 */
342 - (void)addBreakpoint:(Breakpoint*)bp
343 {
344 if (!connected)
345 return;
346
347 NSString* file = [self escapedURIPath:[bp transformedPath]];
348 NSNumber* transaction = [self sendCommandWithCallback:@selector(breakpointReceived:)
349 format:@"breakpoint_set -t line -f %@ -n %i", file, [bp line]];
350 [callbackContext_ setObject:bp forKey:transaction];
351 }
352
353 /**
354 * Removes a breakpoint
355 */
356 - (void)removeBreakpoint:(Breakpoint*)bp
357 {
358 if (!connected)
359 {
360 return;
361 }
362
363 [self sendCommandWithCallback:nil format:@"breakpoint_remove -d %i", [bp debuggerId]];
364 }
365
366
367 // Socket and Stream Callbacks /////////////////////////////////////////////////
368 #pragma mark Callbacks
369
370 /**
371 * Called by SocketWrapper after the connection is successful. This immediately calls
372 * -[SocketWrapper receive] to clear the way for communication, though the information
373 * could be useful server information that we don't use right now.
374 */
375 - (void)socketDidAccept
376 {
377 connected = YES;
378 transactionID = 1;
379 stackFrames_ = [[NSMutableDictionary alloc] init];
380 self.queuedWrites = [NSMutableArray array];
381 writeQueueLock_ = [NSRecursiveLock new];
382 callTable_ = [NSMutableDictionary new];
383 callbackContext_ = [NSMutableDictionary new];
384 }
385
386 /**
387 * Receives errors from the SocketWrapper and updates the display
388 */
389 - (void)errorEncountered:(NSString*)error
390 {
391 [delegate errorEncountered:error];
392 }
393
394 /**
395 * Creates, connects to, and schedules a CFSocket.
396 */
397 - (void)connect
398 {
399 // Pass ourselves to the callback so we don't have to use ugly globals.
400 CFSocketContext context;
401 context.version = 0;
402 context.info = self;
403 context.retain = NULL;
404 context.release = NULL;
405 context.copyDescription = NULL;
406
407 // Create the address structure.
408 struct sockaddr_in address;
409 memset(&address, 0, sizeof(address));
410 address.sin_len = sizeof(address);
411 address.sin_family = AF_INET;
412 address.sin_port = htons(port);
413 address.sin_addr.s_addr = htonl(INADDR_ANY);
414
415 // Create the socket signature.
416 CFSocketSignature signature;
417 signature.protocolFamily = PF_INET;
418 signature.socketType = SOCK_STREAM;
419 signature.protocol = IPPROTO_TCP;
420 signature.address = (CFDataRef)[NSData dataWithBytes:&address length:sizeof(address)];
421
422 socket_ = CFSocketCreateWithSocketSignature(kCFAllocatorDefault,
423 &signature, // Socket signature.
424 kCFSocketAcceptCallBack, // Callback types.
425 SocketAcceptCallback, // Callout function pointer.
426 &context); // Context to pass to callout.
427 if (!socket_)
428 {
429 [self errorEncountered:@"Could not open socket."];
430 return;
431 }
432
433 // Allow old, yet-to-be recycled sockets to be reused.
434 BOOL yes = YES;
435 setsockopt(CFSocketGetNative(socket_), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(BOOL));
436
437 // Schedule the socket on the run loop.
438 CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket_, 0);
439 CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
440 CFRelease(source);
441
442 self.status = @"Connecting";
443 }
444
445 /**
446 * Closes a socket and releases the ref.
447 */
448 - (void)close
449 {
450 // The socket goes down, so do the streams, which clean themselves up.
451 CFSocketInvalidate(socket_);
452 CFRelease(socket_);
453 [stackFrames_ release];
454 self.queuedWrites = nil;
455 [writeQueueLock_ release];
456 [callTable_ release];
457 [callbackContext_ release];
458 }
459
460 /**
461 * Notification that the socket disconnected.
462 */
463 - (void)socketDisconnected
464 {
465 [self close];
466 [delegate debuggerDisconnected];
467 }
468
469 /**
470 * Callback from the CFReadStream that there is data waiting to be read.
471 */
472 - (void)readStreamHasData
473 {
474 UInt8 buffer[1024];
475 CFIndex bytesRead = CFReadStreamRead(readStream_, buffer, 1024);
476 const char* charBuffer = (const char*)buffer;
477
478 // We haven't finished reading a packet, so just read more data in.
479 if (currentPacketIndex_ < packetSize_)
480 {
481 currentPacketIndex_ += bytesRead;
482 CFStringRef bufferString = CFStringCreateWithBytes(kCFAllocatorDefault,
483 buffer,
484 bytesRead,
485 kCFStringEncodingUTF8,
486 true);
487 [self.currentPacket appendString:(NSString*)bufferString];
488 CFRelease(bufferString);
489 }
490 // Time to read a new packet.
491 else
492 {
493 // Read the message header: the size.
494 packetSize_ = atoi(charBuffer);
495 currentPacketIndex_ = bytesRead - strlen(charBuffer);
496 CFStringRef bufferString = CFStringCreateWithBytes(kCFAllocatorDefault,
497 buffer + strlen(charBuffer) + 1,
498 bytesRead - strlen(charBuffer) - 1,
499 kCFStringEncodingUTF8,
500 true);
501 self.currentPacket = [NSMutableString stringWithString:(NSString*)bufferString];
502 CFRelease(bufferString);
503 }
504
505 // We have finished reading the packet.
506 if (currentPacketIndex_ >= packetSize_)
507 {
508 packetSize_ = 0;
509 currentPacketIndex_ = 0;
510
511 // Test if we can convert it into an NSXMLDocument.
512 NSError* error = nil;
513 NSXMLDocument* xmlTest = [[NSXMLDocument alloc] initWithXMLString:currentPacket_ options:NSXMLDocumentTidyXML error:&error];
514 if (error)
515 {
516 NSLog(@"Could not parse XML? --- %@", error);
517 NSLog(@"Error UserInfo: %@", [error userInfo]);
518 NSLog(@"This is the XML Document: %@", currentPacket_);
519
520 // We do not want to starve the write queue, so manually parse out the
521 // transaction ID.
522 NSRange location = [currentPacket_ rangeOfString:@"transaction_id"];
523 if (location.location != NSNotFound)
524 {
525 NSUInteger start = location.location + location.length;
526 NSUInteger end = start;
527
528 NSCharacterSet* numericSet = [NSCharacterSet decimalDigitCharacterSet];
529
530 // Loop over the characters after the attribute name to extract the ID.
531 while (end < [currentPacket_ length])
532 {
533 unichar c = [currentPacket_ characterAtIndex:end];
534 if ([numericSet characterIsMember:c])
535 {
536 // If this character is numeric, extend the range to substring.
537 ++end;
538 }
539 else
540 {
541 if (start == end)
542 {
543 // If this character is nonnumeric and we have nothing in the
544 // range, skip this character.
545 ++start;
546 ++end;
547 }
548 else
549 {
550 // We've moved past the numeric ID so we should stop searching.
551 break;
552 }
553 }
554 }
555
556 // If we were able to extract the transaction ID, update the last read.
557 NSRange substringRange = NSMakeRange(start, end - start);
558 NSString* transaction = [currentPacket_ substringWithRange:substringRange];
559 if ([transaction length])
560 {
561 lastReadTransaction_ = [transaction intValue];
562 return;
563 }
564 }
565
566 // Otherwise, assume +1 and hope it works.
567 ++lastReadTransaction_;
568 return;
569 }
570 [self handleResponse:[xmlTest autorelease]];
571 }
572 }
573
574 /**
575 * Writes a command into the write stream. If the stream is ready for writing,
576 * we do so immediately. If not, the command is queued and will be written
577 * when the stream is ready.
578 */
579 - (void)send:(NSString*)command
580 {
581 if (lastReadTransaction_ >= lastWrittenTransaction_ && CFWriteStreamCanAcceptBytes(writeStream_))
582 [self performSend:command];
583 else
584 [queuedWrites_ addObject:command];
585 }
586
587 /**
588 * This performs a blocking send. This should ONLY be called when we know we
589 * have write access to the stream. We will busy wait in case we don't do a full
590 * send.
591 */
592 - (void)performSend:(NSString*)command
593 {
594 BOOL done = NO;
595
596 char* string = (char*)[command UTF8String];
597 int stringLength = strlen(string);
598
599 // Log the command if TransportDebug is enabled.
600 if ([[[[NSProcessInfo processInfo] environment] objectForKey:@"TransportDebug"] boolValue])
601 NSLog(@"--> %@", command);
602
603 // Busy wait while writing. BAADD. Should background this operation.
604 while (!done)
605 {
606 if (CFWriteStreamCanAcceptBytes(writeStream_))
607 {
608 // Include the NULL byte in the string when we write.
609 int bytesWritten = CFWriteStreamWrite(writeStream_, (UInt8*)string, stringLength + 1);
610 if (bytesWritten < 0)
611 {
612 NSLog(@"write error");
613 }
614 // Incomplete write.
615 else if (bytesWritten < strlen(string))
616 {
617 // Adjust the buffer and wait for another chance to write.
618 stringLength -= bytesWritten;
619 memmove(string, string + bytesWritten, stringLength);
620 }
621 else
622 {
623 done = YES;
624
625 // We need to scan the string to find the transactionID.
626 NSRange occurrence = [command rangeOfString:@"-i "];
627 if (occurrence.location == NSNotFound)
628 {
629 NSLog(@"sent %@ without a transaction ID", command);
630 continue;
631 }
632 NSString* transaction = [command substringFromIndex:occurrence.location + occurrence.length];
633 lastWrittenTransaction_ = [transaction intValue];
634 NSLog(@"command = %@", command);
635 NSLog(@"read=%d, write=%d", lastReadTransaction_, lastWrittenTransaction_);
636 }
637 }
638 }
639 }
640
641 - (void)handleResponse:(NSXMLDocument*)response
642 {
643 // Check and see if there's an error.
644 NSArray* error = [[response rootElement] elementsForName:@"error"];
645 if ([error count] > 0)
646 {
647 NSLog(@"Xdebug error: %@", error);
648 [delegate errorEncountered:[[[[error objectAtIndex:0] children] objectAtIndex:0] stringValue]];
649 }
650
651 // If TransportDebug is enabled, log the response.
652 if ([[[[NSProcessInfo processInfo] environment] objectForKey:@"TransportDebug"] boolValue])
653 NSLog(@"<-- %@", response);
654
655 // Get the name of the command from the engine's response.
656 NSInteger transaction = [self transactionIDFromResponse:response];
657 if (transaction < lastReadTransaction_)
658 NSLog(@"out of date transaction %@", response);
659
660 if (transaction != lastWrittenTransaction_)
661 NSLog(@"txn doesn't match last written %@", response);
662
663 lastReadTransaction_ = transaction;
664 NSLog(@"read=%d, write=%d", lastReadTransaction_, lastWrittenTransaction_);
665
666 if ([[[response rootElement] name] isEqualToString:@"init"])
667 {
668 [self initReceived:response];
669 return;
670 }
671
672 NSString* callbackStr = [callTable_ objectForKey:[NSNumber numberWithInt:lastReadTransaction_]];
673 if (callbackStr)
674 {
675 SEL callback = NSSelectorFromString(callbackStr);
676 [self performSelector:callback withObject:response];
677 }
678
679 [self sendQueuedWrites];
680 }
681
682 // Specific Response Handlers //////////////////////////////////////////////////
683 #pragma mark Response Handlers
684
685 /**
686 * Initial packet received. We've started a brand-new connection to the engine.
687 */
688 - (void)initReceived:(NSXMLDocument*)response
689 {
690 // Register any breakpoints that exist offline.
691 for (Breakpoint* bp in [[BreakpointManager sharedManager] breakpoints])
692 [self addBreakpoint:bp];
693
694 // Load the debugger to make it look active.
695 [delegate debuggerConnected];
696
697 // TODO: update the status.
698 }
699
700 /**
701 * Receiver for status updates. This just freshens up the UI.
702 */
703 - (void)updateStatus:(NSXMLDocument*)response
704 {
705 self.status = [[[[response rootElement] attributeForName:@"status"] stringValue] capitalizedString];
706 if (status == nil || [status isEqualToString:@"Stopped"] || [status isEqualToString:@"Stopping"])
707 {
708 connected = NO;
709 [self close];
710 [delegate debuggerDisconnected];
711
712 self.status = @"Stopped";
713 }
714 }
715
716 /**
717 * Step in/out/over and run all take this path. We first get the status of the
718 * debugger and then request fresh stack information.
719 */
720 - (void)debuggerStep:(NSXMLDocument*)response
721 {
722 [self sendCommandWithCallback:@selector(updateStatus:) format:@"status"];
723 NSString* command = [[[response rootElement] attributeForName:@"command"] stringValue];
724
725 // If this is the run command, tell the delegate that a bunch of updates
726 // are coming. Also remove all existing stack routes and request a new stack.
727 // TODO: figure out if we can not clobber the stack every time.
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 [writeQueueLock_ lock];
951 if (lastReadTransaction_ >= lastWrittenTransaction_ && [queuedWrites_ count] > 0)
952 {
953 NSString* command = [queuedWrites_ objectAtIndex:0];
954 NSLog(@"Sending queued write: %@", command);
955
956 // We don't want to block because this is called from the main thread.
957 // |-performSend:| busy waits when the stream is not ready. Bail out
958 // before we do that becuase busy waiting is BAD.
959 if (CFWriteStreamCanAcceptBytes(writeStream_))
960 {
961 [self performSend:command];
962 [queuedWrites_ removeObjectAtIndex:0];
963 }
964 }
965 [writeQueueLock_ unlock];
966 }
967
968 /**
969 * Given a file path, this returns a file:// URI and escapes any spaces for the
970 * debugger engine.
971 */
972 - (NSString*)escapedURIPath:(NSString*)path
973 {
974 // Custon GDBp paths are fine.
975 if ([[path substringToIndex:4] isEqualToString:@"gdbp"])
976 return path;
977
978 // Create a temporary URL that will escape all the nasty characters.
979 NSURL* url = [NSURL fileURLWithPath:path];
980 NSString* urlString = [url absoluteString];
981
982 // Remove the host because this is a file:// URL;
983 urlString = [urlString stringByReplacingOccurrencesOfString:[url host] withString:@""];
984
985 // Escape % for use in printf-style NSString formatters.
986 urlString = [urlString stringByReplacingOccurrencesOfString:@"%" withString:@"%%"];
987 return urlString;
988 }
989
990 /**
991 * Returns the transaction_id from an NSXMLDocument.
992 */
993 - (NSInteger)transactionIDFromResponse:(NSXMLDocument*)response
994 {
995 return [[[[response rootElement] attributeForName:@"transaction_id"] stringValue] intValue];
996 }
997
998 @end