i'm trying to establish an FTP connection within an app. i want to upload several files to a FTP server, all files in one directory. So at first i want to create the remote directory.
- (void) createRemoteDir {
NSURL *destinationDirURL = [NSURL URLWithString: uploadDir];
CFWriteStreamRef writeStreamRef = CFWriteStreamCreateWithFTPURL(NULL, (__bridge CFURLRef) destinationDirURL);
assert(writeStreamRef != NULL);
ftpStream = (__bridge_transfer NSOutputStream *) writeStreamRef;
BOOL success = [ftpStream setProperty: ftpUser forKey: (id)kCFStreamPropertyFTPUserName];
if (success) {
NSLog(#"\tsuccessfully set the user name");
}
success = [ftpStream setProperty: ftpPass forKey: (id)kCFStreamPropertyFTPPassword];
if (success) {
NSLog(#"\tsuccessfully set the password");
}
ftpStream.delegate = self;
[ftpStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
// open stream
[ftpStream open];
}
This code doesn't work when executed in a background thread using the following call:
[self performSelectorInBackground: #selector(createRemoteDir) withObject: nil];
my guess is that the (background-threads) runloop isn't running?
If i send the message inside the main thread the uploading just works fine:
[self createRemoteDir];
as the runloop of the main thread is up and running.
but fairly large files are going to be uploaded; so i want to put that workload in a background thread.
but how and where do i set up the NSRunLoop, so that the whole uploading happens in a background thread? Apples documentation on NSRunLoops (especially how to start them without using a timer/input source, as in this case) didn't help me out.
I found/created a solution that at least works for me.
with the above method (createRemoteDir), the following code applied and worked for me:
NSError *error;
createdDirectory = FALSE;
/*
only 'prepares' the stream for upload
- doesn't actually upload anything until the runloop of this background thread is run
*/
[self createRemoteDir];
NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
do {
if(![currentRunLoop runMode: NSDefaultRunLoopMode beforeDate: [NSDate distantFuture]]) {
// log error if the runloop invocation failed
error = [[NSError alloc] initWithDomain: #"org.mJae.FTPUploadTrial"
code: 23
userInfo: nil];
}
} while (!createdDirectory && !error);
// close stream, remove from runloop
[ftpStream close];
[ftpStream removeFromRunLoop: [NSRunLoop currentRunLoop] forMode: NSDefaultRunLoopMode];
if (error) {
// handle error
}
It runs in a background thread and creates the directory on the ftp server.
I like it more than other examples where runloops are only run for an assumed small interval, say 1second.
[NSDate distantFuture]
is a date in the futur (several centuries, according to Apples documentation). But that's good as the "break-condition" is handled by my class property createdDirectory - or the occurance of an error while starting the runloop.
I can't explain why it works without me explicitly attaching an input source to the runloop (NSTimer or NSPort), but my guess is, it is sufficient that the NSOutputStream is scheduled in the runloop of the background thread (see createRemoteDir).
You could also try to use a dispatch_async call to perform your createRemoteDir in the background. It's much simpler to use and you won't have to worry about managing extra threads.
Here's what the code would look like:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self createRemoteDir];
});
Related
I got a synchronous request inside a custom operation that looks like this:
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession* session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
NSURLSessionDataTask* task = [session dataTaskWithRequest:request completionHandler:^(NSData *_data, NSURLResponse *_response, NSError *_error)
{
self->data = [_data retain];
self->tempResponse = _response;
self->tempError = [_error retain];
self->done = true;
}];
[task resume];
// wait until the connection has finished downloading the data or the operation gets cancelled
while (!self->done && !self.isCancelled)
{
[[NSRunLoop currentRunLoop] run];
}
This code is pretty old but has worked through several iOS versions (only replaced NSURLConnection at some point). Now under iOS 10 on the iPad Pro this code will freeze my app. If I put the app into the background and reopen it, it will run again. Also if I put breakpoints on [task resume] and self->data = [_data retain]; no freeze will happen at all.
I found one way to fix it inside the code, by adding an NSLog to the run loop:
while (!self->done && !self.isCancelled)
{
NSLog(#"BLAH!");
[[NSRunLoop currentRunLoop] run];
}
This eats quite some performance and won't help in the long run since all NSLogs are removed for the release configuration.
So I need a way to fix this bug. Maybe the code is too old and there's a new way to do it, I have no idea. Any help is appreciated.
The -run method will go forever until all timers etc. are removed from the run loop. The documentation states:
Manually removing all known input sources and timers from the run loop
is not a guarantee that the run loop will exit. macOS can install and
remove additional input sources as needed to process requests targeted
at the receiver’s thread. Those sources could therefore prevent the
run loop from exiting.
So, it's possible that iOS 10 is adding another input source which is only removed when the device is backgrounded, or something like that. Or, the -run method was perhaps always returning immediately before, if there were no timers or sources (but that would be a busy wait using lots of CPU). You really don't have much control using that method, and possibly some of the others.
I have used the following NSRunLoop category method in test case code, but I'm not sure I'd recommend it for production use as it will use CPU heavily, though maybe changing to check every .1 second (or longer) would help, instead of using [NSDate date], which is a 0 interval.
- (BOOL)runWithTimeout:(NSTimeInterval)timeout untilCondition:(BOOL (^)(void))testBlock
{
NSDate *limitDate = [NSDate dateWithTimeIntervalSinceNow:timeout];
while (1)
{
if (testBlock())
return YES;
if ([NSDate timeIntervalSinceReferenceDate] > [limitDate timeIntervalSinceReferenceDate])
return NO;
if (![self runMode:NSDefaultRunLoopMode beforeDate:[NSDate date]])
return NO;
}
}
The more usual way to do a synchronous call with NSURLSession (in a subthread, I am assuming) is to use a semaphore:
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
NSURLSessionDataTask* task = [session dataTaskWithRequest:request completionHandler:^(NSData *_data, NSURLResponse *_response, NSError *_error)
{
self->data = [_data retain];
self->tempResponse = _response;
self->tempError = [_error retain];
dispatch_semaphore_signal(sem);
}];
[task resume];
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
That may be complicated by the need to also check cancelled -- but you could make the semaphore an instance variable, and override the -cancel method to also call the semaphore. Make sure you don't use that semaphore instance again, as you could have multiple signals (and you may want to check self.isCancelled in the network completion block to make sure you don't do any additional work if previously cancelled).
I'm trying to update an old Mac OS program I wrote in ASOC (mostly Applescript, but some ObjC objects for things like web service access). I used a synchronous connection:
NSData *resultsData = [NSURLConnection sendSynchronousRequest: req returningResponse: &response error: &err];
The server credentials were embedded in the URL. This worked fine for me since the program really could not continue to do anything while the data was being fetched. A change to the server authentication method however has forced the need for changes to this application. I have tried all the usual workarounds with a NSURLCredential but that still does not work with this service.
So it looks like I will need to change to the asynchronous call:
[[NSURLConnection alloc] initWithRequest:request
delegate:self
startImmediately:YES];
I have this working with the appropriate delegate methods, most importantly:
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
Although I'd love to just use some form of delay loop to check for when the data has finished loading (essentially making it synchronous again), I have not found a way to do this that does not actually block the connection.
I am able to use a NSTimer to wait for the data before continuing:
set theJobListTimer to current application's NSTimer's scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(0.05, me, "jobListTimerFired:", "", true)
on jobListTimerFired_(theTimer)
(jobListData as list) & count of jobListData
if count of jobListData ≠ 0 then
log "jobListTimerFired_ done"
tell theTimer to invalidate()
setUpJobList(jobListData)
end if
end jobListTimerFired_
but this is clumsy and does not work while I'm in a modal dialog:
set buttonReturned to current application's NSApp's runModalForWindow_(collectionWindow)
(I have a drop down in the dialog that needs to be updated with the results of the web service call). Right now, the delegate methods are blocked until the modal is dismissed.
Is there no simple way to emulate the synchronous call using the async methods?
Trying to use semaphore, I changed code to:
- (void) startConnection:(int)reqType :(NSMutableURLRequest *)request {
requestType = [NSNumber numberWithInt:reqType];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// This could be any block that is run asynchronously
void (^myBlock)(void) = ^(void) {
self.connection = [[NSURLConnection alloc] initWithRequest:request
delegate:self
startImmediately:YES];
myBlock();
if (self.connection) {
// create an object to hold the received data
self.receivedData = [NSMutableData data];
NSLog(#"connection started %#", requestType);
}
dispatch_time_t timeOut = dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC);
dispatch_semaphore_wait(semaphore, timeOut);
dispatch_release(semaphore);
semaphore = NULL;
}
then in the connection handler:
- (void) connectionDidFinishLoading:(NSURLConnection *)connection
{
NSLog(#"connectionDidFinishLoading %#", requestType);
NSString *returnData = [[NSString alloc] initWithData:receivedData
encoding:NSUTF8StringEncoding] ;
// NSLog(#"connectionDidFinishLoading %#", returnData);
[self handleData:requestType :returnData];
[self terminate];
if(semaphore) {
dispatch_semaphore_signal(semaphore);
}
}
However, the connectionDidFinishLoading handler (and for that matter the didReceiveResponse and didReceiveData handlers) do not get called until after the 10 second dispatch timeout. What am I missing here?
You can use dispatch_semaphore_wait to make any asynchronous API into a synchronous one again.
Here's an example:
__block BOOL accessGranted = NO;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// This could be any block that is run asynchronously
ABAddressBookRequestAccessWithCompletion(addressBook, ^(bool granted, CFErrorRef error) {
accessGranted = granted;
if(semaphore) {
dispatch_semaphore_signal(semaphore);
}
});
// This will block until the semaphore has been signaled
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_release(semaphore);
semaphore = NULL;
return accessGranted;
Found the answer here:
iOS, NSURLConnection: Delegate Callbacks on Different Thread?
I knew the connection was running on a different thread and tried various other while loops to wait for it to finish. But this was REALLY the magic line:
while(!self->finished]){
//This line below is the magic!
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
I'm currently trying to run my whole network stuff in a background thread, as it currently blocks the main thread, when the server is not reachable (i.e.).
I'm currently creating the network connection through the following code. Is there a simple way to run this in a new background thread?
An how can I throw back the received message to the main thread? An how can I send messages through the background thread?
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)ipAdress, port, &readStream, &writeStream);
inputStream = (__bridge NSInputStream *)readStream;
outputStream = (__bridge NSOutputStream *)writeStream;
[inputStream setDelegate:self];
[outputStream setDelegate:self];
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[inputStream open];
[outputStream open];
here is a tutorial that does exactly what you're talking about. although it focuses more on audio streaming, but the principles are exactly the same (ie in terms of spawning a worker thread, having it talk with the parent thread etc etc).
the idea is simple.. you create a new thread and have it handle the streaming work, and then you schedule the stream reader with the run loop that belongs to the thread you just created. The stream will have callbacks that will be fired when ceratain events happen (ie you get some data, the connect times out etc).. in the callback methods you can alert or communicate with the mainthread (which is the thread that handles the UI).
Here is some code to point you in the right direction, but if you download the code from the above tutorial and follow through.. you'll get it:
// create a new thread
internalThread =
[[NSThread alloc]
initWithTarget:self
selector:#selector(startInternal)
object:nil];
[internalThread start];
// creating a stream inside the 'startInternal' thread*
stream = CFReadStreamCreateForHTTPRequest(NULL, message);
// open stream
CFReadStreamOpen(stream)
// set callback functions
// ie say: if there are bites available in the stream, fire a callback etc
CFStreamClientContext context = {0, self, NULL, NULL, NULL};
CFReadStreamSetClient(
stream,
kCFStreamEventHasBytesAvailable | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered,
ASReadStreamCallBack,
&context);
// schedule stream in current thread runloop, so that we DON'T block the mainthread
CFReadStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
// create the callback function to handle reading from stream
// NOTE: see where else in the code this function is named (ie CFReadStreamSetClient)
static void ASReadStreamCallBack
(
CFReadStreamRef aStream,
CFStreamEventType eventType,
void* inClientInfo
)
{
//handle events you registered above
// ie
if (eventType == kCFStreamEventHasBytesAvailable) {
// handle network data here..
..
// if something goes wrong, create an alert and run it through the main thread:
UIAlertView *alert = [
[[UIAlertView alloc]
initWithTitle:title
message:message
delegate:self
cancelButtonTitle:NSLocalizedString(#"OK", #"")
otherButtonTitles: nil]
autorelease];
[alert
performSelector:#selector(show)
onThread:[NSThread mainThread]
withObject:nil
waitUntilDone:NO];
}
}
I have a Download object which handles NSURLConnection.
Then I have NSOperation object (DownloadOperation) which holds Download object as property.
Download object has ability to start/pause/resume/cancel.
This is the main method of DownloadOperation
- (void)main
{
#autoreleasepool {
BOOL isDone = NO;
if (![self isCancelled]) {
[_download start]; //Download object start (creates NSURLConnection internally)
}
NSDate *distantFuture = [NSDate distantFuture];
while(!isDone && ![self isCancelled]) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:distantFuture];
if (!_download.isActive) { //internal state of Download
isDone = YES;
}
}
[self completeOperation]; //this sets finished and executing flags
}
}
From outside (from UI), I manipulate with Download object: Start, Pause, Resume, Cancel.
And internally I am changing its state so when Download is finished or canceled, isActive is set to NO and while loop should end.
This works if I start Download and let it finish (in background, NSURLConnection finished and called delegate -connectionDidFinish...)
If I pause/resume Download, download will continue to download and finish (and change its internal state: isActive -> NO).
On pause I cancel NSURLConnection and on resume I create new.
Or if I cancel the download, it will also be inactive (NSURLConnection is canceled).
But here is the problem:
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:distantFuture]; never returns in those cases (when I cancel NSURLConnection) and my "if" statement is never handled so this DownloadOperation is always considered running and will never exit my NSOperationQueue.
It looks like there is no event that could be fired that would cause that runloop to wake up.
I tried
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];
and this kind of works but not so smooth and I don't think it is the best solution.
What I really want to know is, how to force that NSRunLoop to wake up (how to fire some event that will cause it to wake up) and continue my while loop?
Here's what I did.
Good thing is that I have notifications in my app and I know exactly when downloads are changing. So I've made an instance variable NSThread currentThread and at the beginning of main I call currentThread = [NSThread currentThread].
After I receive notification which I know should cause my thread to wake up I call:
[self performSelector:#selector(wakeUpThread:) onThread:currentThread withObject:nil waitUntilDone:NO];
- (void)wakeUpThread does nothing, it is empty method, but purpose of this is to wake up thread and cause my run loop to continue.
I wrote a quick objective-C method that uses Amazon's AWS iOS SDK to synchronously download a file from my Amazon S3 Bucket in my iPad app. This is an enterprise app, and I am using reachability to detect WiFi before allowing synchronization with S3. It is working fine for short downloads (those in kilobytes), but with files that are around 20-30 megs, it will continue to download into the stream and the file will continue growing. I've not let it go to see if it will eventually stop/crash, but I've watched a file that was 30 megs go past 90 megs in my iOS Simulator. I've read into several cold threads where some have experienced the same and I really need an answer.
Here is my method...
- (void)retrieveRemoteFile:(NSString *)fileName {
NSString *destinationFileName = [NSString stringWithFormat:#"%#%#",[self getBucketDirectory],fileName];
AmazonS3Client *s3 = [[[AmazonS3Client alloc] initWithAccessKey:ACCESS_KEY_ID withSecretKey:SECRET_KEY] autorelease];
NSOutputStream *stream = [[NSOutputStream alloc] initToFileAtPath:destinationFileName append:NO];
[stream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[stream open];
S3GetObjectRequest *request = [[S3GetObjectRequest alloc] initWithKey:fileName withBucket:BUCKET];
request.outputStream = stream;
[s3 getObject:request];
[stream close];
[stream release];
[request release];
}
Re-evaluating the situation, I'd really like to get my method using an ASynchronous Request and use a S3RequestDelegate object to help me update the bytesIn as it's downloading. In their archive, there is a sample in S3AsyncViewController that should show how to do what I want. I've added S3RequestDelegate.h/.m into my project, and implemented a S3RequestDelegate in my .h like this...
#import "S3RequestDelegate.h"
... {
S3RequestDelegate *s3Delegate;
}
I've altered my retrieveRemoteFile method to look a little like this (it's changed all day and I haven't gotten anywhere)
- (void)retrieveRemoteFile:(NSString *)fileName {
NSString *destinationFileName = [NSString stringWithFormat:#"%#%#",[self getBucketDirectory],fileName];
AmazonS3Client *s3 = [[[AmazonS3Client alloc] initWithAccessKey:ACCESS_KEY_ID withSecretKey:SECRET_KEY] autorelease];
NSOutputStream *stream = [[NSOutputStream alloc] initToFileAtPath:destinationFileName append:NO];
//[stream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
//[stream open];
S3GetObjectRequest *request = [[S3GetObjectRequest alloc] initWithKey:fileName withBucket:BUCKET];
request.outputStream = stream;
s3Delegate = [[S3RequestDelegate alloc] init];
[request setDelegate:s3Delegate];
[s3 getObject:request];
//[stream close];
//[stream release];
//[request release];
}
As you can see, I've set the S3GetObjectRequest delegate with setDelegate to my S3RequestDelegate pointer s3Delegate. I've added breakpoints in all of the delegate methods of the S3RequestDelegate object, but none of them are executing. In looking for a received file on my simulator, nothing is even getting downloaded now.
The sample makes it look like all you need to do is set a delegate to make it asynchronous. It also makes it look like you don't need to manage the stream object, and whether you do or not, nothing gets downloaded. I'm setting the delegate and it's never running any of the delegate methods; didReceiveResponse, didCompleteWithResponse, didReceiveData, didSendData, totalBytesExpectedToWrite, didFailWithError or didFailWithServiceException.
I was setting up my request to run in an OperationsQueue. Removed this code and kick off the Asynchronous transfer on the main thread and the delegate functions handle transferring the next file in the array. I've incorporated a status bar and cancel button and it worked out great.