3 * Copyright (c) 2007 - 2011, 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 "NetworkConnection.h"
18 #import "NetworkConnectionPrivate.h"
20 #import "AppDelegate.h"
21 #import "LoggingController.h"
22 #include "NetworkCallbackController.h"
24 // Other Run Loop Callbacks ////////////////////////////////////////////////////
26 void PerformQuitSignal(void* info)
28 NetworkConnection* obj = (NetworkConnection*)info;
29 [obj performQuitSignal];
32 ////////////////////////////////////////////////////////////////////////////////
34 @implementation NetworkConnection
36 @synthesize port = port_;
37 @synthesize connected = connected_;
38 @synthesize delegate = delegate_;
39 @synthesize lastReadTransaction = lastReadTransaction_;
40 @synthesize currentPacket = currentPacket_;
41 @synthesize lastWrittenTransaction = lastWrittenTransaction_;
42 @synthesize queuedWrites = queuedWrites_;
44 - (id)initWithPort:(NSUInteger)aPort
46 if (self = [super init]) {
54 self.currentPacket = nil;
59 * Kicks off the socket on another thread.
63 if (thread_ && !connected_) {
64 // A thread has been detached but the socket has yet to connect. Do not
65 // spawn a new thread otherwise multiple threads will be blocked on the same
69 [NSThread detachNewThreadSelector:@selector(runNetworkThread) toTarget:self withObject:nil];
73 * Creates, connects to, and schedules a CFSocket.
75 - (void)runNetworkThread
77 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
79 thread_ = [NSThread currentThread];
80 runLoop_ = [NSRunLoop currentRunLoop];
81 callbackController_ = new NetworkCallbackController(self);
83 // Create a source that is used to quit.
84 CFRunLoopSourceContext quitContext = { 0 };
85 quitContext.info = self;
86 quitContext.perform = PerformQuitSignal;
87 quitSource_ = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &quitContext);
88 CFRunLoopAddSource([runLoop_ getCFRunLoop], quitSource_, kCFRunLoopCommonModes);
90 callbackController_->OpenConnection(port_);
96 delete callbackController_;
97 callbackController_ = NULL;
99 CFRunLoopSourceInvalidate(quitSource_);
100 CFRelease(quitSource_);
107 * Called by SocketWrapper after the connection is successful. This immediately calls
108 * -[SocketWrapper receive] to clear the way for communication, though the information
109 * could be useful server information that we don't use right now.
111 - (void)socketDidAccept
115 lastReadTransaction_ = 0;
116 lastWrittenTransaction_ = 0;
117 self.queuedWrites = [NSMutableArray array];
118 writeQueueLock_ = [NSRecursiveLock new];
119 if ([delegate_ respondsToSelector:@selector(connectionDidAccept:)])
120 [delegate_ performSelectorOnMainThread:@selector(connectionDidAccept:)
126 * Closes a socket and releases the ref.
133 if (runLoop_ && quitSource_) {
134 CFRunLoopSourceSignal(quitSource_);
135 CFRunLoopWakeUp([runLoop_ getCFRunLoop]);
140 * Quits the run loop and stops the thread.
142 - (void)performQuitSignal
144 self.queuedWrites = nil;
146 [writeQueueLock_ release];
149 CFRunLoopStop([runLoop_ getCFRunLoop]);
152 callbackController_->CloseConnection();
156 * Notification that the socket disconnected.
158 - (void)socketDisconnected
160 if ([delegate_ respondsToSelector:@selector(connectionDidClose:)])
161 [delegate_ connectionDidClose:self];
165 * Writes a command into the write stream. If the stream is ready for writing,
166 * we do so immediately. If not, the command is queued and will be written
167 * when the stream is ready.
169 - (void)send:(NSString*)command
171 if (lastReadTransaction_ >= lastWrittenTransaction_ && callbackController_->WriteStreamCanAcceptBytes()) {
172 [self performSend:command];
174 [writeQueueLock_ lock];
175 [queuedWrites_ addObject:command];
176 [writeQueueLock_ unlock];
178 [self sendQueuedWrites];
182 * This will send a command to the debugger engine. It will append the
183 * transaction ID automatically. It accepts a NSString command along with a
184 * a variable number of arguments to substitute into the command, a la
185 * +[NSString stringWithFormat:]. Returns the transaction ID as a NSNumber.
187 - (NSNumber*)sendCommandWithFormat:(NSString*)format, ...
189 // Collect varargs and format command.
191 va_start(args, format);
192 NSString* command = [[NSString alloc] initWithFormat:format arguments:args];
195 NSNumber* callbackKey = [NSNumber numberWithInt:transactionID++];
196 NSString* taggedCommand = [NSString stringWithFormat:@"%@ -i %@", [command autorelease], callbackKey];
197 [self performSelector:@selector(send:)
199 withObject:taggedCommand
200 waitUntilDone:connected_];
206 * Certain commands expect encoded data to be the the last, unnamed parameter
207 * of the command. In these cases, inserting the transaction ID at the end is
208 * incorrect, so clients use this method to have |{txn}| replaced with the
211 - (NSNumber*)sendCustomCommandWithFormat:(NSString*)format, ...
213 // Collect varargs and format command.
215 va_start(args, format);
216 NSString* command = [[[NSString alloc] initWithFormat:format arguments:args] autorelease];
219 NSNumber* callbackKey = [NSNumber numberWithInt:transactionID++];
220 NSString* taggedCommand = [command stringByReplacingOccurrencesOfString:@"{txn}"
221 withString:[callbackKey stringValue]];
222 [self performSelector:@selector(send:)
224 withObject:taggedCommand
225 waitUntilDone:connected_];
231 * Given a file path, this returns a file:// URI and escapes any spaces for the
234 - (NSString*)escapedURIPath:(NSString*)path
236 // Custon GDBp paths are fine.
237 if ([[path substringToIndex:4] isEqualToString:@"gdbp"])
240 // Create a temporary URL that will escape all the nasty characters.
241 NSURL* url = [NSURL fileURLWithPath:path];
242 NSString* urlString = [url absoluteString];
244 // Remove the host because this is a file:// URL;
245 urlString = [urlString stringByReplacingOccurrencesOfString:[url host] withString:@""];
247 // Escape % for use in printf-style NSString formatters.
248 urlString = [urlString stringByReplacingOccurrencesOfString:@"%" withString:@"%%"];
253 * Returns the transaction_id from an NSXMLDocument.
255 - (NSInteger)transactionIDFromResponse:(NSXMLDocument*)response
257 return [[[[response rootElement] attributeForName:@"transaction_id"] stringValue] intValue];
261 * Scans a command string for the transaction ID component. If it is not found,
262 * returns NSNotFound.
264 - (NSInteger)transactionIDFromCommand:(NSString*)command
266 NSRange occurrence = [command rangeOfString:@"-i "];
267 if (occurrence.location == NSNotFound)
269 NSString* transaction = [command substringFromIndex:occurrence.location + occurrence.length];
270 return [transaction intValue];
273 // Private /////////////////////////////////////////////////////////////////////
276 // Delegate Thread-Safe Wrappers ///////////////////////////////////////////////
279 * Receives errors from the SocketWrapper and updates the display
281 - (void)errorEncountered:(NSString*)error
283 if (![delegate_ respondsToSelector:@selector(errorEncountered:)])
285 [delegate_ performSelectorOnMainThread:@selector(errorEncountered:)
290 - (LogEntry*)recordSend:(NSString*)command
292 LoggingController* logger = [[AppDelegate instance] loggingController];
293 LogEntry* entry = [LogEntry newSendEntry:command];
294 entry.lastReadTransactionID = lastReadTransaction_;
295 entry.lastWrittenTransactionID = lastWrittenTransaction_;
296 [logger performSelectorOnMainThread:@selector(recordEntry:)
299 return [entry autorelease];
302 - (LogEntry*)recordReceive:(NSString*)command
304 LoggingController* logger = [[AppDelegate instance] loggingController];
305 LogEntry* entry = [LogEntry newReceiveEntry:command];
306 entry.lastReadTransactionID = lastReadTransaction_;
307 entry.lastWrittenTransactionID = lastWrittenTransaction_;
308 [logger performSelectorOnMainThread:@selector(recordEntry:)
311 return [entry autorelease];
314 // Stream Managers /////////////////////////////////////////////////////////////
317 * Callback from the CFReadStream that there is data waiting to be read.
319 - (void)readStreamHasData:(CFReadStreamRef)stream
321 const NSUInteger kBufferSize = 1024;
322 UInt8 buffer[kBufferSize];
323 CFIndex bufferOffset = 0; // Starting point in |buffer| to work with.
324 CFIndex bytesRead = CFReadStreamRead(stream, buffer, kBufferSize);
325 const char* charBuffer = (const char*)buffer;
327 // The read loop works by going through the buffer until all the bytes have
329 while (bufferOffset < bytesRead) {
330 // Find the NULL separator, or the end of the string.
331 NSUInteger partLength = 0;
332 for (CFIndex i = bufferOffset; i < bytesRead && charBuffer[i] != '\0'; ++i, ++partLength) ;
334 // If there is not a current packet, set some state.
335 if (!self.currentPacket) {
336 // Read the message header: the size. This will be |partLength| bytes.
337 packetSize_ = atoi(charBuffer + bufferOffset);
338 currentPacketIndex_ = 0;
339 self.currentPacket = [NSMutableString stringWithCapacity:packetSize_];
340 bufferOffset += partLength + 1; // Pass over the NULL byte.
341 continue; // Spin the loop to begin reading actual data.
344 // Substring the byte stream and append it to the packet string.
345 CFStringRef bufferString = CFStringCreateWithBytes(kCFAllocatorDefault,
346 buffer + bufferOffset, // Byte pointer, offset by start index.
347 partLength, // Length.
348 kCFStringEncodingUTF8,
350 [self.currentPacket appendString:(NSString*)bufferString];
351 CFRelease(bufferString);
354 currentPacketIndex_ += partLength;
355 bufferOffset += partLength + 1;
357 // If this read finished the packet, handle it and reset.
358 if (currentPacketIndex_ >= packetSize_) {
359 [self handlePacket:[[currentPacket_ retain] autorelease]];
360 self.currentPacket = nil;
362 currentPacketIndex_ = 0;
368 * Performs the packet handling of a raw string XML packet. From this point on,
369 * the packets are associated with a transaction and are then dispatched.
371 - (void)handlePacket:(NSString*)packet
373 // Test if we can convert it into an NSXMLDocument.
374 NSError* error = nil;
375 NSXMLDocument* xml = [[NSXMLDocument alloc] initWithXMLString:currentPacket_
376 options:NSXMLDocumentTidyXML
378 // TODO: Remove this assert before stable release. Flush out any possible
379 // issues during testing.
382 // Validate the transaction.
383 NSInteger transaction = [self transactionIDFromResponse:xml];
384 if (transaction < lastReadTransaction_) {
385 NSLog(@"Transaction #%d is out of date (lastRead = %d). Dropping packet: %@",
386 transaction, lastReadTransaction_, packet);
389 if (transaction != lastWrittenTransaction_) {
390 NSLog(@"Transaction #%d received out of order. lastRead = %d, lastWritten = %d. Continuing.",
391 transaction, lastReadTransaction_, lastWrittenTransaction_);
394 lastReadTransaction_ = transaction;
396 // Log this receive event.
397 LogEntry* log = [self recordReceive:currentPacket_];
400 // Finally, dispatch the handler for this response.
401 [self handleResponse:[xml autorelease]];
404 - (void)handleResponse:(NSXMLDocument*)response
406 // Check and see if there's an error.
407 NSArray* error = [[response rootElement] elementsForName:@"error"];
408 if ([error count] > 0) {
409 NSLog(@"Xdebug error: %@", error);
410 NSString* errorMessage = [[[[error objectAtIndex:0] children] objectAtIndex:0] stringValue];
411 [self errorEncountered:errorMessage];
414 if ([[[response rootElement] name] isEqualToString:@"init"]) {
416 [delegate_ performSelectorOnMainThread:@selector(handleInitialResponse:)
422 if ([delegate_ respondsToSelector:@selector(handleResponse:)])
423 [delegate_ performSelectorOnMainThread:@selector(handleResponse:)
427 [self sendQueuedWrites];
431 * This performs a blocking send. This should ONLY be called when we know we
432 * have write access to the stream. We will busy wait in case we don't do a full
435 - (void)performSend:(NSString*)command
437 // If this is an out-of-date transaction, do not bother sending it.
438 NSInteger transaction = [self transactionIDFromCommand:command];
439 if (transaction != NSNotFound && transaction < lastWrittenTransaction_)
442 if (callbackController_->WriteString(command)) {
443 // We need to scan the string to find the transactionID.
444 if (transaction == NSNotFound) {
445 NSLog(@"sent %@ without a transaction ID", command);
447 lastWrittenTransaction_ = transaction;
450 // Log this trancation.
451 [self recordSend:command];
455 * Checks if there are unsent commands in the |queuedWrites_| queue and sends
456 * them if it's OK to do so. This will not block.
458 - (void)sendQueuedWrites
463 [writeQueueLock_ lock];
464 if (lastReadTransaction_ >= lastWrittenTransaction_ && [queuedWrites_ count] > 0) {
465 NSString* command = [queuedWrites_ objectAtIndex:0];
467 // We don't want to block because this is called from the main thread.
468 // |-performSend:| busy waits when the stream is not ready. Bail out
469 // before we do that becuase busy waiting is BAD.
470 if (callbackController_->WriteStreamCanAcceptBytes()) {
471 [self performSend:command];
472 [queuedWrites_ removeObjectAtIndex:0];
475 [writeQueueLock_ unlock];