4

I am using a FTP library for iOS (nkreipke/FTPManager). It works great. I can download a list of directories, upload images, etc. The problem is that if you leave the app open for like 5 minutes doing nothing and then you try to download or upload, nothing happens.

I've been debugging it and found out that the NSStreamDelegate method - (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent never gets called after that short period of time of being inactive.

Here is the code:

- (NSArray*) _contentsOfServer:(FMServer*)server {
BOOL success = YES;

action = _FMCurrentActionContentsOfServer;

fileSize = 0;

self.directoryListingData = [[NSMutableData alloc] init];

NSURL* dest = [server.destination ftpURLForPort:server.port];
And(success, (dest != nil));
Check(success);

if (![dest.absoluteString hasSuffix:@"/"]) {
    //if the url does not end with an '/' the method fails.
    //no problem, we can fix this.
    dest = [NSURL URLWithString:[NSString stringWithFormat:@"%@/",dest.absoluteString]];
}

CFReadStreamRef readStream = CFReadStreamCreateWithFTPURL(NULL, (__bridge CFURLRef)dest);
And(success, (readStream != NULL));
if (!success) return nil;
self.serverReadStream = (__bridge_transfer NSInputStream*) readStream;

And(success, [self.serverReadStream setProperty:server.username forKey:(id)kCFStreamPropertyFTPUserName]);
And(success, [self.serverReadStream setProperty:server.password forKey:(id)kCFStreamPropertyFTPPassword]);
if (!success) return nil;

self.bufferOffset = 0;
self.bufferLimit = 0;

currentRunLoop = CFRunLoopGetCurrent();

self.serverReadStream.delegate = self;
[self.serverReadStream open];
[self.serverReadStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

CFRunLoopRun(); //<- Hangs here.

And(success, streamSuccess);
if (!success) return nil;

NSArray* directoryContents = [self _createListingArrayFromDirectoryListingData:self.directoryListingData];
self.directoryListingData = nil;

return directoryContents;

serverReadStream is a NSInputStream object.

When the action ends:

if (self.serverReadStream) {
    [self.serverReadStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.serverReadStream.delegate = nil;
    [self.serverReadStream close];
    self.serverReadStream = nil;
}

This code is located at FTPManager.m. I've been looking for answers around the internet but couldn't find any. I don't know why the NSStreamDelegate method gets called when used constantly but after some time of inactivity, it doesn't get called.

It would be nice if someone could help me.

Thanks.

Axort
  • 2,034
  • 2
  • 23
  • 24

1 Answers1

1

I'm using the same library in my iPad application and I was stuck in the same CFRunLoopRun() call when I was trying to refresh the FTP server content when resuming my application.

I had to do two changes to the FTPManager code.

First, I've changed the order of

[self.serverReadStream open];
[self.serverReadStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

to

[self.serverReadStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[self.serverReadStream open];

The reason why is that the stream cannot by opened. An error event is raised during the call to open but if the stream is not schedule to the run loop, we will never receive the error event. So scheduling before opening will allow us to get the error event.

The other change which was needed in my case was to disable the persistent connection. It seems that after a suspend/resume cycle of the device, the internal socket used by the stream is getting staled and is always returning an error when trying to access it.

Adding the following line in the stream configuration has solve the issue on my side :

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

Hope this helps.

Vincent
  • 3,656
  • 1
  • 23
  • 32