5

我正在通过NSTask. 这些工具可能会运行几秒钟,并不断将文本输出到stdout. 最终,该工具将自行终止。我的应用程序使用readInBackgroundAndNotify.

如果我在工具退出后立即停止处理异步输出,我经常会丢失一些到那时尚未交付的输出。

这意味着我必须再等一会儿,让 RunLoop 处理待处理的读取通知。当我阅读了该工具写入管道的所有内容时,如何判断?

这个问题可以在下面的代码中通过删除runMode:调用行来验证 - 然后程序将打印零行已被处理。因此,似乎在进程退出时,队列中已经有一个通知等待传递,并且传递是通过runMode:调用发生的。

runMode: 现在,在工具退出后简单地调用一次似乎就足够了,但我的测试表明它不是 - 有时(有大量输出数据),这仍然只会处理部分剩余数据。

注意:诸如使调用的工具超出某些文本结尾标记之类的解决方法不是我寻求的解决方案。我相信必须有一些正确的方法来做到这一点,从而以某种方式发出管道流的结束信号,这就是我在答案中寻找的。

示例代码

下面的代码可以粘贴到新的 Xcode 项目AppDelegate.m文件中。

运行时,它会调用一个生成更长输出的工具,然后使用waitUntilExit. 如果它会立即删除outputFileHandleReadCompletionObserver,则将丢失该工具的大部分输出。通过在runMode:一秒钟内添加调用,工具的所有输出都会被接收 - 当然,这个定时循环不是最佳的。

而且我想保持runModal函数同步,即它在接收到工具的所有输出之前不会返回。如果这很重要,它确实在我的实际程序中以自己的方式运行(我看到 Peter Hosey 的评论waitUntilExit会阻止 UI,但在我的情况下这不是问题)。

- (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);
}
4

2 回答 2

1

这很简单。在观察者块中,当data.length为 0 时移除观察者并调用terminate.

代码将在该waitUntilExit行之后继续。

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

    __block int lineCount = 0;
    __block bool finished = false;

    NSPipe *outputPipe = [NSPipe pipe];
    theTask.standardOutput = outputPipe;
    NSFileHandle *outputFileHandle = outputPipe.fileHandleForReading;
    NSString __block *prevPartialLine = @"";
    [[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;
            }];
        } else {
            [[NSNotificationCenter defaultCenter] removeObserver:self name:NSFileHandleReadCompletionNotification object:nil];
            [theTask terminate];
            finished = true;
        }
        [note.object readInBackgroundAndNotify];
    }];

    NSParameterAssert(outputFileHandle);
    [outputFileHandle readInBackgroundAndNotify];

    // Start the task
    [theTask launch];

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

    // Wait until all data from the pipe has been received
    while (!finished) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.0001]];
    }

    NSLog(@"Lines processed: %d (should be: %d)", lineCount, expected);
}
于 2020-01-10T17:28:18.340 回答
0

问题waitUntilExit在于它并不总是按照人们的想法行事。文档中提到了以下内容

waitUntilExit不保证在waitUntilExit返回terminationHandler 之前块已经完全执行。

看来这正是您遇到的问题;这是一个竞赛条件。等待的waitUntilExit时间不够长,lineCount变量在完成之前到达NSTask。解决方案可能是使用semaphoreor dispatch_group,尽管目前还不清楚您是否想走那条路——这似乎不是一个容易解决的问题。

*几个月前我遇到了类似的问题,但不幸的是仍未解决

于 2020-01-13T21:30:10.100 回答