summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSho Amano <samano@xevo.com>2018-04-23 20:07:57 +0900
committerSho Amano <samano@xevo.com>2018-07-03 15:02:33 +0900
commit5fc4e5b75d7db966780e713a111afc85170ab853 (patch)
tree8c1a4247238f6e7f8252feda400f47300f5b21d2
parent51d3908b239ddf17379781b64da79f7c9a909b49 (diff)
downloadsdl_ios-5fc4e5b75d7db966780e713a111afc85170ab853.tar.gz
Rewrite TCP transport using CFNetwork API
-rw-r--r--SmartDeviceLink-iOS.xcodeproj/project.pbxproj12
-rw-r--r--SmartDeviceLink/SDLProtocol.m8
-rw-r--r--SmartDeviceLink/SDLTCPTransport.h5
-rw-r--r--SmartDeviceLink/SDLTCPTransport.m363
-rw-r--r--SmartDeviceLinkTests/TransportSpecs/SDLTCPTransportSpec.m637
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