NSUserScriptTask的困难

7

我一直在尝试使用最近的NSUserScriptTask类及其子类来解决问题(请参见thisthis),到目前为止,我已经解决了一些问题,但仍有一些问题需要解决。正如你从docs中看到的那样,NSUserScriptTask不允许取消任务。因此,我决定创建一个简单的可执行文件,以脚本路径作为参数运行脚本。这样,我就可以使用NSTask从我的主应用程序启动助手,并在必要时调用[task terminate]。然而,我需要:

  • 主应用程序接收来自启动助手的输出和错误
  • 启动助手只在NSUserScriptTask完成时终止
主应用程序的代码很简单:只需使用正确的信息启动一个NSTask。以下是我现在拥有的内容(为了简单起见,我忽略了与安全范围书签等相关的代码,这些都不是问题。但请记住,这是在沙盒中运行的):
// 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];

记住,我需要终止处理程序仅在以下情况下运行:(a)任务被取消 (b)任务由于脚本完成而自行终止。
现在,在帮助者方面,事情开始变得棘手,至少对我来说是这样。为了简单起见,让我们假设脚本是一个AppleScript文件(因此我使用NSUserAppleScriptTask子类 - 在真实世界中,我必须适应三种类型的任务)。以下是我目前所拥有的:
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;
}

现在,我有三个问题(这是我想通过这个SO条目提出的问题)。首先,“完成任务”从未被打印出来(该块从未被调用),因为任务甚至没有开始执行。相反,我在控制台上看到了这个:
MessageTracer: msgtracer_vlog_with_keys:377: odd number of keys (domain: com.apple.automation.nsuserscripttask_run, last key: com.apple.message.signature)

我尝试从主应用程序运行完全相同的代码,它完成时没有问题(但是从主应用程序中,我失去了取消脚本的能力)。

其次,我只想在完成处理程序被调用后到达main的结尾(return 0;)。但我不知道该怎么做。

第三,每当有错误或来自辅助程序的输出时,我希望将这些错误/输出发送回应用程序,应用程序将通过errorPipe/outputPipe接收它们。像fprintf(stderr/stdout, "string")这样的东西可以解决问题,但我不确定这是否是正确的方法。

因此,简而言之,对于第一和第二个问题的任何帮助都会受到赞赏。第三个问题,我只想确保这就是我应该这样做的方式。

谢谢


你看过XPC了吗? - Dov
@Dov 你为什么认为它更适合?我很好奇,我对XPC服务了解甚少。 - Alex
@jxpx777 是的,XPC确实解决了第二和第三个问题(而且它更好,因为我只针对10.8),但第一个问题怎么办呢? 我想我将不得不尝试并查看从XPC服务运行是否仍会产生错误...如果不会,那就解决了。 - Alex
你确定路径被正确解析了,而不是指向容器内部而不是真实位置吗? - jxpx777
@jxpx777 是的,我正在使用绝对路径,任何沙盒应用程序都只能读取 ~/Library/Application Scripts/com.devname.appname/。而且应用程序和助手都是沙盒化的,但是 NSUserScriptTask 在应用程序上可以工作,但不幸的是在助手上不能工作。 - Alex
显示剩余4条评论
2个回答

4
问题1:子任务无法运行,因为其父任务立即退出。(关于“键数为奇数”的日志消息是NSUserScriptTask中的一个错误,这是因为您的助手没有捆绑标识符,但对您的问题没有影响。)它之所以立即退出,是因为它没有等待完成块触发,这就带来了以下问题...
问题2:如何等待异步完成块?这在其他地方已经得到回答,包括等待多个网络请求全部执行完成 - 包括它们的完成块,但是简要概括一下,使用调度组,类似于以下内容:
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);

如果您想等待带有完成块的任何调用,可以使用相同的模式。 如果您希望在组完成时获得另一种通知而不是等待它,请改用dispatch_group_notify而不是dispatch_group_wait


谢谢,这是一个很酷的方法。但它仍然没有解决我的问题,因为现在,虽然我在控制台上看到了“完成任务”而且没有“错误:…”行,但脚本仍然没有运行。例如,我正在运行的测试AppleScript是say "Hello",但我听不到任何声音……而且它完成得太快了,不像AppleScript。 (顺便说一下,我仍然看到“键的数量是奇数”的消息) - Alex
让我来澄清一下:你看到了“已完成任务”的消息,所以我们知道回调正在触发,回调的error参数为nil,所以它认为它成功了,但是你什么也听不到?如果你直接从命令行运行助手会发生什么?或者如果你将脚本更改为仅计算(例如2+2)并记录结果会发生什么? - Chris N
关于“奇数个键”的问题:正如我之前所说,这基本无害——只是一些诊断代码失败了。如果您真的很在意,可以在您的助手中添加一个具有CFBundleIdentifier的嵌入式plist;请参见http://stackoverflow.com/questions/8234946/adding-version-infomation-to-an-osx-command-line-application。 - Chris N
这完全不影响我。但是在进一步测试后,我发现有时运行脚本时会出现错误,但有时不会。它总是相同的错误:Error running script: Error Domain=NSCocoaErrorDomain Code=257 "The file “Hello.scpt” couldn’t be opened because you don’t have permission to view it." UserInfo=0x102504800 {NSURL=file://localhost/Users/Alex/Library/Application20%Scripts/appID/Hello.scpt, NSLocalizedFailureReason=Script file is not in the application scripts folder.} - Alex
将苹果脚本中的代码更改为简单的 c=a*b; log c(伪代码),并从命令行运行,会产生与 say "Hello" 相同的错误。 - Alex
显示剩余3条评论

3
作为一个旁注,你在分配NSUserAppleScriptTask后测试error的方式是不正确的。只有当函数结果为nil(或NO,或表示失败的其他任何内容)时,error的值才被定义。如果函数成功(如果返回非nil,则知道它成功),那么error可能是任何东西--函数可能将其设置为nil,可能将其留空,甚至可能用真实对象填充它。(另请参见NSError** error的意义是什么?

我明白了,你是对的。我应该用 if (task) 替换 if (error) - Alex

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接