diff options
author | Sho Amano <samano@xevo.com> | 2018-04-23 20:07:57 +0900 |
---|---|---|
committer | Sho Amano <samano@xevo.com> | 2018-07-03 15:02:33 +0900 |
commit | 5fc4e5b75d7db966780e713a111afc85170ab853 (patch) | |
tree | 8c1a4247238f6e7f8252feda400f47300f5b21d2 | |
parent | 51d3908b239ddf17379781b64da79f7c9a909b49 (diff) | |
download | sdl_ios-5fc4e5b75d7db966780e713a111afc85170ab853.tar.gz |
Rewrite TCP transport using CFNetwork API
-rw-r--r-- | SmartDeviceLink-iOS.xcodeproj/project.pbxproj | 12 | ||||
-rw-r--r-- | SmartDeviceLink/SDLProtocol.m | 8 | ||||
-rw-r--r-- | SmartDeviceLink/SDLTCPTransport.h | 5 | ||||
-rw-r--r-- | SmartDeviceLink/SDLTCPTransport.m | 363 | ||||
-rw-r--r-- | SmartDeviceLinkTests/TransportSpecs/SDLTCPTransportSpec.m | 637 |
5 files changed, 907 insertions, 118 deletions
diff --git a/SmartDeviceLink-iOS.xcodeproj/project.pbxproj b/SmartDeviceLink-iOS.xcodeproj/project.pbxproj index 71dad2c02..33e2e6222 100644 --- a/SmartDeviceLink-iOS.xcodeproj/project.pbxproj +++ b/SmartDeviceLink-iOS.xcodeproj/project.pbxproj @@ -1252,6 +1252,7 @@ E9C32B9D1AB20C5900F283AF /* EAAccessory+SDLProtocols.m in Sources */ = {isa = PBXBuildFile; fileRef = E9C32B991AB20C5900F283AF /* EAAccessory+SDLProtocols.m */; }; E9C32B9E1AB20C5900F283AF /* EAAccessoryManager+SDLProtocols.h in Headers */ = {isa = PBXBuildFile; fileRef = E9C32B9A1AB20C5900F283AF /* EAAccessoryManager+SDLProtocols.h */; }; E9C32B9F1AB20C5900F283AF /* EAAccessoryManager+SDLProtocols.m in Sources */ = {isa = PBXBuildFile; fileRef = E9C32B9B1AB20C5900F283AF /* EAAccessoryManager+SDLProtocols.m */; }; + EE5D1B33208EBCA900D17216 /* SDLTCPTransportSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = EE5D1B32208EBCA900D17216 /* SDLTCPTransportSpec.m */; }; EED5C9FE1F4D18D100F04000 /* SDLH264Packetizer.h in Headers */ = {isa = PBXBuildFile; fileRef = EED5C9FD1F4D18D100F04000 /* SDLH264Packetizer.h */; }; EED5CA001F4D18DC00F04000 /* SDLRAWH264Packetizer.h in Headers */ = {isa = PBXBuildFile; fileRef = EED5C9FF1F4D18DC00F04000 /* SDLRAWH264Packetizer.h */; }; EED5CA021F4D18EC00F04000 /* SDLRAWH264Packetizer.m in Sources */ = {isa = PBXBuildFile; fileRef = EED5CA011F4D18EC00F04000 /* SDLRAWH264Packetizer.m */; }; @@ -2635,6 +2636,7 @@ E9C32B991AB20C5900F283AF /* EAAccessory+SDLProtocols.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "EAAccessory+SDLProtocols.m"; sourceTree = "<group>"; }; E9C32B9A1AB20C5900F283AF /* EAAccessoryManager+SDLProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "EAAccessoryManager+SDLProtocols.h"; sourceTree = "<group>"; }; E9C32B9B1AB20C5900F283AF /* EAAccessoryManager+SDLProtocols.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "EAAccessoryManager+SDLProtocols.m"; sourceTree = "<group>"; }; + EE5D1B32208EBCA900D17216 /* SDLTCPTransportSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDLTCPTransportSpec.m; sourceTree = "<group>"; }; EED5C9FD1F4D18D100F04000 /* SDLH264Packetizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDLH264Packetizer.h; sourceTree = "<group>"; }; EED5C9FF1F4D18DC00F04000 /* SDLRAWH264Packetizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDLRAWH264Packetizer.h; sourceTree = "<group>"; }; EED5CA011F4D18EC00F04000 /* SDLRAWH264Packetizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDLRAWH264Packetizer.m; sourceTree = "<group>"; }; @@ -4290,6 +4292,7 @@ 5D59DD451B14FDD000BE744D /* ProxySpecs */, 5DB92D201AC47AC400C15BB0 /* UtilitiesSpecs */, 1680B1041A9CD7AD00DBD79E /* ProtocolSpecs */, + EE5D1B31208EBC7100D17216 /* TransportSpecs */, 162E81E01A9BDE8A00906325 /* RPCSpecs */, 5D61FA2D1A84237100846EE7 /* Supporting Files */, 167ED9451A9BCE5D00797BE5 /* SwiftSpec.swift */, @@ -5244,6 +5247,14 @@ name = "@categories"; sourceTree = "<group>"; }; + EE5D1B31208EBC7100D17216 /* TransportSpecs */ = { + isa = PBXGroup; + children = ( + EE5D1B32208EBCA900D17216 /* SDLTCPTransportSpec.m */, + ); + path = TransportSpecs; + sourceTree = "<group>"; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -6609,6 +6620,7 @@ 5D0A9F911F15550400CC80DD /* SDLSystemCapabilityTypeSpec.m in Sources */, 5DBF0D601F3B3DB4008AF2C9 /* SDLControlFrameVideoStartServiceAckSpec.m in Sources */, 162E83311A9BDE8B00906325 /* SDLListFilesSpec.m in Sources */, + EE5D1B33208EBCA900D17216 /* SDLTCPTransportSpec.m in Sources */, DA9F7EB01DCC063400ACAE48 /* SDLLocationDetailsSpec.m in Sources */, 5DC978261B7A38640012C2F1 /* SDLGlobalsSpec.m in Sources */, 162E82FF1A9BDE8B00906325 /* SDLTextAlignmentSpec.m in Sources */, diff --git a/SmartDeviceLink/SDLProtocol.m b/SmartDeviceLink/SDLProtocol.m index 89bd5aeb8..405fdd3bc 100644 --- a/SmartDeviceLink/SDLProtocol.m +++ b/SmartDeviceLink/SDLProtocol.m @@ -106,6 +106,14 @@ NS_ASSUME_NONNULL_BEGIN [self handleBytesFromTransport:receivedData]; } +- (void)onError:(NSError *)error { + for (id<SDLProtocolListener> listener in self.protocolDelegateTable.allObjects) { + if ([listener respondsToSelector:@selector(onTransportError:)]) { + [listener onTransportError:error]; + } + } +} + #pragma mark - Start Service - (void)startServiceWithType:(SDLServiceType)serviceType payload:(nullable NSData *)payload { diff --git a/SmartDeviceLink/SDLTCPTransport.h b/SmartDeviceLink/SDLTCPTransport.h index 6349bb1a8..a83cfd99c 100644 --- a/SmartDeviceLink/SDLTCPTransport.h +++ b/SmartDeviceLink/SDLTCPTransport.h @@ -5,9 +5,7 @@ NS_ASSUME_NONNULL_BEGIN -@interface SDLTCPTransport : NSObject <SDLTransportType> { - _Nullable CFSocketRef socket; -} +@interface SDLTCPTransport : NSObject <SDLTransportType, NSStreamDelegate> /** * Convenience init @@ -27,6 +25,7 @@ NS_ASSUME_NONNULL_BEGIN * The port number of Core */ @property (strong, nonatomic) NSString *portNumber; +@property (nonatomic, assign) NSUInteger receiveBufferSize; /** * The subscribed listener diff --git a/SmartDeviceLink/SDLTCPTransport.m b/SmartDeviceLink/SDLTCPTransport.m index b8b0a9520..0d6c445f9 100644 --- a/SmartDeviceLink/SDLTCPTransport.m +++ b/SmartDeviceLink/SDLTCPTransport.m @@ -1,43 +1,41 @@ +// // SDLTCPTransport.m +// SmartDeviceLink +// +// Created by Sho Amano on 2018/04/23. +// Copyright © 2018 Xevo Inc. All rights reserved. // - #import "SDLTCPTransport.h" -#import "SDLLogConstants.h" +#import "SDLMutableDataQueue.h" #import "SDLLogMacros.h" -#import "SDLLogManager.h" -#import "SDLHexUtility.h" -#import <errno.h> -#import <netdb.h> -#import <netinet/in.h> -#import <signal.h> -#import <stdio.h> -#import <sys/socket.h> -#import <sys/types.h> -#import <sys/wait.h> -#import <unistd.h> NS_ASSUME_NONNULL_BEGIN -// C function forward declarations. -int call_socket(const char *hostname, const char *port); -static void TCPCallback(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void *data, void *info); - -@interface SDLTCPTransport () { - dispatch_queue_t _sendQueue; -} - +NSString *const TCPIOThreadName = @"com.smartdevicelink.tcpiothread"; +NSTimeInterval const IOThreadWaitSecs = 1.0; +NSUInteger const DefaultReceiveBufferSize = 16 * 1024; +NSTimeInterval ConnectionTimeoutSecs = 30.0; + +@interface SDLTCPTransport () + +@property (nullable, nonatomic, strong) NSThread *ioThread; +@property (nonatomic, strong) dispatch_semaphore_t ioThreadStoppedSemaphore; +@property (nonatomic, strong) SDLMutableDataQueue *sendDataQueue; +@property (nullable, nonatomic, strong) NSInputStream *inputStream; +@property (nullable, nonatomic, strong) NSOutputStream *outputStream; +@property (nonatomic, assign) BOOL outputStreamHasSpace; +@property (nullable, nonatomic, strong) NSTimer *connectionTimer; +@property (nonatomic, assign) BOOL transportErrorNotified; @end - @implementation SDLTCPTransport - (instancetype)init { if (self = [super init]) { - _sendQueue = dispatch_queue_create("com.sdl.transport.tcp.transmit", DISPATCH_QUEUE_SERIAL); - SDLLogD(@"TCP Transport initialization"); + _receiveBufferSize = DefaultReceiveBufferSize; + _sendDataQueue = [[SDLMutableDataQueue alloc] init]; } - return self; } @@ -52,125 +50,260 @@ static void TCPCallback(CFSocketRef socket, CFSocketCallBackType type, CFDataRef } - (void)dealloc { + SDLLogD(@"SDLTCPTransport dealloc"); [self disconnect]; } +#pragma mark - SDLAbstractTransport methods + +// Note: When a connection is refused (e.g. TCP port number is not correct) or timed out (e.g. invalid IP address), +// then onTransportDisconnected will be notified *without* onTransportConnected event in advance. - (void)connect { - __weak typeof(self) weakself = self; - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - __strong typeof(self) strongself = weakself; - SDLLogD(@"Attemping to connect"); - - int sock_fd = call_socket([self.hostName UTF8String], [self.portNumber UTF8String]); - if (sock_fd < 0) { - SDLLogE(@"Server not ready, connection failed"); - return; - } - - CFSocketContext socketCtxt = {0, (__bridge void *)(self), NULL, NULL, NULL}; - strongself->socket = CFSocketCreateWithNative(kCFAllocatorDefault, sock_fd, kCFSocketDataCallBack | kCFSocketConnectCallBack, (CFSocketCallBack)&TCPCallback, &socketCtxt); - CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, strongself->socket, 0); - CFRunLoopRef loop = CFRunLoopGetCurrent(); - CFRunLoopAddSource(loop, source, kCFRunLoopDefaultMode); - CFRelease(source); - }]; + if (self.ioThread != nil) { + SDLLogW(@"TCP transport is already connected"); + return; + } + + unsigned int port; + int num = [self.portNumber intValue]; + if (0 <= num && num <= 65535) { + port = (unsigned int)num; + } else { + // specify an invalid port, so that once connection is initiated we will receive an error + // through delegate + port = 65536; + } + + self.ioThread = [[NSThread alloc] initWithTarget:self selector:@selector(sdl_tcpTransportEventLoop) object:nil]; + [self.ioThread setName:TCPIOThreadName]; + self.ioThreadStoppedSemaphore = dispatch_semaphore_create(0); + + CFReadStreamRef readStream = NULL; + CFWriteStreamRef writeStream = NULL; + CFStringRef hostName = (__bridge CFStringRef)self.hostName; + + CFStreamCreatePairWithSocketToHost(NULL, hostName, port, &readStream, &writeStream); + + // this transport is mainly for video streaming + CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVideo); + CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVideo); + + self.inputStream = (__bridge_transfer NSInputStream *)readStream; + self.outputStream = (__bridge_transfer NSOutputStream *)writeStream; + + [self.ioThread start]; +} + +- (void)disconnect { + if (self.ioThread == nil) { + // already disconnected + return; + } + + SDLLogD(@"Disconnecting TCP transport"); + + [self sdl_cancelIOThread]; + + long ret = dispatch_semaphore_wait(self.ioThreadStoppedSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(IOThreadWaitSecs * NSEC_PER_SEC))); + if (ret == 0) { + SDLLogD(@"TCP transport thread stopped"); + } else { + SDLLogE(@"Failed to stop TCP transport thread"); + } + self.ioThread = nil; + + self.inputStream = nil; + self.outputStream = nil; + + [self.sendDataQueue removeAllObjects]; + self.transportErrorNotified = NO; } - (void)sendData:(NSData *)msgBytes { - dispatch_async(_sendQueue, ^{ - @autoreleasepool { - SDLLogBytes(msgBytes, SDLLogBytesDirectionTransmit); - CFSocketError e = CFSocketSendData(self->socket, NULL, (__bridge CFDataRef)msgBytes, 10000); - if (e != kCFSocketSuccess) { - NSString *errorCause = nil; - switch (e) { - case kCFSocketTimeout: - errorCause = @"Socket Timeout Error."; - break; - - case kCFSocketError: - default: - errorCause = @"Socket Error."; - break; - } - - SDLLogE(@"Socket send error: %@", errorCause); + [self.sendDataQueue enqueueBuffer:msgBytes.mutableCopy]; + + [self performSelector:@selector(sdl_writeToStream) onThread:self.ioThread withObject:nil waitUntilDone:NO]; +} + +#pragma mark - Run loop + +- (void)sdl_tcpTransportEventLoop { + @autoreleasepool { + [self sdl_setupStream:self.inputStream]; + [self sdl_setupStream:self.outputStream]; + + // JFYI: NSStream itself has a connection timeout (about 1 minute). If you specify a large timeout value, + // you may get the NSStream's timeout event first. + self.connectionTimer = [NSTimer scheduledTimerWithTimeInterval:ConnectionTimeoutSecs target:self selector:@selector(sdl_onConnectionTimedOut:) userInfo:nil repeats:NO]; + + // these will initiate a connection to remote server + SDLLogD(@"Connecting to %@:%@ ...", self.hostName, self.portNumber); + [self.inputStream open]; + [self.outputStream open]; + + while (![self.ioThread isCancelled]) { + BOOL ret = [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + if (!ret) { + SDLLogW(@"Failed to start TCP transport run loop"); + break; } } - }); -} + SDLLogD(@"TCP transport run loop terminated"); -- (void)disconnect { - SDLLogD(@"Disconnect connection"); - - if (socket != nil) { - CFSocketInvalidate(socket); - CFRelease(socket); - socket = nil; + [self sdl_teardownStream:self.inputStream]; + [self sdl_teardownStream:self.outputStream]; + + [self.connectionTimer invalidate]; + + dispatch_semaphore_signal(self.ioThreadStoppedSemaphore); } } -@end +- (void)sdl_setupStream:(NSStream *)stream { + [stream setDelegate:self]; + [stream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; +} + +- (void)sdl_teardownStream:(NSStream *)stream { + NSStreamStatus streamStatus = stream.streamStatus; + if (streamStatus != NSStreamStatusNotOpen && streamStatus != NSStreamStatusClosed) { + [stream close]; + } + [stream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; + [stream setDelegate:nil]; +} -// C functions -int call_socket(const char *hostname, const char *port) { - int status, sock; - struct addrinfo hints; - struct addrinfo *servinfo; +- (void)sdl_cancelIOThread { + // set cancel flag + [self.ioThread cancel]; + // wake up the run loop in case we don't have any I/O event + [self performSelector:@selector(sdl_doNothing) onThread:self.ioThread withObject:nil waitUntilDone:NO]; +} - memset(&hints, 0, sizeof hints); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; +#pragma mark - Stream events +// these methods run only on the I/O thread (i.e. invoked from the run loop) + +- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode { + switch (eventCode) { + case NSStreamEventOpenCompleted: + // We will get two NSStreamEventOpenCompleted events (for both input and output streams) and + // we don't need both. Let's use the one of output stream since we need to make sure that + // output stream is ready before Proxy sending Start Service frame. + if (aStream == self.outputStream) { + SDLLogD(@"TCP transport connected"); + [self.connectionTimer invalidate]; + [self.delegate onTransportConnected]; + } + break; + case NSStreamEventHasBytesAvailable: + [self sdl_readFromStream]; + break; + case NSStreamEventHasSpaceAvailable: + self.outputStreamHasSpace = YES; + [self sdl_writeToStream]; + break; + case NSStreamEventErrorOccurred: + SDLLogW(@"TCP transport error occurred with %@ stream: %@", aStream == self.inputStream ? @"input" : @"output", aStream.streamError); + [self sdl_onStreamError:aStream]; + break; + case NSStreamEventEndEncountered: + SDLLogD(@"TCP transport %@ stream end encountered", aStream == self.inputStream ? @"input" : @"output"); + [self sdl_onStreamEnd:aStream]; + break; + default: + SDLLogW(@"Unknown TCP stream event: %lu", (unsigned long)eventCode); + break; + } +} - //no host name?, no problem, get local host - if (hostname == nil) { - char localhost[128]; - gethostname(localhost, sizeof localhost); - hostname = (const char *)&localhost; +- (void)sdl_readFromStream { + uint8_t *buffer = malloc(self.receiveBufferSize); + NSInteger readBytes = [self.inputStream read:buffer maxLength:self.receiveBufferSize]; + if (readBytes < 0) { + SDLLogW(@"TCP transport read error: %@", self.inputStream.streamError); + [self sdl_onStreamError:self.inputStream]; + free(buffer); + return; + } else if (readBytes == 0) { + SDLLogD(@"TCP transport input stream closed"); + [self sdl_onStreamEnd:self.inputStream]; + free(buffer); + return; } - //getaddrinfo setup - if ((status = getaddrinfo(hostname, port, &hints, &servinfo)) != 0) { - fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status)); - return (-1); + NSData *data = [NSData dataWithBytesNoCopy:buffer length:(NSUInteger)readBytes freeWhenDone:YES]; + [self.delegate onDataReceived:data]; +} + +- (void)sdl_writeToStream { + if (!self.outputStreamHasSpace) { + return; + } + if ([self.sendDataQueue count] == 0) { + // If send queue is empty, outputStreamHasSpace flag stays in YES. So once sendData is + // called, write to the stream will be attempted immediately. + return; } - //get socket - if ((sock = socket(servinfo->ai_family, servinfo->ai_socktype, servinfo->ai_protocol)) < 0) - return (-1); + NSMutableData *buffer = [self.sendDataQueue frontBuffer]; + NSUInteger bufferLen = buffer.length; + + NSInteger bytesWritten = [self.outputStream write:buffer.bytes maxLength:bufferLen]; + if (bytesWritten < 0) { + SDLLogW(@"TCP transport write error: %@", self.outputStream.streamError); + [self sdl_onStreamError:self.outputStream]; + return; + } else if (bytesWritten == 0) { + SDLLogD(@"TCP transport output stream closed"); + [self sdl_onStreamEnd:self.outputStream]; + return; + } - //connect - if (connect(sock, servinfo->ai_addr, servinfo->ai_addrlen) < 0) { - close(sock); - return (-1); + if (bytesWritten == bufferLen) { + // we have consumed all of data in this buffer + [self.sendDataQueue popBuffer]; + } else { + [buffer replaceBytesInRange:NSMakeRange(0, (NSUInteger)bytesWritten) withBytes:NULL length:0]; } - freeaddrinfo(servinfo); // free the linked-list - return (sock); + // the output stream may still have some spaces, but let's wait for another + // NSStreamEventHasSpaceAvailable event before writing + self.outputStreamHasSpace = NO; } -static void TCPCallback(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void *data, void *info) { - if (kCFSocketConnectCallBack == type) { - SDLTCPTransport *transport = (__bridge SDLTCPTransport *)info; - [transport.delegate onTransportConnected]; - } else if (kCFSocketDataCallBack == type) { - SDLTCPTransport *transport = (__bridge SDLTCPTransport *)info; +- (void)sdl_onConnectionTimedOut:(NSTimer *)timer { + SDLLogW(@"TCP connection timed out"); + [self sdl_cancelIOThread]; - // Check if Core disconnected from us - if (CFDataGetLength((CFDataRef)data) <= 0) { - SDLLogW(@"Remote system terminated connection, data packet length 0"); - [transport.delegate onTransportDisconnected]; + if (!self.transportErrorNotified) { + [self.delegate onTransportDisconnected]; + self.transportErrorNotified = YES; + } +} - return; - } +- (void)sdl_onStreamError:(NSStream *)stream { + // stop I/O thread + [self sdl_cancelIOThread]; - // Handle the data we received - NSData *convertedData = [NSData dataWithBytes:(UInt8 *)CFDataGetBytePtr((CFDataRef)data) length:(NSUInteger)CFDataGetLength((CFDataRef)data)]; - SDLLogBytes(convertedData, SDLLogBytesDirectionReceive); - [transport.delegate onDataReceived:convertedData]; - } else { - SDLLogW(@"Unhandled callback type: %lu", type); + // avoid notifying multiple error events + if (!self.transportErrorNotified) { + [self.delegate onTransportDisconnected]; + self.transportErrorNotified = YES; } } +- (void)sdl_onStreamEnd:(NSStream *)stream { + [self sdl_cancelIOThread]; + + if (!self.transportErrorNotified) { + [self.delegate onTransportDisconnected]; + self.transportErrorNotified = YES; + } +} + +- (void)sdl_doNothing { +} + +@end + NS_ASSUME_NONNULL_END diff --git a/SmartDeviceLinkTests/TransportSpecs/SDLTCPTransportSpec.m b/SmartDeviceLinkTests/TransportSpecs/SDLTCPTransportSpec.m new file mode 100644 index 000000000..db7fe419d --- /dev/null +++ b/SmartDeviceLinkTests/TransportSpecs/SDLTCPTransportSpec.m @@ -0,0 +1,637 @@ +// +// SDLTCPTransportSpec.m +// SmartDeviceLinkTests +// +// Created by Sho Amano on 2018/04/24. +// Copyright © 2018 Xevo Inc. All rights reserved. +// + +#import <Foundation/Foundation.h> +#import <Quick/Quick.h> +#import <Nimble/Nimble.h> +#import <OCMock/OCMock.h> + +#import "SDLTCPTransport.h" + +#import <sys/types.h> +#import <sys/socket.h> +#import <netdb.h> +#import <sys/select.h> +#import <sys/time.h> +#import <fcntl.h> +#import <stdio.h> +#import <string.h> +#import <errno.h> + +#define RECV_BUF_SIZE (1024) +#define MAX_SERVER_SOCKET_NUM (16) +#define THREAD_STOP_WAIT_SEC (1.0) + +@protocol TestTCPServerDelegate +- (void)onClientConnected; +- (void)onClientDataReceived:(NSData *)data; +- (void)onClientShutdown; +- (void)onClientError; +@end + +@interface TestTCPServer : NSObject { + int _serverSockets[MAX_SERVER_SOCKET_NUM]; + int _internalSockets[2]; + int _clientSocket; // supports only one client +} + +@property (nullable, nonatomic, weak) id<TestTCPServerDelegate> delegate; +@property (nullable, nonatomic, strong) NSThread *thread; +@property (nonatomic, strong) dispatch_semaphore_t threadStoppedSemaphore; +@property (nonatomic, strong) NSMutableArray<NSMutableData*> *sendData; +@property (nonatomic, assign) BOOL enableSOReuseAddr; +@end + +@implementation TestTCPServer + +- (instancetype)init { + if (self = [super init]) { + for (unsigned int i = 0; i < MAX_SERVER_SOCKET_NUM; i++) { + _serverSockets[i] = -1; + } + _sendData = [[NSMutableArray alloc] init]; + _enableSOReuseAddr = YES; + } + return self; +} + +- (void)dealloc { + [self teardown]; +} + +- (BOOL)setup:(NSString *)hostName port:(NSString *)port { + int ret = socketpair(PF_UNIX, SOCK_STREAM, 0, _internalSockets); + if (ret < 0) { + NSLog(@"SDLTCPTransportSpec: socketpair() failed"); + return NO; + } + if (!([self configureSocket:_internalSockets[0]] && [self configureSocket:_internalSockets[1]])) { + return NO; + } + + struct addrinfo hints, *res; + hints.ai_family = PF_INET6; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + hints.ai_flags = AI_PASSIVE /* server socket */ + | AI_NUMERICSERV /* 2nd arg is numeric port number */ + | AI_ALL | AI_V4MAPPED; /* return both IPv4 and IPv6 addresses */ + + ret = getaddrinfo([hostName UTF8String], [port UTF8String], &hints, &res); + if (ret != 0) { + NSLog(@"Error: SDLTCPTransportSpec getaddrinfo() failed, %s", gai_strerror(ret)); + return NO; + } + + int socketNum = 0; + for (struct addrinfo *info = res; info != NULL && socketNum < (MAX_SERVER_SOCKET_NUM - 1); info = info->ai_next) { + int sock = socket(info->ai_family, info->ai_socktype, info->ai_protocol); + if (sock < 0) { + NSLog(@"Error SDLTCPTransportSpec server socket creation failed"); + continue; + } + + if (![self configureServerSocket:sock]) { + close(sock); + continue; + } + + ret = bind(sock, info->ai_addr, info->ai_addrlen); + if (ret < 0) { + NSLog(@"Error SDLTCPTransportSpec server socket bind() failed: %s", strerror(errno)); + close(sock); + continue; + } + + ret = listen(sock, 5); + if (ret < 0) { + NSLog(@"Error SDLTCPTransportSpec server socket listen() failed: %s", strerror(errno)); + close(sock); + continue; + } + + _serverSockets[socketNum] = sock; + socketNum++; + } + freeaddrinfo(res); + + _clientSocket = -1; + + // create a thread and run + self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil]; + self.threadStoppedSemaphore = dispatch_semaphore_create(0); + NSLog(@"SDLTCPTransportSpec starting TCP server"); + [self.thread start]; + + return YES; +} + +- (BOOL)teardown { + if (self.thread == nil) { + return YES; + } + + BOOL result = YES; + + // wake up select() and let it stop + shutdown(_internalSockets[1], SHUT_WR); + + long ret = dispatch_semaphore_wait(self.threadStoppedSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(THREAD_STOP_WAIT_SEC * NSEC_PER_SEC))); + if (ret != 0) { + NSLog(@"Error: SDLTCPTransportSpec thread doesn't stop"); + result = NO; + } + self.thread = nil; + + for (unsigned int i = 0; i < MAX_SERVER_SOCKET_NUM; i++) { + if (_serverSockets[i] >= 0) { + close(_serverSockets[i]); + _serverSockets[i] = -1; + } + } + if (_internalSockets[0] >= 0) { + close(_internalSockets[0]); + _internalSockets[0] = -1; + } + if (_internalSockets[1] >= 0) { + close(_internalSockets[1]); + _internalSockets[1] = -1; + } + + [self.sendData removeAllObjects]; + return result; +} + +- (void)send:(NSData *)data { + [self.sendData addObject:[data mutableCopy]]; + + // wake up select() + char buf[1] = {'a'}; + send(_internalSockets[1], buf, sizeof(buf), 0); +} + +- (BOOL)shutdownClient { + if (_clientSocket < 0) { + // client is not connected + return NO; + } + int ret = shutdown(_clientSocket, SHUT_WR); + if (ret != 0) { + NSLog(@"SDLTCPTransportSpec shutdown() for client socket failed: %s", strerror(errno)); + return NO; + } + return YES; +} + +- (void)run:(id)userInfo { + BOOL running = YES; + BOOL internalFailure = NO; + int ret; + + while (running) { + fd_set readfds; + fd_set writefds; + int maxFd = 0; + + FD_ZERO(&readfds); + FD_ZERO(&writefds); + + for (unsigned int i = 0; _serverSockets[i] >= 0; i++) { + FD_SET(_serverSockets[i], &readfds); + if (_serverSockets[i] > maxFd) { + maxFd = _serverSockets[i]; + } + } + FD_SET(_internalSockets[0], &readfds); + if (_internalSockets[0] > maxFd) { + maxFd = _internalSockets[0]; + } + + if (_clientSocket >= 0) { + FD_SET(_clientSocket, &readfds); + if ([self.sendData count] > 0) { + FD_SET(_clientSocket, &writefds); + } + + if (_clientSocket > maxFd) { + maxFd = _clientSocket; + } + } + + ret = select(maxFd + 1, &readfds, &writefds, NULL, NULL); + if (ret < 0) { + NSLog(@"Error: SDLTCPTransportSpec TCP server select() failed"); + internalFailure = YES; + break; + } + + // client socket check + if (_clientSocket >= 0) { + if (FD_ISSET(_clientSocket, &readfds)) { + char buf[RECV_BUF_SIZE]; + ssize_t recvLen = recv(_clientSocket, buf, sizeof(buf), 0); + if (recvLen < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // this is not an error + } else { + NSLog(@"SDLTCPTransportSpec recv() for client socket failed: %s", strerror(errno)); + [self.delegate onClientError]; + close(_clientSocket); + _clientSocket = -1; + } + } else if (recvLen == 0) { + [self.delegate onClientShutdown]; + // keep the socket open in case we have some more data to send + } else { + NSData *data = [NSData dataWithBytes:buf length:recvLen]; + [self.delegate onClientDataReceived:data]; + } + } + if (FD_ISSET(_clientSocket, &writefds)) { + NSMutableData *data = self.sendData[0]; + ssize_t sentLen = send(_clientSocket, data.bytes, data.length, 0); + if (sentLen < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // this is not an error + } else { + NSLog(@"SDLTCPTransportSpec send() for client socket failed: %s", strerror(errno)); + [self.delegate onClientError]; + close(_clientSocket); + _clientSocket = -1; + } + } else if (sentLen > 0) { + if (data.length == (NSUInteger)sentLen) { + [self.sendData removeObjectAtIndex:0]; + } else { + [data replaceBytesInRange:NSMakeRange(0, sentLen) withBytes:NULL length:0]; + } + } + } + } + + // server socket check + for (unsigned int i = 0; _serverSockets[i] >= 0; i++) { + int sock = _serverSockets[i]; + if (FD_ISSET(sock, &readfds)) { + struct sockaddr_storage addr; + socklen_t addrlen; + ret = accept(sock, (struct sockaddr *)&addr, &addrlen); + if (ret < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // this is not an error + continue; + } else { + NSLog(@"Error: SDLTCPTransportSpec TCP server accept() failed: %s", strerror(errno)); + internalFailure = YES; + running = NO; + break; + } + } + + if (_clientSocket >= 0) { + NSLog(@"Error: SDLTCPTransportSpec TCP server received more than one connections"); + } + + if (![self configureSocket:ret]) { + close(ret); + internalFailure = YES; + running = NO; + break; + }; + + _clientSocket = ret; + [self.delegate onClientConnected]; + } + } + + // internal pipe check + if (FD_ISSET(_internalSockets[0], &readfds)) { + char buf[16]; + ssize_t recvLen = recv(_internalSockets[0], buf, sizeof(buf), 0); + if (recvLen < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // this is not an error + } else { + NSLog(@"Error: SDLTCPTransportSpec TCP server recv() failed for internal pipe: %s", strerror(errno)); + internalFailure = YES; + break; + } + } else if (recvLen == 0) { + NSLog(@"SDLTCPTransportSpec stopping TCP server"); + break; + } + } + } + + if (_clientSocket >= 0) { + close(_clientSocket); + _clientSocket = -1; + } + + expect(internalFailure == NO); + + dispatch_semaphore_signal(self.threadStoppedSemaphore); +} + +- (BOOL)configureSocket:(int)sock { + // make the socket non-blocking + int flags; + flags = fcntl(sock, F_GETFL, 0); + if (flags == -1) { + NSLog(@"Error: SDLTCPTransportSpec fcntl (F_GETFL) failed"); + return NO; + } + int ret = fcntl(sock, F_SETFL, flags | O_NONBLOCK); + if (ret == -1) { + NSLog(@"Error: SDLTCPTransportSpec fcntl (F_SETFL) failed: %s", strerror(errno)); + return NO; + } + + // don't generate SIGPIPE signal + int val = 1; + ret = setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, &val, sizeof(val)); + if (ret != 0) { + NSLog(@"Error: SDLTCPTransportSpec setsockopt() failed"); + return NO; + } + + return YES; +} + +- (BOOL)configureServerSocket:(int)sock { + if (![self configureSocket:sock]) { + return NO; + } + + if (self.enableSOReuseAddr) { + int val = 1; + int ret = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)); + if (ret != 0) { + NSLog(@"Error: SDLTCPTransportSpec setsockopt() failed"); + return NO; + } + } + + return YES; +} +@end + +@interface SDLTCPTransport () +// verify some internal properties +@property (nullable, nonatomic, strong) NSThread *ioThread; +@property (nullable, nonatomic, strong) NSInputStream *inputStream; +@property (nullable, nonatomic, strong) NSOutputStream *outputStream; +@end + +QuickSpecBegin(SDLTCPTransportSpec) + +describe(@"SDLTCPTransport", ^ { + __block SDLTCPTransport *transport = nil; + __block id transportDelegateMock = nil; + __block TestTCPServer *server = nil; + __block id serverDelegateMock = nil; + + beforeEach(^{ + transport = [[SDLTCPTransport alloc] init]; + transport.hostName = @"localhost"; + transport.portNumber = @"12345"; + transportDelegateMock = OCMProtocolMock(@protocol(SDLTransportDelegate)); + transport.delegate = transportDelegateMock; + + server = [[TestTCPServer alloc] init]; + serverDelegateMock = OCMProtocolMock(@protocol(TestTCPServerDelegate)); + server.delegate = serverDelegateMock; + }); + + afterEach(^{ + transport.delegate = nil; + server.delegate = nil; + + [transport disconnect]; + transport = nil; + transportDelegateMock = nil; + + [server teardown]; + server = nil; + serverDelegateMock = nil; + }); + + it(@"Should be able to connect to specified TCP server and disconnect from it", ^ { + BOOL ret = [server setup:@"localhost" port:@"12345"]; + expect(ret); + + OCMExpect([serverDelegateMock onClientConnected]); + OCMExpect([transportDelegateMock onTransportConnected]); + + [transport connect]; + + OCMVerifyAllWithDelay(serverDelegateMock, 0.5); + OCMVerifyAllWithDelay(transportDelegateMock, 0.5); + + expect(transport.ioThread != nil); + expect(transport.inputStream != nil); + expect(transport.outputStream != nil); + + [transport disconnect]; + + expect(transport.ioThread == nil); + expect(transport.inputStream == nil); + expect(transport.outputStream == nil); + }); + + it(@"Should generate a disconnect event when connection is refused", ^ { + // Start the server without SO_REUSEADDR then close it. Then the port will not be owned by anybody for a while. + server.enableSOReuseAddr = NO; + BOOL ret = [server setup:@"localhost" port:@"12346"]; + expect(ret); + [server teardown]; + server = nil; + + OCMExpect([transportDelegateMock onTransportDisconnected]); + + transport.portNumber = @"12346"; + [transport connect]; + + OCMVerifyAllWithDelay(transportDelegateMock, 0.5); + + [transport disconnect]; + + expect(transport.ioThread == nil); + expect(transport.inputStream == nil); + expect(transport.outputStream == nil); + }); + + it(@"Should generate a disconnect event when connection is timed out", ^ { + OCMExpect([transportDelegateMock onTransportDisconnected]); + + transport.hostName = @"127.0.0.2"; + [transport connect]; + + // timeout value should be longer than 'ConnectionTimeoutSecs' in SDLTCPTransport + OCMVerifyAllWithDelay(transportDelegateMock, 60.0); + + [transport disconnect]; + + expect(transport.ioThread == nil); + expect(transport.inputStream == nil); + expect(transport.outputStream == nil); + }); + + it(@"Should generate a disconnect event when input parameter is invalid", ^ { + OCMExpect([transportDelegateMock onTransportDisconnected]); + + transport.portNumber = @"abcde"; + [transport connect]; + + OCMVerifyAllWithDelay(transportDelegateMock, 0.5); + + [transport disconnect]; + + expect(transport.ioThread == nil); + expect(transport.inputStream == nil); + expect(transport.outputStream == nil); + }); + + it(@"Should send out data when send is called", ^ { + BOOL ret = [server setup:@"localhost" port:@"12345"]; + expect(ret); + + char buf[256]; + snprintf(buf, sizeof(buf), "This is dummy message."); + NSData *testData = [NSData dataWithBytes:buf length:strlen(buf)]; + NSMutableData *receivedData = [[NSMutableData alloc] init]; + + OCMExpect([serverDelegateMock onClientConnected]); + OCMStub([serverDelegateMock onClientDataReceived:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained NSData *data; + [invocation getArgument:&data atIndex:2]; // first argument is index 2 + [receivedData appendData:data]; + NSLog(@"mock server received %lu bytes", data.length); + }); + + OCMExpect([transportDelegateMock onTransportConnected]); + + [transport connect]; + [transport sendData:testData]; + + OCMVerifyAllWithDelay(serverDelegateMock, 0.5); + OCMVerifyAllWithDelay(transportDelegateMock, 0.5); + + [NSThread sleepForTimeInterval:0.5]; + expect([receivedData isEqualToData:testData]); + + [transport disconnect]; + }); + + it(@"Should send out data even if send is called some time after", ^ { + BOOL ret = [server setup:@"localhost" port:@"12345"]; + expect(ret); + + char buf1[256], buf2[256]; + snprintf(buf1, sizeof(buf1), "This is another dummy message."); + snprintf(buf2, sizeof(buf2), "followed by 12345678901234567890123456"); + NSData *testData1 = [NSData dataWithBytes:buf1 length:strlen(buf1)]; + NSData *testData2 = [NSData dataWithBytes:buf2 length:strlen(buf2)]; + NSMutableData *expectedData = [NSMutableData dataWithData:testData1]; + [expectedData appendData:testData2]; + + __block NSMutableData *receivedData = [[NSMutableData alloc] init]; + + OCMExpect([serverDelegateMock onClientConnected]); + OCMStub([serverDelegateMock onClientDataReceived:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained NSData *data; + [invocation getArgument:&data atIndex:2]; // first argument is index 2 + [receivedData appendData:data]; + NSLog(@"mock server received %lu bytes", data.length); + }); + + OCMExpect([transportDelegateMock onTransportConnected]); + + [transport connect]; + + // check that transport still sends out data long after NSStreamEventHasSpaceAvailable event + [NSThread sleepForTimeInterval:1.0]; + [transport sendData:testData1]; + [transport sendData:testData2]; + + OCMVerifyAllWithDelay(serverDelegateMock, 0.5); + OCMVerifyAllWithDelay(transportDelegateMock, 0.5); + + [NSThread sleepForTimeInterval:0.5]; + expect([receivedData isEqualToData:expectedData]); + + // don't receive further delegate events + server.delegate = nil; + + [transport disconnect]; + }); + + it(@"Should invoke onDataReceived delegate when received some data", ^ { + BOOL ret = [server setup:@"localhost" port:@"12345"]; + expect(ret); + + char buf1[256], buf2[256]; + snprintf(buf1, sizeof(buf1), "This is test data."); + snprintf(buf2, sizeof(buf2), "This is another chunk of data."); + NSData *testData1 = [NSData dataWithBytes:buf1 length:strlen(buf1)]; + NSData *testData2 = [NSData dataWithBytes:buf2 length:strlen(buf2)]; + NSMutableData *expectedData = [NSMutableData dataWithData:testData1]; + [expectedData appendData:testData2]; + + OCMExpect([transportDelegateMock onTransportConnected]); + + NSMutableData *receivedData = [[NSMutableData alloc] init]; + OCMStub([transportDelegateMock onDataReceived:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained NSData *data; + [invocation getArgument:&data atIndex:2]; // first argument is index 2 + [receivedData appendData:data]; + NSLog(@"client received %lu bytes", data.length); + }); + + OCMExpect([serverDelegateMock onClientConnected]); + + [transport connect]; + + // wait until connected + OCMVerifyAllWithDelay(serverDelegateMock, 0.5); + [server send:testData1]; + [server send:testData2]; + + OCMVerifyAllWithDelay(transportDelegateMock, 0.5); + + [NSThread sleepForTimeInterval:0.5]; + expect([receivedData isEqualToData:expectedData]); + + [transport disconnect]; + }); + + it(@"Should generate disconnected event after peer closed connection", ^ { + BOOL ret = [server setup:@"localhost" port:@"12345"]; + expect(ret); + + OCMExpect([serverDelegateMock onClientConnected]); + OCMExpect([transportDelegateMock onTransportConnected]); + + [transport connect]; + + OCMVerifyAllWithDelay(serverDelegateMock, 0.5); + OCMVerifyAllWithDelay(transportDelegateMock, 0.5); + + OCMExpect([transportDelegateMock onTransportDisconnected]); + + // Close the writing connection. This will notify the client that peer closed the connection. + ret = [server shutdownClient]; + expect(ret); + + OCMVerifyAllWithDelay(transportDelegateMock, 0.5); + + [transport disconnect]; + }); +}); + +QuickSpecEnd |