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 "DebuggerConnection.h"
19 #import <sys/socket.h>
20 #import <netinet/in.h>
22 #import "AppDelegate.h"
23 #import "LoggingController.h"
25 // DebuggerConnection (Private) ////////////////////////////////////////////////
27 @interface DebuggerConnection ()
29 @property (assign
) CFSocketRef socket
;
30 @property (assign
) CFReadStreamRef readStream
;
31 @property NSUInteger lastReadTransaction
;
32 @property (retain
) NSMutableString
* currentPacket
;
33 @property (assign
) CFWriteStreamRef writeStream
;
34 @property NSUInteger lastWrittenTransaction
;
35 @property (retain
) NSMutableArray
* queuedWrites
;
37 - (void)connectionThreadStart
;
39 - (void)socketDidAccept
;
40 - (void)socketDisconnected
;
41 - (void)readStreamHasData
;
43 - (void)performSend
:(NSString
*)command
;
44 - (void)sendQueuedWrites
;
46 - (void)performQuitSignal
;
48 - (void)handleResponse
:(NSXMLDocument
*)response
;
49 - (void)handlePacket
:(NSString
*)packet
;
51 // Threadsafe wrappers for the delegate's methods.
52 - (void)errorEncountered
:(NSString
*)error
;
53 - (LogEntry
*)recordSend
:(NSString
*)command
;
54 - (LogEntry
*)recordReceive
:(NSString
*)command
;
58 // CFNetwork Callbacks /////////////////////////////////////////////////////////
60 void ReadStreamCallback(CFReadStreamRef stream
, CFStreamEventType eventType
, void* connectionRaw
)
62 DebuggerConnection
* connection
= (DebuggerConnection
*)connectionRaw
;
65 case kCFStreamEventHasBytesAvailable
:
66 [connection readStreamHasData
];
69 case kCFStreamEventErrorOccurred
:
71 CFErrorRef error
= CFReadStreamCopyError(stream
);
72 CFReadStreamUnscheduleFromRunLoop(stream
, CFRunLoopGetCurrent(), kCFRunLoopCommonModes
);
73 CFReadStreamClose(stream
);
75 [connection errorEncountered
:[[(NSError
*)error autorelease
] description
]];
79 case kCFStreamEventEndEncountered
:
80 CFReadStreamUnscheduleFromRunLoop(stream
, CFRunLoopGetCurrent(), kCFRunLoopCommonModes
);
81 CFReadStreamClose(stream
);
83 [connection socketDisconnected
];
88 void WriteStreamCallback(CFWriteStreamRef stream
, CFStreamEventType eventType
, void* connectionRaw
)
90 DebuggerConnection
* connection
= (DebuggerConnection
*)connectionRaw
;
93 case kCFStreamEventCanAcceptBytes
:
94 [connection sendQueuedWrites
];
97 case kCFStreamEventErrorOccurred
:
99 CFErrorRef error
= CFWriteStreamCopyError(stream
);
100 CFWriteStreamUnscheduleFromRunLoop(stream
, CFRunLoopGetCurrent(), kCFRunLoopCommonModes
);
101 CFWriteStreamClose(stream
);
103 [connection errorEncountered
:[[(NSError
*)error autorelease
] description
]];
107 case kCFStreamEventEndEncountered
:
108 CFWriteStreamUnscheduleFromRunLoop(stream
, CFRunLoopGetCurrent(), kCFRunLoopCommonModes
);
109 CFWriteStreamClose(stream
);
111 [connection socketDisconnected
];
116 void SocketAcceptCallback(CFSocketRef socket
,
117 CFSocketCallBackType callbackType
,
122 assert(callbackType
== kCFSocketAcceptCallBack
);
123 NSLog(@
"SocketAcceptCallback()");
125 DebuggerConnection
* connection
= (DebuggerConnection
*)connectionRaw
;
127 CFReadStreamRef readStream
;
128 CFWriteStreamRef writeStream
;
130 // Create the streams on the socket.
131 CFStreamCreatePairWithSocket(kCFAllocatorDefault
,
132 *(CFSocketNativeHandle
*)data
, // Socket handle.
133 &readStream
, // Read stream in-pointer.
134 &writeStream
); // Write stream in-pointer.
136 // Create struct to register callbacks for the stream.
137 CFStreamClientContext context
;
139 context.info
= connection
;
140 context.retain
= NULL
;
141 context.release
= NULL
;
142 context.copyDescription
= NULL
;
144 // Set the client of the read stream.
145 CFOptionFlags readFlags
=
146 kCFStreamEventOpenCompleted |
147 kCFStreamEventHasBytesAvailable |
148 kCFStreamEventErrorOccurred |
149 kCFStreamEventEndEncountered
;
150 if (CFReadStreamSetClient(readStream
, readFlags
, ReadStreamCallback
, &context
))
151 // Schedule in run loop to do asynchronous communication with the engine.
152 CFReadStreamScheduleWithRunLoop(readStream
, CFRunLoopGetCurrent(), kCFRunLoopCommonModes
);
156 // Open the stream now that it's scheduled on the run loop.
157 if (!CFReadStreamOpen(readStream
))
159 CFStreamError error
= CFReadStreamGetError(readStream
);
160 NSLog(@
"error! %@", error
);
164 // Set the client of the write stream.
165 CFOptionFlags writeFlags
=
166 kCFStreamEventOpenCompleted |
167 kCFStreamEventCanAcceptBytes |
168 kCFStreamEventErrorOccurred |
169 kCFStreamEventEndEncountered
;
170 if (CFWriteStreamSetClient(writeStream
, writeFlags
, WriteStreamCallback
, &context
))
171 // Schedule it in the run loop to receive error information.
172 CFWriteStreamScheduleWithRunLoop(writeStream
, CFRunLoopGetCurrent(), kCFRunLoopCommonModes
);
176 // Open the write stream.
177 if (!CFWriteStreamOpen(writeStream
))
179 CFStreamError error
= CFWriteStreamGetError(writeStream
);
180 NSLog(@
"error! %@", error
);
184 connection.readStream
= readStream
;
185 connection.writeStream
= writeStream
;
186 [connection socketDidAccept
];
189 // Other Run Loop Callbacks ////////////////////////////////////////////////////
191 void PerformQuitSignal(void* info
)
193 DebuggerConnection
* obj
= (DebuggerConnection
*)info
;
194 [obj performQuitSignal
];
197 ////////////////////////////////////////////////////////////////////////////////
199 @implementation DebuggerConnection
201 @synthesize port
= port_
;
202 @synthesize connected
= connected_
;
203 @synthesize delegate
= delegate_
;
205 @synthesize socket
= socket_
;
206 @synthesize readStream
= readStream_
;
207 @synthesize lastReadTransaction
= lastReadTransaction_
;
208 @synthesize currentPacket
= currentPacket_
;
209 @synthesize writeStream
= writeStream_
;
210 @synthesize lastWrittenTransaction
= lastWrittenTransaction_
;
211 @synthesize queuedWrites
= queuedWrites_
;
213 - (id)initWithPort
:(NSUInteger
)aPort
215 if (self = [super init
])
224 self.currentPacket
= nil;
229 * Kicks off the socket on another thread.
233 if (thread_
&& !connected_
) {
234 // A thread has been detached but the socket has yet to connect. Do not
235 // spawn a new thread otherwise multiple threads will be blocked on the same
239 [NSThread detachNewThreadSelector
:@selector(connectionThreadStart
) toTarget
:self withObject
:nil];
243 * Creates, connects to, and schedules a CFSocket.
245 - (void)connectionThreadStart
247 NSAutoreleasePool
* pool
= [[NSAutoreleasePool alloc
] init
];
249 thread_
= [NSThread currentThread
];
250 runLoop_
= [NSRunLoop currentRunLoop
];
252 // Pass ourselves to the callback so we don't have to use ugly globals.
253 CFSocketContext context
;
256 context.retain
= NULL
;
257 context.release
= NULL
;
258 context.copyDescription
= NULL
;
260 // Create the address structure.
261 struct sockaddr_in address
;
262 memset(&address
, 0, sizeof(address
));
263 address.sin_len
= sizeof(address
);
264 address.sin_family
= AF_INET
;
265 address.sin_port
= htons(port_
);
266 address.sin_addr.s_addr
= htonl(INADDR_ANY
);
268 // Create the socket signature.
269 CFSocketSignature signature
;
270 signature.protocolFamily
= PF_INET
;
271 signature.socketType
= SOCK_STREAM
;
272 signature.protocol
= IPPROTO_TCP
;
273 signature.address
= (CFDataRef
)[NSData dataWithBytes
:&address length
:sizeof(address
)];
276 socket_
= CFSocketCreateWithSocketSignature(kCFAllocatorDefault
,
277 &signature
, // Socket signature.
278 kCFSocketAcceptCallBack
, // Callback types.
279 SocketAcceptCallback
, // Callout function pointer.
280 &context
); // Context to pass to callout.
282 [self errorEncountered
:@
"Could not open socket."];
287 // Allow old, yet-to-be recycled sockets to be reused.
289 setsockopt(CFSocketGetNative(socket_
), SOL_SOCKET
, SO_REUSEADDR
, &yes
, sizeof(BOOL));
290 setsockopt(CFSocketGetNative(socket_
), SOL_SOCKET
, SO_REUSEPORT
, &yes
, sizeof(BOOL));
292 // Schedule the socket on the run loop.
293 CFRunLoopSourceRef source
= CFSocketCreateRunLoopSource(kCFAllocatorDefault
, socket_
, 0);
294 CFRunLoopAddSource([runLoop_ getCFRunLoop
], source
, kCFRunLoopCommonModes
);
297 // Create a source that is used to quit.
298 CFRunLoopSourceContext quitContext
= { 0 };
299 quitContext.version
= 0;
300 quitContext.info
= self;
301 quitContext.perform
= PerformQuitSignal
;
302 quitSource_
= CFRunLoopSourceCreate(kCFAllocatorDefault
, 0, &quitContext
);
303 CFRunLoopAddSource([runLoop_ getCFRunLoop
], quitSource_
, kCFRunLoopCommonModes
);
310 CFRunLoopSourceInvalidate(quitSource_
);
311 CFRelease(quitSource_
);
318 * Called by SocketWrapper after the connection is successful. This immediately calls
319 * -[SocketWrapper receive] to clear the way for communication, though the information
320 * could be useful server information that we don't use right now.
322 - (void)socketDidAccept
326 lastReadTransaction_
= 0;
327 lastWrittenTransaction_
= 0;
328 self.queuedWrites
= [NSMutableArray array
];
329 writeQueueLock_
= [NSRecursiveLock
new];
330 if ([delegate_ respondsToSelector
:@selector(connectionDidAccept
:)])
331 [delegate_ performSelectorOnMainThread
:@selector(connectionDidAccept
:)
337 * Closes a socket and releases the ref.
344 if (runLoop_
&& quitSource_
) {
345 CFRunLoopSourceSignal(quitSource_
);
346 CFRunLoopWakeUp([runLoop_ getCFRunLoop
]);
351 * Quits the run loop and stops the thread.
353 - (void)performQuitSignal
355 self.queuedWrites
= nil;
357 [writeQueueLock_ release
];
360 CFRunLoopStop([runLoop_ getCFRunLoop
]);
363 // The socket goes down, so do the streams, which clean themselves up.
365 NSLog(@
"invalidating socket");
366 CFSocketInvalidate(socket_
);
373 * Notification that the socket disconnected.
375 - (void)socketDisconnected
378 // The state still is connected, which means that we did not get here
379 // through normal disconnected procedure (a call to |-close|, followed by
380 // the downing of the socket and the stream, which also produces this
381 // messsage). Instead, the stream callbacks encountered EOF unexpectedly.
384 if ([delegate_ respondsToSelector
:@selector(connectionDidClose
:)])
385 [delegate_ connectionDidClose
:self];
389 * Writes a command into the write stream. If the stream is ready for writing,
390 * we do so immediately. If not, the command is queued and will be written
391 * when the stream is ready.
393 - (void)send
:(NSString
*)command
395 if (lastReadTransaction_
>= lastWrittenTransaction_
&& CFWriteStreamCanAcceptBytes(writeStream_
)) {
396 [self performSend
:command
];
398 [writeQueueLock_ lock
];
399 [queuedWrites_ addObject
:command
];
400 [writeQueueLock_ unlock
];
402 [self sendQueuedWrites
];
406 * This will send a command to the debugger engine. It will append the
407 * transaction ID automatically. It accepts a NSString command along with a
408 * a variable number of arguments to substitute into the command, a la
409 * +[NSString stringWithFormat:]. Returns the transaction ID as a NSNumber.
411 - (NSNumber
*)sendCommandWithFormat
:(NSString
*)format
, ...
413 // Collect varargs and format command.
415 va_start(args
, format
);
416 NSString
* command
= [[NSString alloc
] initWithFormat
:format arguments
:args
];
419 NSNumber
* callbackKey
= [NSNumber numberWithInt
:transactionID
++];
420 NSString
* taggedCommand
= [NSString stringWithFormat
:@
"%@ -i %@", [command autorelease
], callbackKey
];
421 [self performSelector
:@selector(send
:)
423 withObject
:taggedCommand
424 waitUntilDone
:connected_
];
430 * Given a file path, this returns a file:// URI and escapes any spaces for the
433 - (NSString
*)escapedURIPath
:(NSString
*)path
435 // Custon GDBp paths are fine.
436 if ([[path substringToIndex
:4] isEqualToString
:@
"gdbp"])
439 // Create a temporary URL that will escape all the nasty characters.
440 NSURL
* url
= [NSURL fileURLWithPath
:path
];
441 NSString
* urlString
= [url absoluteString
];
443 // Remove the host because this is a file:// URL;
444 urlString
= [urlString stringByReplacingOccurrencesOfString
:[url host
] withString
:@
""];
446 // Escape % for use in printf-style NSString formatters.
447 urlString
= [urlString stringByReplacingOccurrencesOfString
:@
"%" withString
:@
"%%"];
452 * Returns the transaction_id from an NSXMLDocument.
454 - (NSInteger
)transactionIDFromResponse
:(NSXMLDocument
*)response
456 return [[[[response rootElement
] attributeForName
:@
"transaction_id"] stringValue
] intValue
];
460 * Scans a command string for the transaction ID component. If it is not found,
461 * returns NSNotFound.
463 - (NSInteger
)transactionIDFromCommand
:(NSString
*)command
465 NSRange occurrence
= [command rangeOfString
:@
"-i "];
466 if (occurrence.location
== NSNotFound
)
468 NSString
* transaction
= [command substringFromIndex
:occurrence.location
+ occurrence.length
];
469 return [transaction intValue
];
472 // Private /////////////////////////////////////////////////////////////////////
475 // Delegate Thread-Safe Wrappers ///////////////////////////////////////////////
478 * Receives errors from the SocketWrapper and updates the display
480 - (void)errorEncountered
:(NSString
*)error
482 if (![delegate_ respondsToSelector
:@selector(errorEncountered
:)])
484 [delegate_ performSelectorOnMainThread
:@selector(errorEncountered
:)
489 - (LogEntry
*)recordSend
:(NSString
*)command
491 LoggingController
* logger
= [[AppDelegate instance
] loggingController
];
492 LogEntry
* entry
= [LogEntry newSendEntry
:command
];
493 entry.lastReadTransactionID
= lastReadTransaction_
;
494 entry.lastWrittenTransactionID
= lastWrittenTransaction_
;
495 [logger performSelectorOnMainThread
:@selector(recordEntry
:)
498 return [entry autorelease
];
501 - (LogEntry
*)recordReceive
:(NSString
*)command
503 LoggingController
* logger
= [[AppDelegate instance
] loggingController
];
504 LogEntry
* entry
= [LogEntry newReceiveEntry
:command
];
505 entry.lastReadTransactionID
= lastReadTransaction_
;
506 entry.lastWrittenTransactionID
= lastWrittenTransaction_
;
507 [logger performSelectorOnMainThread
:@selector(recordEntry
:)
510 return [entry autorelease
];
513 // Stream Managers /////////////////////////////////////////////////////////////
516 * Callback from the CFReadStream that there is data waiting to be read.
518 - (void)readStreamHasData
520 const NSUInteger kBufferSize
= 1024;
521 UInt8 buffer
[kBufferSize
];
522 CFIndex bufferOffset
= 0; // Starting point in |buffer| to work with.
523 CFIndex bytesRead
= CFReadStreamRead(readStream_
, buffer
, kBufferSize
);
524 const char* charBuffer
= (const char*)buffer
;
526 // The read loop works by going through the buffer until all the bytes have
528 while (bufferOffset
< bytesRead
)
530 // Find the NULL separator, or the end of the string.
531 NSUInteger partLength
= 0;
532 for (NSUInteger i
= bufferOffset
; i
< bytesRead
&& charBuffer
[i
] != '\0'; ++i
, ++partLength
) ;
534 // If there is not a current packet, set some state.
535 if (!self.currentPacket
)
537 // Read the message header: the size. This will be |partLength| bytes.
538 packetSize_
= atoi(charBuffer
+ bufferOffset
);
539 currentPacketIndex_
= 0;
540 self.currentPacket
= [NSMutableString stringWithCapacity
:packetSize_
];
541 bufferOffset
+= partLength
+ 1; // Pass over the NULL byte.
542 continue; // Spin the loop to begin reading actual data.
545 // Substring the byte stream and append it to the packet string.
546 CFStringRef bufferString
= CFStringCreateWithBytes(kCFAllocatorDefault
,
547 buffer
+ bufferOffset
, // Byte pointer, offset by start index.
548 partLength
, // Length.
549 kCFStringEncodingUTF8
,
551 [self.currentPacket appendString
:(NSString
*)bufferString
];
552 CFRelease(bufferString
);
555 currentPacketIndex_
+= partLength
;
556 bufferOffset
+= partLength
+ 1;
558 // If this read finished the packet, handle it and reset.
559 if (currentPacketIndex_
>= packetSize_
)
561 [self handlePacket
:[[currentPacket_ retain
] autorelease
]];
562 self.currentPacket
= nil;
564 currentPacketIndex_
= 0;
570 * Performs the packet handling of a raw string XML packet. From this point on,
571 * the packets are associated with a transaction and are then dispatched.
573 - (void)handlePacket
:(NSString
*)packet
575 // Test if we can convert it into an NSXMLDocument.
576 NSError
* error
= nil;
577 NSXMLDocument
* xmlTest
= [[NSXMLDocument alloc
] initWithXMLString
:currentPacket_ options
:NSXMLDocumentTidyXML error
:&error
];
579 // Try to recover if we encountered an error.
582 // We do not want to starve the write queue, so manually parse out the
584 NSRange location
= [currentPacket_ rangeOfString
:@
"transaction_id"];
585 if (location.location
!= NSNotFound
)
587 NSUInteger start
= location.location
+ location.length
;
588 NSUInteger end
= start
;
590 NSCharacterSet
* numericSet
= [NSCharacterSet decimalDigitCharacterSet
];
592 // Loop over the characters after the attribute name to extract the ID.
593 while (end
< [currentPacket_ length
])
595 unichar c
= [currentPacket_ characterAtIndex
:end
];
596 if ([numericSet characterIsMember
:c
])
598 // If this character is numeric, extend the range to substring.
605 // If this character is nonnumeric and we have nothing in the
606 // range, skip this character.
612 // We've moved past the numeric ID so we should stop searching.
618 // If we were able to extract the transaction ID, update the last read.
619 NSRange substringRange
= NSMakeRange(start
, end
- start
);
620 NSString
* transactionStr
= [currentPacket_ substringWithRange
:substringRange
];
621 if ([transactionStr length
])
622 lastReadTransaction_
= [transactionStr intValue
];
625 // Otherwise, assume +1 and hope it works.
626 ++lastReadTransaction_
;
627 } else /*if (!reconnect_)*/ {
628 // See if the transaction can be parsed out.
629 NSInteger transaction
= [self transactionIDFromResponse
:xmlTest
];
630 if (transaction
< lastReadTransaction_
) {
631 NSLog(@
"tx = %d vs %d", transaction
, lastReadTransaction_
);
632 NSLog(@
"out of date transaction %@", packet
);
636 if (transaction
!= lastWrittenTransaction_
)
637 NSLog(@
"txn %d <> %d last written, %d last read", transaction
, lastWrittenTransaction_
, lastReadTransaction_
);
639 lastReadTransaction_
= transaction
;
642 // Log this receive event.
643 LogEntry
* log
= [self recordReceive
:currentPacket_
];
646 // Finally, dispatch the handler for this response.
647 [self handleResponse
:[xmlTest autorelease
]];
650 - (void)handleResponse
:(NSXMLDocument
*)response
652 // Check and see if there's an error.
653 NSArray
* error
= [[response rootElement
] elementsForName
:@
"error"];
654 if ([error count
] > 0)
656 NSLog(@
"Xdebug error: %@", error
);
657 NSString
* errorMessage
= [[[[error objectAtIndex
:0] children
] objectAtIndex
:0] stringValue
];
658 [self errorEncountered
:errorMessage
];
661 if ([[[response rootElement
] name
] isEqualToString
:@
"init"]) {
663 [delegate_ performSelectorOnMainThread
:@selector(handleInitialResponse
:)
669 if ([delegate_ respondsToSelector
:@selector(handleResponse
:)])
670 [delegate_ performSelectorOnMainThread
:@selector(handleResponse
:)
674 [self sendQueuedWrites
];
678 * This performs a blocking send. This should ONLY be called when we know we
679 * have write access to the stream. We will busy wait in case we don't do a full
682 - (void)performSend
:(NSString
*)command
684 // If this is an out-of-date transaction, do not bother sending it.
685 NSInteger transaction
= [self transactionIDFromCommand
:command
];
686 if (transaction
!= NSNotFound
&& transaction
< lastWrittenTransaction_
)
691 char* string
= (char*)[command UTF8String
];
692 int stringLength
= strlen(string
);
694 // Busy wait while writing. BAADD. Should background this operation.
697 if (CFWriteStreamCanAcceptBytes(writeStream_
))
699 // Include the NULL byte in the string when we write.
700 int bytesWritten
= CFWriteStreamWrite(writeStream_
, (UInt8
*)string
, stringLength
+ 1);
701 if (bytesWritten
< 0)
703 NSLog(@
"write error");
706 else if (bytesWritten
< strlen(string
))
708 // Adjust the buffer and wait for another chance to write.
709 stringLength
-= bytesWritten
;
710 memmove(string
, string
+ bytesWritten
, stringLength
);
716 // We need to scan the string to find the transactionID.
717 if (transaction
== NSNotFound
)
719 NSLog(@
"sent %@ without a transaction ID", command
);
722 lastWrittenTransaction_
= transaction
;
727 // Log this trancation.
728 [self recordSend
:command
];
732 * Checks if there are unsent commands in the |queuedWrites_| queue and sends
733 * them if it's OK to do so. This will not block.
735 - (void)sendQueuedWrites
740 [writeQueueLock_ lock
];
741 if (lastReadTransaction_
>= lastWrittenTransaction_
&& [queuedWrites_ count
] > 0)
743 NSString
* command
= [queuedWrites_ objectAtIndex
:0];
745 // We don't want to block because this is called from the main thread.
746 // |-performSend:| busy waits when the stream is not ready. Bail out
747 // before we do that becuase busy waiting is BAD.
748 if (CFWriteStreamCanAcceptBytes(writeStream_
))
750 [self performSend
:command
];
751 [queuedWrites_ removeObjectAtIndex
:0];
754 [writeQueueLock_ unlock
];