Skip to content

Commit

Permalink
Merge pull request #1910 from kmicha19-ford/issue-799-easessions-left…
Browse files Browse the repository at this point in the history
…-open

Refactor IAP Transport / Fix EASessions left open
  • Loading branch information
joeljfischer committed Mar 15, 2021
2 parents b4ed102 + f597491 commit cf1f568
Show file tree
Hide file tree
Showing 13 changed files with 501 additions and 698 deletions.
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 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, 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 Waiting 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 {
SDLLogV(@"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");
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
23 changes: 21 additions & 2 deletions SmartDeviceLink/private/SDLIAPDataSession.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,31 @@
//

#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 +41,11 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (instancetype)initWithAccessory:(nullable EAAccessory *)accessory delegate:(id<SDLIAPDataSessionDelegate>)delegate forProtocol:(NSString *)protocol;

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

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

NS_ASSUME_NONNULL_END

0 comments on commit cf1f568

Please sign in to comment.