summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Fischer <joeljfischer@gmail.com>2018-08-14 15:22:57 -0400
committerGitHub <noreply@github.com>2018-08-14 15:22:57 -0400
commitef0cfa3a77a32d36ce0bcb8c2fb8d580b378dc2b (patch)
tree08b13aac2f8c8f228f2a95ea40deb68ee8609d80
parent3cb0f3be950203b8c0737ffe36debb2628d8d707 (diff)
parentc1883ffb63162b4a213675b841147f9bcaac9e29 (diff)
downloadsdl_ios-ef0cfa3a77a32d36ce0bcb8c2fb8d580b378dc2b.tar.gz
Merge pull request #946 from XevoInc/feature/rewrite_tcp_transport
TCP Transport Rewrite
-rw-r--r--SmartDeviceLink-iOS.xcodeproj/project.pbxproj18
-rw-r--r--SmartDeviceLink/SDLError.h8
-rw-r--r--SmartDeviceLink/SDLError.m39
-rw-r--r--SmartDeviceLink/SDLErrorConstants.h25
-rw-r--r--SmartDeviceLink/SDLNotificationConstants.h1
-rw-r--r--SmartDeviceLink/SDLNotificationConstants.m1
-rw-r--r--SmartDeviceLink/SDLNotificationDispatcher.m4
-rw-r--r--SmartDeviceLink/SDLProtocol.m8
-rw-r--r--SmartDeviceLink/SDLProtocolListener.h9
-rw-r--r--SmartDeviceLink/SDLProxy.m4
-rw-r--r--SmartDeviceLink/SDLTCPTransport.h4
-rw-r--r--SmartDeviceLink/SDLTCPTransport.m405
-rw-r--r--SmartDeviceLink/SDLTransportDelegate.h1
-rw-r--r--SmartDeviceLinkTests/TestUtilities/TestTCPServer.h71
-rw-r--r--SmartDeviceLinkTests/TestUtilities/TestTCPServer.m370
-rw-r--r--SmartDeviceLinkTests/TransportSpecs/SDLTCPTransportSpec.m291
16 files changed, 1143 insertions, 116 deletions
diff --git a/SmartDeviceLink-iOS.xcodeproj/project.pbxproj b/SmartDeviceLink-iOS.xcodeproj/project.pbxproj
index c819cf763..7320fe8dd 100644
--- a/SmartDeviceLink-iOS.xcodeproj/project.pbxproj
+++ b/SmartDeviceLink-iOS.xcodeproj/project.pbxproj
@@ -1358,6 +1358,8 @@
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 */; };
+ EEA41D45210BA8CF0006CB6E /* TestTCPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = EEA41D44210BA8CF0006CB6E /* TestTCPServer.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 */; };
@@ -2848,6 +2850,9 @@
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>"; };
+ EEA41D43210BA89B0006CB6E /* TestTCPServer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TestTCPServer.h; path = TestUtilities/TestTCPServer.h; sourceTree = "<group>"; };
+ EEA41D44210BA8CF0006CB6E /* TestTCPServer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = TestTCPServer.m; path = TestUtilities/TestTCPServer.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>"; };
@@ -4549,6 +4554,8 @@
5DB1BCE21D2455FD002FFC37 /* Connection Manager */,
5D6035D0202CD46200A429C9 /* SDLSpecUtilities.h */,
5D6035D1202CD46200A429C9 /* SDLSpecUtilities.m */,
+ EEA41D43210BA89B0006CB6E /* TestTCPServer.h */,
+ EEA41D44210BA8CF0006CB6E /* TestTCPServer.m */,
);
name = "Test Utilities";
sourceTree = "<group>";
@@ -4606,6 +4613,7 @@
5D59DD451B14FDD000BE744D /* ProxySpecs */,
5DB92D201AC47AC400C15BB0 /* UtilitiesSpecs */,
1680B1041A9CD7AD00DBD79E /* ProtocolSpecs */,
+ EE5D1B31208EBC7100D17216 /* TransportSpecs */,
162E81E01A9BDE8A00906325 /* RPCSpecs */,
5D61FA2D1A84237100846EE7 /* Supporting Files */,
167ED9451A9BCE5D00797BE5 /* SwiftSpec.swift */,
@@ -5564,6 +5572,14 @@
name = "@categories";
sourceTree = "<group>";
};
+ EE5D1B31208EBC7100D17216 /* TransportSpecs */ = {
+ isa = PBXGroup;
+ children = (
+ EE5D1B32208EBCA900D17216 /* SDLTCPTransportSpec.m */,
+ );
+ path = TransportSpecs;
+ sourceTree = "<group>";
+ };
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -6954,6 +6970,7 @@
162E82F01A9BDE8B00906325 /* SDLPowerModeQualificationStatusSpec.m in Sources */,
162E82CD1A9BDE8A00906325 /* SDLAudioStreamingStateSpec.m in Sources */,
1EE8C4461F3837D200FDC2CF /* SDLModuleDataSpec.m in Sources */,
+ EEA41D45210BA8CF0006CB6E /* TestTCPServer.m in Sources */,
1E89B0DE2031636000A47992 /* SDLSeatControlDataSpec.m in Sources */,
162E831A1A9BDE8B00906325 /* SDLOnLockScreenStatusSpec.m in Sources */,
162E83431A9BDE8B00906325 /* SDLSyncPDataSpec.m in Sources */,
@@ -7024,6 +7041,7 @@
5D0A9F911F15550400CC80DD /* SDLSystemCapabilityTypeSpec.m in Sources */,
5DBF0D601F3B3DB4008AF2C9 /* SDLControlFrameVideoStartServiceAckSpec.m in Sources */,
162E83311A9BDE8B00906325 /* SDLListFilesSpec.m in Sources */,
+ EE5D1B33208EBCA900D17216 /* SDLTCPTransportSpec.m in Sources */,
1EAA477C2036BD6D000FE74B /* SDLSDLSRGBColorSpec.m in Sources */,
DA9F7EB01DCC063400ACAE48 /* SDLLocationDetailsSpec.m in Sources */,
5DC978261B7A38640012C2F1 /* SDLGlobalsSpec.m in Sources */,
diff --git a/SmartDeviceLink/SDLError.h b/SmartDeviceLink/SDLError.h
index 9711d8e51..4a0cbd7ca 100644
--- a/SmartDeviceLink/SDLError.h
+++ b/SmartDeviceLink/SDLError.h
@@ -23,6 +23,7 @@ extern SDLErrorDomain *const SDLErrorDomainTextAndGraphicManager;
extern SDLErrorDomain *const SDLErrorDomainSoftButtonManager;
extern SDLErrorDomain *const SDLErrorDomainMenuManager;
extern SDLErrorDomain *const SDLErrorDomainChoiceSetManager;
+extern SDLErrorDomain *const SDLErrorDomainTransport;
@interface NSError (SDLErrors)
@@ -65,6 +66,13 @@ extern SDLErrorDomain *const SDLErrorDomainChoiceSetManager;
+ (NSError *)sdl_choiceSetManager_choiceDeletionFailed:(NSDictionary *)userInfo;
+ (NSError *)sdl_choiceSetManager_choiceUploadFailed:(NSDictionary *)userInfo;
+#pragma mark Transport
+
++ (NSError *)sdl_transport_unknownError;
++ (NSError *)sdl_transport_connectionRefusedError;
++ (NSError *)sdl_transport_connectionTimedOutError;
++ (NSError *)sdl_transport_networkDownError;
+
@end
@interface NSException (SDLExceptions)
diff --git a/SmartDeviceLink/SDLError.m b/SmartDeviceLink/SDLError.m
index 4f067339b..2597a563d 100644
--- a/SmartDeviceLink/SDLError.m
+++ b/SmartDeviceLink/SDLError.m
@@ -18,6 +18,7 @@ SDLErrorDomain *const SDLErrorDomainTextAndGraphicManager = @"com.sdl.textandgra
SDLErrorDomain *const SDLErrorDomainSoftButtonManager = @"com.sdl.softbuttonmanager.error";
SDLErrorDomain *const SDLErrorDomainMenuManager = @"com.sdl.menumanager.error";
SDLErrorDomain *const SDLErrorDomainChoiceSetManager = @"com.sdl.choicesetmanager.error";
+SDLErrorDomain *const SDLErrorDomainTransport = @"com.sdl.transport.error";
@implementation NSError (SDLErrors)
@@ -217,6 +218,44 @@ SDLErrorDomain *const SDLErrorDomainChoiceSetManager = @"com.sdl.choicesetmanage
return [NSError errorWithDomain:SDLErrorDomainChoiceSetManager code:SDLChoiceSetManagerErrorUploadFailed userInfo:userInfo];
}
+#pragma mark Transport
+
++ (NSError *)sdl_transport_unknownError {
+ NSDictionary<NSString *, NSString *> *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"TCP connection error", nil),
+ NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"TCP connection cannot be established due to unknown error.", nil),
+ NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Make sure that correct IP address and TCP port number are specified, and the phone is connected to the correct Wi-Fi network.", nil)
+ };
+ return [NSError errorWithDomain:SDLErrorDomainTransport code:SDLTransportErrorUnknown userInfo:userInfo];
+}
+
++ (NSError *)sdl_transport_connectionRefusedError {
+ NSDictionary<NSString *, NSString *> *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"TCP connection cannot be established", nil),
+ NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"The TCP connection is refused by head unit. Possible causes are that the specified TCP port number is not correct, or SDL Core is not running properly on the head unit.", nil),
+ NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Make sure that correct IP address and TCP port number are specified. Also, make sure that SDL Core on the head unit enables TCP transport.", nil)
+ };
+ return [NSError errorWithDomain:SDLErrorDomainTransport code:SDLTransportErrorConnectionRefused userInfo:userInfo];
+}
+
++ (NSError *)sdl_transport_connectionTimedOutError {
+ NSDictionary<NSString *, NSString *> *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"TCP connection timed out", nil),
+ NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"The TCP connection cannot be established within a given time. Possible causes are that the specified IP address is not correct, or the connection is blocked by a firewall.", nil),
+ NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Make sure that correct IP address and TCP port number are specified. Also, make sure that the head unit's system configuration accepts TCP connections.", nil)
+ };
+ return [NSError errorWithDomain:SDLErrorDomainTransport code:SDLTransportErrorConnectionTimedOut userInfo:userInfo];
+}
+
++ (NSError *)sdl_transport_networkDownError {
+ NSDictionary<NSString *, NSString *> *userInfo = @{
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Network is not available", nil),
+ NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"TCP connection cannot be established because the phone is not connected to the network. Possible causes are: Wi-Fi being disabled on the phone or the phone is connected to a wrong Wi-Fi network.", nil),
+ NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Make sure that the phone is connected to the Wi-Fi network that has the head unit on it. Also, make sure that correct IP address and TCP port number are specified.", nil)
+ };
+ return [NSError errorWithDomain:SDLErrorDomainTransport code:SDLTransportErrorNetworkDown userInfo:userInfo];
+}
+
@end
diff --git a/SmartDeviceLink/SDLErrorConstants.h b/SmartDeviceLink/SDLErrorConstants.h
index 260203c95..8503d8be8 100644
--- a/SmartDeviceLink/SDLErrorConstants.h
+++ b/SmartDeviceLink/SDLErrorConstants.h
@@ -120,3 +120,28 @@ typedef NS_ENUM(NSInteger, SDLChoiceSetManagerError) {
SDLChoiceSetManagerErrorDeletionFailed = -2,
SDLChoiceSetManagerErrorUploadFailed = -3,
};
+
+/**
+ * Errors associated with transport.
+ */
+typedef NS_ENUM(NSInteger, SDLTransportError) {
+ /**
+ * Connection cannot be established due to a reason not listed here.
+ */
+ SDLTransportErrorUnknown = -1,
+ /**
+ * TCP connection is refused.
+ * Probably specified port number is invalid, or SDL Core is not running on the head unit.
+ */
+ SDLTransportErrorConnectionRefused = -2,
+ /**
+ * TCP connection cannot be established within given time.
+ * Probably because of wrong IP address, or the connection may be blocked by a firewall.
+ */
+ SDLTransportErrorConnectionTimedOut = -3,
+ /**
+ * TCP connection cannot be established since network is down.
+ * Probably the phone is not connected to the correct network.
+ */
+ SDLTransportErrorNetworkDown = -4,
+};
diff --git a/SmartDeviceLink/SDLNotificationConstants.h b/SmartDeviceLink/SDLNotificationConstants.h
index 1a0133118..a2a371d3f 100644
--- a/SmartDeviceLink/SDLNotificationConstants.h
+++ b/SmartDeviceLink/SDLNotificationConstants.h
@@ -106,6 +106,7 @@ extern SDLNotificationUserInfoKey const SDLNotificationUserInfoObject;
#pragma mark - General notifications
extern SDLNotificationName const SDLTransportDidDisconnect;
extern SDLNotificationName const SDLTransportDidConnect;
+extern SDLNotificationName const SDLTransportConnectError;
extern SDLNotificationName const SDLDidReceiveError;
extern SDLNotificationName const SDLDidReceiveLockScreenIcon;
extern SDLNotificationName const SDLDidBecomeReady;
diff --git a/SmartDeviceLink/SDLNotificationConstants.m b/SmartDeviceLink/SDLNotificationConstants.m
index 7bda755a1..35f12b473 100644
--- a/SmartDeviceLink/SDLNotificationConstants.m
+++ b/SmartDeviceLink/SDLNotificationConstants.m
@@ -16,6 +16,7 @@ SDLNotificationUserInfoKey const SDLNotificationUserInfoObject = @"SDLNotificati
#pragma mark - General notifications
SDLNotificationName const SDLTransportDidDisconnect = @"com.sdl.transport.disconnect";
SDLNotificationName const SDLTransportDidConnect = @"com.sdl.transport.connect";
+SDLNotificationName const SDLTransportConnectError = @"com.sdl.transport.connectError";
SDLNotificationName const SDLDidReceiveError = @"com.sdl.general.error";
SDLNotificationName const SDLDidReceiveLockScreenIcon = @"com.sdl.general.lockscreenIconReceived";
SDLNotificationName const SDLDidBecomeReady = @"com.sdl.notification.managerReady";
diff --git a/SmartDeviceLink/SDLNotificationDispatcher.m b/SmartDeviceLink/SDLNotificationDispatcher.m
index d28842852..f2a4b6950 100644
--- a/SmartDeviceLink/SDLNotificationDispatcher.m
+++ b/SmartDeviceLink/SDLNotificationDispatcher.m
@@ -59,6 +59,10 @@ NS_ASSUME_NONNULL_BEGIN
[self postNotificationName:SDLTransportDidDisconnect infoObject:nil];
}
+- (void)onTransportError:(NSError *)error {
+ [self postNotificationName:SDLTransportConnectError infoObject:error];
+}
+
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincompatible-pointer-types"
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/SDLProtocolListener.h b/SmartDeviceLink/SDLProtocolListener.h
index 35d6a7623..90685a46c 100644
--- a/SmartDeviceLink/SDLProtocolListener.h
+++ b/SmartDeviceLink/SDLProtocolListener.h
@@ -80,6 +80,15 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)onError:(NSString *)info exception:(NSException *)e;
+/**
+ * Called when an error is notified from transport.
+ *
+ * Note: currently, this is used only by TCP transport.
+ *
+ * @param error The type of the error
+ */
+- (void)onTransportError:(NSError *)error;
+
@end
NS_ASSUME_NONNULL_END
diff --git a/SmartDeviceLink/SDLProxy.m b/SmartDeviceLink/SDLProxy.m
index 69ee0618f..5c78bc68b 100644
--- a/SmartDeviceLink/SDLProxy.m
+++ b/SmartDeviceLink/SDLProxy.m
@@ -243,6 +243,10 @@ static float DefaultConnectionTimeout = 45.0;
[self invokeMethodOnDelegates:@selector(onError:) withObject:e];
}
+- (void)onTransportError:(NSError *)error {
+ [self invokeMethodOnDelegates:@selector(onTransportError:) withObject:error];
+}
+
- (void)handleProtocolStartServiceACKMessage:(SDLProtocolMessage *)startServiceACK {
// Turn off the timer, the start session response came back
[self.startSessionTimer cancel];
diff --git a/SmartDeviceLink/SDLTCPTransport.h b/SmartDeviceLink/SDLTCPTransport.h
index 6349bb1a8..5b7ac09f0 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
diff --git a/SmartDeviceLink/SDLTCPTransport.m b/SmartDeviceLink/SDLTCPTransport.m
index b8b0a9520..f56641fe7 100644
--- a/SmartDeviceLink/SDLTCPTransport.m
+++ b/SmartDeviceLink/SDLTCPTransport.m
@@ -1,43 +1,45 @@
+//
// 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 "SDLError.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, assign) NSUInteger receiveBufferSize;
+@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 transportConnected;
+@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 +54,302 @@ static void TCPCallback(CFSocketRef socket, CFSocketCallBackType type, CFDataRef
}
- (void)dealloc {
+ SDLLogD(@"SDLTCPTransport dealloc");
[self disconnect];
}
+#pragma mark - Stream Lifecycle
+
+// Note: When a connection is refused (e.g. TCP port number is not correct) or timed out (e.g. invalid IP address), then onError will be notified.
- (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.name = 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;
+ self.transportConnected = NO;
}
+#pragma mark - Data Transmission
+
- (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.delegate = 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 {
+ [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 - NSStreamDelegate
+// this method runs only on the I/O thread (i.e. invoked from the run loop)
+
+- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
+ switch (eventCode) {
+ case NSStreamEventNone: {
+ // nothing to do
+ } break;
+ 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.transportConnected = YES;
+ [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;
+ }
+}
- //no host name?, no problem, get local host
- if (hostname == nil) {
- char localhost[128];
- gethostname(localhost, sizeof localhost);
- hostname = (const char *)&localhost;
+#pragma mark - Stream event handlers
+// these methods run only on the I/O thread (i.e. invoked from the run loop)
+
+- (void)sdl_readFromStream {
+ NSAssert([[NSThread currentThread] isEqual:self.ioThread], @"sdl_readFromStream is called on a wrong thread!");
+
+ BytePtr 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 {
+ NSAssert([[NSThread currentThread] isEqual:self.ioThread], @"sdl_writeToStream is called on a wrong thread!");
+
+ if (!self.outputStreamHasSpace || [self.sendDataQueue count] == 0) {
+ 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 {
+ NSAssert([[NSThread currentThread] isEqual:self.ioThread], @"sdl_onConnectionTimedOut is called on a wrong thread!");
- // Check if Core disconnected from us
- if (CFDataGetLength((CFDataRef)data) <= 0) {
- SDLLogW(@"Remote system terminated connection, data packet length 0");
- [transport.delegate onTransportDisconnected];
+ SDLLogW(@"TCP connection timed out");
+ [self sdl_cancelIOThread];
- return;
- }
+ if (!self.transportErrorNotified) {
+ NSAssert(!self.transportConnected, @"transport should not be connected in this case");
+ [self.delegate onError:[NSError sdl_transport_connectionTimedOutError]];
+ self.transportErrorNotified = YES;
+ }
+}
+
+- (void)sdl_onStreamError:(NSStream *)stream {
+ NSAssert([[NSThread currentThread] isEqual:self.ioThread], @"sdl_onStreamError is called on a wrong thread!");
+
+ // stop I/O thread
+ [self sdl_cancelIOThread];
+
+ // avoid notifying multiple error events
+ if (self.transportErrorNotified) {
+ return;
+ }
- // 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];
+ if (self.transportConnected) {
+ // transport is disconnected while running
+ [self.delegate onTransportDisconnected];
+ self.transportErrorNotified = YES;
+ } else if ([stream.streamError.domain isEqualToString:NSPOSIXErrorDomain]) {
+ // connection error
+
+ // According to Apple's document "Error Objects, Domains, and Codes", the 'code' values of NSPOSIXErrorDomain are actually errno values.
+ NSError *error;
+ switch (stream.streamError.code) {
+ case ECONNREFUSED: {
+ SDLLogD(@"TCP connection error: ECONNREFUSED");
+ error = [NSError sdl_transport_connectionRefusedError];
+ } break;
+ case ETIMEDOUT: {
+ SDLLogD(@"TCP connection error: ETIMEDOUT");
+ error = [NSError sdl_transport_connectionTimedOutError];
+ } break;
+ case ENETDOWN: {
+ SDLLogD(@"TCP connection error: ENETDOWN");
+ error = [NSError sdl_transport_networkDownError];
+ } break;
+ case ENETUNREACH: {
+ // This is just for safe. I did not observe ENETUNREACH error on iPhone.
+ SDLLogD(@"TCP connection error: ENETUNREACH");
+ error = [NSError sdl_transport_networkDownError];
+ } break;
+ default: {
+ SDLLogD(@"TCP connection error: unknown error %ld", (long)stream.streamError.code);
+ error = [NSError sdl_transport_unknownError];
+ } break;
+ }
+ [self.delegate onError:error];
+ self.transportErrorNotified = YES;
} else {
- SDLLogW(@"Unhandled callback type: %lu", type);
+ SDLLogE(@"Unhandled stream error! %@", stream.streamError);
}
}
+- (void)sdl_onStreamEnd:(NSStream *)stream {
+ NSAssert([[NSThread currentThread] isEqual:self.ioThread], @"sdl_onStreamEnd is called on a wrong thread!");
+
+ [self sdl_cancelIOThread];
+
+ if (!self.transportErrorNotified) {
+ [self.delegate onTransportDisconnected];
+ self.transportErrorNotified = YES;
+ }
+}
+
+- (void)sdl_doNothing {}
+
+@end
+
NS_ASSUME_NONNULL_END
diff --git a/SmartDeviceLink/SDLTransportDelegate.h b/SmartDeviceLink/SDLTransportDelegate.h
index f81e2c043..c8350918a 100644
--- a/SmartDeviceLink/SDLTransportDelegate.h
+++ b/SmartDeviceLink/SDLTransportDelegate.h
@@ -21,6 +21,7 @@ NS_ASSUME_NONNULL_BEGIN
* @param receivedData The data received from Core
*/
- (void)onDataReceived:(NSData *)receivedData;
+- (void)onError:(NSError *)error;
@end
diff --git a/SmartDeviceLinkTests/TestUtilities/TestTCPServer.h b/SmartDeviceLinkTests/TestUtilities/TestTCPServer.h
new file mode 100644
index 000000000..9cbd0ec39
--- /dev/null
+++ b/SmartDeviceLinkTests/TestUtilities/TestTCPServer.h
@@ -0,0 +1,71 @@
+//
+// TestTCPServer.h
+// SmartDeviceLink-iOS
+//
+// Created by Sho Amano on 2018/07/27.
+// Copyright © 2018 Xevo Inc. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Delegate to receive various events from the test TCP server
+ */
+@protocol TestTCPServerDelegate
+- (void)onClientConnected;
+- (void)onClientDataReceived:(NSData *)data;
+- (void)onClientShutdown;
+- (void)onClientError;
+@end
+
+@interface TestTCPServer : NSObject
+
+/**
+ * Sets up a TCP server that listens on specified host and port
+ *
+ * Note that this server cannot accept more than one connections from client(s).
+ *
+ * @param hostName Host name that the server will listen on
+ * @param portNumber TCP port number of the server
+ * @return YES when initialization is successful, NO otherwise
+ */
+- (BOOL)setup:(NSString *)hostName port:(NSString *)port;
+
+/**
+ * Shuts down the server, forcefully closing client connection
+ *
+ * @return YES when the server is successfully stopped, NO otherwise
+ */
+- (BOOL)teardown;
+
+/**
+ * Asynchronously sends data to connected client
+ *
+ * @param data Data to send
+ */
+- (void)send:(NSData *)data;
+
+/**
+ * Gracefully shuts down the connection between client.
+ *
+ * This method triggers shutdown(SHUT_WR) which is to notify that the server does not have any more data to send.
+ *
+ * @return YES if shutdown process is succeeded, NO if it's failed or client is not connected
+ */
+- (BOOL)shutdownClient;
+
+/**
+ * The delegate to receive server events
+ */
+@property (nullable, nonatomic, weak) id<TestTCPServerDelegate> delegate;
+
+/**
+ * Configure this flag to YES to enable SO_REUSEADDR option
+ */
+@property (nonatomic, assign) BOOL enableSOReuseAddr;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/SmartDeviceLinkTests/TestUtilities/TestTCPServer.m b/SmartDeviceLinkTests/TestUtilities/TestTCPServer.m
new file mode 100644
index 000000000..c7b332949
--- /dev/null
+++ b/SmartDeviceLinkTests/TestUtilities/TestTCPServer.m
@@ -0,0 +1,370 @@
+//
+// TestTCPServer.m
+// SmartDeviceLinkTests
+//
+// Created by Sho Amano on 2018/07/27.
+// Copyright © 2018 Xevo Inc. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import <Nimble/Nimble.h>
+
+#import "TestTCPServer.h"
+
+#import <sys/types.h>
+#import <sys/socket.h>
+#import <netdb.h>
+#import <sys/select.h>
+#import <sys/time.h>
+#import <fcntl.h>
+#import <string.h>
+#import <errno.h>
+
+#define MAX_SERVER_SOCKET_NUM (16)
+#define RECV_BUF_SIZE (1024)
+#define THREAD_STOP_WAIT_SEC (1.0)
+
+@interface TestTCPServer() {
+ int _serverSockets[MAX_SERVER_SOCKET_NUM];
+ int _internalSockets[2];
+ int _clientSocket; // supports only one client
+}
+
+@property (nullable, nonatomic, strong) NSThread *thread;
+@property (nonatomic, strong) dispatch_semaphore_t threadStoppedSemaphore;
+@property (nonatomic, strong) NSMutableArray<NSMutableData*> *sendData;
+@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(@"TestTCPServer: 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: TestTCPServer 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: TestTCPServer 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: TestTCPServer server socket bind() failed: %s", strerror(errno));
+ close(sock);
+ continue;
+ }
+
+ ret = listen(sock, 5);
+ if (ret < 0) {
+ NSLog(@"Error: TestTCPServer 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(@"TestTCPServer: 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: TestTCPServer 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(@"TestTCPServer: 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: TestTCPServer 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(@"TestTCPServer: 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(@"TestTCPServer: 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: TestTCPServer TCP server accept() failed: %s", strerror(errno));
+ internalFailure = YES;
+ running = NO;
+ break;
+ }
+ }
+
+ if (_clientSocket >= 0) {
+ NSLog(@"Error: TestTCPServer 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: TestTCPServer TCP server recv() failed for internal pipe: %s", strerror(errno));
+ internalFailure = YES;
+ break;
+ }
+ } else if (recvLen == 0) {
+ NSLog(@"TestTCPServer: 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: TestTCPServer fcntl (F_GETFL) failed");
+ return NO;
+ }
+ int ret = fcntl(sock, F_SETFL, flags | O_NONBLOCK);
+ if (ret == -1) {
+ NSLog(@"Error: TestTCPServer 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: TestTCPServer 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: TestTCPServer setsockopt() failed");
+ return NO;
+ }
+ }
+
+ return YES;
+}
+@end
diff --git a/SmartDeviceLinkTests/TransportSpecs/SDLTCPTransportSpec.m b/SmartDeviceLinkTests/TransportSpecs/SDLTCPTransportSpec.m
new file mode 100644
index 000000000..e9a25f834
--- /dev/null
+++ b/SmartDeviceLinkTests/TransportSpecs/SDLTCPTransportSpec.m
@@ -0,0 +1,291 @@
+//
+// 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 "SDLError.h"
+#import "TestTCPServer.h"
+
+#import <stdio.h>
+
+@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 invoke onError delegate 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 onError:[OCMArg checkWithBlock:^BOOL(NSError *error) {
+ if (error.domain == SDLErrorDomainTransport && error.code == SDLTransportErrorConnectionRefused) {
+ return YES;
+ } else {
+ return NO;
+ }
+ }]]);
+
+ 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 invoke onError delegate when connection is timed out", ^ {
+ OCMExpect([transportDelegateMock onError:[OCMArg checkWithBlock:^BOOL(NSError *error) {
+ if (error.domain == SDLErrorDomainTransport && error.code == SDLTransportErrorConnectionTimedOut) {
+ return YES;
+ } else {
+ return NO;
+ }
+ }]]);
+
+ 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 invoke onError delegate when input parameter is invalid", ^ {
+ OCMExpect([transportDelegateMock onError:[OCMArg checkWithBlock:^BOOL(NSError *error) {
+ if (error.domain == SDLErrorDomainTransport && error.code == SDLTransportErrorUnknown) {
+ return YES;
+ } else {
+ return NO;
+ }
+ }]]);
+
+ 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