Remove SocketWrapper and start using CFSocket with CFStreams in GDBpConnection. This...
[macgdbp.git] / Source / GDBpConnection.m
1 /*
2 * MacGDBp
3 * Copyright (c) 2007 - 2009, 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 <sys/socket.h>
18 #import <netinet/in.h>
19
20 #import "GDBpConnection.h"
21
22 #import "AppDelegate.h"
23
24 // GDBpConnection (Private) ////////////////////////////////////////////////////
25
26 @interface GDBpConnection ()
27 @property(readwrite, copy) NSString* status;
28 @property (assign) CFSocketRef socket;
29 @property (assign) CFReadStreamRef readStream;
30 @property (retain) NSMutableString* currentPacket;
31 @property (assign) CFWriteStreamRef writeStream;
32
33 - (void)connect;
34 - (void)close;
35 - (void)socketDidAccept;
36 - (void)socketDisconnected;
37 - (void)readStreamHasData;
38 - (void)send:(NSString*)command;
39 - (void)errorEncountered:(NSString*)error;
40
41 - (NSString*)createCommand:(NSString*)cmd, ...;
42 - (NSXMLDocument*)processData:(NSString*)data;
43 - (StackFrame*)createStackFrame:(int)depth;
44 - (StackFrame*)createCurrentStackFrame;
45 - (void)updateStatus;
46 - (NSString*)escapedURIPath:(NSString*)path;
47 - (void)doSocketAccept:_nil;
48 @end
49
50 // CFNetwork Callbacks /////////////////////////////////////////////////////////
51
52 void ReadStreamCallback(CFReadStreamRef stream, CFStreamEventType eventType, void* connectionRaw)
53 {
54 NSLog(@"ReadStreamCallback()");
55 GDBpConnection* connection = (GDBpConnection*)connectionRaw;
56 switch (eventType)
57 {
58 case kCFStreamEventHasBytesAvailable:
59 [connection readStreamHasData];
60 break;
61
62 case kCFStreamEventErrorOccurred:
63 {
64 CFErrorRef error = CFReadStreamCopyError(stream);
65 CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
66 CFReadStreamClose(stream);
67 CFRelease(stream);
68 [connection errorEncountered:[[(NSError*)error autorelease] description]];
69 break;
70 }
71
72 case kCFStreamEventEndEncountered:
73 CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
74 CFReadStreamClose(stream);
75 CFRelease(stream);
76 [connection socketDisconnected];
77 break;
78 };
79 }
80
81 void WriteStreamCallback(CFWriteStreamRef stream, CFStreamEventType eventType, void* connectionRaw)
82 {
83 NSLog(@"WriteStreamCallback()");
84 GDBpConnection* connection = (GDBpConnection*)connectionRaw;
85 switch (eventType)
86 {
87 case kCFStreamEventErrorOccurred:
88 {
89 CFErrorRef error = CFWriteStreamCopyError(stream);
90 CFWriteStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
91 CFWriteStreamClose(stream);
92 CFRelease(stream);
93 [connection errorEncountered:[[(NSError*)error autorelease] description]];
94 break;
95 }
96
97 case kCFStreamEventEndEncountered:
98 CFWriteStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
99 CFWriteStreamClose(stream);
100 CFRelease(stream);
101 [connection socketDisconnected];
102 break;
103 }
104 }
105
106 void SocketAcceptCallback(CFSocketRef socket,
107 CFSocketCallBackType callbackType,
108 CFDataRef address,
109 const void* data,
110 void* connectionRaw)
111 {
112 assert(callbackType == kCFSocketAcceptCallBack);
113 NSLog(@"SocketAcceptCallback()");
114
115 GDBpConnection* connection = (GDBpConnection*)connectionRaw;
116
117 CFReadStreamRef readStream;
118 CFWriteStreamRef writeStream;
119
120 // Create the streams on the socket.
121 CFStreamCreatePairWithSocket(kCFAllocatorDefault,
122 *(CFSocketNativeHandle*)data, // Socket handle.
123 &readStream, // Read stream in-pointer.
124 &writeStream); // Write stream in-pointer.
125
126 // Create struct to register callbacks for the stream.
127 CFStreamClientContext context;
128 context.version = 0;
129 context.info = connection;
130 context.retain = NULL;
131 context.release = NULL;
132 context.copyDescription = NULL;
133
134 // Set the client of the read stream.
135 CFOptionFlags readFlags =
136 kCFStreamEventOpenCompleted |
137 kCFStreamEventHasBytesAvailable |
138 kCFStreamEventErrorOccurred |
139 kCFStreamEventEndEncountered;
140 if (CFReadStreamSetClient(readStream, readFlags, ReadStreamCallback, &context))
141 // Schedule in run loop to do asynchronous communication with the engine.
142 CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
143 else
144 return;
145
146 NSLog(@"Read stream scheduled");
147
148 // Open the stream now that it's scheduled on the run loop.
149 if (!CFReadStreamOpen(readStream))
150 {
151 CFStreamError error = CFReadStreamGetError(readStream);
152 NSLog(@"error! %@", error);
153 return;
154 }
155
156 NSLog(@"Read stream opened");
157
158 // Set the client of the write stream.
159 CFOptionFlags writeFlags =
160 kCFStreamEventOpenCompleted |
161 kCFStreamEventErrorOccurred |
162 kCFStreamEventEndEncountered;
163 if (CFWriteStreamSetClient(writeStream, writeFlags, WriteStreamCallback, &context))
164 // Schedule it in the run loop to receive error information.
165 CFWriteStreamScheduleWithRunLoop(writeStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
166 else
167 return;
168
169 NSLog(@"Write stream scheduled");
170
171 // Open the write stream.
172 if (!CFWriteStreamOpen(writeStream))
173 {
174 CFStreamError error = CFWriteStreamGetError(writeStream);
175 NSLog(@"error! %@", error);
176 return;
177 }
178
179 NSLog(@"Write stream opened");
180
181 connection.readStream = readStream;
182 connection.writeStream = writeStream;
183 [connection socketDidAccept];
184 }
185
186 // GDBpConnection //////////////////////////////////////////////////////////////
187
188 @implementation GDBpConnection
189 @synthesize socket = socket_;
190 @synthesize readStream = readStream_;
191 @synthesize currentPacket = currentPacket_;
192 @synthesize writeStream = writeStream_;
193 @synthesize status;
194 @synthesize delegate;
195
196 /**
197 * Creates a new DebuggerConnection and initializes the socket from the given connection
198 * paramters.
199 */
200 - (id)initWithPort:(int)aPort
201 {
202 if (self = [super init])
203 {
204 port = aPort;
205 connected = NO;
206
207 [[BreakpointManager sharedManager] setConnection:self];
208
209 [self connect];
210 }
211 return self;
212 }
213
214 /**
215 * Deallocates the object
216 */
217 - (void)dealloc
218 {
219 [self close];
220 self.currentPacket = nil;
221
222 [super dealloc];
223 }
224
225 /**
226 * Gets the port number
227 */
228 - (int)port
229 {
230 return port;
231 }
232
233 /**
234 * Returns the name of the remote host
235 */
236 - (NSString*)remoteHost
237 {
238 if (!connected)
239 {
240 return @"(DISCONNECTED)";
241 }
242 // TODO: Either impl or remove.
243 return @"";
244 }
245
246 /**
247 * Returns whether or not we have an active connection
248 */
249 - (BOOL)isConnected
250 {
251 return connected;
252 }
253
254 /**
255 * Called by SocketWrapper after the connection is successful. This immediately calls
256 * -[SocketWrapper receive] to clear the way for communication, though the information
257 * could be useful server information that we don't use right now.
258 */
259 - (void)socketDidAccept
260 {
261 [self performSelectorOnMainThread:@selector(doSocketAccept:) withObject:nil waitUntilDone:YES];
262 }
263
264 /**
265 * Receives errors from the SocketWrapper and updates the display
266 */
267 - (void)errorEncountered:(NSString*)error
268 {
269 [delegate errorEncountered:error];
270 }
271
272 /**
273 * Reestablishes communication with the remote debugger so that a new connection doesn't have to be
274 * created every time you want to debug a page
275 */
276 - (void)reconnect
277 {
278 [self close];
279 self.status = @"Connecting";
280 [self connect];
281 }
282
283 /**
284 * Creates an entirely new stack and returns it as an array of StackFrame objects.
285 */
286 - (NSArray*)getCurrentStack
287 {
288 // get the total stack depth
289 [socket send:[self createCommand:@"stack_depth"]];
290 NSXMLDocument* doc = [self processData:[socket receive]];
291 int depth = [[[[doc rootElement] attributeForName:@"depth"] stringValue] intValue];
292
293 // get all stack frames
294 NSMutableArray* stack = [NSMutableArray arrayWithCapacity:depth];
295 for (int i = 0; i < depth; i++)
296 {
297 StackFrame* frame = [self createStackFrame:i];
298 [stack insertObject:frame atIndex:i];
299 }
300
301 return stack;
302 }
303
304 /**
305 * Tells the debugger to continue running the script. Returns the current stack frame.
306 */
307 - (void)run
308 {
309 [socket send:[self createCommand:@"run"]];
310 [socket receive];
311
312 [self updateStatus];
313 }
314
315 /**
316 * Tells the debugger to step into the current command.
317 */
318 - (void)stepIn
319 {
320 [socket send:[self createCommand:@"step_into"]];
321 [socket receive];
322
323 [self updateStatus];
324 }
325
326 /**
327 * Tells the debugger to step out of the current context
328 */
329 - (void)stepOut
330 {
331 [socket send:[self createCommand:@"step_out"]];
332 [socket receive];
333
334 [self updateStatus];
335 }
336
337 /**
338 * Tells the debugger to step over the current function
339 */
340 - (void)stepOver
341 {
342 [socket send:[self createCommand:@"step_over"]];
343 [socket receive];
344
345 [self updateStatus];
346 }
347
348 /**
349 * Tells the debugger engine to get a specifc property. This also takes in the NSXMLElement
350 * that requested it so that the child can be attached.
351 */
352 - (NSArray*)getProperty:(NSString*)property
353 {
354 [socket send:[self createCommand:[NSString stringWithFormat:@"property_get -n \"%@\"", property]]];
355
356 NSXMLDocument* doc = [self processData:[socket receive]];
357
358 /*
359 <response>
360 <property> <!-- this is the one we requested -->
361 <property ... /> <!-- these are what we want -->
362 </property>
363 </repsonse>
364 */
365
366 // we now have to detach all the children so we can insert them into another document
367 NSXMLElement* parent = (NSXMLElement*)[[doc rootElement] childAtIndex:0];
368 NSArray* children = [parent children];
369 [parent setChildren:nil];
370 return children;
371 }
372
373 #pragma mark Breakpoints
374
375 /**
376 * Send an add breakpoint command
377 */
378 - (void)addBreakpoint:(Breakpoint*)bp
379 {
380 if (!connected)
381 return;
382
383 NSString* file = [self escapedURIPath:[bp transformedPath]];
384 NSString* cmd = [self createCommand:[NSString stringWithFormat:@"breakpoint_set -t line -f %@ -n %i", file, [bp line]]];
385 [socket send:cmd];
386 NSXMLDocument* info = [self processData:[socket receive]];
387 [bp setDebuggerId:[[[[info rootElement] attributeForName:@"id"] stringValue] intValue]];
388 }
389
390 /**
391 * Removes a breakpoint
392 */
393 - (void)removeBreakpoint:(Breakpoint*)bp
394 {
395 if (!connected)
396 {
397 return;
398 }
399
400 [socket send:[self createCommand:[NSString stringWithFormat:@"breakpoint_remove -d %i", [bp debuggerId]]]];
401 [socket receive];
402 }
403
404 #pragma mark Private
405
406 /**
407 * Creates, connects to, and schedules a CFSocket.
408 */
409 - (void)connect
410 {
411 // Pass ourselves to the callback so we don't have to use ugly globals.
412 CFSocketContext context;
413 context.version = 0;
414 context.info = self;
415 context.retain = NULL;
416 context.release = NULL;
417 context.copyDescription = NULL;
418
419 // Create the address structure.
420 struct sockaddr_in address;
421 memset(&address, 0, sizeof(address));
422 address.sin_len = sizeof(address);
423 address.sin_family = AF_INET;
424 address.sin_port = htons(port);
425 address.sin_addr.s_addr = htonl(INADDR_ANY);
426
427 // Create the socket signature.
428 CFSocketSignature signature;
429 signature.protocolFamily = PF_INET;
430 signature.socketType = SOCK_STREAM;
431 signature.protocol = IPPROTO_TCP;
432 signature.address = (CFDataRef)[NSData dataWithBytes:&address length:sizeof(address)];
433
434 socket_ = CFSocketCreateWithSocketSignature(kCFAllocatorDefault,
435 &signature, // Socket signature.
436 kCFSocketAcceptCallBack, // Callback types.
437 SocketAcceptCallback, // Callout function pointer.
438 &context); // Context to pass to callout.
439 if (!socket_)
440 {
441 [self errorEncountered:@"Could not open socket."];
442 return;
443 }
444
445 // Allow old, yet-to-be recycled sockets to be reused.
446 BOOL yes = YES;
447 setsockopt(CFSocketGetNative(socket_), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(BOOL));
448
449 // Schedule the socket on the run loop.
450 CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket_, 0);
451 CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
452 CFRelease(source);
453
454 self.status = @"Connecting";
455 }
456
457 /**
458 * Closes a socket and releases the ref.
459 */
460 - (void)close
461 {
462 // The socket goes down, so do the streams, which clean themselves up.
463 CFSocketInvalidate(socket_);
464 CFRelease(socket_);
465 }
466
467 /**
468 * Notification that the socket disconnected.
469 */
470 - (void)socketDisconnected
471 {
472 [self close];
473 [delegate debuggerDisconnected];
474 }
475
476 /**
477 * Callback from the CFReadStream that there is data waiting to be read.
478 */
479 - (void)readStreamHasData
480 {
481 UInt8 buffer[1024];
482 CFIndex bytesRead = CFReadStreamRead(readStream_, buffer, 1024);
483 const char* charBuffer = (const char*)buffer;
484
485 // We haven't finished reading a packet, so just read more data in.
486 if (currentPacketIndex_ < packetSize_)
487 {
488 [currentPacket_ appendFormat:@"%s", buffer];
489 currentPacketIndex_ += bytesRead;
490 }
491 // Time to read a new packet.
492 else
493 {
494 // Read the message header: the size.
495 packetSize_ = atoi(charBuffer);
496 currentPacketIndex_ = bytesRead - strlen(charBuffer);
497 self.currentPacket = [NSMutableString stringWithFormat:@"%s", buffer + strlen(charBuffer) + 1];
498 }
499
500 // We have finished reading the packet.
501 if (currentPacketIndex_ >= packetSize_)
502 {
503 packetSize_ = 0;
504 currentPacketIndex_ = 0;
505
506 // Test if we can convert it into an NSXMLDocument.
507 NSError* error = nil;
508 NSXMLDocument* xmlTest = [[NSXMLDocument alloc] initWithXMLString:currentPacket_ options:NSXMLDocumentTidyXML error:&error];
509 if (error)
510 NSLog(@"FAILED XML TEST: %@", error);
511 [xmlTest release];
512 }
513 }
514
515 /**
516 * Writes a command into the write stream. This does use blocking IO.
517 */
518 - (void)send:(NSString*)command
519 {
520 BOOL done = NO;
521
522 char* string = (char*)[command UTF8String];
523 int stringLength = strlen(string);
524
525 // Busy wait while writing. BAADD. Should background this operation.
526 while (!done)
527 {
528 if (CFWriteStreamCanAcceptBytes(writeStream_))
529 {
530 // Include the NULL byte in the string when we write.
531 int bytesWritten = CFWriteStreamWrite(writeStream_, (UInt8*)string, stringLength + 1);
532 if (bytesWritten < 0)
533 {
534 NSLog(@"write error");
535 }
536 // Incomplete write.
537 else if (bytesWritten < strlen(string))
538 {
539 // Adjust the buffer and wait for another chance to write.
540 stringLength -= bytesWritten;
541 memmove(string, string + bytesWritten, stringLength);
542 }
543 else
544 {
545 done = YES;
546 }
547 }
548 }
549 }
550
551 /**
552 * Helper method to create a string command with the -i <transaction id> automatically tacked on. Takes
553 * a variable number of arguments and parses the given command with +[NSString stringWithFormat:]
554 */
555 - (NSString*)createCommand:(NSString*)cmd, ...
556 {
557 // collect varargs
558 va_list argList;
559 va_start(argList, cmd);
560 NSString* format = [[NSString alloc] initWithFormat:cmd arguments:argList]; // format the command
561 va_end(argList);
562
563 if ([[[[NSProcessInfo processInfo] environment] objectForKey:@"TransportDebug"] boolValue])
564 NSLog(@"--> %@", cmd);
565
566 return [NSString stringWithFormat:@"%@ -i %d", [format autorelease], transactionID++];
567 }
568
569 /**
570 * Helper function to parse the NSData into an NSXMLDocument
571 */
572 - (NSXMLDocument*)processData:(NSString*)data
573 {
574 if (data == nil)
575 return nil;
576
577 NSError* parseError = nil;
578 NSXMLDocument* doc = [[NSXMLDocument alloc] initWithXMLString:data options:0 error:&parseError];
579 if (parseError)
580 {
581 NSLog(@"Could not parse XML? --- %@", parseError);
582 NSLog(@"Error UserInfo: %@", [parseError userInfo]);
583 NSLog(@"This is the XML Document: %@", data);
584 return nil;
585 }
586
587 // check and see if there's an error
588 NSArray* error = [[doc rootElement] elementsForName:@"error"];
589 if ([error count] > 0)
590 {
591 NSLog(@"Xdebug error: %@", error);
592 [delegate errorEncountered:[[[[error objectAtIndex:0] children] objectAtIndex:0] stringValue]];
593 return nil;
594 }
595
596 if ([[[[NSProcessInfo processInfo] environment] objectForKey:@"TransportDebug"] boolValue])
597 NSLog(@"<-- %@", doc);
598
599 return [doc autorelease];
600 }
601
602 /**
603 * Generates a stack frame for the given depth
604 */
605 - (StackFrame*)createStackFrame:(int)stackDepth
606 {
607 // get the stack frame
608 [socket send:[self createCommand:@"stack_get -d %d", stackDepth]];
609 NSXMLDocument* doc = [self processData:[socket receive]];
610 if (doc == nil)
611 return nil;
612
613 NSXMLElement* xmlframe = [[[doc rootElement] children] objectAtIndex:0];
614
615 // get the names of all the contexts
616 [socket send:[self createCommand:@"context_names -d 0"]];
617 NSXMLElement* contextNames = [[self processData:[socket receive]] rootElement];
618 NSMutableArray* variables = [NSMutableArray array];
619 for (NSXMLElement* context in [contextNames children])
620 {
621 NSString* name = [[context attributeForName:@"name"] stringValue];
622 int cid = [[[context attributeForName:@"id"] stringValue] intValue];
623
624 // fetch the contexts
625 [socket send:[self createCommand:[NSString stringWithFormat:@"context_get -d %d -c %d", stackDepth, cid]]];
626 NSArray* addVars = [[[self processData:[socket receive]] rootElement] children];
627 if (addVars != nil && name != nil)
628 [variables addObjectsFromArray:addVars];
629 }
630
631 // get the source
632 NSString* filename = [[xmlframe attributeForName:@"filename"] stringValue];
633 NSString* escapedFilename = [filename stringByReplacingOccurrencesOfString:@"%" withString:@"%%"]; // escape % in URL chars
634 [socket send:[self createCommand:[NSString stringWithFormat:@"source -f %@", escapedFilename]]];
635 NSString* source = [[[self processData:[socket receive]] rootElement] value]; // decode base64
636
637 // create stack frame
638 StackFrame* frame = [[StackFrame alloc]
639 initWithIndex:stackDepth
640 withFilename:filename
641 withSource:source
642 atLine:[[[xmlframe attributeForName:@"lineno"] stringValue] intValue]
643 inFunction:[[xmlframe attributeForName:@"where"] stringValue]
644 withVariables:variables
645 ];
646
647 return [frame autorelease];
648 }
649
650 /**
651 * Creates a StackFrame based on the current position in the debugger
652 */
653 - (StackFrame*)createCurrentStackFrame
654 {
655 return [self createStackFrame:0];
656 }
657
658 /**
659 * Fetches the value of and sets the status instance variable
660 */
661 - (void)updateStatus
662 {
663 [socket send:[self createCommand:@"status"]];
664 NSXMLDocument* doc = [self processData:[socket receive]];
665 self.status = [[[[doc rootElement] attributeForName:@"status"] stringValue] capitalizedString];
666
667 if (status == nil || [status isEqualToString:@"Stopped"] || [status isEqualToString:@"Stopping"])
668 {
669 connected = NO;
670 [self close];
671
672 [delegate debuggerDisconnected];
673
674 self.status = @"Stopped";
675 }
676 }
677
678 /**
679 * Given a file path, this returns a file:// URI and escapes any spaces for the
680 * debugger engine.
681 */
682 - (NSString*)escapedURIPath:(NSString*)path
683 {
684 // Custon GDBp paths are fine.
685 if ([[path substringToIndex:4] isEqualToString:@"gdbp"])
686 return path;
687
688 // Create a temporary URL that will escape all the nasty characters.
689 NSURL* url = [NSURL fileURLWithPath:path];
690 NSString* urlString = [url absoluteString];
691
692 // Remove the host because this is a file:// URL;
693 urlString = [urlString stringByReplacingOccurrencesOfString:[url host] withString:@""];
694
695 // Escape % for use in printf-style NSString formatters.
696 urlString = [urlString stringByReplacingOccurrencesOfString:@"%" withString:@"%%"];
697 return urlString;
698 }
699
700 /**
701 * Helper method for |-socketDidAccept| to be called on the main thread.
702 */
703 - (void)doSocketAccept:_nil
704 {
705 connected = YES;
706 transactionID = 0;
707 [socket receive];
708 [self updateStatus];
709
710 // register any breakpoints that exist offline
711 for (Breakpoint* bp in [[BreakpointManager sharedManager] breakpoints])
712 {
713 [self addBreakpoint:bp];
714 }
715
716 // Load the debugger to make it look active.
717 [delegate debuggerConnected];
718 }
719
720 @end