Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 799 easessions close on disconnect #1

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions SmartDeviceLink/private/SDLIAPControlSession.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ NS_ASSUME_NONNULL_BEGIN
*
* When the protocol string is received from Core, the control session is closed as a new session with Core must be established with the received protocol string. Core has ~10 seconds to send the protocol string, otherwise the control session is closed and new attempt is made to establish a control session with Core.
*/
@interface SDLIAPControlSession : SDLIAPSession
@interface SDLIAPControlSession: NSObject <SDLIAPSessionDelegate>

- (instancetype)init NS_UNAVAILABLE;

Expand All @@ -32,7 +32,27 @@ NS_ASSUME_NONNULL_BEGIN
* @param delegate The control session delegate
* @return A SDLIAPControlSession object
*/
- (instancetype)initWithAccessory:(nullable EAAccessory *)accessory delegate:(id<SDLIAPControlSessionDelegate>)delegate;
- (instancetype)initWithAccessory:(nullable EAAccessory *)accessory delegate:(id<SDLIAPControlSessionDelegate>)delegate forProtocol:(NSString *)protocol;

/**
* Closes the SDLIAPSession used by the SDLIAPControlSession
*/
- (void) closeSession;

/**
* Returns whether the session has open I/O streams.
*/
@property (assign, nonatomic, readonly, getter=isSessionInProgress) BOOL sessionInProgress;

/**
* The accessory used to create and the EASession.
*/
@property (nullable, strong, nonatomic, readonly) EAAccessory *accessory;

/**
* The unique ID assigned to the session between the app and accessory. If no session exists the value will be 0.
*/
@property (assign, nonatomic, readonly) NSUInteger connectionID;

@end

Expand Down
209 changes: 48 additions & 161 deletions SmartDeviceLink/private/SDLIAPControlSession.m
Original file line number Diff line number Diff line change
Expand Up @@ -22,206 +22,94 @@
int const ProtocolIndexTimeoutSeconds = 10;

@interface SDLIAPControlSession ()

@property (nullable, strong, nonatomic) SDLTimer *protocolIndexTimer;
@property (weak, nonatomic) id<SDLIAPControlSessionDelegate> delegate;

@property (nullable, nonatomic, strong) SDLIAPSession *iapSession;
@end

@implementation SDLIAPControlSession

#pragma mark - Session lifecycle

- (instancetype)initWithAccessory:(nullable EAAccessory *)accessory delegate:(id<SDLIAPControlSessionDelegate>)delegate {
SDLLogV(@"SDLIAPControlSession init");

self = [super initWithAccessory:accessory forProtocol:ControlProtocolString];
if (!self) { return nil; }

- (instancetype)initWithAccessory:(nullable EAAccessory *)accessory delegate:(id<SDLIAPControlSessionDelegate>)delegate forProtocol:(NSString *)protocol {
SDLLogD(@"SDLIAPControlSession init with protocol %@ and accessory %@", protocol, accessory);
self = [super init];
_iapSession = [[SDLIAPSession alloc] initWithAccessory:accessory forProtocol:protocol iAPSessionDelegate:self];
_protocolIndexTimer = nil;
_delegate = delegate;
SDLLogD(@"SDLIAPControlSession Wait for the protocol string from Core, setting timeout timer for %d seconds", ProtocolIndexTimeoutSeconds);
self.protocolIndexTimer = [self sdl_createControlSessionProtocolIndexStringDataTimeoutTimer];

return self;
}

#pragma mark Start

- (void)startSession {
if (self.accessory == nil) {
SDLLogW(@"There is no control session in progress, attempting to create a new control session.");
[self.delegate controlSessionShouldRetry];
} else {
SDLLogD(@"Starting a control session with accessory (%@)", self.accessory.name);
__weak typeof(self) weakSelf = self;
[self sdl_startStreamsWithCompletionHandler:^(BOOL success) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!success) {
SDLLogW(@"Control session failed to setup with accessory: %@. Attempting to create a new control session", strongSelf.accessory);
[strongSelf destroySessionWithCompletionHandler:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf.delegate controlSessionShouldRetry];
}];
} else {
SDLLogD(@"Waiting for the protocol string from Core, setting timeout timer for %d seconds", ProtocolIndexTimeoutSeconds);
strongSelf.protocolIndexTimer = [strongSelf sdl_createControlSessionProtocolIndexStringDataTimeoutTimer];
}
}];
}
- (void) closeSession {
[self.iapSession closeSession];
}

/// Opens the input and output streams for the session on the main thread.
/// @discussion We must close the input/output streams from the same thread that owns the streams' run loop, otherwise if the streams are closed from another thread a random crash may occur. Since only a small amount of data will be transmitted on this stream before it is closed, we will open and close the streams on the main thread instead of creating a separate thread.
- (void)sdl_startStreamsWithCompletionHandler:(void (^)(BOOL success))completionHandler {
if (![super createSession]) {
return completionHandler(NO);
}

__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) strongSelf = weakSelf;

