summaryrefslogtreecommitdiff
path: root/SmartDeviceLink/SDLIAPSession.m
blob: 98cc358137c9a7dce04c99aab891fa0403d42436 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
//
//  SDLIAPSession.m
//

#import "SDLIAPSession.h"
#import "SDLLogMacros.h"
#import "SDLMutableDataQueue.h"
#import "SDLStreamDelegate.h"
#import "SDLTimer.h"

NS_ASSUME_NONNULL_BEGIN

NSString *const IOStreamThreadName = @"com.smartdevicelink.iostream";
NSTimeInterval const StreamThreadWaitSecs = 10.0;

@interface SDLIAPSession ()

@property (nonatomic, assign) BOOL isInputStreamOpen;
@property (nonatomic, assign) BOOL isOutputStreamOpen;
@property (nonatomic, assign) BOOL isDataSession;
@property (nullable, nonatomic, strong) NSThread *ioStreamThread;
@property (nonatomic, strong) SDLMutableDataQueue *sendDataQueue;
@property (nonatomic, strong) dispatch_semaphore_t canceledSemaphore;

@end


@implementation SDLIAPSession

#pragma mark - Lifecycle

- (instancetype)initWithAccessory:(EAAccessory *)accessory forProtocol:(NSString *)protocol {
    SDLLogD(@"SDLIAPSession initWithAccessory:%@ forProtocol:%@", accessory, protocol);

    self = [super init];
    if (self) {
        _isDataSession = [protocol isEqualToString:@"com.smartdevicelink.prot0"] ? NO : YES;
        _accessory = accessory;
        _protocol = protocol;
        _canceledSemaphore = dispatch_semaphore_create(0);
        _sendDataQueue = [[SDLMutableDataQueue alloc] init];
        _isInputStreamOpen = NO;
        _isOutputStreamOpen = NO;
    }
    return self;
}


#pragma mark - Public Stream Lifecycle

- (BOOL)start {
    __weak typeof(self) weakSelf = self;
    SDLLogD(@"Opening EASession withAccessory:%@ forProtocol:%@", _accessory.name, _protocol);

    // TODO: This assignment should be broken out of the if and the if / else should be flipped.
    if ((self.easession = [[EASession alloc] initWithAccessory:self.accessory forProtocol:self.protocol])) {
        __strong typeof(self) strongSelf = weakSelf;

        SDLLogD(@"Created Session Object");

        strongSelf.streamDelegate.streamErrorHandler = [self streamErroredHandler];
        strongSelf.streamDelegate.streamOpenHandler = [self streamOpenedHandler];
        if (self.isDataSession) {
            self.streamDelegate.streamHasSpaceHandler = [self sdl_streamHasSpaceHandler];
            // Start I/O event loop processing events in iAP channel
            self.ioStreamThread = [[NSThread alloc] initWithTarget:self selector:@selector(sdl_accessoryEventLoop) object:nil];
            [self.ioStreamThread setName:IOStreamThreadName];
            [self.ioStreamThread start];
        } else {
            // Set up control session -- no need for its own thread
            [self startStream:self.easession.outputStream];
            [self startStream:self.easession.inputStream];
        }
        return YES;

    } else {
        SDLLogE(@"Error: Could Not Create Session Object");
        return NO;
    }
}

- (void)stop {
    if ([NSThread isMainThread]) {
        [self sdl_stop];
    } else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self sdl_stop];
        });
    }
}

- (void)sdl_stop {
    NSAssert(NSThread.isMainThread, @"%@ must only be called on the main thread", NSStringFromSelector(_cmd));
    if (self.isDataSession) {
        [self.ioStreamThread cancel];

        long lWait = dispatch_semaphore_wait(self.canceledSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(StreamThreadWaitSecs * NSEC_PER_SEC)));
        if (lWait == 0) {
            SDLLogW(@"Stream thread cancelled");
        } else {
            SDLLogE(@"Failed to cancel stream thread");
        }
        self.ioStreamThread = nil;
        self.isDataSession = NO;
    } else {
        // Stop control session
        [self stopStream:self.easession.outputStream];
        [self stopStream:self.easession.inputStream];
    }
    self.easession = nil;
}

- (BOOL)isStopped {
    return !self.isOutputStreamOpen && !self.isInputStreamOpen;
}

#pragma mark - data send methods

- (void)sendData:(NSData *)data {
    // Enqueue the data for transmission on the IO thread
    [self.sendDataQueue enqueueBuffer:data.mutableCopy];

    [self performSelector:@selector(sdl_dequeueAndWriteToOutputStream) onThread:self.ioStreamThread withObject:nil waitUntilDone:NO];
}

- (void)sdl_dequeueAndWriteToOutputStream {
    NSOutputStream *ostream = self.easession.outputStream;
    if (!ostream.hasSpaceAvailable) {
        return;
    }
    
    NSMutableData *remainder = [self.sendDataQueue frontBuffer];

    if (remainder != nil && ostream.streamStatus == NSStreamStatusOpen) {
        NSUInteger bytesRemaining = remainder.length;
        NSInteger bytesWritten = [ostream write:remainder.bytes maxLength:bytesRemaining];
        if (bytesWritten < 0) {
            if (ostream.streamError != nil) {
                [self sdl_handleOutputStreamWriteError:ostream.streamError];
            } else {
                // The write operation failed but there is no further information about the error. This can occur when disconnecting from an external accessory.
                SDLLogE(@"Output stream write operation failed");
            }
        } else if (bytesWritten == bytesRemaining) {
            // Remove the data from the queue
            [self.sendDataQueue popBuffer];
        } else {
            // Cleave the sent bytes from the data, the remainder will sit at the head of the queue
            [remainder replaceBytesInRange:NSMakeRange(0, (NSUInteger)bytesWritten) withBytes:NULL length:0];
        }
    }
}

