Как прочитать все оставшиеся выходные данные readInBackgroundAndNotify после завершения NSTask? - PullRequest
0 голосов
/ 19 октября 2019

Я вызываю инструмент командной строки через NSTask. Запуск занимает несколько секунд и выводит текст постоянно на stdout. В конце концов, инструмент прекратит работу самостоятельно. Мое приложение считывает свои выходные данные асинхронно с readInBackgroundAndNotify.

Если я перестану обрабатывать асинхронный вывод, как только выйдет инструмент, я потеряю часть своего вывода.

Это означает, что мне придется немного подождать, что позволяет RunLoop обрабатывать ожидающие read уведомления. Как узнать, когда я закончу с этим?

Эту проблему можно проверить с помощью кода ниже, удалив строку с помощью вызова runMode: - тогда программа напечатает этонулевые строки были обработаны. Таким образом, кажется, что в момент завершения процесса в очереди уже есть уведомление, ожидающее доставки, и эта доставка происходит посредством вызова runMode:.

Теперь может показаться, что простовызова runMode: один раз после выхода из инструмента может быть достаточно, но мои тесты показывают, что это не так - иногда (с большими объемами выходных данных) это все равно будет обрабатывать только части оставшихся данных.

Итак, мне нужно условие, которое я могу проверить, чтобы убедиться, что все буферизованные выходные данные были доставлены.

Ниже приведен мой тестовый код, которыйможет быть вставлен в файл AppDelegate.m нового проекта Xcode. Сначала он ожидает завершения инструмента с waitUntilExit. Если бы он немедленно удалил outputFileHandleReadCompletionObserver, большая часть вывода инструмента была бы пропущена. При добавлении вызова runMode: в течение секунды, все выходные данные инструмента принимаются - конечно, этот временной цикл меньше оптимального. Как мне сделать это правильно?

И я хотел бы сохранить функцию runModal синхронной, то есть она не должна возвращаться, пока не получит все выходные данные инструмента. Он работает в своей собственной программе, если это имеет значение (я видел комментарий Питера Хоси, предупреждающий, что waitUntilExit заблокирует пользовательский интерфейс, но в моем случае это не будет проблемой).

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    [self runTool];
}

- (void)runTool
{
    // Retrieve 200 lines of text by invoking `head -n 200 /usr/share/dict/words`
    NSTask *theTask = [[NSTask alloc] init];
    theTask.qualityOfService = NSQualityOfServiceUserInitiated;
    theTask.launchPath = @"/usr/bin/head";
    theTask.arguments = @[@"-n", @"200", @"/usr/share/dict/words"];

    __block int lineCount = 0;

    NSPipe *outputPipe = [NSPipe pipe];
    theTask.standardOutput = outputPipe;
    NSFileHandle *outputFileHandle = outputPipe.fileHandleForReading;
    NSString __block *prevPartialLine = @"";
    id <NSObject> outputFileHandleReadCompletionObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSFileHandleReadCompletionNotification object:outputFileHandle queue:nil usingBlock:^(NSNotification * _Nonnull note)
    {
        // Read the output from the cmdline tool
        NSData *data = [note.userInfo objectForKey:NSFileHandleNotificationDataItem];
        if (data.length > 0) {
            // go over each line
            NSString *output = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            NSArray *lines = [[prevPartialLine stringByAppendingString:output] componentsSeparatedByString:@"\n"];
            prevPartialLine = [lines lastObject];
            NSInteger lastIdx = lines.count - 1;
            [lines enumerateObjectsUsingBlock:^(NSString *line, NSUInteger idx, BOOL * _Nonnull stop) {
                if (idx == lastIdx) return; // skip the last (= incomplete) line as it's not terminated by a LF
                // now we can process `line`
                lineCount += 1;
            }];
        }
        [note.object readInBackgroundAndNotify];
    }];

    NSParameterAssert(outputFileHandle);
    [outputFileHandle readInBackgroundAndNotify];

    // Start the task
    [theTask launch];

    // Wait until it is finished
    [theTask waitUntilExit];

    // Wait one more second so that we can process any remaining output from the tool
    NSDate *endDate = [NSDate dateWithTimeIntervalSinceNow:1];
    while ([NSDate.date compare:endDate] == NSOrderedAscending) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
    }

    [[NSNotificationCenter defaultCenter] removeObserver:outputFileHandleReadCompletionObserver];

    NSLog(@"Lines processed: %d", lineCount);
}
...