NSInputStream stops sending NSStreamEventHasBytesAvailable event

I’ve seen a number of issues around EASession inputStream hasAvailableBytes (this is a serial BT connection) and none totally match this odd behavior and wondering if this has been seen by others and if there’s been a resolution/issue found.

In this case the amount of data to be read will be > 1MB, however after ~28000 bytes the NSStreamEventHasBytesAvailable event type isn’t sent anymore. The basis for the code is from the EADemo, and have tried a number of variations, but with the same result. I’ve also tried variations that start polling the NSInputStream after the NSStreamEventOpenCompleted event, but end up getting the same results.


I also have seen issues regarding iOS 11 and accessory issues that have been filed as bugs with Apple... however I also am testing with 10.2 and get the same behavior (so assuming can rule iOS issues out)


The setup for the stream is (note this is setup in the main queue, though I’ve also tried it with a background queue):


// open a session with the accessory and set up the input and output stream on the default run loop
- (BOOL)openDataSession
{
    [_accessory setDelegate:self];
    self.dataSession = [[EASession alloc] initWithAccessory:self.accessory
                                                forProtocol:dataProtocolString];

    if (self.dataSession) {
        NSInputStream* inputStream = self.dataSession.inputStream;
        [inputStream setDelegate:self];
        [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        [inputStream open];
    
        [[self.dataSession outputStream] setDelegate:self];
        [[self.dataSession outputStream] scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        [[self.dataSession outputStream] open];
    } else {
        NSLog(@"creating file transfer session failed");
    }

    return (self.dataSession != nil);
}


the event handler is:


- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
    NSError* error;
    NSLog(@"Received stream handle event code: %x - stream: %@", eventCode, aStream);
    switch (eventCode) {
        case NSStreamEventNone:
            break;
        case NSStreamEventOpenCompleted:
          /
            break;
        case NSStreamEventHasBytesAvailable:
          if ([aStream isEqual:self.dataSession.inputStream]) {
                [self doDataTransfer];
          }
            break;
        case NSStreamEventHasSpaceAvailable:
            [self _writeData];
            break;
        case NSStreamEventErrorOccurred:
            error = self.dataSession.inputStream.streamError;
            NSLog(@"Stream error: %@", error.debugDescription);
            break;
        case NSStreamEventEndEncountered:
            break;
        default:
            break;
    }
}


and the code to read the stream is (note also tried variations of this with no effect)


- (void)doDataTransfer
{
#define EAD_INPUT_BUFFER_SIZE 4096
    uint8_t buf[EAD_INPUT_BUFFER_SIZE];

    while ([[self.dataSession inputStream] hasBytesAvailable])
    {
        NSInteger bytesRead = [[self.dataSession inputStream] read:buf maxLength:EAD_INPUT_BUFFER_SIZE];
        if (_readData == nil) {
            _readData = [[NSMutableData alloc] init];
        }
    
  /
        if (bytesRead == 0) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                    beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];
            continue;
        }
    
        [_readData appendBytes:(void *)buf length:bytesRead];
        NSLog(@"_readData %d bytes from input stream", bytesRead);
    
    }
    NSLog(@"Done with doDataTransfer");
}


And what is seen in the log is:

[607:124939] Received stream handle event code: 1 - stream: <EAInputStream: 0x15e4e210>
[607:124939] Received stream handle event code: 2 - stream: <EAInputStream: 0x15e4e210>
[607:124939] _readData 2036 bytes from input stream
[607:124939] Done with doDataTransfer
[607:124939] Received stream handle event code: 2 - stream: <EAInputStream: 0x15e4e210>
[607:124939] _readData 2036 bytes from input stream
[607:124939] Done with doDataTransfer
[607:124939] Received stream handle event code: 2 - stream: <EAInputStream: 0x15e4e210>
[607:124939] _readData 2036 bytes from input stream
[607:124939] Done with doDataTransfer
[607:124939] Received stream handle event code: 2 - stream: <EAInputStream: 0x15e4e210>
[607:124939] _readData 2036 bytes from input stream
[607:124939] Done with doDataTransfer
...


The Received event code, read data, Done message triplets go for 13~14 times (~28000 bytes) then the events just stop coming in.

Replies

You wrote:

note this is setup in the main queue, though I’ve also tried it with a background queue

which is also worrying. Doing this on the main queue is fine because your stream event sources will get scheduled on the main thread’s run loop. Doing it on any other queue is likely to be problematic because there’s no guarantee that the thread running your block will ever run its run loop. If you want to do this sort of thing on a secondary thread, it’s best to created a dedicated thread for that.

For the moment I recommend that you do this on the main queue, so you can ignore this whole issue while you get the basics working.

I don’t know anything about EA streams (sorry) but the code you’ve shown is very troubling. If you ran the same code with a TCP stream (which is what I specialise in) I would not be surprised about you running into problems like this.

My specific concerns relate to the way that you read data off the stream:

  • It’s bad form to call

    -hasBytesAvailable
    in an
    NSStreamEventHasBytesAvailable
    event handler. Each event handler is supposed to do the work that it can (in this case, read one chunk of data) and then return. If there’s more work to do, you’ll get another event about that.
  • Running the run loop inside your

    NSStreamEventHasBytesAvailable
    handler is a really bad idea.

I recommend that you structure your stream event handling code as follows:

  1. When you get an

    NSStreamEventHasBytesAvailable
    , call
    -read:maxLength:
    to read a chunk of data into a buffer.
  2. If the read returns 0, return and wait for the next event (for a TCP stream that next event would be a

    NSStreamEventEndEncountered
    event, but I’m not 100% sure that EA streams guarantee that).
  3. If the read returns a negative number, return. You should then get a

    NSStreamEventErrorOccurred
    event.
  4. If the read returns a positive number, copy that many bytes from your temporary buffer to your main read buffer.

  5. If your read buffer contains enough data for one or more ‘messages’, remove those messages from the buffer and process them.

With this approach there’s never any need to call

-hasBytesAvailable
. After getting an
NSStreamEventHasBytesAvailable
event, the act of calling
-read:maxLength:
clears an internal flag on the stream that ensure that, if more data is available, you’ll get another
NSStreamEventHasBytesAvailable
event.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Hi Quinn,


I just wanted to say THANK YOU for your reply to this question. We've had an intermittent NSStream "hang" for a while and it was do to us looping in our NSStreamHasBytesAvailable event such as...


while ([[self.dataSession inputStream] hasBytesAvailable])


...trying to read all the data available in small chunks. Somehow, very intermittently, this would cause the next NSStreamHasBytesAvailable to be lost and then it would appear like our socket was dead since no other events would be dispatched. This was Mac to iPad/iPhone traffic over USB and would only happen every 5-10 minutes or so.


Thanks again!


Werner Sharp