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