Lazily load the complete stack frame for anything but the current one.
[macgdbp.git] / Source / DebuggerProcessor.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 "DebuggerProcessor.h"
18
19 #import "AppDelegate.h"
20
21 // GDBpConnection (Private) ////////////////////////////////////////////////////
22
23 @interface DebuggerProcessor ()
24 @property (readwrite, copy) NSString* status;
25
26 - (void)recordCallback:(SEL)callback forTransaction:(NSNumber*)txn;
27
28 - (void)updateStatus:(NSXMLDocument*)response;
29 - (void)debuggerStep:(NSXMLDocument*)response;
30 - (void)rebuildStack:(NSXMLDocument*)response;
31 - (void)getStackFrame:(NSXMLDocument*)response;
32 - (void)setSource:(NSXMLDocument*)response;
33 - (void)contextsReceived:(NSXMLDocument*)response;
34 - (void)variablesReceived:(NSXMLDocument*)response;
35 - (void)propertiesReceived:(NSXMLDocument*)response;
36
37 @end
38
39 // GDBpConnection //////////////////////////////////////////////////////////////
40
41 @implementation DebuggerProcessor
42
43 @synthesize status;
44 @synthesize delegate;
45
46 /**
47 * Creates a new DebuggerConnection and initializes the socket from the given connection
48 * paramters.
49 */
50 - (id)initWithPort:(NSUInteger)aPort
51 {
52 if (self = [super init])
53 {
54 stackFrames_ = [[NSMutableDictionary alloc] init];
55 callbackContext_ = [NSMutableDictionary new];
56 callTable_ = [NSMutableDictionary new];
57
58 [[BreakpointManager sharedManager] setConnection:self];
59 connection_ = [[DebuggerConnection alloc] initWithPort:aPort];
60 connection_.delegate = self;
61 [connection_ connect];
62 }
63 return self;
64 }
65
66 /**
67 * Deallocates the object
68 */
69 - (void)dealloc
70 {
71 [connection_ close];
72 [stackFrames_ release];
73 [callTable_ release];
74 [callbackContext_ release];
75 [super dealloc];
76 }
77
78
79 // Getters /////////////////////////////////////////////////////////////////////
80 #pragma mark Getters
81
82 /**
83 * Gets the port number
84 */
85 - (NSUInteger)port
86 {
87 return [connection_ port];
88 }
89
90 /**
91 * Returns the name of the remote host
92 */
93 - (NSString*)remoteHost
94 {
95 if (![connection_ connected])
96 return @"(DISCONNECTED)";
97
98 // TODO: Either impl or remove.
99 return @"";
100 }
101
102 /**
103 * Returns whether or not we have an active connection
104 */
105 - (BOOL)isConnected
106 {
107 return [connection_ connected];
108 }
109
110 // Commands ////////////////////////////////////////////////////////////////////
111 #pragma mark Commands
112
113 /**
114 * Reestablishes communication with the remote debugger so that a new connection doesn't have to be
115 * created every time you want to debug a page
116 */
117 - (void)reconnect
118 {
119 [connection_ close];
120 self.status = @"Connecting";
121 [connection_ connect];
122 }
123
124 /**
125 * Tells the debugger to continue running the script. Returns the current stack frame.
126 */
127 - (void)run
128 {
129 NSNumber* tx = [connection_ sendCommandWithFormat:@"run"];
130 [self recordCallback:@selector(debuggerStep:) forTransaction:tx];
131 }
132
133 /**
134 * Tells the debugger to step into the current command.
135 */
136 - (void)stepIn
137 {
138 NSNumber* tx = [connection_ sendCommandWithFormat:@"step_into"];
139 [self recordCallback:@selector(debuggerStep:) forTransaction:tx];
140 }
141
142 /**
143 * Tells the debugger to step out of the current context
144 */
145 - (void)stepOut
146 {
147 NSNumber* tx = [connection_ sendCommandWithFormat:@"step_out"];
148 [self recordCallback:@selector(debuggerStep:) forTransaction:tx];
149 }
150
151 /**
152 * Tells the debugger to step over the current function
153 */
154 - (void)stepOver
155 {
156 NSNumber* tx = [connection_ sendCommandWithFormat:@"step_over"];
157 [self recordCallback:@selector(debuggerStep:) forTransaction:tx];
158 }
159
160 /**
161 * Tells the debugger engine to get a specifc property. This also takes in the NSXMLElement
162 * that requested it so that the child can be attached.
163 */
164 - (NSInteger)getProperty:(NSString*)property
165 {
166 NSNumber* tx = [connection_ sendCommandWithFormat:@"property_get -n \"%@\"", property];
167 [self recordCallback:@selector(propertiesReceived:) forTransaction:tx];
168 }
169
170 - (void)loadStackFrame:(StackFrame*)frame
171 {
172 if (frame.loaded)
173 return;
174
175 NSNumber* routingNumber = [NSNumber numberWithInt:frame.routingID];
176
177 // Get the source code of the file. Escape % in URL chars.
178 NSString* escapedFilename = [frame.filename stringByReplacingOccurrencesOfString:@"%" withString:@"%%"];
179 NSNumber* transaction = [connection_ sendCommandWithFormat:@"source -f %@", escapedFilename];
180 [self recordCallback:@selector(setSource:) forTransaction:transaction];
181 [callbackContext_ setObject:routingNumber forKey:transaction];
182
183 // Get the names of all the contexts.
184 transaction = [connection_ sendCommandWithFormat:@"context_names -d %d", frame.index];
185 [self recordCallback:@selector(contextsReceived:) forTransaction:transaction];
186 [callbackContext_ setObject:routingNumber forKey:transaction];
187
188 // This frame will be fully loaded.
189 frame.loaded = YES;
190 }
191
192 // Breakpoint Management ///////////////////////////////////////////////////////
193 #pragma mark Breakpoints
194
195 /**
196 * Send an add breakpoint command
197 */
198 - (void)addBreakpoint:(Breakpoint*)bp
199 {
200 if (![connection_ connected])
201 return;
202
203 NSString* file = [connection_ escapedURIPath:[bp transformedPath]];
204 NSNumber* tx = [connection_ sendCommandWithFormat:@"breakpoint_set -t line -f %@ -n %i", file, [bp line]];
205 [self recordCallback:@selector(breakpointReceived:) forTransaction:tx];
206 [callbackContext_ setObject:bp forKey:tx];
207 }
208
209 /**
210 * Removes a breakpoint
211 */
212 - (void)removeBreakpoint:(Breakpoint*)bp
213 {
214 if (![connection_ connected])
215 return;
216
217 [connection_ sendCommandWithFormat:@"breakpoint_remove -d %i", [bp debuggerId]];
218 }
219
220 // Specific Response Handlers //////////////////////////////////////////////////
221 #pragma mark Response Handlers
222
223 /**
224 * Initial packet received. We've started a brand-new connection to the engine.
225 */
226 - (void)handleInitialResponse:(NSXMLDocument*)response
227 {
228 // Register any breakpoints that exist offline.
229 for (Breakpoint* bp in [[BreakpointManager sharedManager] breakpoints])
230 [self addBreakpoint:bp];
231
232 // Load the debugger to make it look active.
233 [delegate debuggerConnected];
234
235 // TODO: update the status.
236 }
237
238 - (void)handleResponse:(NSXMLDocument*)response
239 {
240 NSInteger transactionID = [connection_ transactionIDFromResponse:response];
241 NSNumber* key = [NSNumber numberWithInt:transactionID];
242 NSString* callbackStr = [callTable_ objectForKey:key];
243 if (callbackStr)
244 {
245 SEL callback = NSSelectorFromString(callbackStr);
246 [self performSelector:callback withObject:response];
247 }
248 [callTable_ removeObjectForKey:key];
249 }
250
251 /**
252 * Receiver for status updates. This just freshens up the UI.
253 */
254 - (void)updateStatus:(NSXMLDocument*)response
255 {
256 self.status = [[[[response rootElement] attributeForName:@"status"] stringValue] capitalizedString];
257 if (status == nil || [status isEqualToString:@"Stopped"] || [status isEqualToString:@"Stopping"])
258 {
259 [connection_ close];
260 [delegate debuggerDisconnected];
261
262 self.status = @"Stopped";
263 }
264 }
265
266 /**
267 * Step in/out/over and run all take this path. We first get the status of the
268 * debugger and then request fresh stack information.
269 */
270 - (void)debuggerStep:(NSXMLDocument*)response
271 {
272 [self updateStatus:response];
273 if (![connection_ connected])
274 return;
275
276 // If this is the run command, tell the delegate that a bunch of updates
277 // are coming. Also remove all existing stack routes and request a new stack.
278 // TODO: figure out if we can not clobber the stack every time.
279 NSString* command = [[[response rootElement] attributeForName:@"command"] stringValue];
280 if (YES || [command isEqualToString:@"run"])
281 {
282 if ([delegate respondsToSelector:@selector(clobberStack)])
283 [delegate clobberStack];
284 [stackFrames_ removeAllObjects];
285 NSNumber* tx = [connection_ sendCommandWithFormat:@"stack_depth"];
286 [self recordCallback:@selector(rebuildStack:) forTransaction:tx];
287 stackFirstTransactionID_ = [tx intValue];
288 }
289 }
290
291 /**
292 * We ask for the stack_depth and now we clobber the stack and start rebuilding
293 * it.
294 */
295 - (void)rebuildStack:(NSXMLDocument*)response
296 {
297 NSInteger depth = [[[[response rootElement] attributeForName:@"depth"] stringValue] intValue];
298
299 if (stackFirstTransactionID_ == [connection_ transactionIDFromResponse:response])
300 stackDepth_ = depth;
301
302 // We now need to alloc a bunch of stack frames and get the basic information
303 // for them.
304 for (NSInteger i = 0; i < depth; i++)
305 {
306 // Use the transaction ID to create a routing path.
307 NSNumber* routingID = [connection_ sendCommandWithFormat:@"stack_get -d %d", i];
308 [self recordCallback:@selector(getStackFrame:) forTransaction:routingID];
309 [stackFrames_ setObject:[StackFrame alloc] forKey:routingID];
310 }
311 }
312
313 /**
314 * The initial rebuild of the stack frame. We now have enough to initialize
315 * a StackFrame object.
316 */
317 - (void)getStackFrame:(NSXMLDocument*)response
318 {
319 // Get the routing information.
320 NSInteger routingID = [connection_ transactionIDFromResponse:response];
321 if (routingID < stackFirstTransactionID_)
322 return;
323 NSNumber* routingNumber = [NSNumber numberWithInt:routingID];
324
325 // Make sure we initialized this frame in our last |-rebuildStack:|.
326 StackFrame* frame = [stackFrames_ objectForKey:routingNumber];
327 if (!frame)
328 return;
329
330 NSXMLElement* xmlframe = [[[response rootElement] children] objectAtIndex:0];
331
332 // Initialize the stack frame.
333 [frame initWithIndex:[[[xmlframe attributeForName:@"level"] stringValue] intValue]
334 withFilename:[[xmlframe attributeForName:@"filename"] stringValue]
335 withSource:nil
336 atLine:[[[xmlframe attributeForName:@"lineno"] stringValue] intValue]
337 inFunction:[[xmlframe attributeForName:@"where"] stringValue]
338 withVariables:nil];
339 frame.routingID = routingID;
340
341 // Only get the complete frame for the first level. The other frames will get
342 // information loaded lazily when the user clicks on one.
343 if (frame.index == 0) {
344 [self loadStackFrame:frame];
345 }
346
347 if ([delegate respondsToSelector:@selector(newStackFrame:)])
348 [delegate newStackFrame:frame];
349 }
350
351 /**
352 * Callback for setting the source of a file while rebuilding a specific stack
353 * frame.
354 */
355 - (void)setSource:(NSXMLDocument*)response
356 {
357 NSNumber* transaction = [NSNumber numberWithInt:[connection_ transactionIDFromResponse:response]];
358 if ([transaction intValue] < stackFirstTransactionID_)
359 return;
360 NSNumber* routingNumber = [callbackContext_ objectForKey:transaction];
361 if (!routingNumber)
362 return;
363
364 [callbackContext_ removeObjectForKey:transaction];
365 StackFrame* frame = [stackFrames_ objectForKey:routingNumber];
366 if (!frame)
367 return;
368
369 frame.source = [[response rootElement] value];
370
371 if ([delegate respondsToSelector:@selector(sourceUpdated:)])
372 [delegate sourceUpdated:frame];
373 }
374
375 /**
376 * Enumerates all the contexts of a given stack frame. We then in turn get the
377 * contents of each one of these contexts.
378 */
379 - (void)contextsReceived:(NSXMLDocument*)response
380 {
381 // Get the stack frame's routing ID and use it again.
382 NSNumber* receivedTransaction = [NSNumber numberWithInt:[connection_ transactionIDFromResponse:response]];
383 if ([receivedTransaction intValue] < stackFirstTransactionID_)
384 return;
385 NSNumber* routingID = [callbackContext_ objectForKey:receivedTransaction];
386 if (!routingID)
387 return;
388
389 // Get the stack frame by the |routingID|.
390 StackFrame* frame = [stackFrames_ objectForKey:routingID];
391
392 NSXMLElement* contextNames = [response rootElement];
393 for (NSXMLElement* context in [contextNames children])
394 {
395 NSInteger cid = [[[context attributeForName:@"id"] stringValue] intValue];
396
397 // Fetch each context's variables.
398 NSNumber* tx = [connection_ sendCommandWithFormat:@"context_get -d %d -c %d", frame.index, cid];
399 [self recordCallback:@selector(variablesReceived:) forTransaction:tx];
400 [callbackContext_ setObject:routingID forKey:tx];
401 }
402 }
403
404 /**
405 * Receives the variables from the context and attaches them to the stack frame.
406 */
407 - (void)variablesReceived:(NSXMLDocument*)response
408 {
409 // Get the stack frame's routing ID and use it again.
410 NSInteger transaction = [connection_ transactionIDFromResponse:response];
411 if (transaction < stackFirstTransactionID_)
412 return;
413 NSNumber* receivedTransaction = [NSNumber numberWithInt:transaction];
414 NSNumber* routingID = [callbackContext_ objectForKey:receivedTransaction];
415 if (!routingID)
416 return;
417
418 // Get the stack frame by the |routingID|.
419 StackFrame* frame = [stackFrames_ objectForKey:routingID];
420
421 NSMutableArray* variables = [NSMutableArray array];
422
423 // Merge the frame's existing variables.
424 if (frame.variables)
425 [variables addObjectsFromArray:frame.variables];
426
427 // Add these new variables.
428 NSArray* addVariables = [[response rootElement] children];
429 if (addVariables)
430 [variables addObjectsFromArray:addVariables];
431
432 frame.variables = variables;
433 }
434
435 /**
436 * Callback from a |-getProperty:| request.
437 */
438 - (void)propertiesReceived:(NSXMLDocument*)response
439 {
440 NSInteger transaction = [connection_ transactionIDFromResponse:response];
441
442 /*
443 <response>
444 <property> <!-- this is the one we requested -->
445 <property ... /> <!-- these are what we want -->
446 </property>
447 </repsonse>
448 */
449
450 // Detach all the children so we can insert them into another document.
451 NSXMLElement* parent = (NSXMLElement*)[[response rootElement] childAtIndex:0];
452 NSArray* children = [parent children];
453 [parent setChildren:nil];
454
455 [delegate receivedProperties:children forTransaction:transaction];
456 }
457
458 /**
459 * Callback for setting a breakpoint.
460 */
461 - (void)breakpointReceived:(NSXMLDocument*)response
462 {
463 NSNumber* transaction = [NSNumber numberWithInt:[connection_ transactionIDFromResponse:response]];
464 Breakpoint* bp = [callbackContext_ objectForKey:transaction];
465 if (!bp)
466 return;
467
468 [callbackContext_ removeObjectForKey:callbackContext_];
469 [bp setDebuggerId:[[[[response rootElement] attributeForName:@"id"] stringValue] intValue]];
470 }
471
472 // Private /////////////////////////////////////////////////////////////////////
473
474 - (void)recordCallback:(SEL)callback forTransaction:(NSNumber*)txn
475 {
476 [callTable_ setObject:NSStringFromSelector(callback) forKey:txn];
477 }
478
479 @end