gpt4 book ai didi

macos - NSTask 结束后如何读取 readInBackgroundAndNotify 的所有剩余输出?

转载 作者:行者123 更新时间:2023-12-02 04:18:28 27 4
gpt4 key购买 nike

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

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

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

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

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

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

示例代码

下面的代码可以粘贴到新 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);
}

最佳答案

这很简单。在观察者 block 中,当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);
}

关于macos - NSTask 结束后如何读取 readInBackgroundAndNotify 的所有剩余输出?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58459095/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com