8

How can a connection be closed when the NSOutputStream has finished sending data?

After searching around i have found that the event NSStreamEventEndEncountered is only called if the server drops the connection. not if the OutputStream has finished the data to send.

StreamStatus is Always returning 0 (connection closed) or 2 (connection open) but never 4 (writing data).

since both methods mentioned above are not telling me enough about the write process i am not able to find a way do determine if the Stream is still writing or if it has finished and i can close the connection now.

After 5 days of googleling and trying i am totally out of ideas... Any help appreciated. Thanks

EDIT ADDED CODE AS REQUESTED:

- (void)startSend:(NSString *)filePath

{

BOOL                    success;

NSURL *                 url;



assert(filePath != nil);

assert([[NSFileManager defaultManager] fileExistsAtPath:filePath]);

assert( [filePath.pathExtension isEqual:@"png"] || [filePath.pathExtension isEqual:@"jpg"] );



assert(self.networkStream == nil);      // don't tap send twice in a row!

assert(self.fileStream == nil);         // ditto



// First get and check the URL.

...
....
.....


// If the URL is bogus, let the user know.  Otherwise kick off the connection.

...
....
.....


if ( ! success) {

    self.statusLabel.text = @"Invalid URL";

} else {



    // Open a stream for the file we're going to send.  We do not open this stream; 

    // NSURLConnection will do it for us.



    self.fileStream = [NSInputStream inputStreamWithFileAtPath:filePath];

    assert(self.fileStream != nil);



    [self.fileStream open];



    // Open a CFFTPStream for the URL.



    self.networkStream = CFBridgingRelease(

        CFWriteStreamCreateWithFTPURL(NULL, (__bridge CFURLRef) url)

    );

    assert(self.networkStream != nil);



    if ([self.usernameText.text length] != 0) {

        success = [self.networkStream setProperty:self.usernameText.text forKey:(id)kCFStreamPropertyFTPUserName];

        assert(success);

        success = [self.networkStream setProperty:self.passwordText.text forKey:(id)kCFStreamPropertyFTPPassword];

        assert(success);

    }



    self.networkStream.delegate = self;

    [self.networkStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

    ///////******** LINE ADDED BY ME TO DISONNECT FROM FTP AFTER CLOSING CONNECTION *********////////////

    [self.networkStream setProperty:(id)kCFBooleanFalse forKey:(id)kCFStreamPropertyFTPAttemptPersistentConnection];

    ///////******** END LINE ADDED BY ME *********//////////// 

    [self.networkStream open];



    // Tell the UI we're sending.



    [self sendDidStart];

}

}



- (void)stopSendWithStatus:(NSString *)statusString

{

if (self.networkStream != nil) {

    [self.networkStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

    self.networkStream.delegate = nil;

    [self.networkStream close];

    self.networkStream = nil;

}

if (self.fileStream != nil) {

    [self.fileStream close];

    self.fileStream = nil;

}

[self sendDidStopWithStatus:statusString];

}



- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode

// An NSStream delegate callback that's called when events happen on our 

// network stream.

{

#pragma unused(aStream)

assert(aStream == self.networkStream);



switch (eventCode) {

    case NSStreamEventOpenCompleted: {

        [self updateStatus:@"Opened connection"];

    } break;

    case NSStreamEventHasBytesAvailable: {

        assert(NO);     // should never happen for the output stream

    } break;

    case NSStreamEventHasSpaceAvailable: {

        [self updateStatus:@"Sending"];



        // If we don't have any data buffered, go read the next chunk of data.



        if (self.bufferOffset == self.bufferLimit) {

            NSInteger   bytesRead;



            bytesRead = [self.fileStream read:self.buffer maxLength:kSendBufferSize];



            if (bytesRead == -1) {

                [self stopSendWithStatus:@"File read error"];

            } else if (bytesRead == 0) {

                [self stopSendWithStatus:nil];

            } else {

                self.bufferOffset = 0;

                self.bufferLimit  = bytesRead;

            }

        }



        // If we're not out of data completely, send the next chunk.



        if (self.bufferOffset != self.bufferLimit) {

            NSInteger   bytesWritten;

            bytesWritten = [self.networkStream write:&self.buffer[self.bufferOffset] maxLength:self.bufferLimit - self.bufferOffset];

            assert(bytesWritten != 0);

            if (bytesWritten == -1) {

                [self stopSendWithStatus:@"Network write error"];

            } else {

                self.bufferOffset += bytesWritten;

            }

        }

    } break;

    case NSStreamEventErrorOccurred: {

        [self stopSendWithStatus:@"Stream open error"];

    } break;

    case NSStreamEventEndEncountered: {

        // FOR WHATEVER REASON THIS IS NEVER CALLED!!!!

    } break;

    default: {

        assert(NO);

    } break;

}

}
sharkyenergy
  • 3,842
  • 10
  • 46
  • 97

1 Answers1

4

There can be two interpretations to your question. If what you are asking is "I have a NSOutputStream and I'm finished writing to it how do I signal this?" then the answer is as simple as call the close method on it.

Alternately, If what you are really saying is "I have a NSInputStream and I want to know when I've reached the end-of-stream" then you can look at hasBytesAvailable or streamStatus == NSStreamStatusAtEnd.

For your information, to actually get the status NSStreamStatusWriting you would need to be calling the streamStatus method from another thread while this thread is calling write:maxLength:.

--- Edit: Code Suggestion

The reason you would never get notified is that an output stream is never finished (unless it's a fixed size stream, which an FTP stream is not). It's the input stream that gets "finished" at which point you can close your output stream. That's the answer to your original question.

As a further suggestion, I would skip run loop scheduling and the "event processing" except for handling errors on the output stream. Then I would put the read/write code into a NSOperation subclass and send it off into a NSOperationQueue. By keeping a reference to the NSOperations in that queue you would be able to cancel them easily and even show a progress bar by adding a percentComplete property. I've tested the code below and it works. Replace my memory output stream with your FTP output stream. You will notice that I have skipped the validations, which you should keep of course. They should probably be done outside the NSOperation to make it easier to query the user.

@interface NSSendFileOperation : NSOperation<NSStreamDelegate> {

    NSInputStream  *_inputStream;
    NSOutputStream *_outputStream;

    uint8_t *_buffer;
}

@property (copy) NSString* sourceFilePath;
@property (copy) NSString* targetFilePath;
@property (copy) NSString* username;
@property (copy) NSString* password;

@end


@implementation NSSendFileOperation

- (void) main
{
    static int kBufferSize = 4096;

    _inputStream  = [NSInputStream inputStreamWithFileAtPath:self.sourceFilePath];
    _outputStream = [NSOutputStream outputStreamToMemory];
    _outputStream.delegate = self;

    [_inputStream open];
    [_outputStream open];

    _buffer = calloc(1, kBufferSize);

    while (_inputStream.hasBytesAvailable) {
        NSInteger bytesRead = [_inputStream read:_buffer maxLength:kBufferSize];
        if (bytesRead > 0) {
            [_outputStream write:_buffer maxLength:bytesRead];
            NSLog(@"Wrote %ld bytes to output stream",bytesRead);
        }
    }

    NSData *outputData = [_outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey];
    NSLog(@"Wrote a total of %lu bytes to output stream.", outputData.length);

    free(_buffer);
    _buffer = NULL;

    [_outputStream close];
    [_inputStream close];
}

- (void) stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
    // Handle output stream errors such as disconnections here
}

@end


int main (int argc, const char * argv[])
{
    @autoreleasepool {

        NSOperationQueue *sendQueue = [[NSOperationQueue alloc] init];

        NSSendFileOperation *sendOp = [[NSSendFileOperation alloc] init];
        sendOp.username = @"test";
        sendOp.password = @"test";
        sendOp.sourceFilePath = @"/Users/eric/bin/data/english-words.txt";
        sendOp.targetFilePath = @"/Users/eric/Desktop/english-words.txt";

        [sendQueue addOperation:sendOp];
        [sendQueue waitUntilAllOperationsAreFinished];
    }
    return 0;
}
aLevelOfIndirection
  • 3,522
  • 14
  • 18
  • hello! my situation is this: the inputstream reads the file, the outputstream writes it to a ftp server. when the inputstream is finished writing into the buffer, i close teh connection. but this is creating a problem, because the output stream did not write the whole buffer to the ftp when i am closing the connection. so i would need to know when the outputstream has finished writing so that i can close the connection without chopping off the end of the file. thanks! – sharkyenergy Apr 29 '13 at 06:01
  • The problem then is not that you close the input stream but that you release the buffer into which you have read your data along with it (input stream and buffer are two distinct objects). If you have finished reading the input then you have copied the all data to your buffer so it's OK to close the input stream. Only release the buffer after you have finished writing to the output stream. – aLevelOfIndirection Apr 29 '13 at 06:07
  • 1
    its closing the output stream that creates problems. i had to disable "attemptPersistentConnection" because i cant have the connection constantly open. so after i upload the file i must close the outputstream. problem is i dont know when the outputstream is finished doing its work. i only know when the input stream is finished.. – sharkyenergy Apr 29 '13 at 08:01
  • 1
    this is exactly the same problem, without an answer: http://stackoverflow.com/questions/12845158/nsstream-dont-send-all-data – sharkyenergy Apr 29 '13 at 08:05
  • maybe calling `NSStreamStatus` froma different thread tells me when i can safely close teh connection.. but how can i do that? i mean, how can i call it from a different thread? – sharkyenergy Apr 29 '13 at 09:49
  • Edit your post with the output code and I will tell you how to modify it. – aLevelOfIndirection Apr 29 '13 at 10:32
  • i dont have it with me at the moment, so i can post it only tonight. but it is the very same cose used in apple's `Simple FTP Sample` in the PUT.m/PUT.h file. with the exception that i set the attemptPersistentConnection to false before opening the stream. thanks for your help really appreciate it! – sharkyenergy Apr 29 '13 at 10:54
  • Done! added code as requested. i marked the line that i added and removed unneded parts (replaced with series of dots..) thanks! – sharkyenergy Apr 29 '13 at 17:15
  • Thank you very much for your code! Will test it asap! Could you please explain in simple words how this code solves my problem? If i execute MY code,comment out the partthat closes the connection and monitor the transfer i see this: upload starts, data is beeing sent to the ftp, after 2 minutes the whole file has been read and written to the outputstream so i get the report that the file has been uploaded, but the monitoring still shows that the prog is uploading for 15 more seconds.Like if the output stream has a buffer that needs to be still sent.I can make a clip tomorrow if needed. Thanks! – sharkyenergy Apr 29 '13 at 20:22
  • You are setting the delegate on your networkStream (NSOutputStream). If I understood the documentation correctly, the only time NSOutputStream triggers a NSStreamEventEndEncountered event is when it reaches the end of a __fixed-sized__ output, which you get with `outputStreamToBuffer:capacity:` for example. For unbounded outputs there is no why for the stream to know that the end has been reached. – aLevelOfIndirection Apr 30 '13 at 07:28
  • hello, thanks for clearifying. i did not have a chance to test it yet (and wont till tomorrow) but i am ALMOST sure that your code has the same problem as mine. you only note the problem if you have SLOW upload speeds. you could try to upload a file to a FTP and capping the speed at 20 kbps. if i am right then when the upload is finished the file online will be a few KB smaller then the original file. (unless i am missing something in your code..) obviously the file you are uploading has to be big enough to note the difference.. so at least 500 kb.. – sharkyenergy Apr 30 '13 at 07:56
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/29176/discussion-between-alevelofindirection-and-just-me) – aLevelOfIndirection Apr 30 '13 at 09:07