Finished enough of the basic refactoring to get things working.
[macgdbp.git] / Source / DebuggerConnection.m
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 "DebuggerConnection.h"
18
19 #import <sys/socket.h>
20 #import <netinet/in.h>
21
22 #import "AppDelegate.h"
23 #import "LoggingController.h"
24
25 // DebuggerConnection (Private) ////////////////////////////////////////////////
26
27 @interface DebuggerConnection ()
28
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;
36
37 @end
38
39 // CFNetwork Callbacks /////////////////////////////////////////////////////////
40
41 void ReadStreamCallback(CFReadStreamRef stream, CFStreamEventType eventType, void* connectionRaw)
42 {
43 DebuggerConnection* connection = (DebuggerConnection*)connectionRaw;
44 switch (eventType)
45 {
46 case kCFStreamEventHasBytesAvailable:
47 [connection readStreamHasData];
48 break;
49
50 case kCFStreamEventErrorOccurred:
51 {
52 CFErrorRef error = CFReadStreamCopyError(stream);
53 CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
54 CFReadStreamClose(stream);
55 CFRelease(stream);
56 [connection errorEncountered:[[(NSError*)error autorelease] description]];
57 break;
58 }
59
60 case kCFStreamEventEndEncountered:
61 CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
62 CFReadStreamClose(stream);
63 CFRelease(stream);
64 [connection socketDisconnected];
65 break;
66 };
67 }
68
69 void WriteStreamCallback(CFWriteStreamRef stream, CFStreamEventType eventType, void* connectionRaw)
70 {
71 DebuggerConnection* connection = (DebuggerConnection*)connectionRaw;
72 switch (eventType)
73 {
74 case kCFStreamEventCanAcceptBytes:
75 [connection sendQueuedWrites];
76 break;
77
78 case kCFStreamEventErrorOccurred:
79 {
80 CFErrorRef error = CFWriteStreamCopyError(stream);
81 CFWriteStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
82 CFWriteStreamClose(stream);
83 CFRelease(stream);
84 [connection errorEncountered:[[(NSError*)error autorelease] description]];
85 break;
86 }
87
88 case kCFStreamEventEndEncountered:
89 CFWriteStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
90 CFWriteStreamClose(stream);
91 CFRelease(stream);
92 [connection socketDisconnected];
93 break;
94 }
95 }
96
97 void SocketAcceptCallback(CFSocketRef socket,
98 CFSocketCallBackType callbackType,
99 CFDataRef address,
100 const void* data,
101 void* connectionRaw)
102 {
103 assert(callbackType == kCFSocketAcceptCallBack);
104 NSLog(@"SocketAcceptCallback()");
105
106 DebuggerConnection* connection = (DebuggerConnection*)connectionRaw;
107
108 CFReadStreamRef readStream;
109 CFWriteStreamRef writeStream;
110
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.
116
117 // Create struct to register callbacks for the stream.
118 CFStreamClientContext context;
119 context.version = 0;
120 context.info = connection;
121 context.retain = NULL;
122 context.release = NULL;
123 context.copyDescription = NULL;
124
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);
134 else
135 return;
136
137 // Open the stream now that it's scheduled on the run loop.
138 if (!CFReadStreamOpen(readStream))
139 {
140 CFStreamError error = CFReadStreamGetError(readStream);
141 NSLog(@"error! %@", error);
142 return;
143 }
144
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);
154 else
155 return;
156
157 // Open the write stream.
158 if (!CFWriteStreamOpen(writeStream))
159 {
160 CFStreamError error = CFWriteStreamGetError(writeStream);
161 NSLog(@"error! %@", error);
162 return;
163 }
164
165 connection.readStream = readStream;
166 connection.writeStream = writeStream;
167 [connection socketDidAccept];
168 }
169
170 ////////////////////////////////////////////////////////////////////////////////
171
172 @implementation DebuggerConnection
173
174 @synthesize port = port_;
175 @synthesize connected = connected_;
176 @synthesize delegate = delegate_;
177
178 @synthesize socket = socket_;
179 @synthesize readStream = readStream_;
180 @synthesize lastReadTransaction = lastReadTransaction_;
181 @synthesize currentPacket = currentPacket_;
182 @synthesize writeStream = writeStream_;
183 @synthesize lastWrittenTransaction = lastWrittenTransaction_;
184 @synthesize queuedWrites = queuedWrites_;
185
186 - (id)initWithPort:(NSUInteger)aPort
187 {
188 if (self = [super init])
189 {
190 port_ = aPort;
191 }
192 return self;
193 }
194
195 - (void)dealloc
196 {
197 self.currentPacket = nil;
198 [super dealloc];
199 }
200
201 /**
202 * Called by SocketWrapper after the connection is successful. This immediately calls
203 * -[SocketWrapper receive] to clear the way for communication, though the information
204 * could be useful server information that we don't use right now.
205 */
206 - (void)socketDidAccept
207 {
208 connected_ = YES;
209 transactionID = 1;
210 self.queuedWrites = [NSMutableArray array];
211 writeQueueLock_ = [NSRecursiveLock new];
212 callTable_ = [NSMutableDictionary new];
213 }
214
215 /**
216 * Receives errors from the SocketWrapper and updates the display
217 */
218 - (void)errorEncountered:(NSString*)error
219 {
220 [delegate_ errorEncountered:error];
221 }
222
223 /**
224 * Creates, connects to, and schedules a CFSocket.
225 */
226 - (void)connect
227 {
228 // Pass ourselves to the callback so we don't have to use ugly globals.
229 CFSocketContext context;
230 context.version = 0;
231 context.info = self;
232 context.retain = NULL;
233 context.release = NULL;
234 context.copyDescription = NULL;
235
236 // Create the address structure.
237 struct sockaddr_in address;
238 memset(&address, 0, sizeof(address));
239 address.sin_len = sizeof(address);
240 address.sin_family = AF_INET;
241 address.sin_port = htons(port_);
242 address.sin_addr.s_addr = htonl(INADDR_ANY);
243
244 // Create the socket signature.
245 CFSocketSignature signature;
246 signature.protocolFamily = PF_INET;
247 signature.socketType = SOCK_STREAM;
248 signature.protocol = IPPROTO_TCP;
249 signature.address = (CFDataRef)[NSData dataWithBytes:&address length:sizeof(address)];
250
251 socket_ = CFSocketCreateWithSocketSignature(kCFAllocatorDefault,
252 &signature, // Socket signature.
253 kCFSocketAcceptCallBack, // Callback types.
254 SocketAcceptCallback, // Callout function pointer.
255 &context); // Context to pass to callout.
256 if (!socket_)
257 {
258 [self errorEncountered:@"Could not open socket."];
259 return;
260 }
261
262 // Allow old, yet-to-be recycled sockets to be reused.
263 BOOL yes = YES;
264 setsockopt(CFSocketGetNative(socket_), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(BOOL));
265
266 // Schedule the socket on the run loop.
267 CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket_, 0);
268 CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
269 CFRelease(source);
270 }
271
272 /**
273 * Closes a socket and releases the ref.
274 */
275 - (void)close
276 {
277 // The socket goes down, so do the streams, which clean themselves up.
278 CFSocketInvalidate(socket_);
279 CFRelease(socket_);
280 self.queuedWrites = nil;
281 [writeQueueLock_ release];
282 [callTable_ release];
283 }
284
285 /**
286 * Notification that the socket disconnected.
287 */
288 - (void)socketDisconnected
289 {
290 [self close];
291 [delegate_ connectionDidCose:self];
292 }
293
294 /**
295 * Callback from the CFReadStream that there is data waiting to be read.
296 */
297 - (void)readStreamHasData
298 {
299 const NSUInteger kBufferSize = 1024;
300 UInt8 buffer[kBufferSize];
301 CFIndex bufferOffset = 0; // Starting point in |buffer| to work with.
302 CFIndex bytesRead = CFReadStreamRead(readStream_, buffer, kBufferSize);
303 const char* charBuffer = (const char*)buffer;
304
305 // The read loop works by going through the buffer until all the bytes have
306 // been processed.
307 while (bufferOffset < bytesRead)
308 {
309 // Find the NULL separator, or the end of the string.
310 NSUInteger partLength = 0;
311 for (NSUInteger i = bufferOffset; i < bytesRead && charBuffer[i] != '\0'; ++i, ++partLength) ;
312
313 // If there is not a current packet, set some state.
314 if (!self.currentPacket)
315 {
316 // Read the message header: the size. This will be |partLength| bytes.
317 packetSize_ = atoi(charBuffer + bufferOffset);
318 currentPacketIndex_ = 0;
319 self.currentPacket = [NSMutableString stringWithCapacity:packetSize_];
320 bufferOffset += partLength + 1; // Pass over the NULL byte.
321 continue; // Spin the loop to begin reading actual data.
322 }
323
324 // Substring the byte stream and append it to the packet string.
325 CFStringRef bufferString = CFStringCreateWithBytes(kCFAllocatorDefault,
326 buffer + bufferOffset, // Byte pointer, offset by start index.
327 partLength, // Length.
328 kCFStringEncodingUTF8,
329 true);
330 [self.currentPacket appendString:(NSString*)bufferString];
331 CFRelease(bufferString);
332
333 // Advance counters.
334 currentPacketIndex_ += partLength;
335 bufferOffset += partLength + 1;
336
337 // If this read finished the packet, handle it and reset.
338 NSLog(@"cpi %d ps %d br %d ds %d", currentPacketIndex_, packetSize_, bytesRead, partLength);
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 }
408 else
409 {
410 // See if the transaction can be parsed out.
411 NSInteger transaction = [self transactionIDFromResponse:xmlTest];
412 if (transaction < lastReadTransaction_)
413 {
414 NSLog(@"tx = %d vs %d", transaction, lastReadTransaction_);
415 NSLog(@"out of date transaction %@", packet);
416 return;
417 }
418
419 if (transaction != lastWrittenTransaction_)
420 NSLog(@"txn %d <> %d last written, %d last read", transaction, lastWrittenTransaction_, lastReadTransaction_);
421
422 lastReadTransaction_ = transaction;
423 }
424
425 // Log this receive event.
426 LoggingController* logger = [(AppDelegate*)[NSApp delegate] loggingController];
427 LogEntry* log = [logger recordReceive:currentPacket_];
428 log.error = error;
429 log.lastWrittenTransactionID = lastWrittenTransaction_;
430 log.lastReadTransactionID = lastReadTransaction_;
431
432 // Finally, dispatch the handler for this response.
433 [self handleResponse:[xmlTest autorelease]];
434 }
435
436 /**
437 * Writes a command into the write stream. If the stream is ready for writing,
438 * we do so immediately. If not, the command is queued and will be written
439 * when the stream is ready.
440 */
441 - (void)send:(NSString*)command
442 {
443 if (lastReadTransaction_ >= lastWrittenTransaction_ && CFWriteStreamCanAcceptBytes(writeStream_))
444 [self performSend:command];
445 else
446 [queuedWrites_ addObject:command];
447 [self sendQueuedWrites];
448 }
449
450 /**
451 * This performs a blocking send. This should ONLY be called when we know we
452 * have write access to the stream. We will busy wait in case we don't do a full
453 * send.
454 */
455 - (void)performSend:(NSString*)command
456 {
457 // If this is an out-of-date transaction, do not bother sending it.
458 NSInteger transaction = [self transactionIDFromCommand:command];
459 if (transaction != NSNotFound && transaction < lastWrittenTransaction_)
460 return;
461
462 BOOL done = NO;
463
464 char* string = (char*)[command UTF8String];
465 int stringLength = strlen(string);
466
467 // Busy wait while writing. BAADD. Should background this operation.
468 while (!done)
469 {
470 if (CFWriteStreamCanAcceptBytes(writeStream_))
471 {
472 // Include the NULL byte in the string when we write.
473 int bytesWritten = CFWriteStreamWrite(writeStream_, (UInt8*)string, stringLength + 1);
474 if (bytesWritten < 0)
475 {
476 NSLog(@"write error");
477 }
478 // Incomplete write.
479 else if (bytesWritten < strlen(string))
480 {
481 // Adjust the buffer and wait for another chance to write.
482 stringLength -= bytesWritten;
483 memmove(string, string + bytesWritten, stringLength);
484 }
485 else
486 {
487 done = YES;
488
489 // We need to scan the string to find the transactionID.
490 if (transaction == NSNotFound)
491 {
492 NSLog(@"sent %@ without a transaction ID", command);
493 continue;
494 }
495 lastWrittenTransaction_ = transaction;
496 }
497 }
498 }
499
500 // Log this trancation.
501 LoggingController* logger = [(AppDelegate*)[NSApp delegate] loggingController];
502 LogEntry* log = [logger recordSend:command];
503 log.lastWrittenTransactionID = lastWrittenTransaction_;
504 log.lastReadTransactionID = lastReadTransaction_;
505 }
506
507 - (void)handleResponse:(NSXMLDocument*)response
508 {
509 // Check and see if there's an error.
510 NSArray* error = [[response rootElement] elementsForName:@"error"];
511 if ([error count] > 0)
512 {
513 NSLog(@"Xdebug error: %@", error);
514 [delegate_ errorEncountered:[[[[error objectAtIndex:0] children] objectAtIndex:0] stringValue]];
515 }
516
517 if ([[[response rootElement] name] isEqualToString:@"init"])
518 {
519 [delegate_ handleInitialResponse:response];
520 return;
521 }
522
523 NSString* callbackStr = [callTable_ objectForKey:[NSNumber numberWithInt:lastReadTransaction_]];
524 if (callbackStr)
525 {
526 SEL callback = NSSelectorFromString(callbackStr);
527 [delegate_ performSelector:callback withObject:response];
528 }
529
530 [self sendQueuedWrites];
531 }
532
533 #pragma mark Private
534
535 /**
536 * This will send a command to the debugger engine. It will append the
537 * transaction ID automatically. It accepts a NSString command along with a
538 * a variable number of arguments to substitute into the command, a la
539 * +[NSString stringWithFormat:]. Returns the transaction ID as a NSNumber.
540 */
541 - (NSNumber*)sendCommandWithCallback:(SEL)callback format:(NSString*)format, ...
542 {
543 // Collect varargs and format command.
544 va_list args;
545 va_start(args, format);
546 NSString* command = [[NSString alloc] initWithFormat:format arguments:args];
547 va_end(args);
548
549 NSNumber* callbackKey = [NSNumber numberWithInt:transactionID++];
550 if (callback)
551 [callTable_ setObject:NSStringFromSelector(callback) forKey:callbackKey];
552
553 [self send:[NSString stringWithFormat:@"%@ -i %@", [command autorelease], callbackKey]];
554
555 return callbackKey;
556 }
557
558 /**
559 * Checks if there are unsent commands in the |queuedWrites_| queue and sends
560 * them if it's OK to do so. This will not block.
561 */
562 - (void)sendQueuedWrites
563 {
564 if (!connected_)
565 return;
566
567 [writeQueueLock_ lock];
568 if (lastReadTransaction_ >= lastWrittenTransaction_ && [queuedWrites_ count] > 0)
569 {
570 NSString* command = [queuedWrites_ objectAtIndex:0];
571
572 // We don't want to block because this is called from the main thread.
573 // |-performSend:| busy waits when the stream is not ready. Bail out
574 // before we do that becuase busy waiting is BAD.
575 if (CFWriteStreamCanAcceptBytes(writeStream_))
576 {
577 [self performSend:command];
578 [queuedWrites_ removeObjectAtIndex:0];
579 }
580 }
581 [writeQueueLock_ unlock];
582 }
583
584
585 /**
586 * Returns the transaction_id from an NSXMLDocument.
587 */
588 - (NSInteger)transactionIDFromResponse:(NSXMLDocument*)response
589 {
590 return [[[[response rootElement] attributeForName:@"transaction_id"] stringValue] intValue];
591 }
592
593 /**
594 * Scans a command string for the transaction ID component. If it is not found,
595 * returns NSNotFound.
596 */
597 - (NSInteger)transactionIDFromCommand:(NSString*)command
598 {
599 NSRange occurrence = [command rangeOfString:@"-i "];
600 if (occurrence.location == NSNotFound)
601 return NSNotFound;
602 NSString* transaction = [command substringFromIndex:occurrence.location + occurrence.length];
603 return [transaction intValue];
604 }
605
606 /**
607 * Given a file path, this returns a file:// URI and escapes any spaces for the
608 * debugger engine.
609 */
610 - (NSString*)escapedURIPath:(NSString*)path
611 {
612 // Custon GDBp paths are fine.
613 if ([[path substringToIndex:4] isEqualToString:@"gdbp"])
614 return path;
615
616 // Create a temporary URL that will escape all the nasty characters.
617 NSURL* url = [NSURL fileURLWithPath:path];
618 NSString* urlString = [url absoluteString];
619
620 // Remove the host because this is a file:// URL;
621 urlString = [urlString stringByReplacingOccurrencesOfString:[url host] withString:@""];
622
623 // Escape % for use in printf-style NSString formatters.
624 urlString = [urlString stringByReplacingOccurrencesOfString:@"%" withString:@"%%"];
625 return urlString;
626 }
627
628 @end