Rewrite variable and property loading.
[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 "DebuggerModel.h"
21 #import "modp_b64.h"
22 #import "NSXMLElementAdditions.h"
23
24 @interface DebuggerBackEnd ()
25 @property(readwrite, copy, nonatomic) NSString* status;
26 @end
27
28 @implementation DebuggerBackEnd {
29 // The connection to the debugger engine.
30 NSUInteger _port;
31 ProtocolClient* _client;
32
33 // Whether or not a debugging session is currently active.
34 BOOL _active;
35 }
36
37 @synthesize status = _status;
38 @synthesize autoAttach = _autoAttach;
39 @synthesize model = _model;
40 @synthesize delegate = _delegate;
41
42 - (id)initWithPort:(NSUInteger)aPort
43 {
44 if (self = [super init]) {
45 [[BreakpointManager sharedManager] setConnection:self];
46 _port = aPort;
47 _client = [[ProtocolClient alloc] initWithDelegate:self];
48
49 _autoAttach = [[NSUserDefaults standardUserDefaults] boolForKey:@"DebuggerAttached"];
50
51 if (self.autoAttach)
52 [_client connectOnPort:_port];
53 }
54 return self;
55 }
56
57 - (void)dealloc {
58 [_client release];
59 [super dealloc];
60 }
61
62 // Getters /////////////////////////////////////////////////////////////////////
63 #pragma mark Getters
64
65 /**
66 * Gets the port number
67 */
68 - (NSUInteger)port {
69 return _port;
70 }
71
72 /**
73 * Returns whether or not we have an active connection
74 */
75 - (BOOL)isConnected {
76 return _active;
77 }
78
79 /**
80 * Sets the attached state of the debugger. This will open and close the
81 * connection as appropriate.
82 */
83 - (void)setAutoAttach:(BOOL)flag {
84 if (flag == _autoAttach)
85 return;
86
87 if (_autoAttach)
88 [_client disconnect];
89 else
90 [_client connectOnPort:_port];
91
92 _autoAttach = flag;
93 }
94
95 // Commands ////////////////////////////////////////////////////////////////////
96 #pragma mark Commands
97
98 /**
99 * Tells the debugger to continue running the script. Returns the current stack frame.
100 */
101 - (void)run {
102 [_client sendCommandWithFormat:@"run" handler:^(NSXMLDocument* message) {
103 [self debuggerStep:message];
104 }];
105 }
106
107 /**
108 * Tells the debugger to step into the current command.
109 */
110 - (void)stepIn {
111 [_client sendCommandWithFormat:@"step_into" handler:^(NSXMLDocument* message) {
112 [self debuggerStep:message];
113 }];
114 }
115
116 /**
117 * Tells the debugger to step out of the current context
118 */
119 - (void)stepOut {
120 [_client sendCommandWithFormat:@"step_out" handler:^(NSXMLDocument* message) {
121 [self debuggerStep:message];
122 }];
123 }
124
125 /**
126 * Tells the debugger to step over the current function
127 */
128 - (void)stepOver {
129 [_client sendCommandWithFormat:@"step_over" handler:^(NSXMLDocument* message) {
130 [self debuggerStep:message];
131 }];
132 }
133
134 /**
135 * Halts execution of the script.
136 */
137 - (void)stop {
138 [_client disconnect];
139 _active = NO;
140 self.status = @"Stopped";
141 }
142
143 /**
144 * Ends the current debugging session.
145 */
146 - (void)detach {
147 [_client sendCommandWithFormat:@"detach"];
148 _active = NO;
149 self.status = @"Stopped";
150 }
151
152 - (void)loadStackFrame:(StackFrame*)frame {
153 if (frame.loaded)
154 return;
155
156 // Get the source code of the file. Escape % in URL chars.
157 if ([frame.filename length]) {
158 ProtocolClientMessageHandler handler = ^(NSXMLDocument* message) {
159 frame.source = [[message rootElement] base64DecodedValue];
160 if ([self.delegate respondsToSelector:@selector(sourceUpdated:)])
161 [self.delegate sourceUpdated:frame];
162 };
163 [_client sendCommandWithFormat:@"source -f %@" handler:handler, frame.filename];
164 }
165
166 // Get the names of all the contexts.
167 ProtocolClientMessageHandler handler = ^(NSXMLDocument* message) {
168 [self loadContexts:message forFrame:frame];
169 };
170 [_client sendCommandWithFormat:@"context_names -d %d" handler:handler, frame.index];
171
172 // This frame will be fully loaded.
173 frame.loaded = YES;
174 }
175
176 - (void)loadVariableNode:(VariableNode*)variable
177 forStackFrame:(StackFrame*)frame {
178 if (variable.children.count == variable.childCount)
179 return;
180
181 [self loadVariableNode:variable forStackFrame:frame dataPage:0 loadedData:@[]];
182 }
183
184 - (void)loadVariableNode:(VariableNode*)variable
185 forStackFrame:(StackFrame*)frame
186 dataPage:(unsigned int)dataPage
187 loadedData:(NSArray*)loadedData {
188 ProtocolClientMessageHandler handler = ^(NSXMLDocument* message) {
189 /*
190 <response>
191 <property> <!-- this is the one we requested -->
192 <property ... /> <!-- these are what we want -->
193 </property>
194 </repsonse>
195 */
196
197 // Detach all the children so we can insert them into another document.
198 NSXMLElement* parent = (NSXMLElement*)[[message rootElement] childAtIndex:0];
199 NSArray* children = [parent children];
200 [parent setChildren:nil];
201
202 // Check to see if there are more children to load.
203 NSArray* newLoadedData = [loadedData arrayByAddingObjectsFromArray:children];
204
205 unsigned int totalChildren = [[[parent attributeForName:@"numchildren"] stringValue] integerValue];
206 if ([newLoadedData count] < totalChildren) {
207 [self loadVariableNode:variable
208 forStackFrame:frame
209 dataPage:dataPage + 1
210 loadedData:newLoadedData];
211 } else {
212 [variable setChildrenFromXMLChildren:newLoadedData];
213 }
214 };
215 [_client sendCommandWithFormat:@"property_get -d %d -n %@ -p %u" handler:handler, frame.index, variable.fullName, dataPage];
216 }
217
218 // Breakpoint Management ///////////////////////////////////////////////////////
219 #pragma mark Breakpoints
220
221 /**
222 * Send an add breakpoint command
223 */
224 - (void)addBreakpoint:(Breakpoint*)bp {
225 if (!_active)
226 return;
227
228 NSString* file = [ProtocolClient escapedFilePathURI:[bp transformedPath]];
229 ProtocolClientMessageHandler handler = ^(NSXMLDocument* message) {
230 [bp setDebuggerId:[[[[message rootElement] attributeForName:@"id"] stringValue] intValue]];
231 };
232 [_client sendCommandWithFormat:@"breakpoint_set -t line -f %@ -n %i" handler:handler, file, [bp line]];
233 }
234
235 /**
236 * Removes a breakpoint
237 */
238 - (void)removeBreakpoint:(Breakpoint*)bp {
239 if (!_active)
240 return;
241
242 [_client sendCommandWithFormat:@"breakpoint_remove -d %i", [bp debuggerId]];
243 }
244
245 /**
246 * Sends a string to be evaluated by the engine.
247 */
248 - (void)evalScript:(NSString*)str callback:(void (^)(NSString*))callback {
249 if (!_active)
250 return;
251
252 char* encodedString = malloc(modp_b64_encode_len([str length]));
253 modp_b64_encode(encodedString, [str UTF8String], [str length]);
254 ProtocolClientMessageHandler handler = ^(NSXMLDocument* message) {
255 NSXMLElement* parent = (NSXMLElement*)[[message rootElement] childAtIndex:0];
256 NSString* value = [parent base64DecodedValue];
257 callback(value);
258 };
259 [_client sendCustomCommandWithFormat:@"eval -i {txn} -- %s" handler:handler, encodedString];
260 free(encodedString);
261 }
262
263 // Protocol Client Delegate ////////////////////////////////////////////////////
264 #pragma mark Protocol Client Delegate
265
266 - (void)debuggerEngineConnected:(ProtocolClient*)client {
267 _active = YES;
268 [_model onNewConnection];
269 }
270
271 /**
272 * Called when the connection is finally closed. This will reopen the listening
273 * socket if the debugger remains attached.
274 */
275 - (void)debuggerEngineDisconnected:(ProtocolClient*)client {
276 _active = NO;
277
278 if ([self.delegate respondsToSelector:@selector(debuggerDisconnected)])
279 [self.delegate debuggerDisconnected];
280
281 if (self.autoAttach)
282 [_client connectOnPort:_port];
283 }
284
285 - (void)protocolClient:(ProtocolClient*)client receivedInitialMessage:(NSXMLDocument*)message {
286 [self handleInitialResponse:message];
287 }
288
289 - (void)protocolClient:(ProtocolClient*)client receivedErrorMessage:(NSXMLDocument*)message {
290 NSArray* error = [[message rootElement] elementsForName:@"error"];
291 if ([error count] > 0) {
292 NSLog(@"Xdebug error: %@", error);
293 NSString* errorMessage = [[[[error objectAtIndex:0] children] objectAtIndex:0] stringValue];
294 [self errorEncountered:errorMessage];
295 }
296 }
297
298 // Specific Response Handlers //////////////////////////////////////////////////
299 #pragma mark Response Handlers
300
301 - (void)errorEncountered:(NSString*)error {
302 [self.delegate errorEncountered:error];
303 }
304
305 /**
306 * Initial packet received. We've started a brand-new connection to the engine.
307 */
308 - (void)handleInitialResponse:(NSXMLDocument*)response {
309 if (!self.autoAttach) {
310 [_client sendCommandWithFormat:@"detach"];
311 return;
312 }
313
314 _active = YES;
315
316 // Register any breakpoints that exist offline.
317 for (Breakpoint* bp in [[BreakpointManager sharedManager] breakpoints])
318 [self addBreakpoint:bp];
319
320 // Load the debugger to make it look active.
321 [self.delegate debuggerConnected];
322
323 // TODO: update the status.
324 }
325
326 /**
327 * Receiver for status updates. This just freshens up the UI.
328 */
329 - (void)updateStatus:(NSXMLDocument*)response {
330 self.status = [[[[response rootElement] attributeForName:@"status"] stringValue] capitalizedString];
331 _active = YES;
332 if (!_status || [_status isEqualToString:@"Stopped"]) {
333 [_delegate debuggerDisconnected];
334 _active = NO;
335 } else if ([_status isEqualToString:@"Stopping"]) {
336 [_client sendCommandWithFormat:@"stop"];
337 _active = NO;
338 }
339 self.model.status = self.status;
340 }
341
342 /**
343 * Step in/out/over and run all take this path. We first get the status of the
344 * debugger and then request fresh stack information.
345 */
346 - (void)debuggerStep:(NSXMLDocument*)response {
347 [self updateStatus:response];
348 if (![self isConnected])
349 return;
350
351 [_client sendCommandWithFormat:@"stack_depth" handler:^(NSXMLDocument* message) {
352 [self rebuildStack:message];
353 }];
354 }
355
356 /**
357 * We ask for the stack_depth and now we clobber the stack and start rebuilding
358 * it.
359 */
360 - (void)rebuildStack:(NSXMLDocument*)response {
361 NSUInteger depth = [[[[response rootElement] attributeForName:@"depth"] stringValue] intValue];
362
363 // Start with frame 0. If this is a shifted frame, then only it needs to be
364 // re-loaded. If it is not shifted, see if another frame on the stack is equal
365 // to it; if so, then the frames up to that must be discarded. If not, this is
366 // a new stack frame that should be inserted at the top of the stack. Finally,
367 // the sice of the stack is trimmed to |depth| from the bottom.
368
369 __block NSMutableArray* tempStack = [[NSMutableArray alloc] init];
370
371 for (NSUInteger i = 0; i < depth; ++i) {
372 ProtocolClientMessageHandler handler = ^(NSXMLDocument* message) {
373 [tempStack addObject:[self transformXMLToStackFrame:message]];
374 if (i == depth - 1) {
375 [self.model updateStack:[tempStack autorelease]];
376 }
377 };
378 [_client sendCommandWithFormat:@"stack_get -d %d" handler:handler, i];
379 }
380 }
381
382 /**
383 * Creates a StackFrame object from an NSXMLDocument response from the "stack_get"
384 * command.
385 */
386 - (StackFrame*)transformXMLToStackFrame:(NSXMLDocument*)response {
387 NSXMLElement* xmlframe = (NSXMLElement*)[[[response rootElement] children] objectAtIndex:0];
388 StackFrame* frame = [[[StackFrame alloc] init] autorelease];
389 frame.index = [[[xmlframe attributeForName:@"level"] stringValue] intValue];
390 frame.filename = [[xmlframe attributeForName:@"filename"] stringValue];
391 frame.lineNumber = [[[xmlframe attributeForName:@"lineno"] stringValue] intValue];
392 frame.function = [[xmlframe attributeForName:@"where"] stringValue];
393 return frame;
394 }
395
396 /**
397 * Enumerates all the contexts of a given stack frame. We then in turn get the
398 * contents of each one of these contexts.
399 */
400 - (void)loadContexts:(NSXMLDocument*)response forFrame:(StackFrame*)frame {
401 NSXMLElement* contextNames = [response rootElement];
402 for (NSXMLElement* context in [contextNames children]) {
403 NSInteger cid = [[[context attributeForName:@"id"] stringValue] intValue];
404
405 // Fetch each context's variables.
406 ProtocolClientMessageHandler handler = ^(NSXMLDocument* message) {
407 NSMutableArray* variables = [NSMutableArray array];
408
409 // Merge the frame's existing variables.
410 if (frame.variables)
411 [variables addObjectsFromArray:frame.variables];
412
413 // Add these new variables.
414 NSArray* addVariables = [[message rootElement] children];
415 if (addVariables) {
416 for (NSXMLElement* elm in addVariables) {
417 VariableNode* node = [[VariableNode alloc] initWithXMLNode:elm];
418 [variables addObject:[node autorelease]];
419 }
420 }
421
422 frame.variables = variables;
423 };
424 [_client sendCommandWithFormat:@"context_get -d %d -c %d" handler:handler, frame.index, cid];
425 }
426 }
427
428 @end