NSUserScriptTask difficulties

appstore-sandbox, cocoa, nstask, objective-c

Solution

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`.

Problem

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

Original source

Related problems