SDLLogD(@"Created the control session successfully");
[super startStream:strongSelf.eaSession.outputStream];
[super startStream:strongSelf.eaSession.inputStream];

return completionHandler(YES);
});
- (nullable EAAccessory *) accessory {
return self.iapSession.accessory;
}

#pragma mark Stop

/// Makes sure the session is closed and destroyed on the main thread.
/// @param disconnectCompletionHandler Handler called when the session has disconnected
- (void)destroySessionWithCompletionHandler:(void (^)(void))disconnectCompletionHandler {
SDLLogD(@"Destroying the control session");
dispatch_async(dispatch_get_main_queue(), ^{
[self sdl_stopAndDestroySession];
return disconnectCompletionHandler();
});
- (NSUInteger)connectionID {
return self.iapSession.connectionID;
}

/// Closes the session streams and then destroys the session.
- (void)sdl_stopAndDestroySession {
NSAssert(NSThread.isMainThread, @"%@ must only be called on the main thread", NSStringFromSelector(_cmd));

[super stopStream:self.eaSession.outputStream];
[super stopStream:self.eaSession.inputStream];
[super cleanupClosedSession];
- (BOOL)isSessionInProgress {
return [self.iapSession isSessionInProgress];
}


#pragma mark - NSStreamDelegate

/**
* Handles events on the input/output streams of the open session.
*
* @param stream The stream (either input or output) that the event occured on
* @param eventCode The stream event code
*/
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
switch (eventCode) {
case NSStreamEventOpenCompleted: {
[self sdl_streamDidOpen:stream];
break;
}
case NSStreamEventHasBytesAvailable: {
[self sdl_streamHasBytesAvailable:(NSInputStream *)stream];
break;
}
case NSStreamEventErrorOccurred: {
[self sdl_streamDidError:stream];
break;
}
case NSStreamEventEndEncountered: {
[self sdl_streamDidEnd:stream];
break;
}
case NSStreamEventNone:
case NSStreamEventHasSpaceAvailable:
default: {
break;
}
}
}

/**
* Called when the session gets a `NSStreamEventOpenCompleted`. When both the input and output streams open, start a timer to get data from Core within a certain timeframe.
*
* @param stream The stream that got the event code.
*/
- (void)sdl_streamDidOpen:(NSStream *)stream {
if (stream == [self.eaSession outputStream]) {
SDLLogD(@"Control session output stream opened");
self.isOutputStreamOpen = YES;
} else if (stream == [self.eaSession inputStream]) {
SDLLogD(@"Control session input stream opened");
self.isInputStreamOpen = YES;
}

// When both streams are open, session initialization is complete. Let the delegate know.
if (self.isInputStreamOpen && self.isOutputStreamOpen) {
SDLLogV(@"Control session I/O streams opened for protocol: %@", self.protocolString);
- (void) streamsDidOpen {
SDLLogD(@"SDLIAPControlSession streams opened for control session instance %@", self);
if (self.delegate != nil) {
[self sdl_startControlSessionProtocolIndexStringDataTimeoutTimer];
}
}

/**
* Called when the session gets a `NSStreamEventEndEncountered` event code. The current session is closed and a new session is attempted.
*/
- (void)sdl_streamDidEnd:(NSStream *)stream {
SDLLogD(@"Control stream ended");

// End events come in pairs, only perform this once per set.
[self.protocolIndexTimer cancel];

__weak typeof(self) weakSelf = self;
[self destroySessionWithCompletionHandler:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf.delegate controlSessionShouldRetry];
}];
- (void) streamsDidEnd {
SDLLogD(@"SDLIAPControlSession EASession stream ended");
if (self.delegate != nil) {
[self.delegate controlSessionDidEnd];
}
}

- (void)streamHasSpaceToWrite {}

/**
* Called when the session gets a `NSStreamEventHasBytesAvailable` event code. A protocol string is created from the received data. Since a new session needs to be established with the protocol string, the current session is closed and a new session is created.
*/
- (void)sdl_streamHasBytesAvailable:(NSInputStream *)inputStream {
SDLLogV(@"Control stream received data");

- (void)streamHasBytesAvailable:(NSInputStream *)inputStream {
SDLLogD(@"SDLIAPControlSession EASession stream received data");
// Read in the stream a single byte at a time
uint8_t buf[1];
NSInteger len = [inputStream read:buf maxLength:1];
if (len <= 0) {
SDLLogV(@"No data in the control stream");
SDLLogD(@"No data in the control stream");
return;
}

// If we have data from the control stream, use the data to create the protocol string needed to establish the data session.
// If we have data from the control stream, use the data to create the protocol string needed to establish a data session.
NSString *indexedProtocolString = [NSString stringWithFormat:@"%@%@", IndexedProtocolStringPrefix, @(buf[0])];
SDLLogD(@"Control Stream will switch to protocol %@", indexedProtocolString);

// Destroy the control session as it is no longer needed, and then create the data session.
__weak typeof(self) weakSelf = self;
[self destroySessionWithCompletionHandler:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf.accessory.isConnected) {
[strongSelf.protocolIndexTimer cancel];
[strongSelf.delegate controlSession:strongSelf didReceiveProtocolString:indexedProtocolString];
}
}];
SDLLogD(@"SDLIAPControlSession EASession Stream will switch to protocol %@", indexedProtocolString);

[self.protocolIndexTimer cancel];
if (self.delegate != nil) {
[self.delegate controlSession:self didReceiveProtocolString:indexedProtocolString];
}
}

/**
* Called when the session gets a `NSStreamEventErrorOccurred` event code. The current session is closed and a new session is attempted.
*/
- (void)sdl_streamDidError:(NSStream *)stream {
SDLLogE(@"Control stream error");

- (void)streamDidError {
SDLLogE(@"SDLIAPControlSession stream error");
[self.protocolIndexTimer cancel];
__weak typeof(self) weakSelf = self;
[self destroySessionWithCompletionHandler:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf.delegate controlSessionShouldRetry];
}];
if (self.delegate != nil) {
[self.delegate controlSessionDidEnd];
}
}

