3 * Copyright (c) 2007 - 2010, Blue Static <http://www.bluestatic.org>
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.
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.
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
17 #import <sys/socket.h>
18 #import <netinet/in.h>
20 #import "GDBpConnection.h"
22 #import "AppDelegate.h"
24 // GDBpConnection (Private) ////////////////////////////////////////////////////
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 @property (retain
) NSMutableArray
* queuedWrites
;
36 - (void)socketDidAccept
;
37 - (void)socketDisconnected
;
38 - (void)readStreamHasData
;
39 - (void)send
:(NSString
*)command
;
40 - (void)performSend
:(NSString
*)command
;
41 - (void)errorEncountered
:(NSString
*)error
;
43 - (void)handleResponse
:(NSXMLDocument
*)response
;
44 - (void)initReceived
:(NSXMLDocument
*)response
;
45 - (void)updateStatus
:(NSXMLDocument
*)response
;
46 - (void)debuggerStep
:(NSXMLDocument
*)response
;
48 - (NSString
*)createCommand
:(NSString
*)cmd
, ...
;
49 - (StackFrame
*)createStackFrame
:(int)depth
;
50 - (StackFrame
*)createCurrentStackFrame
;
51 - (NSString
*)escapedURIPath
:(NSString
*)path
;
54 // CFNetwork Callbacks /////////////////////////////////////////////////////////
56 void ReadStreamCallback(CFReadStreamRef stream
, CFStreamEventType eventType
, void* connectionRaw
)
58 NSLog(@
"ReadStreamCallback()");
59 GDBpConnection
* connection
= (GDBpConnection
*)connectionRaw
;
62 case kCFStreamEventHasBytesAvailable
:
63 [connection readStreamHasData
];
66 case kCFStreamEventErrorOccurred
:
68 CFErrorRef error
= CFReadStreamCopyError(stream
);
69 CFReadStreamUnscheduleFromRunLoop(stream
, CFRunLoopGetCurrent(), kCFRunLoopCommonModes
);
70 CFReadStreamClose(stream
);
72 [connection errorEncountered
:[[(NSError
*)error autorelease
] description
]];
76 case kCFStreamEventEndEncountered
:
77 CFReadStreamUnscheduleFromRunLoop(stream
, CFRunLoopGetCurrent(), kCFRunLoopCommonModes
);
78 CFReadStreamClose(stream
);
80 [connection socketDisconnected
];
85 void WriteStreamCallback(CFWriteStreamRef stream
, CFStreamEventType eventType
, void* connectionRaw
)
87 NSLog(@
"WriteStreamCallback()");
88 GDBpConnection
* connection
= (GDBpConnection
*)connectionRaw
;
91 case kCFStreamEventCanAcceptBytes
:
92 NSLog(@
"can accept bytes");
93 if ([connection.queuedWrites count
] > 0)
95 NSString
* command
= [connection.queuedWrites objectAtIndex
:0];
96 [connection performSend
:command
];
97 [connection.queuedWrites removeObjectAtIndex
:0];
101 case kCFStreamEventErrorOccurred
:
103 CFErrorRef error
= CFWriteStreamCopyError(stream
);
104 CFWriteStreamUnscheduleFromRunLoop(stream
, CFRunLoopGetCurrent(), kCFRunLoopCommonModes
);
105 CFWriteStreamClose(stream
);
107 [connection errorEncountered
:[[(NSError
*)error autorelease
] description
]];
111 case kCFStreamEventEndEncountered
:
112 CFWriteStreamUnscheduleFromRunLoop(stream
, CFRunLoopGetCurrent(), kCFRunLoopCommonModes
);
113 CFWriteStreamClose(stream
);
115 [connection socketDisconnected
];
120 void SocketAcceptCallback(CFSocketRef socket
,
121 CFSocketCallBackType callbackType
,
126 assert(callbackType
== kCFSocketAcceptCallBack
);
127 NSLog(@
"SocketAcceptCallback()");
129 GDBpConnection
* connection
= (GDBpConnection
*)connectionRaw
;
131 CFReadStreamRef readStream
;
132 CFWriteStreamRef writeStream
;
134 // Create the streams on the socket.
135 CFStreamCreatePairWithSocket(kCFAllocatorDefault
,
136 *(CFSocketNativeHandle
*)data
, // Socket handle.
137 &readStream
, // Read stream in-pointer.
138 &writeStream
); // Write stream in-pointer.
140 // Create struct to register callbacks for the stream.
141 CFStreamClientContext context
;
143 context.info
= connection
;
144 context.retain
= NULL
;
145 context.release
= NULL
;
146 context.copyDescription
= NULL
;
148 // Set the client of the read stream.
149 CFOptionFlags readFlags
=
150 kCFStreamEventOpenCompleted |
151 kCFStreamEventHasBytesAvailable |
152 kCFStreamEventErrorOccurred |
153 kCFStreamEventEndEncountered
;
154 if (CFReadStreamSetClient(readStream
, readFlags
, ReadStreamCallback
, &context
))
155 // Schedule in run loop to do asynchronous communication with the engine.
156 CFReadStreamScheduleWithRunLoop(readStream
, CFRunLoopGetCurrent(), kCFRunLoopCommonModes
);
160 NSLog(@
"Read stream scheduled");
162 // Open the stream now that it's scheduled on the run loop.
163 if (!CFReadStreamOpen(readStream
))
165 CFStreamError error
= CFReadStreamGetError(readStream
);
166 NSLog(@
"error! %@", error
);
170 NSLog(@
"Read stream opened");
172 // Set the client of the write stream.
173 CFOptionFlags writeFlags
=
174 kCFStreamEventOpenCompleted |
175 kCFStreamEventCanAcceptBytes |
176 kCFStreamEventErrorOccurred |
177 kCFStreamEventEndEncountered
;
178 if (CFWriteStreamSetClient(writeStream
, writeFlags
, WriteStreamCallback
, &context
))
179 // Schedule it in the run loop to receive error information.
180 CFWriteStreamScheduleWithRunLoop(writeStream
, CFRunLoopGetCurrent(), kCFRunLoopCommonModes
);
184 NSLog(@
"Write stream scheduled");
186 // Open the write stream.
187 if (!CFWriteStreamOpen(writeStream
))
189 CFStreamError error
= CFWriteStreamGetError(writeStream
);
190 NSLog(@
"error! %@", error
);
194 NSLog(@
"Write stream opened");
196 connection.readStream
= readStream
;
197 connection.writeStream
= writeStream
;
198 [connection socketDidAccept
];
201 // GDBpConnection //////////////////////////////////////////////////////////////
203 @implementation GDBpConnection
204 @synthesize socket
= socket_
;
205 @synthesize readStream
= readStream_
;
206 @synthesize currentPacket
= currentPacket_
;
207 @synthesize writeStream
= writeStream_
;
208 @synthesize queuedWrites
= queuedWrites_
;
210 @synthesize delegate
;
213 * Creates a new DebuggerConnection and initializes the socket from the given connection
216 - (id)initWithPort
:(int)aPort
218 if (self = [super init
])
223 [[BreakpointManager sharedManager
] setConnection
:self];
231 * Deallocates the object
236 self.currentPacket
= nil;
242 * Gets the port number
250 * Returns the name of the remote host
252 - (NSString
*)remoteHost
256 return @
"(DISCONNECTED)";
258 // TODO: Either impl or remove.
263 * Returns whether or not we have an active connection
271 * Called by SocketWrapper after the connection is successful. This immediately calls
272 * -[SocketWrapper receive] to clear the way for communication, though the information
273 * could be useful server information that we don't use right now.
275 - (void)socketDidAccept
282 * Receives errors from the SocketWrapper and updates the display
284 - (void)errorEncountered
:(NSString
*)error
286 [delegate errorEncountered
:error
];
290 * Reestablishes communication with the remote debugger so that a new connection doesn't have to be
291 * created every time you want to debug a page
296 self.status
= @
"Connecting";
301 * Creates an entirely new stack and returns it as an array of StackFrame objects.
303 - (NSArray
*)getCurrentStack
305 // get the total stack depth
306 [socket send
:[self createCommand
:@
"stack_depth"]];
307 NSXMLDocument
* doc
= [self processData
:[socket receive
]];
308 int depth
= [[[[doc rootElement
] attributeForName
:@
"depth"] stringValue
] intValue
];
310 // get all stack frames
311 NSMutableArray
* stack
= [NSMutableArray arrayWithCapacity
:depth
];
312 for (int i
= 0; i
< depth
; i
++)
314 StackFrame
* frame
= [self createStackFrame
:i
];
315 [stack insertObject
:frame atIndex
:i
];
322 * Tells the debugger to continue running the script. Returns the current stack frame.
326 [self send
:[self createCommand
:@
"run"]];
330 * Tells the debugger to step into the current command.
334 [self send
:[self createCommand
:@
"step_into"]];
338 * Tells the debugger to step out of the current context
342 [self send
:[self createCommand
:@
"step_out"]];
346 * Tells the debugger to step over the current function
350 [self send
:[self createCommand
:@
"step_over"]];
354 * Tells the debugger engine to get a specifc property. This also takes in the NSXMLElement
355 * that requested it so that the child can be attached.
357 - (NSArray
*)getProperty
:(NSString
*)property
359 [socket send
:[self createCommand
:[NSString stringWithFormat
:@
"property_get -n \"%@\"", property
]]];
361 NSXMLDocument
* doc
= [self processData
:[socket receive
]];
365 <property> <!-- this is the one we requested -->
366 <property ... /> <!-- these are what we want -->
371 // we now have to detach all the children so we can insert them into another document
372 NSXMLElement
* parent
= (NSXMLElement
*)[[doc rootElement
] childAtIndex
:0];
373 NSArray
* children
= [parent children
];
374 [parent setChildren
:nil];
378 #pragma mark Breakpoints
381 * Send an add breakpoint command
383 - (void)addBreakpoint
:(Breakpoint
*)bp
388 NSString
* file
= [self escapedURIPath
:[bp transformedPath
]];
389 NSString
* cmd
= [self createCommand
:[NSString stringWithFormat
:@
"breakpoint_set -t line -f %@ -n %i", file
, [bp line
]]];
391 NSXMLDocument
* info
= [self processData
:[socket receive
]];
392 [bp setDebuggerId
:[[[[info rootElement
] attributeForName
:@
"id"] stringValue
] intValue
]];
396 * Removes a breakpoint
398 - (void)removeBreakpoint
:(Breakpoint
*)bp
405 [socket send
:[self createCommand
:[NSString stringWithFormat
:@
"breakpoint_remove -d %i", [bp debuggerId
]]]];
409 #pragma mark Socket and Stream Callbacks
412 * Creates, connects to, and schedules a CFSocket.
416 // Pass ourselves to the callback so we don't have to use ugly globals.
417 CFSocketContext context
;
420 context.retain
= NULL
;
421 context.release
= NULL
;
422 context.copyDescription
= NULL
;
424 // Create the address structure.
425 struct sockaddr_in address
;
426 memset(&address
, 0, sizeof(address
));
427 address.sin_len
= sizeof(address
);
428 address.sin_family
= AF_INET
;
429 address.sin_port
= htons(port
);
430 address.sin_addr.s_addr
= htonl(INADDR_ANY
);
432 // Create the socket signature.
433 CFSocketSignature signature
;
434 signature.protocolFamily
= PF_INET
;
435 signature.socketType
= SOCK_STREAM
;
436 signature.protocol
= IPPROTO_TCP
;
437 signature.address
= (CFDataRef
)[NSData dataWithBytes
:&address length
:sizeof(address
)];
439 socket_
= CFSocketCreateWithSocketSignature(kCFAllocatorDefault
,
440 &signature
, // Socket signature.
441 kCFSocketAcceptCallBack
, // Callback types.
442 SocketAcceptCallback
, // Callout function pointer.
443 &context
); // Context to pass to callout.
446 [self errorEncountered
:@
"Could not open socket."];
450 // Allow old, yet-to-be recycled sockets to be reused.
452 setsockopt(CFSocketGetNative(socket_
), SOL_SOCKET
, SO_REUSEADDR
, &yes
, sizeof(BOOL));
454 // Schedule the socket on the run loop.
455 CFRunLoopSourceRef source
= CFSocketCreateRunLoopSource(kCFAllocatorDefault
, socket_
, 0);
456 CFRunLoopAddSource(CFRunLoopGetCurrent(), source
, kCFRunLoopCommonModes
);
459 self.status
= @
"Connecting";
463 * Closes a socket and releases the ref.
467 // The socket goes down, so do the streams, which clean themselves up.
468 CFSocketInvalidate(socket_
);
473 * Notification that the socket disconnected.
475 - (void)socketDisconnected
478 [delegate debuggerDisconnected
];
482 * Callback from the CFReadStream that there is data waiting to be read.
484 - (void)readStreamHasData
487 CFIndex bytesRead
= CFReadStreamRead(readStream_
, buffer
, 1024);
488 const char* charBuffer
= (const char*)buffer
;
490 // We haven't finished reading a packet, so just read more data in.
491 if (currentPacketIndex_
< packetSize_
)
493 [currentPacket_ appendFormat
:@
"%s", buffer
];
494 currentPacketIndex_
+= bytesRead
;
496 // Time to read a new packet.
499 // Read the message header: the size.
500 packetSize_
= atoi(charBuffer
);
501 currentPacketIndex_
= bytesRead
- strlen(charBuffer
);
502 self.currentPacket
= [NSMutableString stringWithFormat
:@
"%s", buffer
+ strlen(charBuffer
) + 1];
505 // We have finished reading the packet.
506 if (currentPacketIndex_
>= packetSize_
)
509 currentPacketIndex_
= 0;
511 // Test if we can convert it into an NSXMLDocument.
512 NSError
* error
= nil;
513 NSXMLDocument
* xmlTest
= [[NSXMLDocument alloc
] initWithXMLString
:currentPacket_ options
:NSXMLDocumentTidyXML error
:&error
];
516 NSLog(@
"Could not parse XML? --- %@", error
);
517 NSLog(@
"Error UserInfo: %@", [error userInfo
]);
518 NSLog(@
"This is the XML Document: %@", currentPacket_
);
521 [self handleResponse
:[xmlTest autorelease
]];
526 * Writes a command into the write stream. If the stream is ready for writing,
527 * we do so immediately. If not, the command is queued and will be written
528 * when the stream is ready.
530 - (void)send
:(NSString
*)command
532 if (CFWriteStreamCanAcceptBytes(writeStream_
))
533 [self performSend
:command
];
535 [queuedWrites_ addObject
:command
];
539 * This performs a blocking send. This should ONLY be called when we know we
540 * have write access to the stream. We will busy wait in case we don't do a full
543 - (void)performSend
:(NSString
*)command
547 char* string
= (char*)[command UTF8String
];
548 int stringLength
= strlen(string
);
550 // Busy wait while writing. BAADD. Should background this operation.
553 if (CFWriteStreamCanAcceptBytes(writeStream_
))
555 // Include the NULL byte in the string when we write.
556 int bytesWritten
= CFWriteStreamWrite(writeStream_
, (UInt8
*)string
, stringLength
+ 1);
557 if (bytesWritten
< 0)
559 NSLog(@
"write error");
562 else if (bytesWritten
< strlen(string
))
564 // Adjust the buffer and wait for another chance to write.
565 stringLength
-= bytesWritten
;
566 memmove(string
, string
+ bytesWritten
, stringLength
);
576 #pragma mark Response Handlers
578 - (void)handleResponse
:(NSXMLDocument
*)response
580 // Check and see if there's an error.
581 NSArray
* error
= [[response rootElement
] elementsForName
:@
"error"];
582 if ([error count
] > 0)
584 NSLog(@
"Xdebug error: %@", error
);
585 [delegate errorEncountered
:[[[[error objectAtIndex
:0] children
] objectAtIndex
:0] stringValue
]];
588 // If TransportDebug is enabled, log the response.
589 if ([[[[NSProcessInfo processInfo
] environment
] objectForKey
:@
"TransportDebug"] boolValue
])
590 NSLog(@
"<-- %@", response
);
592 // Get the name of the command from the engine's response.
593 NSString
* command
= [[[response rootElement
] attributeForName
:@
"command"] stringValue
];
595 // Dispatch the command response to an appropriate handler.
596 if ([[[response rootElement
] name
] isEqualToString
:@
"init"])
597 [self initReceived
:response
];
598 else if ([command isEqualToString
:@
"status"])
599 [self updateStatus
:response
];
600 else if ([command isEqualToString
:@
"run"] ||
[command isEqualToString
:@
"step_into"] ||
601 [command isEqualToString
:@
"step_over"] ||
[command isEqualToString
:@
"step_out"])
602 [self debuggerStep
:response
];
605 - (void)initReceived
:(NSXMLDocument
*)response
607 // Register any breakpoints that exist offline.
608 for (Breakpoint
* bp
in [[BreakpointManager sharedManager
] breakpoints
])
609 [self addBreakpoint
:bp
];
611 // Load the debugger to make it look active.
612 [delegate debuggerConnected
];
614 [self send
:[self createCommand
:@
"status"]];
618 * Fetches the value of and sets the status instance variable
620 - (void)updateStatus
:(NSXMLDocument
*)response
622 self.status
= [[[[response rootElement
] attributeForName
:@
"status"] stringValue
] capitalizedString
];
623 if (status
== nil ||
[status isEqualToString
:@
"Stopped"] ||
[status isEqualToString
:@
"Stopping"])
627 [delegate debuggerDisconnected
];
629 self.status
= @
"Stopped";
633 - (void)debuggerStep
:(NSXMLDocument
*)response
635 [self send
:[self createCommand
:@
"status"]];
641 * Helper method to create a string command with the -i <transaction id> automatically tacked on. Takes
642 * a variable number of arguments and parses the given command with +[NSString stringWithFormat:]
644 - (NSString
*)createCommand
:(NSString
*)cmd
, ...
648 va_start(argList
, cmd
);
649 NSString
* format
= [[NSString alloc
] initWithFormat
:cmd arguments
:argList
]; // format the command
652 if ([[[[NSProcessInfo processInfo
] environment
] objectForKey
:@
"TransportDebug"] boolValue
])
653 NSLog(@
"--> %@", cmd
);
655 return [NSString stringWithFormat
:@
"%@ -i %d", [format autorelease
], transactionID
++];
659 * Generates a stack frame for the given depth
661 - (StackFrame
*)createStackFrame
:(int)stackDepth
663 // get the stack frame
664 [socket send
:[self createCommand
:@
"stack_get -d %d", stackDepth
]];
665 NSXMLDocument
* doc
= [self processData
:[socket receive
]];
669 NSXMLElement
* xmlframe
= [[[doc rootElement
] children
] objectAtIndex
:0];
671 // get the names of all the contexts
672 [socket send
:[self createCommand
:@
"context_names -d 0"]];
673 NSXMLElement
* contextNames
= [[self processData
:[socket receive
]] rootElement
];
674 NSMutableArray
* variables
= [NSMutableArray array
];
675 for (NSXMLElement
* context
in [contextNames children
])
677 NSString
* name
= [[context attributeForName
:@
"name"] stringValue
];
678 int cid
= [[[context attributeForName
:@
"id"] stringValue
] intValue
];
680 // fetch the contexts
681 [socket send
:[self createCommand
:[NSString stringWithFormat
:@
"context_get -d %d -c %d", stackDepth
, cid
]]];
682 NSArray
* addVars
= [[[self processData
:[socket receive
]] rootElement
] children
];
683 if (addVars
!= nil && name
!= nil)
684 [variables addObjectsFromArray
:addVars
];
688 NSString
* filename
= [[xmlframe attributeForName
:@
"filename"] stringValue
];
689 NSString
* escapedFilename
= [filename stringByReplacingOccurrencesOfString
:@
"%" withString
:@
"%%"]; // escape % in URL chars
690 [socket send
:[self createCommand
:[NSString stringWithFormat
:@
"source -f %@", escapedFilename
]]];
691 NSString
* source
= [[[self processData
:[socket receive
]] rootElement
] value
]; // decode base64
693 // create stack frame
694 StackFrame
* frame
= [[StackFrame alloc
]
695 initWithIndex
:stackDepth
696 withFilename
:filename
698 atLine
:[[[xmlframe attributeForName
:@
"lineno"] stringValue
] intValue
]
699 inFunction
:[[xmlframe attributeForName
:@
"where"] stringValue
]
700 withVariables
:variables
703 return [frame autorelease
];
707 * Creates a StackFrame based on the current position in the debugger
709 - (StackFrame
*)createCurrentStackFrame
711 return [self createStackFrame
:0];
715 * Given a file path, this returns a file:// URI and escapes any spaces for the
718 - (NSString
*)escapedURIPath
:(NSString
*)path
720 // Custon GDBp paths are fine.
721 if ([[path substringToIndex
:4] isEqualToString
:@
"gdbp"])
724 // Create a temporary URL that will escape all the nasty characters.
725 NSURL
* url
= [NSURL fileURLWithPath
:path
];
726 NSString
* urlString
= [url absoluteString
];
728 // Remove the host because this is a file:// URL;
729 urlString
= [urlString stringByReplacingOccurrencesOfString
:[url host
] withString
:@
""];
731 // Escape % for use in printf-style NSString formatters.
732 urlString
= [urlString stringByReplacingOccurrencesOfString
:@
"%" withString
:@
"%%"];