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