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