如何在 NSTask 结束后读取 readInBackgroundAndNotify 的所有剩余输出?
How to read all remaining output of readInBackgroundAndNotify after NSTask has ended?
我正在通过 NSTask
调用各种命令行工具。这些工具可能 运行 几秒钟,并不断输出文本到 stdout
。最终,该工具将自行终止。我的应用程序使用 readInBackgroundAndNotify
.
异步读取其输出
如果我在工具退出后立即停止处理异步输出,我通常会丢失一些到那时尚未交付的输出。
这意味着我必须等待更长的时间,让 RunLoop 处理未决的 read 通知。 我如何知道我已读取该工具写入管道的所有内容?
可以在下面的代码中通过删除带有 runMode:
调用的行来验证此问题 - 然后程序将打印零行已处理。因此,似乎在进程退出时,队列中已经有一个等待传递的通知,并且该传递是通过 runMode:
调用进行的。
现在,似乎在工具退出后简单地调用 runMode:
一次 可能就足够了,但我的测试表明它不是 - 有时(与大量的输出数据),这仍然只会处理剩余数据的一部分。
注意:我寻求的解决方案不是使调用的工具超出某些文本结束标记。我相信必须有一些正确的方法来做到这一点,从而以某种方式指示管道流的末端,这就是我在答案中寻找的。
示例代码
可以将下面的代码粘贴到新的 Xcode 项目的 AppDelegate.m
文件中。
当 运行 时,它会调用生成一些较长输出的工具,然后等待使用 waitUntilExit
终止该工具。如果它随后立即删除 outputFileHandleReadCompletionObserver
,那么该工具的大部分输出将丢失。通过添加持续一秒的 runMode:
调用,可以接收到该工具的所有输出——当然,这个定时循环不是最佳的。
而且我想保持 runModal
函数同步,即它在收到该工具的所有输出之前不应 return。它在我的实际程序中按自己的方式执行 运行,如果这很重要(我看到 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);
}
很简单。在观察者块中,当 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
的问题在于它的行为并不总是像人们想象的那样。 documenation中提到以下内容:
waitUntilExit does not guarantee that the terminationHandler
block has been fully executed before waitUntilExit returns.
看来这正是您遇到的问题;这是一个竞争条件。 waitUntilExit
等待的时间不够长,lineCount
变量在 NSTask
完成之前到达。解决方案可能是使用 semaphore
或 dispatch_group
,尽管不清楚您是否想走那条路——这似乎不是一个容易解决的问题。
*几个月前我遇到过类似的问题,不幸的是仍然没有解决。
我正在通过 NSTask
调用各种命令行工具。这些工具可能 运行 几秒钟,并不断输出文本到 stdout
。最终,该工具将自行终止。我的应用程序使用 readInBackgroundAndNotify
.
如果我在工具退出后立即停止处理异步输出,我通常会丢失一些到那时尚未交付的输出。
这意味着我必须等待更长的时间,让 RunLoop 处理未决的 read 通知。 我如何知道我已读取该工具写入管道的所有内容?
可以在下面的代码中通过删除带有 runMode:
调用的行来验证此问题 - 然后程序将打印零行已处理。因此,似乎在进程退出时,队列中已经有一个等待传递的通知,并且该传递是通过 runMode:
调用进行的。
现在,似乎在工具退出后简单地调用 runMode:
一次 可能就足够了,但我的测试表明它不是 - 有时(与大量的输出数据),这仍然只会处理剩余数据的一部分。
注意:我寻求的解决方案不是使调用的工具超出某些文本结束标记。我相信必须有一些正确的方法来做到这一点,从而以某种方式指示管道流的末端,这就是我在答案中寻找的。
示例代码
可以将下面的代码粘贴到新的 Xcode 项目的 AppDelegate.m
文件中。
当 运行 时,它会调用生成一些较长输出的工具,然后等待使用 waitUntilExit
终止该工具。如果它随后立即删除 outputFileHandleReadCompletionObserver
,那么该工具的大部分输出将丢失。通过添加持续一秒的 runMode:
调用,可以接收到该工具的所有输出——当然,这个定时循环不是最佳的。
而且我想保持 runModal
函数同步,即它在收到该工具的所有输出之前不应 return。它在我的实际程序中按自己的方式执行 运行,如果这很重要(我看到 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);
}
很简单。在观察者块中,当 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
的问题在于它的行为并不总是像人们想象的那样。 documenation中提到以下内容:
waitUntilExit does not guarantee that the
terminationHandler
block has been fully executed before waitUntilExit returns.
看来这正是您遇到的问题;这是一个竞争条件。 waitUntilExit
等待的时间不够长,lineCount
变量在 NSTask
完成之前到达。解决方案可能是使用 semaphore
或 dispatch_group
,尽管不清楚您是否想走那条路——这似乎不是一个容易解决的问题。
*几个月前我遇到过类似的问题,不幸的是仍然没有解决。