oolite/src/Core/Debug/OODebugTCPConsoleClient.m

711 lines
18 KiB
Objective-C

/*
OODebugTCPConsoleClient.m
Oolite Debug OXP
Copyright (C) 2009 Jens Ayton
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef OO_EXCLUDE_DEBUG_SUPPORT
#import "OODebugTCPConsoleClient.h"
#import "OODebugTCPConsoleProtocol.h"
#import "OODebugMonitor.h"
#import "OOFunctionAttributes.h"
#import "OOLogging.h"
#import <stdint.h>
#if OOLITE_WINDOWS
#import <winsock2.h>
#else
#import <arpa/inet.h> // For htonl
#endif
#import "OOCollectionExtractors.h"
#import "OOTCPStreamDecoder.h"
#ifdef OO_LOG_DEBUG_PROTOCOL_PACKETS
static void LogSendPacket(NSDictionary *packet);
#else
#define LogSendPacket(packet) do {} while (0)
#endif
static void DecoderPacket(void *cbInfo, OOALStringRef packetType, OOALDictionaryRef packet);
static void DecoderError(void *cbInfo, OOALStringRef errorDesc);
OOINLINE BOOL StatusIsSendable(OOTCPClientConnectionStatus status)
{
return status == kOOTCPClientStartedConnectionStage1 || status == kOOTCPClientStartedConnectionStage2 || status == kOOTCPClientConnected;
}
@interface OODebugTCPConsoleClient (OOPrivate)
- (void) closeConnection;
- (BOOL) sendBytes:(const void *)bytes count:(size_t)count;
- (void) sendDictionary:(NSDictionary *)dictionary;
- (void) sendPacket:(NSString *)packetType
withParameters:(NSDictionary *)parameters;
- (void) sendPacket:(NSString *)packetType
withValue:(id)value
forParameter:(NSString *)paramKey;
- (void) readData;
- (void) dispatchPacket:(NSDictionary *)packet ofType:(NSString *)packetType;
- (void) handleApproveConnectionPacket:(NSDictionary *)packet;
- (void) handleRejectConnectionPacket:(NSDictionary *)packet;
- (void) handleCloseConnectionPacket:(NSDictionary *)packet;
- (void) handleNoteConfigurationChangePacket:(NSDictionary *)packet;
- (void) handlePerformCommandPacket:(NSDictionary *)packet;
- (void) handleRequestConfigurationValuePacket:(NSDictionary *)packet;
- (void) handlePingPacket:(NSDictionary *)packet;
- (void) handlePongPacket:(NSDictionary *)packet;
- (void) disconnectFromServerWithMessage:(NSString *)message;
- (void) breakConnectionWithMessage:(NSString *)message;
- (void) breakConnectionWithBadStream:(NSStream *)stream;
@end
@implementation OODebugTCPConsoleClient
- (id) init
{
return [self initWithAddress:nil port:0];
}
- (id) initWithAddress:(NSString *)address port:(uint16_t)port
{
BOOL OK = NO;
NSDictionary *parameters = nil;
if (address == nil) address = @"127.0.0.1";
if (port == 0) port = kOOTCPConsolePort;
self = [super init];
if (self != nil)
{
_host = [NSHost hostWithName:address];
if (_host != nil)
{
[_host retain];
[NSStream getStreamsToHost:_host
port:port
inputStream:&_inStream
outputStream:&_outStream];
}
if (_inStream != nil && _outStream != nil)
{
[_inStream retain];
[_outStream retain];
[_inStream setDelegate:self];
[_outStream setDelegate:self];
[_inStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[_outStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[_inStream open];
[_outStream open];
// Need to wait for the streams to reach open status before we can send packets
// TODO: Might be neater to use the handleEvent callback to flag this.. - Micha 20090425
NSRunLoop * myRunLoop = [NSRunLoop currentRunLoop];
NSDate * timeOut = [NSDate dateWithTimeIntervalSinceNow:3]; // Wait up to 3 seconds
while( _host != nil && ([_inStream streamStatus] < 2 || [_outStream streamStatus] < 2) &&
[myRunLoop runMode:NSDefaultRunLoopMode beforeDate:timeOut] )
; // Wait
_decoder = OOTCPStreamDecoderCreate(DecoderPacket, DecoderError, NULL, self);
}
if (_decoder != NULL)
{
OK = YES;
_status = kOOTCPClientStartedConnectionStage1;
// Attempt to connect
parameters = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithUnsignedInt:kOOTCPProtocolVersion_1_1_0], kOOTCPProtocolVersion,
[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"], kOOTCPOoliteVersion,
nil];
[self sendPacket:kOOTCPPacket_RequestConnection
withParameters:parameters];
if (_status == kOOTCPClientStartedConnectionStage1) _status = kOOTCPClientStartedConnectionStage2;
else OK = NO; // Connection failed.
}
if (!OK)
{
OOLog(@"debugTCP.connect.failed", @"Failed to connect to debug console at %@:%i.", address, port);
[self release];
self = nil;
}
}
return self;
}
- (void) dealloc
{
if (StatusIsSendable(_status))
{
[self disconnectFromServerWithMessage:@"TCP console bridge unexpectedly released while active."];
}
if (_monitor)
{
[_monitor disconnectDebugger:self message:@"TCP console bridge unexpectedly released while active."];
}
[self closeConnection];
OOTCPStreamDecoderDestroy(_decoder);
_decoder = NULL;
[super dealloc];
}
- (BOOL)connectDebugMonitor:(in OODebugMonitor *)debugMonitor
errorMessage:(out NSString **)message
{
if (_status == kOOTCPClientConnectionRefused)
{
if (message != NULL) *message = @"Connection refused.";
return NO;
}
if (_status == kOOTCPClientDisconnected)
{
if (message != NULL) *message = @"Cannot reconnect after disconnecting.";
return NO;
}
_monitor = debugMonitor;
return YES;
}
- (void)disconnectDebugMonitor:(in OODebugMonitor *)debugMonitor
message:(in NSString *)message
{
[self disconnectFromServerWithMessage:message];
_monitor = nil;
}
- (oneway void)debugMonitor:(in OODebugMonitor *)debugMonitor
jsConsoleOutput:(in NSString *)output
colorKey:(in NSString *)colorKey
emphasisRange:(in NSRange)emphasisRange
{
NSMutableDictionary *parameters = nil;
NSArray *range = nil;
parameters = [NSMutableDictionary dictionaryWithCapacity:3];
[parameters setObject:output forKey:kOOTCPMessage];
[parameters setObject:colorKey ? colorKey : (NSString *)@"general" forKey:kOOTCPColorKey];
if (emphasisRange.length != 0)
{
range = [NSArray arrayWithObjects:
[NSNumber numberWithUnsignedInt:emphasisRange.location],
[NSNumber numberWithUnsignedInt:emphasisRange.length],
nil];
[parameters setObject:range forKey:kOOTCPEmphasisRanges];
}
[self sendPacket:kOOTCPPacket_ConsoleOutput
withParameters:parameters];
}
- (oneway void)debugMonitorClearConsole:(in OODebugMonitor *)debugMonitor
{
[self sendPacket:kOOTCPPacket_ClearConsole
withParameters:nil];
}
- (oneway void)debugMonitorShowConsole:(in OODebugMonitor *)debugMonitor;
{
[self sendPacket:kOOTCPPacket_ShowConsole
withParameters:nil];
}
- (oneway void)debugMonitor:(in OODebugMonitor *)debugMonitor
noteConfiguration:(in NSDictionary *)configuration
{
[self sendPacket:kOOTCPPacket_NoteConfiguration
withValue:configuration
forParameter:kOOTCPConfiguration];
}
- (oneway void)debugMonitor:(in OODebugMonitor *)debugMonitor
noteChangedConfigrationValue:(in id)newValue
forKey:(in NSString *)key
{
if (newValue != nil)
{
[self sendPacket:kOOTCPPacket_NoteConfiguration
withValue:[NSDictionary dictionaryWithObject:newValue forKey:key]
forParameter:kOOTCPConfiguration];
}
else
{
[self sendPacket:kOOTCPPacket_NoteConfiguration
withValue:[NSArray arrayWithObject:key]
forParameter:kOOTCPRemovedConfigurationKeys];
}
}
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode
{
if (_status > kOOTCPClientConnected) return;
if (stream == _inStream && eventCode == NSStreamEventHasBytesAvailable)
{
[self readData];
}
else if (eventCode == NSStreamEventErrorOccurred)
{
[self breakConnectionWithBadStream:stream];
}
else if (eventCode == NSStreamEventErrorOccurred)
{
[self breakConnectionWithMessage:[NSString stringWithFormat:
@"Console closed the connection."]];
}
}
@end
@implementation OODebugTCPConsoleClient (OOPrivate)
- (void) closeConnection
{
[_inStream close];
[_inStream setDelegate:nil];
[_inStream release];
_inStream = nil;
[_outStream close];
[_outStream setDelegate:nil];
[_outStream release];
_outStream = nil;
[_host release];
_host = nil;
}
- (BOOL) sendBytes:(const void *)bytes count:(size_t)count
{
int written;
if (bytes == NULL || count == 0) return YES;
if (!StatusIsSendable(_status) || _outStream == nil) return NO;
do
{
written = [_outStream write:bytes maxLength:count];
if (written < 1) return NO;
count -= written;
bytes += written;
}
while (count > 0);
return YES;
}
- (void) sendDictionary:(NSDictionary *)dictionary
{
NSData *data = nil;
NSString *errorDesc = nil;
size_t count;
const uint8_t *bytes = NULL;
uint32_t header;
if (dictionary == nil || !StatusIsSendable(_status)) return;
data = [NSPropertyListSerialization dataFromPropertyList:dictionary
format:NSPropertyListXMLFormat_v1_0
errorDescription:&errorDesc];
if (data == nil)
{
OOLog(@"debugTCP.conversionFailure", @"Could not convert dictionary to data for transmission to debug console: %@", errorDesc ? errorDesc : (NSString *)@"unknown error.");
#if OOLITE_RELEASE_PLIST_ERROR_STRINGS
[errorDesc autorelease];
#endif
return;
}
LogSendPacket(dictionary);
count = [data length];
if (count == 0) return;
header = htonl(count);
bytes = [data bytes];
if (![self sendBytes:&header count:sizeof header] || ![self sendBytes:bytes count:count])
{
[self breakConnectionWithBadStream:_outStream];
}
}
- (void) sendPacket:(NSString *)packetType
withParameters:(NSDictionary *)parameters
{
NSMutableDictionary *dict = nil;
if (packetType == nil) return;
if (parameters != nil)
{
dict = [parameters mutableCopy];
[dict setObject:packetType forKey:kOOTCPPacketType];
}
else
{
dict = [[NSDictionary alloc] initWithObjectsAndKeys:packetType, kOOTCPPacketType, nil];
}
[self sendDictionary:dict];
[dict release];
}
- (void) sendPacket:(NSString *)packetType
withValue:(id)value
forParameter:(NSString *)paramKey
{
if (packetType == nil) return;
if (paramKey == nil) value = nil;
[self sendDictionary:[NSDictionary dictionaryWithObjectsAndKeys:
packetType, kOOTCPPacketType,
value, paramKey,
nil]];
}
- (void) readData
{
enum { kBufferSize = 16 << 10 };
uint8_t buffer[kBufferSize];
int length;
NSData *data;
length = [_inStream read:buffer maxLength:kBufferSize];
while( length > 0 )
{
/* This test is superfluous after the rewrite to fix Bug#014643
* TODO: Put the BadStream test back into the code
if (length < 1)
{
// Under GNUstep, but not OS X (currently), -hasBytesAvailable will return YES when the buffer is in fact empty.
if ([_inStream streamStatus] == NSStreamStatusReading) break;
[self breakConnectionWithBadStream:_inStream];
return;
}
*/
data = [NSData dataWithBytesNoCopy:buffer length:length freeWhenDone:NO];
OOTCPStreamDecoderReceiveData(_decoder, data);
length = [_inStream read:buffer maxLength:kBufferSize];
}
}
- (void) dispatchPacket:(NSDictionary *)packet ofType:(NSString *)packetType
{
if (packet == nil || packetType == nil) return;
#define PACKET_CASE(x) else if ([packetType isEqualToString:kOOTCPPacket_##x]) { [self handle##x##Packet:packet]; }
if (0) {}
PACKET_CASE(ApproveConnection)
PACKET_CASE(RejectConnection)
PACKET_CASE(CloseConnection)
PACKET_CASE(NoteConfigurationChange)
PACKET_CASE(PerformCommand)
PACKET_CASE(RequestConfigurationValue)
PACKET_CASE(Ping)
PACKET_CASE(Pong)
else
{
OOLog(@"debugTCP.protocolError.unknownPacketType", @"Unhandled packet type %@.", packetType);
}
}
- (void) handleApproveConnectionPacket:(NSDictionary *)packet
{
NSMutableString *connectedMessage = nil;
NSString *consoleIdentity = nil;
NSString *hostName = nil;
if (_status == kOOTCPClientStartedConnectionStage2)
{
_status = kOOTCPClientConnected;
// Build "Connected..." message with two optional parts, console identity and host name.
connectedMessage = [NSMutableString stringWithString:@"Connected to external debug console"];
consoleIdentity = [packet stringForKey:kOOTCPConsoleIdentity];
if (consoleIdentity != nil) [connectedMessage appendFormat:@" \"%@\"", consoleIdentity];
hostName = [_host name];
if ([hostName length] != 0 &&
![hostName isEqual:@"localhost"] &&
![hostName isEqual:@"127.0.0.1"] &&
![hostName isEqual:@"::1"])
{
[connectedMessage appendFormat:@" at %@", hostName];
}
OOLog(@"debugTCP.connected", @"%@.", connectedMessage);
}
else
{
OOLog(@"debugTCP.protocolError.outOfOrder", @"Got %@ packet from debug console in wrong context.", kOOTCPPacket_ApproveConnection);
}
}
- (void) handleRejectConnectionPacket:(NSDictionary *)packet
{
NSString *message = nil;
if (_status == kOOTCPClientStartedConnectionStage2)
{
_status = kOOTCPClientConnectionRefused;
}
else
{
OOLog(@"debugTCP.protocolError.outOfOrder", @"Got %@ packet from debug console in wrong context.", kOOTCPPacket_RejectConnection);
}
message = [packet stringForKey:kOOTCPMessage];
if (message == nil) message = @"Console refused connection.";
[self breakConnectionWithMessage:message];
}
- (void) handleCloseConnectionPacket:(NSDictionary *)packet
{
NSString *message = nil;
if (!StatusIsSendable(_status))
{
OOLog(@"debugTCP.protocolError.outOfOrder", @"Got %@ packet from debug console in wrong context.", kOOTCPPacket_CloseConnection);
}
message = [packet stringForKey:kOOTCPMessage];
if (message == nil) message = @"Console closed connection.";
[self breakConnectionWithMessage:message];
}
- (void) handleNoteConfigurationChangePacket:(NSDictionary *)packet
{
NSDictionary *configuration = nil;
NSArray *removed = nil;
NSEnumerator *keyEnum = nil;
NSString *key = nil;
id value = nil;
if (_monitor == nil) return;
configuration = [packet dictionaryForKey:kOOTCPConfiguration];
if (configuration != nil)
{
for (keyEnum = [configuration keyEnumerator]; (key = [keyEnum nextObject]); )
{
value = [configuration objectForKey:key];
[_monitor setConfigurationValue:value forKey:key];
}
}
removed = [configuration arrayForKey:kOOTCPRemovedConfigurationKeys];
for (keyEnum = [removed objectEnumerator]; (key = [keyEnum nextObject]); )
{
[_monitor setConfigurationValue:nil forKey:key];
}
}
- (void) handlePerformCommandPacket:(NSDictionary *)packet
{
NSString *message = nil;
message = [packet stringForKey:kOOTCPMessage];
if (message != nil) [_monitor performJSConsoleCommand:message];
}
- (void) handleRequestConfigurationValuePacket:(NSDictionary *)packet
{
NSString *key = nil;
id value = nil;
key = [packet stringForKey:kOOTCPConfigurationKey];
if (key != nil)
{
value = [_monitor configurationValueForKey:key];
[self debugMonitor:_monitor
noteChangedConfigrationValue:value
forKey:key];
}
}
- (void) handlePingPacket:(NSDictionary *)packet
{
id message = nil;
message = [packet objectForKey:kOOTCPMessage];
[self sendPacket:kOOTCPPacket_Pong
withValue:message
forParameter:kOOTCPMessage];
}
- (void) handlePongPacket:(NSDictionary *)packet
{
// Do nothing; we don't currently send pings.
}
- (void) disconnectFromServerWithMessage:(NSString *)message
{
if (StatusIsSendable(_status))
{
[self sendPacket:kOOTCPPacket_CloseConnection
withValue:message
forParameter:kOOTCPMessage];
}
[self closeConnection];
_status = kOOTCPClientDisconnected;
}
- (void) breakConnectionWithMessage:(NSString *)message
{
[self closeConnection];
if (_status != kOOTCPClientConnectionRefused) _status = kOOTCPClientDisconnected;
if ([message length] > 0)
{
OOLog(@"debugTCP.disconnect", @"Debug console disconnected with message %@", message);
}
else
{
OOLog(@"debugTCP.disconnect", @"Debug console disconnected.");
}
#if 0
// Disconnecting causes crashiness for reasons I don't understand, and isn't very important anyway.
[_monitor disconnectDebugger:self message:message];
_monitor = nil;
#endif
}
- (void) breakConnectionWithBadStream:(NSStream *)stream
{
NSString *errorDesc = nil;
NSError *error = nil;
error = [stream streamError];
errorDesc = [error localizedDescription];
if (errorDesc == nil) errorDesc = [error description];
if (errorDesc == nil) errorDesc = @"unknown error.";
[self breakConnectionWithMessage:[NSString stringWithFormat:
@"Lost connection to remote debug console. outStream status: %i, inStream status: %i. Stream error: %@",
[_outStream streamStatus], [_inStream streamStatus], errorDesc]];
}
@end
static void DecoderPacket(void *cbInfo, OOALStringRef packetType, OOALDictionaryRef packet)
{
[(OODebugTCPConsoleClient *)cbInfo dispatchPacket:packet ofType:packetType];
}
static void DecoderError(void *cbInfo, OOALStringRef errorDesc)
{
[(OODebugTCPConsoleClient *)cbInfo breakConnectionWithMessage:errorDesc];
}
#ifdef OO_LOG_DEBUG_PROTOCOL_PACKETS
void LogOOTCPStreamDecoderPacket(NSDictionary *packet)
{
NSData *data = nil;
NSString *xml = nil;
data = [NSPropertyListSerialization dataFromPropertyList:packet format:NSPropertyListXMLFormat_v1_0 errorDescription:NULL];
xml = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
OOLog(@"debugTCP.receive", @"Received packet:\n%@", xml);
}
static void LogSendPacket(NSDictionary *packet)
{
NSData *data = nil;
NSString *xml = nil;
data = [NSPropertyListSerialization dataFromPropertyList:packet format:NSPropertyListXMLFormat_v1_0 errorDescription:NULL];
xml = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
OOLog(@"debugTCP.send", @"Sent packet:\n%@", xml);
}
#endif
#endif /* OO_EXCLUDE_DEBUG_SUPPORT */