NSUserScriptTask трудности

Я пытался сделать (см. это и this) с недавним классом NSUserScriptTask и его подклассами, и до сих пор я решил некоторые проблемы, но некоторые другие еще предстоит решить. Как видно из документов, 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];

Помните, мне нужно, чтобы обработчик завершения запускался только тогда, когда: (а) задача была отменена (б) задача завершилась сама по себе, потому что сценарий завершил работу.

Теперь, на стороне помощника, вещи начинают становиться волосатыми, по крайней мере, для меня. Давайте для простоты представим, что сценарий представляет собой файл 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") делает свое дело, но я не уверен, что это правильный способ сделать это.

Короче говоря, любая помощь в отношении первой и второй проблем приветствуется. Третий я просто хочу убедиться, что я должен это сделать.

Спасибо


person Alex    schedule 25.03.2013    source источник
comment
Вы смотрели XPC?   -  person Dov    schedule 27.03.2013
comment
@Dov Я смотрел на XPC для других целей, но, честно говоря, я не понимал, как его использовать. Как вы думаете, это будет делать здесь?   -  person Alex    schedule 27.03.2013
comment
Я не могу сказать наверняка, потому что я не пробовал, но кажется, что это больше подходит для того, что вы делаете. Возможно, стоит посмотреть.   -  person Dov    schedule 27.03.2013
comment
@Dov Как ты думаешь, почему это больше подходит? Мне любопытно, я действительно практически ничего не знаю об услугах XPC.   -  person Alex    schedule 27.03.2013
comment
Если вы можете ориентироваться на 10.8, есть несколько хороших оболочек Cocoa для XPC. В противном случае вы будете писать свои собственные простые обертки. Я думаю, что XPC вполне подойдет для вашего варианта использования. Другим вариантом может быть использование распределенных уведомлений для сигнализации о завершении задачи и регистрации основного приложения для них. Вспомогательное приложение 1Password использует XPC и распределенные уведомления для связи между вспомогательным и основным приложением в зависимости от ситуации.   -  person jxpx777    schedule 27.03.2013
comment
@ jxpx777 Да, XPC решает вторую и третью проблемы (и это даже лучше, потому что я нацелен только на 10.8), но как насчет первого вопроса? Я думаю, мне придется попробовать и посмотреть, выдает ли по-прежнему запуск из службы XPC ошибку ... Если нет, то это решено.   -  person Alex    schedule 27.03.2013
comment
Вы уверены, что путь определяется правильно и не указывает вам куда-то внутри вашего Контейнера, а не в реальное местоположение?   -  person jxpx777    schedule 28.03.2013
comment
@ jxpx777 Да, я использую абсолютные пути, и любое изолированное приложение имеет доступ только для чтения к ~/Library/Application Scripts/com.devname.appname/. И приложение, и помощник находятся в песочнице, но NSUserScriptTask работает с приложением, а не с помощником.   -  person Alex    schedule 28.03.2013
comment
@ jxpx777 Также обратите внимание, что XPC полезен для меня только в том случае, если я могу выйти из помощника (что, в свою очередь, должно отменить NSUserScriptTask). Именно по этой причине я пытался использовать помощника в первую очередь...   -  person Alex    schedule 28.03.2013


Ответы (2)


Вопрос 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.

person Chris N    schedule 30.03.2013
comment
Спасибо, это классный способ сделать это. Но это все еще не решило мою проблему, потому что теперь, хотя я вижу в консоли @Finished task и нет строки @Error: ..., скрипт все еще не работает. Например, тестовый яблочный скрипт, который я запускаю, это say "Hello", и я ничего не слышу... И он завершается слишком быстро для яблочного скрипта. (кстати, я все еще вижу сообщение odd number of keys) - person Alex; 31.03.2013
comment
Позвольте мне уточнить: вы видите сообщение о завершении задачи, поэтому мы знаем, что обратный вызов срабатывает, а параметр обратного вызова error равен нулю, поэтому он считает, что он выполнен успешно, но вы ничего не слышите? Что произойдет, если вы запустите помощник прямо из командной строки? Или если вы измените сценарий на что-то, предназначенное только для вычислений (например, 2+2), и зарегистрируете результат? - person Chris N; 31.03.2013
comment
Ре. нечетное количество ключей: как я уже говорил, это по сути безвредно — это ошибка какого-то диагностического кода. Если вас это действительно беспокоит, вы можете добавить встроенный plist с CFBundleIdentifier в свой помощник; см. ‹stackoverflow.com/questions/8234946/›. - person Chris N; 31.03.2013
comment
Меня это совсем не беспокоит. Но при дальнейшем тестировании я обнаружил, что иногда при запуске скрипта возникают ошибки, а иногда нет. Всегда одна и та же ошибка: 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.} - person Alex; 31.03.2013
comment
А из командной строки получаю вот такую ​​ошибку (все тот же say "Hello" applescript): Error running script: Error Domain=NSPOSIXErrorDomain Code=2 "The operation couldn’t be completed. /usr/bin/osascript: couldn't set default target: no eligible process with specified descriptor " UserInfo=0x7ffefbe04f60 {NSURL=file://localhost/Users/Alex/Library/Application%20Scripts/net.ACT-Productions.Clockwise/Hello.scpt, NSLocalizedFailureReason=/usr/bin/osascript: couldn't set default target: no eligible process with specified descriptor } - person Alex; 31.03.2013
comment
И изменение на более простой c=a*b; log c (псевдокод) в applescript, запускаемом из командной строки, дает ту же ошибку, что и say "Hello" - person Alex; 31.03.2013
comment
Помучавшись с этим какое-то время, я пришел к выводу, что это не сработает. Его фундаментальный недостаток заключается в том, что уничтожение процесса не приведет к немедленному уничтожению порожденных им служб XPC (launchd может оставить их в живых до 20 секунд), и он особенно несовершенен для NSUserScriptTask из-за того, как он находит папку сценариев приложения. Мой совет — подождать реального API отмены. - person Chris N; 04.04.2013
comment
Спасибо, я уже начал представлять что-то подобное. Спасибо за беспокойство. - person Alex; 05.04.2013

В качестве примечания: то, как вы тестируете error после выделения NSUserAppleScriptTask, неверно. Значение error определяется тогда и только тогда, когда результат функции равен нулю (или НЕТ, или что-то еще, указывающее на ошибку). Если функция завершается успешно (что вы знаете, если она возвращает ненулевое значение), то error может быть любым — функция может установить его в nil, может оставить его неопределенным, она может даже заполнить его реальным объектом. (См. также В чем смысл ошибки (NSError**)?)

person Chris N    schedule 31.03.2013
comment
Я вижу, ты прав. Я должен заменить if (error) на if (task) - person Alex; 31.03.2013