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