#pragma mark - Timer
Expand All @@ -233,17 +121,14 @@ - (void)sdl_streamDidError:(NSStream *)stream {
*/
- (SDLTimer *)sdl_createControlSessionProtocolIndexStringDataTimeoutTimer {
SDLTimer *protocolIndexTimer = [[SDLTimer alloc] initWithDuration:ProtocolIndexTimeoutSeconds repeat:NO];

__weak typeof(self) weakSelf = self;
void (^elapsedBlock)(void) = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
SDLLogW(@"Control session failed to get the protocol string from Core after %d seconds, retrying.", ProtocolIndexTimeoutSeconds);
[strongSelf destroySessionWithCompletionHandler:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf.delegate controlSessionShouldRetry];
}];
SDLLogW(@"SDLIAPControlSession failed to get the protocol string from Core after %d seconds, retrying.", ProtocolIndexTimeoutSeconds);
if (self.delegate != nil) {
[strongSelf.delegate controlSessionDidEnd];
}
};

protocolIndexTimer.elapsedBlock = elapsedBlock;
return protocolIndexTimer;
}
Expand All @@ -259,3 +144,5 @@ - (void)sdl_startControlSessionProtocolIndexStringDataTimeoutTimer {
@end

NS_ASSUME_NONNULL_END


2 changes: 1 addition & 1 deletion SmartDeviceLink/private/SDLIAPControlSessionDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ NS_ASSUME_NONNULL_BEGIN

@protocol SDLIAPControlSessionDelegate <NSObject>

- (void)controlSessionShouldRetry;
- (void)controlSessionDidEnd;
- (void)controlSession:(SDLIAPControlSession *)controlSession didReceiveProtocolString:(NSString *)protocolString;

@end
Expand Down
24 changes: 22 additions & 2 deletions SmartDeviceLink/private/SDLIAPDataSession.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,32 @@
//

#import <Foundation/Foundation.h>

#import "SDLIAPSession.h"

@protocol SDLIAPDataSessionDelegate;


NS_ASSUME_NONNULL_BEGIN

@interface SDLIAPDataSession : SDLIAPSession
@interface SDLIAPDataSession: NSObject <SDLIAPSessionDelegate>

- (instancetype)init NS_UNAVAILABLE;


/**
* Returns whether the session has open I/O streams.
*/
@property (assign, nonatomic, readonly, getter=isSessionInProgress) BOOL sessionInProgress;

/**
* The unique ID assigned to the session between the app and accessory. If no session exists the value will be 0.
*/
@property (assign, nonatomic, readonly) NSUInteger connectionID;

/**
* The accessory with which to open a session.
*/
@property (nullable, strong, nonatomic, readonly) EAAccessory *accessory;
/**
* Creates a new data session.
*
Expand All @@ -28,6 +42,11 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (instancetype)initWithAccessory:(nullable EAAccessory *)accessory delegate:(id<SDLIAPDataSessionDelegate>)delegate forProtocol:(NSString *)protocol;

/**
* Closes the SDLIAPSession used by the SDLIAPControlSession
*/
- (void) closeSession;

/**
* Sends data to Core via the data session.
*
Expand All @@ -38,3 +57,4 @@ NS_ASSUME_NONNULL_BEGIN
@end

NS_ASSUME_NONNULL_END

Loading