Stop crashing with EXC_BAD_ACCESS when expanding properties beyond the fetched depth.
[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 return [tx intValue];
169 }
170
171 - (void)loadStackFrame:(StackFrame*)frame
172 {
173 if (frame.loaded)
174 return;
175
176 NSNumber* routingNumber = [NSNumber numberWithInt:frame.routingID];
177
178 // Get the source code of the file. Escape % in URL chars.
179 NSString* escapedFilename = [frame.filename stringByReplacingOccurrencesOfString:@"%" withString:@"%%"];
180 NSNumber* transaction = [connection_ sendCommandWithFormat:@"source -f %@", escapedFilename];
181 [self recordCallback:@selector(setSource:) forTransaction:transaction];
182 [callbackContext_ setObject:routingNumber forKey:transaction];
183
184 // Get the names of all the contexts.
185 transaction = [connection_ sendCommandWithFormat:@"context_names -d %d", frame.index];
186 [self recordCallback:@selector(contextsReceived:) forTransaction:transaction];
187 [callbackContext_ setObject:routingNumber forKey:transaction];
188
189 // This frame will be fully loaded.
190 frame.loaded = YES;
191 }
192
193 // Breakpoint Management ///////////////////////////////////////////////////////
194 #pragma mark Breakpoints
195
196 /**
197 * Send an add breakpoint command
198 */
199 - (void)addBreakpoint:(Breakpoint*)bp
200 {
201 if (![connection_ connected])
202 return;
203
204 NSString* file = [connection_ escapedURIPath:[bp transformedPath]];
205 NSNumber* tx = [connection_ sendCommandWithFormat:@"breakpoint_set -t line -f %@ -n %i", file, [bp line]];
206 [self recordCallback:@selector(breakpointReceived:) forTransaction:tx];
207 [callbackContext_ setObject:bp forKey:tx];
208 }
209
210 /**
211 * Removes a breakpoint
212 */
213 - (void)removeBreakpoint:(Breakpoint*)bp
214 {
215 if (![connection_ connected])
216 return;
217
218 [connection_ sendCommandWithFormat:@"breakpoint_remove -d %i", [bp debuggerId]];
219 }
220
221 // Specific Response Handlers //////////////////////////////////////////////////
222 #pragma mark Response Handlers
223
224 /**
225 * Initial packet received. We've started a brand-new connection to the engine.
226 */
227 - (void)handleInitialResponse:(NSXMLDocument*)response
228 {
229 // Register any breakpoints that exist offline.
230 for (Breakpoint* bp in [[BreakpointManager sharedManager] breakpoints])
231 [self addBreakpoint:bp];
232
233 // Load the debugger to make it look active.
234 [delegate debuggerConnected];
235
236 // TODO: update the status.
237 }
238
239 - (void)handleResponse:(NSXMLDocument*)response
240 {
241 NSInteger transactionID = [connection_ transactionIDFromResponse:response];
242 NSNumber* key = [NSNumber numberWithInt:transactionID];
243 NSString* callbackStr = [callTable_ objectForKey:key];
244 if (callbackStr)
245 {
246 SEL callback = NSSelectorFromString(callbackStr);
247 [self performSelector:callback withObject:response];
248 }
249 [callTable_ removeObjectForKey:key];
250 }
251
252 /**
253 * Receiver for status updates. This just freshens up the UI.
254 */
255 - (void)updateStatus:(NSXMLDocument*)response
256 {
257 self.status = [[[[response rootElement] attributeForName:@"status"] stringValue] capitalizedString];
258 if (status == nil || [status isEqualToString:@"Stopped"] || [status isEqualToString:@"Stopping"])
259 {
260 [connection_ close];
261 [delegate debuggerDisconnected];
262
263 self.status = @"Stopped";
264 }
265 }
266
267 /**
268 * Step in/out/over and run all take this path. We first get the status of the
269 * debugger and then request fresh stack information.
270 */
271 - (void)debuggerStep:(NSXMLDocument*)response
272 {
273 [self updateStatus:response];
274 if (![connection_ connected])
275 return;
276
277 // If this is the run command, tell the delegate that a bunch of updates
278 // are coming. Also remove all existing stack routes and request a new stack.
279 // TODO: figure out if we can not clobber the stack every time.
280 NSString* command = [[[response rootElement] attributeForName:@"command"] stringValue];
281 if (YES || [command isEqualToString:@"run"])
282 {
283 if ([delegate respondsToSelector:@selector(clobberStack)])
284 [delegate clobberStack];
285 [stackFrames_ removeAllObjects];
286 NSNumber* tx = [connection_ sendCommandWithFormat:@"stack_depth"];
287 [self recordCallback:@selector(rebuildStack:) forTransaction:tx];
288 stackFirstTransactionID_ = [tx intValue];
289 }
290 }
291
292 /**
293 * We ask for the stack_depth and now we clobber the stack and start rebuilding
294 * it.
295 */
296 - (void)rebuildStack:(NSXMLDocument*)response
297 {
298 NSInteger depth = [[[[response rootElement] attributeForName:@"depth"] stringValue] intValue];
299
300 if (stackFirstTransactionID_ == [connection_ transactionIDFromResponse:response])
301 stackDepth_ = depth;
302
303 // We now need to alloc a bunch of stack frames and get the basic information
304 // for them.
305 for (NSInteger i = 0; i < depth; i++)
306 {
307 // Use the transaction ID to create a routing path.
308 NSNumber* routingID = [connection_ sendCommandWithFormat:@"stack_get -d %d", i];
309 [self recordCallback:@selector(getStackFrame:) forTransaction:routingID];
310 [stackFrames_ setObject:[[StackFrame new] autorelease] forKey:routingID];
311 }
312 }
313
314 /**
315 * The initial rebuild of the stack frame. We now have enough to initialize
316 * a StackFrame object.
317 */
318 - (void)getStackFrame:(NSXMLDocument*)response
319 {
320 // Get the routing information.
321 NSInteger routingID = [connection_ transactionIDFromResponse:response];
322 if (routingID < stackFirstTransactionID_)
323 return;
324 NSNumber* routingNumber = [NSNumber numberWithInt:routingID];
325
326 // Make sure we initialized this frame in our last |-rebuildStack:|.
327 StackFrame* frame = [stackFrames_ objectForKey:routingNumber];
328 if (!frame)
329 return;
330
331 NSXMLElement* xmlframe = [[[response rootElement] children] objectAtIndex:0];
332
333 // Initialize the stack frame.
334 frame.index = [[[xmlframe attributeForName:@"level"] stringValue] intValue];
335 frame.filename = [[xmlframe attributeForName:@"filename"] stringValue];
336 frame.lineNumber = [[[xmlframe attributeForName:@"lineno"] stringValue] intValue];
337 frame.function = [[xmlframe attributeForName:@"where"] stringValue];
338 frame.routingID = routingID;
339
340 // Only get the complete frame for the first level. The other frames will get
341 // information loaded lazily when the user clicks on one.
342 if (frame.index == 0) {
343 [self loadStackFrame:frame];
344 }
345
346 if ([delegate respondsToSelector:@selector(newStackFrame:)])
347 [delegate newStackFrame:frame];
348 }
349
350 /**
351 * Callback for setting the source of a file while rebuilding a specific stack
352 * frame.
353 */
354 - (void)setSource:(NSXMLDocument*)response
355 {
356 NSNumber* transaction = [NSNumber numberWithInt:[connection_ transactionIDFromResponse:response]];
357 if ([transaction intValue] < stackFirstTransactionID_)
358 return;
359 NSNumber* routingNumber = [callbackContext_ objectForKey:transaction];
360 if (!routingNumber)
361 return;
362
363 [callbackContext_ removeObjectForKey:transaction];
364 StackFrame* frame = [stackFrames_ objectForKey:routingNumber];
365 if (!frame)
366 return;
367
368 frame.source = [[response rootElement] value];
369
370 if ([delegate respondsToSelector:@selector(sourceUpdated:)])
371 [delegate sourceUpdated:frame];
372 }
373
374 /**
375 * Enumerates all the contexts of a given stack frame. We then in turn get the
376 * contents of each one of these contexts.
377 */
378 - (void)contextsReceived:(NSXMLDocument*)response
379 {
380 // Get the stack frame's routing ID and use it again.
381 NSNumber* receivedTransaction = [NSNumber numberWithInt:[connection_ transactionIDFromResponse:response]];
382 if ([receivedTransaction intValue] < stackFirstTransactionID_)
383 return;
384 NSNumber* routingID = [callbackContext_ objectForKey:receivedTransaction];
385 if (!routingID)
386 return;
387
388 // Get the stack frame by the |routingID|.
389 StackFrame* frame = [stackFrames_ objectForKey:routingID];
390
391 NSXMLElement* contextNames = [response rootElement];
392 for (NSXMLElement* context in [contextNames children])
393 {
394 NSInteger cid = [[[context attributeForName:@"id"] stringValue] intValue];
395
396 // Fetch each context's variables.
397 NSNumber* tx = [connection_ sendCommandWithFormat:@"context_get -d %d -c %d", frame.index, cid];
398 [self recordCallback:@selector(variablesReceived:) forTransaction:tx];
399 [callbackContext_ setObject:routingID forKey:tx];
400 }
401 }
402
403 /**
404 * Receives the variables from the context and attaches them to the stack frame.
405 */
406 - (void)variablesReceived:(NSXMLDocument*)response
407 {
408 // Get the stack frame's routing ID and use it again.
409 NSInteger transaction = [connection_ transactionIDFromResponse:response];
410 if (transaction < stackFirstTransactionID_)
411 return;
412 NSNumber* receivedTransaction = [NSNumber numberWithInt:transaction];
413 NSNumber* routingID = [callbackContext_ objectForKey:receivedTransaction];
414 if (!routingID)
415 return;
416
417 // Get the stack frame by the |routingID|.
418 StackFrame* frame = [stackFrames_ objectForKey:routingID];
419
420 NSMutableArray* variables = [NSMutableArray array];
421
422 // Merge the frame's existing variables.
423 if (frame.variables)
424 [variables addObjectsFromArray:frame.variables];
425
426 // Add these new variables.
427 NSArray* addVariables = [[response rootElement] children];
428 if (addVariables)
429 [variables addObjectsFromArray:addVariables];
430
431 frame.variables = variables;
432 }
433
434 /**
435 * Callback from a |-getProperty:| request.
436 */
437 - (void)propertiesReceived:(NSXMLDocument*)response
438 {
439 NSInteger transaction = [connection_ transactionIDFromResponse:response];
440
441 /*
442 <response>
443 <property> <!-- this is the one we requested -->
444 <property ... /> <!-- these are what we want -->
445 </property>
446 </repsonse>
447 */
448
449 // Detach all the children so we can insert them into another document.
450 NSXMLElement* parent = (NSXMLElement*)[[response rootElement] childAtIndex:0];
451 NSArray* children = [parent children];
452 [parent setChildren:nil];
453
454 [delegate receivedProperties:children forTransaction:transaction];
455 }
456
457 /**
458 * Callback for setting a breakpoint.
459 */
460 - (void)breakpointReceived:(NSXMLDocument*)response
461 {
462 NSNumber* transaction = [NSNumber numberWithInt:[connection_ transactionIDFromResponse:response]];
463 Breakpoint* bp = [callbackContext_ objectForKey:transaction];
464 if (!bp)
465 return;
466
467 [callbackContext_ removeObjectForKey:callbackContext_];
468 [bp setDebuggerId:[[[[response rootElement] attributeForName:@"id"] stringValue] intValue]];
469 }
470
471 // Private /////////////////////////////////////////////////////////////////////
472
473 - (void)recordCallback:(SEL)callback forTransaction:(NSNumber*)txn
474 {
475 [callTable_ setObject:NSStringFromSelector(callback) forKey:txn];
476 }
477
478 @end