NSUserScriptTask difficulties - objective-c

I've been trying to make do (see this and this) with the recent NSUserScriptTask class and its subclasses and so far I've solved some problems, but some others remain to be solved. As you can see from the docs, NSUserScriptTask does not allow for the cancellation of tasks. So, I decided to create a simple executable that takes as arguments the path to the script and runs the script. That way, I can launch the helper from my main app using NSTask and call [task terminate] when necessary. However, I require:
The main app to receive output and errors from the helper it launched
The helper only terminating when the NSUserScriptTask is done
The code for the main app is simple: just launch an NSTask with the proper info. Here's what I have now (for the sake of simplicity I ignored the code for security-scoped bookmarks and the like, which are out of the problem. But don't forget this is running sandboxed):
// Create task
task = [NSTask new];
[task setLaunchPath: [[NSBundle mainBundle] pathForResource: #"ScriptHelper" ofType: #""]];
[task setArguments: [NSArray arrayWithObjects: scriptPath, nil]];
// Create error pipe
NSPipe* errorPipe = [NSPipe new];
[task setStandardError: errorPipe];
// Create output pipe
NSPipe* outputPipe = [NSPipe new];
[task setStandardOutput: outputPipe];
// Set termination handler
[task setTerminationHandler: ^(NSTask* task){
// Save output
NSFileHandle* outFile = [outputPipe fileHandleForReading];
NSString* output = [[NSString alloc] initWithData: [outFile readDataToEndOfFile] encoding: NSUTF8StringEncoding];
if ([output length]) {
[output writeToFile: outputPath atomically: NO encoding: NSUTF8StringEncoding error: nil];
}
// Log errors
NSFileHandle* errFile = [errorPipe fileHandleForReading];
NSString* error = [[NSString alloc] initWithData: [errFile readDataToEndOfFile] encoding: NSUTF8StringEncoding];
if ([error length]) {
[error writeToFile: errorPath atomically: NO encoding: NSUTF8StringEncoding error: nil];
}
// Do some other stuff after the script finished running <-- IMPORTANT!
}];
// Start task
[task launch];
Remember, I need the termination handler to only run when: (a) the task was cancelled (b) the task terminated on its own because the script finished running.
Now, on the helper side things start to get hairy, at least for me. Let's imagine for the sake of simplicity that the script is an AppleScript file (so I use the NSUserAppleScriptTask subclass - on the real world I'd have to accomodate for the three types of tasks). Here's what I got so far:
int main(int argc, const char * argv[])
{
#autoreleasepool {
NSString* filePath = [NSString stringWithUTF8String: argv[1]];
__block BOOL done = NO;
NSError* error;
NSUserAppleScriptTask* task = [[NSUserAppleScriptTask alloc] initWithURL: [NSURL fileURLWithPath: filePath] error: &error];
NSLog(#"Task: %#", task); // Prints: "Task: <NSUserAppleScriptTask: 0x1043001f0>" Everything OK
if (error) {
NSLog(#"Error creating task: %#", error); // This is not printed
return 0;
}
NSLog(#"Starting task");
[task executeWithAppleEvent: nil completionHandler: ^(NSAppleEventDescriptor *result, NSError *error) {
NSLog(#"Finished task");
if (error) {
NSLog(#"Error running task: %#", error);
}
done = YES;
}];
// Wait until (done == YES). How??
}
return 0;
}
Now, I have three questions (which are the ones I want to ask with this SO entry). Firstly, "Finished task" never gets printed (the block never gets called) because the task never even starts executing. Instead, I get this on my console:
MessageTracer: msgtracer_vlog_with_keys:377: odd number of keys (domain: com.apple.automation.nsuserscripttask_run, last key: com.apple.message.signature)
I tried running the exact same code from the main app and it completes without a fuss (but from the main app I lose the ability to cancel the script).
Secondly, I only want to reach the end of main (return 0;) after the completion handler is called. But I have no idea how to do that.
Thridly, whenever there's an error or output from the helper I want to send that error/output back to the app, which will receive them through the errorPipe/outputPipe. Something like fprintf(stderr/stdout, "string") does the trick, but I'm not sure if it is the right way to do it.
So, in short, any help regarding the first and second problems is appreciated. The third one I just want to make sure that's how I'm supposed to do it.
Thanks

Question 1: The sub-task doesn't run because its parent exits immediately. (The log message about "odd number of keys" is a bug in NSUserScriptTask, and happens because your helper doesn't have a bundle identifier, but is otherwise harmless and irrelevant to your problem.) It exits immediately because it's not waiting for the completion block to fire, which brings us to...
Question 2: How do you wait for an asynchronous completion block? This has been answered elsewhere, including Wait until multiple networking requests have all executed - including their completion blocks, but to recap, use dispatch groups, something like this:
dispatch_group_t g = dispatch_group_create();
dispatch_group_enter(g);
[task executeWithAppleEvent:nil completionHandler:^(NSAppleEventDescriptor *result, NSError *e) {
...
dispatch_group_leave(g);
}];
dispatch_group_wait(g, DISPATCH_TIME_FOREVER);
dispatch_release(g);
This same pattern works for any call that has a completion block you want to wait for. If you wanted another notification when the group finishes instead of waiting for it, use dispatch_group_notify instead of dispatch_group_wait.

As a side note, the way you’re testing error after allocating the NSUserAppleScriptTask is incorrect. The value of error is defined if and only if the function result is nil (or NO, or whatever indicates failure). If the function succeeds (which you know if it returns non-nil), then error may be anything -- the function may set it to nil, it may leave it undefined, it may even fill it in with a real object. (See also What's the Point of (NSError**)error?)

Related

recycleURLs:completionHandler: with NIL makes it block until completion?

I was sending a file to the trash with
[NSWorkSpace recycleURLs: myArrayOfOneNsurl
completionHandler: NIL]
myArrayOfOneNsurl is a NSArray created with arrayWithObject: of a single NSURL that was created for an absolute file path with fileURLWithPath:isDirectory:.
The normal way to tell if it is successful is to use the completionHandler and then check if the 2nd arg (NSError) is NIL, which means success.
Is there anyway to check success without this callback? Currently I have setup a loop after calling this, to check for file existence, if 1second has past and it still exists, I declare fail. I was wondering if I set NIL as second arg to recycleURLs:completionHandler: does it make it block until the process completes (regardless of success)? In my tests, the very first check always finds that the file is no longer at its original place (meaning it was trashed), but I'm not sure if my computer is just super fast, or it really is blocking until file operation completes.
For trashing a single file or directory, you can use NSFileManager trashItemAtURL instead. It is synchronous, so you avoid the headaches with the completion callback.
NSError * error = nil;
[[NSFileManager defaultManager] trashItemAtURL:url resultingItemURL:nil error:&error];
This API is async. Passing NULL as completionHandler only means, that you are not interested in result. The only way to know when operation finished and was it successful is using completionHandler.
If you really need to make it sync, you may use following approach (although I don't recommend that):
__block BOOL recycleFinished = NO;
__block NSError *recycleError = nil;
[[NSWorkspace sharedWorkspace] recycleURLs:myArrayOfOneNsurl
completionHandler:^(NSDictionary<NSURL *,NSURL *> * _Nonnull newURLs, NSError * _Nullable error) {
recycleFinished = YES;
recycleError = error;
}];
while (!recycleFinished) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:.5]];
}
// access recycleError
NSLog(#"%#", recycleError);

Obj-C: __block variable not retaining data

I think I might have an async problem going on here, which bites cause I thought I had solved it. Anyway, I am making a bunch of web service calls like so:
//get the client data
__block NSArray* arrClientPAs;
[dataManager getJSONData:strWebService withBlock:^(id results, NSError* error) {
if (error) {
UIAlertView* alert = [[UIAlertView alloc] initWithTitle:#"Getting Client Data Error!" message:error.description delegate:nil cancelButtonTitle:NSLocalizedString(#"Okay", nil) otherButtonTitles:nil, nil];
[alert show];
} else {
arrClientPAs = results;
}
}];
and getJSONData is like so:
- (void) getJSONData : (NSString*) strQuery withBlock:(void (^)(id, NSError *))completion {
NSDictionary* dictNetworkStatus = [networkManager checkNetworkConnectivity];
NetworkStatus networkStatus = [[dictNetworkStatus objectForKey:#"Status"] intValue];
if (networkStatus != NotReachable) {
//set up the url for webservice
NSURL* url = [NSURL URLWithString:strQuery];
NSMutableURLRequest* urlRequest = [NSMutableURLRequest requestWithURL:url];
//set up the url connection
__block id results;
[NSURLConnection sendAsynchronousRequest:urlRequest queue:[NSOperationQueue mainQueue] completionHandler:
^(NSURLResponse* response, NSData* jsonData, NSError* error) {
if (error) {
completion(nil, error);
return;
}
results = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers | NSJSONReadingMutableLeaves | NSJSONReadingAllowFragments error:&error];
completion(results, nil);
}];
} else {
//not connected to a network - data is going to have to come from coredata
}
}
In the first block, if I log arrClientData I can see the data that I am expecting but when I log arrClientData after it it is nil. I was following this SO thread - How to return a BOOL with asynchronous request in a method? (Objective-C) and a couple of others.
Obviously I am trying to get the data after the async call is made. Any help would be appreciated.
The problem lies, I think, in what "asynchronous" means. Here's a diagram:
Step One
__block result;
Step Two - do something asynchonous, including e.g. setting result
Step Three
What order do things happen in here? Step Three happens before Step Two gets finished. That is what asynchronous means: it means, "go right on with this code, don't wait for the asynchronous stuff to finish." So at the time Step Three happens, the result variable has not yet been set to anything.
So, you are just misleading the heck out of yourself with your __block result. __block or no __block, there is no way you are going to find out out what the result is afterwards, because there is no "afterwards". Your code has completed before your __block result is even set. That is why asynchronous code uses a callback (eg. your completion block) which does run afterwards, because it is sequentially part of (appended to) the asynchronous code. You can hand your result downwards through the callback, but you cannot usefully set it upwards from within the block and expect to retrieve it later.
So, your overall structure is like this:
__block NSArray* arrClientPAs; // it's nil
[call getJSONdata] = step one
[call sendAsynchronousRequest]
do the block _asynchronously_ = step two, tries to set arrClientPAs somehow
step three! This happens _before_ step two, ...
... and this entire method ends and is torn down ...
... and arrClientPAs is still nil! 🌻
I repeat: you cannot pass any information UP out of an asynchronous block. You can only go DOWN. You need your asynchronous block to call some method of some independently persistent object to hand it your result and tell it to use that result (and do it carefully, on the main thread, or you will cause havoc). You cannot use any automatic variable for this purpose, such as your declared NSArray variable arrClientPAs; there is no automatic scope any more, the method is over, the automatic variable is gone, there is no more code to run.
Check the value of the 'error 'variable after call:
results = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers | NSJSONReadingMutableLeaves | NSJSONReadingAllowFragments error:&error];
If 'error' isn't nil there is a problem with data which you get in your completion block.
You are mixing styles and confusing the purpose of __block.
Note: When you call a method that will be executed asynchronously you are creating a new execution path which will be executed at some point in the future (which includes immediately) on some thread.
In your getJSONData method you use a __block qualified variable, results, when you should not. The variable is only required within the block and should be declared there:
//set up the url connection
[NSURLConnection sendAsynchronousRequest:urlRequest queue:[NSOperationQueue mainQueue] completionHandler:
^(NSURLResponse* response, NSData* jsonData, NSError* error)
{
if (error) {
completion(nil, error);
return;
}
id results = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers | NSJSONReadingMutableLeaves | NSJSONReadingAllowFragments error:&error];
completion(results, nil);
}];
Declaring the variable outside of the block and adding __block just adds pointless complexity. After the call to sendAsynchronousRequest, returns before the request has been performed, the value of results would not be the value assigned in the block. The call to the completion block is performed on a different execution path and probably will not even be executed until after the call to getJSONData has returned.
However what is correct about your getJSONData method is its model - it takes a completion block which sendAsynchronousRequest's own completion handler will call. This is what is incorrect about your call to getJSONData - the completion block you pass does not pass on the results to another block or pass them to some object, but instead assigns them a local variable, arrClientPAs, declared before the call. This is the same situation as described above for getJSONData and will fail for the same reasons - it is not the arrClientPAs fails to "retain the data" but that you are reading it on in the current execution path before another execution path has written any data to it.
You can address this problem the same way getJSONData does - the enclosing method (not included in your question) can take a completion block (code entered directly into answer, expect typos!):
- (void) getTheClientData: ... completionHandler:(void (^)(id))handler
{
...
//get the client data
[dataManager getJSONData:strWebService withBlock:^(id results, NSError* error) {
if (error) {
UIAlertView* alert = [[UIAlertView alloc] initWithTitle:#"Getting Client Data Error!" message:error.description delegate:nil cancelButtonTitle:NSLocalizedString(#"Okay", nil) otherButtonTitles:nil, nil];
[alert show];
} else {
handler(results); // "return" the result to the handler
}
}];
There is another approach. If and only if getClientData is not executing on the main thread and you wish its behaviour to be synchronous and to return the result of the request then you can issue a sendSynchronousRequest:returningResponse:error: instead of an asynchronous one. This will block the thread getClientData is executing on until the request completes.
In general if you have an asynchronous method which you cannot replace by a synchronous one but require synchronous behaviour you can use semaphores to block your current thread until the asynchronous call completes. For an example of how to do this see this answer.
HTH

NSTask: why program is blocking when read from NSPipe?

I use the NSTask to run shell command and output the data via NSPipe. At first, I using bellow method to read output data, it is no any problem.
- (void)outputAvailable:(NSNotification *)aNotification {
NSString *newOutput;
NSMutableData *allData = [[NSMutableData alloc] init];
NSData *taskData = nil;
if((taskData = [readHandle availableData]) && [taskData length])
newOutput = [[NSString alloc] initWithData:allData encoding:NSASCIIStringEncoding];
NSLog(#"%#", newOutput);
[readHandle readInBackgroundAndNotify];
}
The problem about the method is that it only output 4096 bytes data. So I using while loop to get more data, modify the method like this:
- (void)outputAvailable:(NSNotification *)aNotification {
NSString *newOutput;
NSMutableData *allData; //Added.
NSData *taskData = nil;
while ((taskData = [readHandle availableData]) && [taskData length]) {
[allData appendData:taskData];
}
newOutput = [[NSString alloc] initWithData:allData encoding:NSASCIIStringEncoding];
NSLog(#"%#", newOutput);
[readHandle readInBackgroundAndNotify];
}
Then problem occurs: the program is blocking in the while loop and can not perform the following statements. I ensure that allData is what I wanted, but after appending the last data chunk, it is blocking.
Could you give me some solutions? Thanks.
Your while() loop effectively blocks further notifications, causing the whole program to block waiting for something to flush the buffer.
You should readInBackgroundAndNotify, then pull off availableBytes on each notification, appending it to your NSMutableData (which is likely held in an instance variable). When you handle the notification, don't attempt to wait for more data or do any kind of a while loop. The system will notify you when more data is available.
I.e. the system pushes data to you, you do not pull data from the system.
Ahh... OK. You should still only pull data when there is data available. Your while() loop is doing that. Not enough coffee. My bad.
The final block is most likely because your external process is not closing the pipe; no EOF is received and, thus, the program is waiting forever for more data that never arrives.
Either:
make sure the background task exits
detect when you've received enough data and terminate the process
If you are doing some kind of conversion program (say, tr) where you write data on the processes standard input, then you might need to close the standard input pipe.

NSDATA writeToFile crashes without a reason

I am downloading images from a url using NSDATA and saving them to local file system using
NSData *dataForStorage = [NSData dataWithData:UIImagePNGRepresentation(img)];
BOOL saveResult=[ dataForStorage writeToFile:jpegFilePath options:NSDataWritingAtomic error:&error];
NSLog(#"Write returned error: %#", [error localizedDescription]);
My app crashes randomly without even giving a message, though some files are saved (again randomly). When I run the app in Debug mode, I frequently see "EXC_BAD_ACCESS" but continuing execution succeeds in saving some of the files.
This code is executed in background from:
[self performSelectorInBackground:#selector(loadImageInBackground:) withObject:arr];
Please suggest.
One of the problems in your code is that your running code in a thread without an autorelease pool but are using functions that would require one. Put the following code into the loadImageInBackground method:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// existing code
[pool drain];
This is probably just one of several problems. For further assistance, we need to see the stack trace of the crash.
Just a wild guess : arr is an autoreleased object, so, sometimes it gets deallocated before your selector gets called. Try using [arr copy] and release it after saving it.
I was having the EXACT same problem, but it turned out that the problem was something else: my URL was getting release prematurely. In the end this is what I did and it worked:
I made this call:
[self performSelectorInBackground:#selector(downloadData:) withObject:nil];
And this is the method:
// URL - (NSString) URL for file
// filePath - (NSString) save location on device
-(void)download:(NSString *)URL
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:URL]];
[data writeToFile:filePath atomically:YES];
[pool release];
}
So I think that your download code is correct, but there is some other variable that is getting deallocated early (possibly your path).
Hope this helps! I know the other answers on this page worked for me.

Getting exit status after launching app with NSWorkspace launchApplicationAtURL

I'm kind of new at Mac programming. I am porting a plugin to OSX. I need my application to launch a second app (which I do not control the source for) and then get its exit code. NSWorkspace launchApplicationAtURL works great to launch it with the needed arguments but I can't see how to get the exit code. Is there a way to get it after setting up notification for termination of the second app? I see tools for getting an exit code using NSTask instead. Should I be using that?
The NSWorkspace methods are really for launching independent applications; use NSTask to "run another program as a subprocess and ... monitor that program’s execution" as per the docs.
Here is a simple method to launch an executable and return its standard output - it blocks waiting for completion:
// Arguments:
// atPath: full pathname of executable
// arguments: array of arguments to pass, or nil if none
// Return:
// the standard output, or nil if any error
+ (NSString *) runCommand:(NSString *)atPath withArguments:(NSArray *)arguments
{
NSTask *task = [NSTask new];
NSPipe *pipe = [NSPipe new];
[task setStandardOutput:pipe]; // pipe standard output
[task setLaunchPath:atPath]; // set path
if(arguments != nil)
[task setArguments:arguments]; // set arguments
[task launch]; // execute
NSData *data = [[pipe fileHandleForReading] readDataToEndOfFile]; // read standard output
[task waitUntilExit]; // wait for completion
if ([task terminationStatus] != 0) // check termination status
return nil;
if (data == nil)
return nil;
return [NSString stringWithUTF8Data:data]; // return stdout as string
}
You may not want to block, especially if this is your main UI thread, supply standard input etc.
In fact, this property of the NSTask should do the trick: terminationStatus
From Apple's doc:
Returns the exit status returned by the receiver’s executable.
(int)terminationStatus
I tested it and it works ok. Watch out to test if the task is running first, otherwise an exception will be launched.
if (![aTask isRunning]) {
int status = [aTask terminationStatus];
if (status == ATASK_SUCCESS_VALUE)
NSLog(#"Task succeeded.");
else
NSLog(#"Task failed.");
}
Hope it helps.