- (void)sdl_handleOutputStreamWriteError:(NSError *)error {
    SDLLogE(@"Output stream error: %@", error);
    // TODO: We should look at the domain and the code as a tuple and decide how to handle the error based on both values. For now, if the stream is closed, we will flush the send queue and leave it as-is otherwise so that temporary error conditions can be dealt with by retrying
    if (self.easession == nil ||
        self.easession.outputStream == nil ||
        self.easession.outputStream.streamStatus != NSStreamStatusOpen) {
        [self.sendDataQueue removeAllObjects];
    }
}

#pragma mark - background I/O for data session

// Data session I/O thread
- (void)sdl_accessoryEventLoop {
    @autoreleasepool {
        NSAssert(self.easession, @"_session must be assigned before calling");

        if (!self.easession) {
            return;
        }

        [self startStream:self.easession.inputStream];
        [self startStream:self.easession.outputStream];

        SDLLogD(@"Starting the accessory event loop");
        while (!NSThread.currentThread.cancelled) {
            // Enqueued data will be written to and read from the streams in the runloop
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.25f]];
        }

        SDLLogD(@"Closing the accessory session for id: %tu, name: %@", self.easession.accessory.connectionID, self.easession.accessory.name);

        // Close I/O streams of the iAP session
        [self sdl_closeSession];
        dispatch_semaphore_signal(self.canceledSemaphore);
    }
}

// Must be called on accessoryEventLoop.
- (void)sdl_closeSession {
    if (!self.easession) {
        return;
    }

    SDLLogD(@"Close EASession for accessory id: %tu, name: %@", self.easession.accessory.connectionID, self.easession.accessory.name);

    [self stopStream:[self.easession inputStream]];
    [self stopStream:[self.easession outputStream]];
}


#pragma mark - Private Stream Lifecycle Helpers

- (void)startStream:(NSStream *)stream {
    stream.delegate = self.streamDelegate;
    NSAssert((self.isDataSession && [[NSThread currentThread] isEqual:self.ioStreamThread]) || [NSThread isMainThread], @"startStream is being called on the wrong thread!!!");
    [stream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [stream open];
}

- (void)stopStream:(NSStream *)stream {
    // Verify stream is in a state that can be closed.
    // (N.B. Closing a stream that has not been opened has very, very bad effects.)

    // When you disconect the cable you get a stream end event and come here but stream is already in closed state.
    // Still need to remove from run loop.

    NSAssert((self.isDataSession && [[NSThread currentThread] isEqual:self.ioStreamThread]) || [NSThread isMainThread], @"stopStream is being called on the wrong thread!!!");

    NSUInteger status1 = stream.streamStatus;
    if (status1 != NSStreamStatusNotOpen &&
        status1 != NSStreamStatusClosed) {
        [stream close];
    }

    [stream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [stream setDelegate:nil];

    NSUInteger status2 = stream.streamStatus;
    if (status2 == NSStreamStatusClosed) {
        if (stream == [self.easession inputStream]) {
            SDLLogD(@"Input Stream Closed");
			self.isInputStreamOpen = NO;
        } else if (stream == [self.easession outputStream]) {
            SDLLogD(@"Output Stream Closed");
			self.isOutputStreamOpen = NO;
        }
    }
}


#pragma mark - Stream Handlers

- (SDLStreamOpenHandler)streamOpenedHandler {
    __weak typeof(self) weakSelf = self;

    return ^(NSStream *stream) {
        __strong typeof(weakSelf) strongSelf = weakSelf;

        if (stream == [strongSelf.easession outputStream]) {
            SDLLogD(@"Output Stream Opened");
            strongSelf.isOutputStreamOpen = YES;
        } else if (stream == [strongSelf.easession inputStream]) {
            SDLLogD(@"Input Stream Opened");
            strongSelf.isInputStreamOpen = YES;
        }

        // When both streams are open, session initialization is complete. Let the delegate know.
        if (strongSelf.isInputStreamOpen && strongSelf.isOutputStreamOpen) {
            [strongSelf.delegate onSessionInitializationCompleteForSession:weakSelf];
        }
    };
}

- (SDLStreamErrorHandler)streamErroredHandler {
    __weak typeof(self) weakSelf = self;

    return ^(NSStream *stream) {
        __strong typeof(weakSelf) strongSelf = weakSelf;

        SDLLogW(@"Stream Error: %@", stream);
        [strongSelf.delegate onSessionStreamsEnded:strongSelf];
    };
}

- (SDLStreamHasSpaceHandler)sdl_streamHasSpaceHandler {
    __weak typeof(self) weakSelf = self;

    return ^(NSStream *stream) {
        __strong typeof(weakSelf) strongSelf = weakSelf;

        if (!strongSelf.isDataSession) {
            return;
        }

        [strongSelf sdl_dequeueAndWriteToOutputStream];
    };
}

@end

NS_ASSUME_NONNULL_END