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