我正在通过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);
}