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
;
39 // CFNetwork Callbacks /////////////////////////////////////////////////////////
41 void ReadStreamCallback(CFReadStreamRef stream
, CFStreamEventType eventType
, void* connectionRaw
)
43 DebuggerConnection
* connection
= (DebuggerConnection
*)connectionRaw
;
46 case kCFStreamEventHasBytesAvailable
:
47 [connection readStreamHasData
];
50 case kCFStreamEventErrorOccurred
:
52 CFErrorRef error
= CFReadStreamCopyError(stream
);
53 CFReadStreamUnscheduleFromRunLoop(stream
, CFRunLoopGetCurrent(), kCFRunLoopCommonModes
);
54 CFReadStreamClose(stream
);
56 [connection errorEncountered
:[[(NSError
*)error autorelease
] description
]];
60 case kCFStreamEventEndEncountered
:
61 CFReadStreamUnscheduleFromRunLoop(stream
, CFRunLoopGetCurrent(), kCFRunLoopCommonModes
);
62 CFReadStreamClose(stream
);
64 [connection socketDisconnected
];
69 void WriteStreamCallback(CFWriteStreamRef stream
, CFStreamEventType eventType
, void* connectionRaw
)
71 DebuggerConnection
* connection
= (DebuggerConnection
*)connectionRaw
;
74 case kCFStreamEventCanAcceptBytes
:
75 [connection sendQueuedWrites
];
78 case kCFStreamEventErrorOccurred
:
80 CFErrorRef error
= CFWriteStreamCopyError(stream
);
81 CFWriteStreamUnscheduleFromRunLoop(stream
, CFRunLoopGetCurrent(), kCFRunLoopCommonModes
);
82 CFWriteStreamClose(stream
);
84 [connection errorEncountered
:[[(NSError
*)error autorelease
] description
]];
88 case kCFStreamEventEndEncountered
:
89 CFWriteStreamUnscheduleFromRunLoop(stream
, CFRunLoopGetCurrent(), kCFRunLoopCommonModes
);
90 CFWriteStreamClose(stream
);
92 [connection socketDisconnected
];
97 void SocketAcceptCallback(CFSocketRef socket
,
98 CFSocketCallBackType callbackType
,
103 assert(callbackType
== kCFSocketAcceptCallBack
);
104 NSLog(@
"SocketAcceptCallback()");
106 DebuggerConnection
* connection
= (DebuggerConnection
*)connectionRaw
;
108 CFReadStreamRef readStream
;
109 CFWriteStreamRef writeStream
;
111 // Create the streams on the socket.
112 CFStreamCreatePairWithSocket(kCFAllocatorDefault
,
113 *(CFSocketNativeHandle
*)data
, // Socket handle.
114 &readStream
, // Read stream in-pointer.
115 &writeStream
); // Write stream in-pointer.
117 // Create struct to register callbacks for the stream.
118 CFStreamClientContext context
;
120 context.info
= connection
;
121 context.retain
= NULL
;
122 context.release
= NULL
;
123 context.copyDescription
= NULL
;
125 // Set the client of the read stream.
126 CFOptionFlags readFlags
=
127 kCFStreamEventOpenCompleted |
128 kCFStreamEventHasBytesAvailable |
129 kCFStreamEventErrorOccurred |
130 kCFStreamEventEndEncountered
;
131 if (CFReadStreamSetClient(readStream
, readFlags
, ReadStreamCallback
, &context
))
132 // Schedule in run loop to do asynchronous communication with the engine.
133 CFReadStreamScheduleWithRunLoop(readStream
, CFRunLoopGetCurrent(), kCFRunLoopCommonModes
);
137 // Open the stream now that it's scheduled on the run loop.
138 if (!CFReadStreamOpen(readStream
))
140 CFStreamError error
= CFReadStreamGetError(readStream
);
141 NSLog(@
"error! %@", error
);
145 // Set the client of the write stream.
146 CFOptionFlags writeFlags
=
147 kCFStreamEventOpenCompleted |
148 kCFStreamEventCanAcceptBytes |
149 kCFStreamEventErrorOccurred |
150 kCFStreamEventEndEncountered
;
151 if (CFWriteStreamSetClient(writeStream
, writeFlags
, WriteStreamCallback
, &context
))
152 // Schedule it in the run loop to receive error information.
153 CFWriteStreamScheduleWithRunLoop(writeStream
, CFRunLoopGetCurrent(), kCFRunLoopCommonModes
);
157 // Open the write stream.
158 if (!CFWriteStreamOpen(writeStream
))
160 CFStreamError error
= CFWriteStreamGetError(writeStream
);
161 NSLog(@
"error! %@", error
);
165 connection.readStream
= readStream
;
166 connection.writeStream
= writeStream
;
167 [connection socketDidAccept
];
170 ////////////////////////////////////////////////////////////////////////////////
172 @implementation DebuggerConnection
174 @synthesize socket
= socket_
;
175 @synthesize readStream
= readStream_
;
176 @synthesize lastReadTransaction
= lastReadTransaction_
;
177 @synthesize currentPacket
= currentPacket_
;
178 @synthesize writeStream
= writeStream_
;
179 @synthesize lastWrittenTransaction
= lastWrittenTransaction_
;
180 @synthesize queuedWrites
= queuedWrites_
;
184 self.currentPacket
= nil;
189 * Called by SocketWrapper after the connection is successful. This immediately calls
190 * -[SocketWrapper receive] to clear the way for communication, though the information
191 * could be useful server information that we don't use right now.
193 - (void)socketDidAccept
197 self.queuedWrites
= [NSMutableArray array
];
198 writeQueueLock_
= [NSRecursiveLock
new];
199 callTable_
= [NSMutableDictionary
new];
203 * Receives errors from the SocketWrapper and updates the display
205 - (void)errorEncountered
:(NSString
*)error
207 [delegate_ errorEncountered
:error
];
211 * Creates, connects to, and schedules a CFSocket.
215 // Pass ourselves to the callback so we don't have to use ugly globals.
216 CFSocketContext context
;
219 context.retain
= NULL
;
220 context.release
= NULL
;
221 context.copyDescription
= NULL
;
223 // Create the address structure.
224 struct sockaddr_in address
;
225 memset(&address
, 0, sizeof(address
));
226 address.sin_len
= sizeof(address
);
227 address.sin_family
= AF_INET
;
228 address.sin_port
= htons(port
);
229 address.sin_addr.s_addr
= htonl(INADDR_ANY
);
231 // Create the socket signature.
232 CFSocketSignature signature
;
233 signature.protocolFamily
= PF_INET
;
234 signature.socketType
= SOCK_STREAM
;
235 signature.protocol
= IPPROTO_TCP
;
236 signature.address
= (CFDataRef
)[NSData dataWithBytes
:&address length
:sizeof(address
)];
238 socket_
= CFSocketCreateWithSocketSignature(kCFAllocatorDefault
,
239 &signature
, // Socket signature.
240 kCFSocketAcceptCallBack
, // Callback types.
241 SocketAcceptCallback
, // Callout function pointer.
242 &context
); // Context to pass to callout.
245 [self errorEncountered
:@
"Could not open socket."];
249 // Allow old, yet-to-be recycled sockets to be reused.
251 setsockopt(CFSocketGetNative(socket_
), SOL_SOCKET
, SO_REUSEADDR
, &yes
, sizeof(BOOL));
253 // Schedule the socket on the run loop.
254 CFRunLoopSourceRef source
= CFSocketCreateRunLoopSource(kCFAllocatorDefault
, socket_
, 0);
255 CFRunLoopAddSource(CFRunLoopGetCurrent(), source
, kCFRunLoopCommonModes
);
260 * Closes a socket and releases the ref.
264 // The socket goes down, so do the streams, which clean themselves up.
265 CFSocketInvalidate(socket_
);
267 self.queuedWrites
= nil;
268 [writeQueueLock_ release
];
269 [callTable_ release
];
273 * Notification that the socket disconnected.
275 - (void)socketDisconnected
278 [delegate_ debuggerDisconnected
];
282 * Callback from the CFReadStream that there is data waiting to be read.
284 - (void)readStreamHasData
286 const NSUInteger kBufferSize
= 1024;
287 UInt8 buffer
[kBufferSize
];
288 CFIndex bufferOffset
= 0; // Starting point in |buffer| to work with.
289 CFIndex bytesRead
= CFReadStreamRead(readStream_
, buffer
, kBufferSize
);
290 const char* charBuffer
= (const char*)buffer
;
292 // The read loop works by going through the buffer until all the bytes have
294 while (bufferOffset
< bytesRead
)
296 // Find the NULL separator, or the end of the string.
297 NSUInteger partLength
= 0;
298 for (NSUInteger i
= bufferOffset
; i
< bytesRead
&& charBuffer
[i
] != '\0'; ++i
, ++partLength
) ;
300 // If there is not a current packet, set some state.
301 if (!self.currentPacket
)
303 // Read the message header: the size. This will be |partLength| bytes.
304 packetSize_
= atoi(charBuffer
+ bufferOffset
);
305 currentPacketIndex_
= 0;
306 self.currentPacket
= [NSMutableString stringWithCapacity
:packetSize_
];
307 bufferOffset
+= partLength
+ 1; // Pass over the NULL byte.
308 continue; // Spin the loop to begin reading actual data.
311 // Substring the byte stream and append it to the packet string.
312 CFStringRef bufferString
= CFStringCreateWithBytes(kCFAllocatorDefault
,
313 buffer
+ bufferOffset
, // Byte pointer, offset by start index.
314 partLength
, // Length.
315 kCFStringEncodingUTF8
,
317 [self.currentPacket appendString
:(NSString
*)bufferString
];
318 CFRelease(bufferString
);
321 currentPacketIndex_
+= partLength
;
322 bufferOffset
+= partLength
+ 1;
324 // If this read finished the packet, handle it and reset.
325 NSLog(@
"cpi %d ps %d br %d ds %d", currentPacketIndex_
, packetSize_
, bytesRead
, partLength
);
326 if (currentPacketIndex_
>= packetSize_
)
328 [self handlePacket
:[[currentPacket_ retain
] autorelease
]];
329 self.currentPacket
= nil;
331 currentPacketIndex_
= 0;
337 * Performs the packet handling of a raw string XML packet. From this point on,
338 * the packets are associated with a transaction and are then dispatched.
340 - (void)handlePacket
:(NSString
*)packet
342 // Test if we can convert it into an NSXMLDocument.
343 NSError
* error
= nil;
344 NSXMLDocument
* xmlTest
= [[NSXMLDocument alloc
] initWithXMLString
:currentPacket_ options
:NSXMLDocumentTidyXML error
:&error
];
346 // Try to recover if we encountered an error.
349 // We do not want to starve the write queue, so manually parse out the
351 NSRange location
= [currentPacket_ rangeOfString
:@
"transaction_id"];
352 if (location.location
!= NSNotFound
)
354 NSUInteger start
= location.location
+ location.length
;
355 NSUInteger end
= start
;
357 NSCharacterSet
* numericSet
= [NSCharacterSet decimalDigitCharacterSet
];
359 // Loop over the characters after the attribute name to extract the ID.
360 while (end
< [currentPacket_ length
])
362 unichar c
= [currentPacket_ characterAtIndex
:end
];
363 if ([numericSet characterIsMember
:c
])
365 // If this character is numeric, extend the range to substring.
372 // If this character is nonnumeric and we have nothing in the
373 // range, skip this character.
379 // We've moved past the numeric ID so we should stop searching.
385 // If we were able to extract the transaction ID, update the last read.
386 NSRange substringRange
= NSMakeRange(start
, end
- start
);
387 NSString
* transactionStr
= [currentPacket_ substringWithRange
:substringRange
];
388 if ([transactionStr length
])
389 lastReadTransaction_
= [transactionStr intValue
];
392 // Otherwise, assume +1 and hope it works.
393 ++lastReadTransaction_
;
397 // See if the transaction can be parsed out.
398 NSInteger transaction
= [self transactionIDFromResponse
:xmlTest
];
399 if (transaction
< lastReadTransaction_
)
401 NSLog(@
"tx = %d vs %d", transaction
, lastReadTransaction_
);
402 NSLog(@
"out of date transaction %@", packet
);
406 if (transaction
!= lastWrittenTransaction_
)
407 NSLog(@
"txn %d <> %d last written, %d last read", transaction
, lastWrittenTransaction_
, lastReadTransaction_
);
409 lastReadTransaction_
= transaction
;
412 // Log this receive event.
413 LoggingController
* logger
= [(AppDelegate
*)[NSApp delegate
] loggingController
];
414 LogEntry
* log
= [logger recordReceive
:currentPacket_
];
416 log.lastWrittenTransactionID
= lastWrittenTransaction_
;
417 log.lastReadTransactionID
= lastReadTransaction_
;
419 // Finally, dispatch the handler for this response.
420 [self handleResponse
:[xmlTest autorelease
]];
424 * Writes a command into the write stream. If the stream is ready for writing,
425 * we do so immediately. If not, the command is queued and will be written
426 * when the stream is ready.
428 - (void)send
:(NSString
*)command
430 if (lastReadTransaction_
>= lastWrittenTransaction_
&& CFWriteStreamCanAcceptBytes(writeStream_
))
431 [self performSend
:command
];
433 [queuedWrites_ addObject
:command
];
434 [self sendQueuedWrites
];
438 * This performs a blocking send. This should ONLY be called when we know we
439 * have write access to the stream. We will busy wait in case we don't do a full
442 - (void)performSend
:(NSString
*)command
444 // If this is an out-of-date transaction, do not bother sending it.
445 NSInteger transaction
= [self transactionIDFromCommand
:command
];
446 if (transaction
!= NSNotFound
&& transaction
< lastWrittenTransaction_
)
451 char* string
= (char*)[command UTF8String
];
452 int stringLength
= strlen(string
);
454 // Busy wait while writing. BAADD. Should background this operation.
457 if (CFWriteStreamCanAcceptBytes(writeStream_
))
459 // Include the NULL byte in the string when we write.
460 int bytesWritten
= CFWriteStreamWrite(writeStream_
, (UInt8
*)string
, stringLength
+ 1);
461 if (bytesWritten
< 0)
463 NSLog(@
"write error");
466 else if (bytesWritten
< strlen(string
))
468 // Adjust the buffer and wait for another chance to write.
469 stringLength
-= bytesWritten
;
470 memmove(string
, string
+ bytesWritten
, stringLength
);
476 // We need to scan the string to find the transactionID.
477 if (transaction
== NSNotFound
)
479 NSLog(@
"sent %@ without a transaction ID", command
);
482 lastWrittenTransaction_
= transaction
;
487 // Log this trancation.
488 LoggingController
* logger
= [(AppDelegate
*)[NSApp delegate
] loggingController
];
489 LogEntry
* log
= [logger recordSend
:command
];
490 log.lastWrittenTransactionID
= lastWrittenTransaction_
;
491 log.lastReadTransactionID
= lastReadTransaction_
;
494 - (void)handleResponse
:(NSXMLDocument
*)response
496 // Check and see if there's an error.
497 NSArray
* error
= [[response rootElement
] elementsForName
:@
"error"];
498 if ([error count
] > 0)
500 NSLog(@
"Xdebug error: %@", error
);
501 [delegate_ errorEncountered
:[[[[error objectAtIndex
:0] children
] objectAtIndex
:0] stringValue
]];
504 if ([[[response rootElement
] name
] isEqualToString
:@
"init"])
506 [self initReceived
:response
];
510 NSString
* callbackStr
= [callTable_ objectForKey
:[NSNumber numberWithInt
:lastReadTransaction_
]];
513 SEL callback
= NSSelectorFromString(callbackStr
);
514 [self performSelector
:callback withObject
:response
];
517 [self sendQueuedWrites
];
523 * This will send a command to the debugger engine. It will append the
524 * transaction ID automatically. It accepts a NSString command along with a
525 * a variable number of arguments to substitute into the command, a la
526 * +[NSString stringWithFormat:]. Returns the transaction ID as a NSNumber.
528 - (NSNumber
*)sendCommandWithCallback
:(SEL)callback format
:(NSString
*)format
, ...
530 // Collect varargs and format command.
532 va_start(args
, format
);
533 NSString
* command
= [[NSString alloc
] initWithFormat
:format arguments
:args
];
536 NSNumber
* callbackKey
= [NSNumber numberWithInt
:transactionID
++];
538 [callTable_ setObject
:NSStringFromSelector(callback
) forKey
:callbackKey
];
540 [self send
:[NSString stringWithFormat
:@
"%@ -i %@", [command autorelease
], callbackKey
]];
546 * Checks if there are unsent commands in the |queuedWrites_| queue and sends
547 * them if it's OK to do so. This will not block.
549 - (void)sendQueuedWrites
554 [writeQueueLock_ lock
];
555 if (lastReadTransaction_
>= lastWrittenTransaction_
&& [queuedWrites_ count
] > 0)
557 NSString
* command
= [queuedWrites_ objectAtIndex
:0];
559 // We don't want to block because this is called from the main thread.
560 // |-performSend:| busy waits when the stream is not ready. Bail out
561 // before we do that becuase busy waiting is BAD.
562 if (CFWriteStreamCanAcceptBytes(writeStream_
))
564 [self performSend
:command
];
565 [queuedWrites_ removeObjectAtIndex
:0];
568 [writeQueueLock_ unlock
];
573 * Returns the transaction_id from an NSXMLDocument.
575 - (NSInteger
)transactionIDFromResponse
:(NSXMLDocument
*)response
577 return [[[[response rootElement
] attributeForName
:@
"transaction_id"] stringValue
] intValue
];
581 * Scans a command string for the transaction ID component. If it is not found,
582 * returns NSNotFound.
584 - (NSInteger
)transactionIDFromCommand
:(NSString
*)command
586 NSRange occurrence
= [command rangeOfString
:@
"-i "];
587 if (occurrence.location
== NSNotFound
)
589 NSString
* transaction
= [command substringFromIndex
:occurrence.location
+ occurrence.length
];
590 return [transaction intValue
];