Split out the C++ class into its own file. Also move NetworkConnection's private...
[macgdbp.git] / Source / NetworkConnection.mm
1 /*
2 * MacGDBp
3 * Copyright (c) 2007 - 2010, 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 "NetworkConnection.h"
18 #import "NetworkConnectionPrivate.h"
19
20 #import <sys/socket.h>
21 #import <netinet/in.h>
22
23 #import "AppDelegate.h"
24 #import "LoggingController.h"
25 #include "NetworkCallbackController.h"
26
27 // Other Run Loop Callbacks ////////////////////////////////////////////////////
28
29 void PerformQuitSignal(void* info)
30 {
31 NetworkConnection* obj = (NetworkConnection*)info;
32 [obj performQuitSignal];
33 }
34
35 ////////////////////////////////////////////////////////////////////////////////
36
37 @implementation NetworkConnection
38
39 @synthesize port = port_;
40 @synthesize connected = connected_;
41 @synthesize delegate = delegate_;
42
43 @synthesize socket = socket_;
44 @synthesize readStream = readStream_;
45 @synthesize lastReadTransaction = lastReadTransaction_;
46 @synthesize currentPacket = currentPacket_;
47 @synthesize writeStream = writeStream_;
48 @synthesize lastWrittenTransaction = lastWrittenTransaction_;
49 @synthesize queuedWrites = queuedWrites_;
50
51 - (id)initWithPort:(NSUInteger)aPort
52 {
53 if (self = [super init])
54 {
55 port_ = aPort;
56 }
57 return self;
58 }
59
60 - (void)dealloc
61 {
62 self.currentPacket = nil;
63 [super dealloc];
64 }
65
66 /**
67 * Kicks off the socket on another thread.
68 */
69 - (void)connect
70 {
71 if (thread_ && !connected_) {
72 // A thread has been detached but the socket has yet to connect. Do not
73 // spawn a new thread otherwise multiple threads will be blocked on the same
74 // socket.
75 return;
76 }
77 [NSThread detachNewThreadSelector:@selector(runNetworkThread) toTarget:self withObject:nil];
78 }
79
80 /**
81 * Creates, connects to, and schedules a CFSocket.
82 */
83 - (void)runNetworkThread
84 {
85 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
86
87 thread_ = [NSThread currentThread];
88 runLoop_ = [NSRunLoop currentRunLoop];
89 callbackController_ = new NetworkCallbackController(self);
90
91 // Pass ourselves to the callback so we don't have to use ugly globals.
92 CFSocketContext context = { 0 };
93 context.info = callbackController_;
94
95 // Create the address structure.
96 struct sockaddr_in address;
97 memset(&address, 0, sizeof(address));
98 address.sin_len = sizeof(address);
99 address.sin_family = AF_INET;
100 address.sin_port = htons(port_);
101 address.sin_addr.s_addr = htonl(INADDR_ANY);
102
103 // Create the socket signature.
104 CFSocketSignature signature;
105 signature.protocolFamily = PF_INET;
106 signature.socketType = SOCK_STREAM;
107 signature.protocol = IPPROTO_TCP;
108 signature.address = (CFDataRef)[NSData dataWithBytes:&address length:sizeof(address)];
109
110 do {
111 socket_ = CFSocketCreateWithSocketSignature(kCFAllocatorDefault,
112 &signature, // Socket signature.
113 kCFSocketAcceptCallBack, // Callback types.
114 &NetworkCallbackController::SocketAcceptCallback, // Callout function pointer.
115 &context); // Context to pass to callout.
116 if (!socket_) {
117 [self errorEncountered:@"Could not open socket."];
118 sleep(1);
119 }
120 } while (!socket_);
121
122 // Allow old, yet-to-be recycled sockets to be reused.
123 BOOL yes = YES;
124 setsockopt(CFSocketGetNative(socket_), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(BOOL));
125 setsockopt(CFSocketGetNative(socket_), SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(BOOL));
126
127 // Schedule the socket on the run loop.
128 CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket_, 0);
129 CFRunLoopAddSource([runLoop_ getCFRunLoop], source, kCFRunLoopCommonModes);
130 CFRelease(source);
131
132 // Create a source that is used to quit.
133 CFRunLoopSourceContext quitContext = { 0 };
134 quitContext.info = self;
135 quitContext.perform = PerformQuitSignal;
136 quitSource_ = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &quitContext);
137 CFRunLoopAddSource([runLoop_ getCFRunLoop], quitSource_, kCFRunLoopCommonModes);
138
139 CFRunLoopRun();
140
141 thread_ = nil;
142 runLoop_ = nil;
143 delete callbackController_;
144 callbackController_ = NULL;
145
146 CFRunLoopSourceInvalidate(quitSource_);
147 CFRelease(quitSource_);
148 quitSource_ = NULL;
149
150 [pool release];
151 }
152
153 /**
154 * Called by SocketWrapper after the connection is successful. This immediately calls
155 * -[SocketWrapper receive] to clear the way for communication, though the information
156 * could be useful server information that we don't use right now.
157 */
158 - (void)socketDidAccept
159 {
160 connected_ = YES;
161 transactionID = 1;
162 lastReadTransaction_ = 0;
163 lastWrittenTransaction_ = 0;
164 self.queuedWrites = [NSMutableArray array];
165 writeQueueLock_ = [NSRecursiveLock new];
166 if ([delegate_ respondsToSelector:@selector(connectionDidAccept:)])
167 [delegate_ performSelectorOnMainThread:@selector(connectionDidAccept:)
168 withObject:self
169 waitUntilDone:NO];
170 }
171
172 /**
173 * Closes a socket and releases the ref.
174 */
175 - (void)close
176 {
177 if (thread_) {
178 [thread_ cancel];
179 }
180 if (runLoop_ && quitSource_) {
181 CFRunLoopSourceSignal(quitSource_);
182 CFRunLoopWakeUp([runLoop_ getCFRunLoop]);
183 }
184 }
185
186 /**
187 * Quits the run loop and stops the thread.
188 */
189 - (void)performQuitSignal
190 {
191 self.queuedWrites = nil;
192 connected_ = NO;
193 [writeQueueLock_ release];
194
195 if (runLoop_) {
196 CFRunLoopStop([runLoop_ getCFRunLoop]);
197 }
198
199 // The socket goes down, so do the streams, which clean themselves up.
200 if (socket_) {
201 NSLog(@"invalidating socket");
202 CFSocketInvalidate(socket_);
203 CFRelease(socket_);
204 socket_ = NULL;
205 }
206 }
207
208 /**
209 * Notification that the socket disconnected.
210 */
211 - (void)socketDisconnected
212 {
213 if (connected_) {
214 // The state still is connected, which means that we did not get here
215 // through normal disconnected procedure (a call to |-close|, followed by
216 // the downing of the socket and the stream, which also produces this
217 // messsage). Instead, the stream callbacks encountered EOF unexpectedly.
218 //[self close];
219 }
220 if ([delegate_ respondsToSelector:@selector(connectionDidClose:)])
221 [delegate_ connectionDidClose:self];
222 }
223
224 /**
225 * Writes a command into the write stream. If the stream is ready for writing,
226 * we do so immediately. If not, the command is queued and will be written
227 * when the stream is ready.
228 */
229 - (void)send:(NSString*)command
230 {
231 if (lastReadTransaction_ >= lastWrittenTransaction_ && CFWriteStreamCanAcceptBytes(writeStream_)) {
232 [self performSend:command];
233 } else {
234 [writeQueueLock_ lock];
235 [queuedWrites_ addObject:command];
236 [writeQueueLock_ unlock];
237 }
238 [self sendQueuedWrites];
239 }
240
241 /**
242 * This will send a command to the debugger engine. It will append the
243 * transaction ID automatically. It accepts a NSString command along with a
244 * a variable number of arguments to substitute into the command, a la
245 * +[NSString stringWithFormat:]. Returns the transaction ID as a NSNumber.
246 */
247 - (NSNumber*)sendCommandWithFormat:(NSString*)format, ...
248 {
249 // Collect varargs and format command.
250 va_list args;
251 va_start(args, format);
252 NSString* command = [[NSString alloc] initWithFormat:format arguments:args];
253 va_end(args);
254
255 NSNumber* callbackKey = [NSNumber numberWithInt:transactionID++];
256 NSString* taggedCommand = [NSString stringWithFormat:@"%@ -i %@", [command autorelease], callbackKey];
257 [self performSelector:@selector(send:)
258 onThread:thread_
259 withObject:taggedCommand
260 waitUntilDone:connected_];
261
262 return callbackKey;
263 }
264
265 /**
266 * Given a file path, this returns a file:// URI and escapes any spaces for the
267 * debugger engine.
268 */
269 - (NSString*)escapedURIPath:(NSString*)path
270 {
271 // Custon GDBp paths are fine.
272 if ([[path substringToIndex:4] isEqualToString:@"gdbp"])
273 return path;
274
275 // Create a temporary URL that will escape all the nasty characters.
276 NSURL* url = [NSURL fileURLWithPath:path];
277 NSString* urlString = [url absoluteString];
278
279 // Remove the host because this is a file:// URL;
280 urlString = [urlString stringByReplacingOccurrencesOfString:[url host] withString:@""];
281
282 // Escape % for use in printf-style NSString formatters.
283 urlString = [urlString stringByReplacingOccurrencesOfString:@"%" withString:@"%%"];
284 return urlString;
285 }
286
287 /**
288 * Returns the transaction_id from an NSXMLDocument.
289 */
290 - (NSInteger)transactionIDFromResponse:(NSXMLDocument*)response
291 {
292 return [[[[response rootElement] attributeForName:@"transaction_id"] stringValue] intValue];
293 }
294
295 /**
296 * Scans a command string for the transaction ID component. If it is not found,
297 * returns NSNotFound.
298 */
299 - (NSInteger)transactionIDFromCommand:(NSString*)command
300 {
301 NSRange occurrence = [command rangeOfString:@"-i "];
302 if (occurrence.location == NSNotFound)
303 return NSNotFound;
304 NSString* transaction = [command substringFromIndex:occurrence.location + occurrence.length];
305 return [transaction intValue];
306 }
307
308 // Private /////////////////////////////////////////////////////////////////////
309 #pragma mark Private
310
311 // Delegate Thread-Safe Wrappers ///////////////////////////////////////////////
312
313 /**
314 * Receives errors from the SocketWrapper and updates the display
315 */
316 - (void)errorEncountered:(NSString*)error
317 {
318 if (![delegate_ respondsToSelector:@selector(errorEncountered:)])
319 return;
320 [delegate_ performSelectorOnMainThread:@selector(errorEncountered:)
321 withObject:error
322 waitUntilDone:NO];
323 }
324
325 - (LogEntry*)recordSend:(NSString*)command
326 {
327 LoggingController* logger = [[AppDelegate instance] loggingController];
328 LogEntry* entry = [LogEntry newSendEntry:command];
329 entry.lastReadTransactionID = lastReadTransaction_;
330 entry.lastWrittenTransactionID = lastWrittenTransaction_;
331 [logger performSelectorOnMainThread:@selector(recordEntry:)
332 withObject:entry
333 waitUntilDone:NO];
334 return [entry autorelease];
335 }
336
337 - (LogEntry*)recordReceive:(NSString*)command
338 {
339 LoggingController* logger = [[AppDelegate instance] loggingController];
340 LogEntry* entry = [LogEntry newReceiveEntry:command];
341 entry.lastReadTransactionID = lastReadTransaction_;
342 entry.lastWrittenTransactionID = lastWrittenTransaction_;
343 [logger performSelectorOnMainThread:@selector(recordEntry:)
344 withObject:entry
345 waitUntilDone:NO];
346 return [entry autorelease];
347 }
348
349 // Stream Managers /////////////////////////////////////////////////////////////
350
351 /**
352 * Callback from the CFReadStream that there is data waiting to be read.
353 */
354 - (void)readStreamHasData
355 {
356 const NSUInteger kBufferSize = 1024;
357 UInt8 buffer[kBufferSize];
358 CFIndex bufferOffset = 0; // Starting point in |buffer| to work with.
359 CFIndex bytesRead = CFReadStreamRead(readStream_, buffer, kBufferSize);
360 const char* charBuffer = (const char*)buffer;
361
362 // The read loop works by going through the buffer until all the bytes have
363 // been processed.
364 while (bufferOffset < bytesRead)
365 {
366 // Find the NULL separator, or the end of the string.
367 NSUInteger partLength = 0;
368 for (NSUInteger i = bufferOffset; i < bytesRead && charBuffer[i] != '\0'; ++i, ++partLength) ;
369
370 // If there is not a current packet, set some state.
371 if (!self.currentPacket)
372 {
373 // Read the message header: the size. This will be |partLength| bytes.
374 packetSize_ = atoi(charBuffer + bufferOffset);
375 currentPacketIndex_ = 0;
376 self.currentPacket = [NSMutableString stringWithCapacity:packetSize_];
377 bufferOffset += partLength + 1; // Pass over the NULL byte.
378 continue; // Spin the loop to begin reading actual data.
379 }
380
381 // Substring the byte stream and append it to the packet string.
382 CFStringRef bufferString = CFStringCreateWithBytes(kCFAllocatorDefault,
383 buffer + bufferOffset, // Byte pointer, offset by start index.
384 partLength, // Length.
385 kCFStringEncodingUTF8,
386 true);
387 [self.currentPacket appendString:(NSString*)bufferString];
388 CFRelease(bufferString);
389
390 // Advance counters.
391 currentPacketIndex_ += partLength;
392 bufferOffset += partLength + 1;
393
394 // If this read finished the packet, handle it and reset.
395 if (currentPacketIndex_ >= packetSize_)
396 {
397 [self handlePacket:[[currentPacket_ retain] autorelease]];
398 self.currentPacket = nil;
399 packetSize_ = 0;
400 currentPacketIndex_ = 0;
401 }
402 }
403 }
404
405 /**
406 * Performs the packet handling of a raw string XML packet. From this point on,
407 * the packets are associated with a transaction and are then dispatched.
408 */
409 - (void)handlePacket:(NSString*)packet
410 {
411 // Test if we can convert it into an NSXMLDocument.
412 NSError* error = nil;
413 NSXMLDocument* xmlTest = [[NSXMLDocument alloc] initWithXMLString:currentPacket_ options:NSXMLDocumentTidyXML error:&error];
414
415 // Try to recover if we encountered an error.
416 if (!xmlTest)
417 {
418 // We do not want to starve the write queue, so manually parse out the
419 // transaction ID.
420 NSRange location = [currentPacket_ rangeOfString:@"transaction_id"];
421 if (location.location != NSNotFound)
422 {
423 NSUInteger start = location.location + location.length;
424 NSUInteger end = start;
425
426 NSCharacterSet* numericSet = [NSCharacterSet decimalDigitCharacterSet];
427
428 // Loop over the characters after the attribute name to extract the ID.
429 while (end < [currentPacket_ length])
430 {
431 unichar c = [currentPacket_ characterAtIndex:end];
432 if ([numericSet characterIsMember:c])
433 {
434 // If this character is numeric, extend the range to substring.
435 ++end;
436 }
437 else
438 {
439 if (start == end)
440 {
441 // If this character is nonnumeric and we have nothing in the
442 // range, skip this character.
443 ++start;
444 ++end;
445 }
446 else
447 {
448 // We've moved past the numeric ID so we should stop searching.
449 break;
450 }
451 }
452 }
453
454 // If we were able to extract the transaction ID, update the last read.
455 NSRange substringRange = NSMakeRange(start, end - start);
456 NSString* transactionStr = [currentPacket_ substringWithRange:substringRange];
457 if ([transactionStr length])
458 lastReadTransaction_ = [transactionStr intValue];
459 }
460
461 // Otherwise, assume +1 and hope it works.
462 ++lastReadTransaction_;
463 } else /*if (!reconnect_)*/ {
464 // See if the transaction can be parsed out.
465 NSInteger transaction = [self transactionIDFromResponse:xmlTest];
466 if (transaction < lastReadTransaction_) {
467 NSLog(@"tx = %d vs %d", transaction, lastReadTransaction_);
468 NSLog(@"out of date transaction %@", packet);
469 return;
470 }
471
472 if (transaction != lastWrittenTransaction_)
473 NSLog(@"txn %d <> %d last written, %d last read", transaction, lastWrittenTransaction_, lastReadTransaction_);
474
475 lastReadTransaction_ = transaction;
476 }
477
478 // Log this receive event.
479 LogEntry* log = [self recordReceive:currentPacket_];
480 log.error = error;
481
482 // Finally, dispatch the handler for this response.
483 [self handleResponse:[xmlTest autorelease]];
484 }
485
486 - (void)handleResponse:(NSXMLDocument*)response
487 {
488 // Check and see if there's an error.
489 NSArray* error = [[response rootElement] elementsForName:@"error"];
490 if ([error count] > 0)
491 {
492 NSLog(@"Xdebug error: %@", error);
493 NSString* errorMessage = [[[[error objectAtIndex:0] children] objectAtIndex:0] stringValue];
494 [self errorEncountered:errorMessage];
495 }
496
497 if ([[[response rootElement] name] isEqualToString:@"init"]) {
498 connected_ = YES;
499 [delegate_ performSelectorOnMainThread:@selector(handleInitialResponse:)
500 withObject:response
501 waitUntilDone:NO];
502 return;
503 }
504
505 if ([delegate_ respondsToSelector:@selector(handleResponse:)])
506 [delegate_ performSelectorOnMainThread:@selector(handleResponse:)
507 withObject:response
508 waitUntilDone:NO];
509
510 [self sendQueuedWrites];
511 }
512
513 /**
514 * This performs a blocking send. This should ONLY be called when we know we
515 * have write access to the stream. We will busy wait in case we don't do a full
516 * send.
517 */
518 - (void)performSend:(NSString*)command
519 {
520 // If this is an out-of-date transaction, do not bother sending it.
521 NSInteger transaction = [self transactionIDFromCommand:command];
522 if (transaction != NSNotFound && transaction < lastWrittenTransaction_)
523 return;
524
525 BOOL done = NO;
526
527 char* string = (char*)[command UTF8String];
528 int stringLength = strlen(string);
529
530 // Busy wait while writing. BAADD. Should background this operation.
531 while (!done)
532 {
533 if (CFWriteStreamCanAcceptBytes(writeStream_))
534 {
535 // Include the NULL byte in the string when we write.
536 int bytesWritten = CFWriteStreamWrite(writeStream_, (UInt8*)string, stringLength + 1);
537 if (bytesWritten < 0)
538 {
539 NSLog(@"write error");
540 }
541 // Incomplete write.
542 else if (bytesWritten < strlen(string))
543 {
544 // Adjust the buffer and wait for another chance to write.
545 stringLength -= bytesWritten;
546 memmove(string, string + bytesWritten, stringLength);
547 }
548 else
549 {
550 done = YES;
551
552 // We need to scan the string to find the transactionID.
553 if (transaction == NSNotFound)
554 {
555 NSLog(@"sent %@ without a transaction ID", command);
556 continue;
557 }
558 lastWrittenTransaction_ = transaction;
559 }
560 }
561 }
562
563 // Log this trancation.
564 [self recordSend:command];
565 }
566
567 /**
568 * Checks if there are unsent commands in the |queuedWrites_| queue and sends
569 * them if it's OK to do so. This will not block.
570 */
571 - (void)sendQueuedWrites
572 {
573 if (!connected_)
574 return;
575
576 [writeQueueLock_ lock];
577 if (lastReadTransaction_ >= lastWrittenTransaction_ && [queuedWrites_ count] > 0)
578 {
579 NSString* command = [queuedWrites_ objectAtIndex:0];
580
581 // We don't want to block because this is called from the main thread.
582 // |-performSend:| busy waits when the stream is not ready. Bail out
583 // before we do that becuase busy waiting is BAD.
584 if (CFWriteStreamCanAcceptBytes(writeStream_))
585 {
586 [self performSend:command];
587 [queuedWrites_ removeObjectAtIndex:0];
588 }
589 }
590 [writeQueueLock_ unlock];
591 }
592
593 @end