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