Macos 如何在NSTask结束后读取ReadInBackground和Notify的所有剩余输出?

Macos 如何在NSTask结束后读取ReadInBackground和Notify的所有剩余输出?,macos,nstask,nsfilehandle,nspipe,Macos,Nstask,Nsfilehandle,Nspipe,我通过NSTask调用各种命令行工具。这些工具可能会运行几秒钟,并不断地将文本输出到stdout。最终,该工具将自行终止。我的应用程序使用readInBackgroundAndNotify异步读取其输出 如果我在工具退出后立即停止处理异步输出,我通常会丢失一些尚未交付的输出。 这意味着我必须等待更长的时间,允许RunLoop处理挂起的读取通知当我阅读了工具写入管道的所有内容时,如何判断? 通过使用运行模式:调用删除该行,可以在下面的代码中验证此问题-然后程序将打印已处理的零行。因此,在进程退出时

我通过
NSTask
调用各种命令行工具。这些工具可能会运行几秒钟,并不断地将文本输出到
stdout
。最终,该工具将自行终止。我的应用程序使用
readInBackgroundAndNotify
异步读取其输出

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

这意味着我必须等待更长的时间,允许RunLoop处理挂起的读取通知当我阅读了工具写入管道的所有内容时,如何判断?

通过使用
运行模式:
调用删除该行,可以在下面的代码中验证此问题-然后程序将打印已处理的零行。因此,在进程退出时,队列中已经有一个等待传递的通知,该传递通过
运行模式:
调用进行

现在看来,在工具退出后只需调用一次
runMode:
就足够了,但我的测试表明,事实并非如此——有时(对于大量的输出数据),这仍然只会处理剩余数据的一部分

注意:我寻求的解决方案不是让被调用的工具去掉某个文本结尾标记。我相信一定有某种适当的方法可以做到这一点,即以某种方式发出管道流结束的信号,这就是我在寻找答案的原因。

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

运行时,它调用一个工具,该工具生成一些较长的输出,然后使用
waitUntilExit
等待工具终止。如果随后它将立即删除
输出文件handleReadCompletionObserver
,则该工具的大部分输出将丢失。通过添加
runMode:
调用持续一秒钟,将接收工具的所有输出-当然,这个定时循环不是最佳的

我希望保持
runModal
功能同步,即在收到工具的所有输出之前,它不会返回。如果这有关系的话,它在我的实际程序中确实是按自己的方式运行的(我看到Peter Hosey的一条评论,警告说
waitUntilExit
会阻塞UI,但在我的情况下这不是问题)

-(无效)应用程序设计完成启动:(NSNotification*)通知
{
[自动运行工具];
}
-(无效)运行工具
{
//通过调用'head-n200/usr/share/dict/words'检索200行文本`
NSTask*任务=[[NSTask alloc]init];
task.qualityOfService=nsqualityofservice用户已启动;
theTask.launchPath=@/usr/bin/head”;
task.arguments=@[@“-n”,“200”,“@”/usr/share/dict/words“;
__block int lineCount=0;
NSPipe*输出管道=[NSPipe pipe];
task.standard输出=输出管道;
NSFileHandle*outputFileHandle=outputPipe.fileHandleForReading;
NSString_u_块*prevPartialLine=@”;
id OutputFileHandlerReadCompletionObserver=[[NSNotificationCenter defaultCenter]addObserverForName:NSFileHandlerReadCompletionNotification对象:outputFileHandle队列:nil usingBlock:^(NSNotification*\非空注释)
{
//从cmdline工具读取输出
NSData*data=[note.userInfo objectForKey:NSFileHandleNotificationDataItem];
如果(data.length>0){
//检查每一行
NSString*输出=[[NSString alloc]initWithData:数据编码:NSUTF8StringEncoding];
NSArray*lines=[[prevPartialLine stringByAppendingString:output]由字符串分隔的组件:@“\n”];
prevPartialLine=[lines lastObject];
NSInteger lastIdx=lines.count-1;
[行enumerateObjectsUsingBlock:^(NSString*行,NSU整数idx,BOOL*\u非空停止){
if(idx==lastIdx)return;//跳过最后一行(=不完整),因为它没有被LF终止
//现在我们可以处理'line'了`
行数+=1;
}];
}
[note.object readInBackgroundAndNotify];
}];
NSParameterAssert(outputFileHandle);
[outputFileHandle readInBackgroundAndNotify];
//开始任务
[任务启动];
//等到它完成
[task waitUntilExit];
//再等一秒钟,以便我们可以处理该工具的任何剩余输出
NSDate*endDate=[NSDate DATE WITH TIMEINTERVALICENCENNOW:1];
while([NSDate.date比较:endDate]==取消搜索){
[[NSRunLoop currentRunLoop]运行模式:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
[[NSNotificationCenter defaultCenter]removeObserver:OutputFileHandlerReadCompletionObserver];
NSLog(@“已处理的行数:%d”,行数);
}

这很简单。当
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);
}

waitUntilExit
的问题在于它的行为并不总是像人们想象的那样。报告中提到了以下内容:

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

看来这正是你面临的问题;这是一个比赛条件。
waitUntilExit
等待的时间不够长,在
NSTask
完成之前,已到达
lineCount
变量。解决方案可能是使用
信号灯
调度组
,尽管不清楚您是否想走这条路-这似乎不是一个容易解决的问题

*我也经历过类似的情况