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
变量。解决方案可能是使用信号灯
或调度组
,尽管不清楚您是否想走这条路-这似乎不是一个容易解决的问题
*我也经历过类似的情况