Я вызываю инструмент командной строки через NSTask
. Запуск занимает несколько секунд и выводит текст постоянно на stdout
. В конце концов, инструмент прекратит работу самостоятельно. Мое приложение считывает свои выходные данные асинхронно с readInBackgroundAndNotify
.
Если я перестану обрабатывать асинхронный вывод, как только выйдет инструмент, я потеряю часть своего вывода.
Это означает, что мне придется немного подождать, что позволяет RunLoop обрабатывать ожидающие read уведомления. Как узнать, когда я закончу с этим?
Эту проблему можно проверить с помощью кода ниже, удалив строку с помощью вызова runMode:
- тогда программа напечатает этонулевые строки были обработаны. Таким образом, кажется, что в момент завершения процесса в очереди уже есть уведомление, ожидающее доставки, и эта доставка происходит посредством вызова runMode:
.
Теперь может показаться, что простовызова runMode:
один раз после выхода из инструмента может быть достаточно, но мои тесты показывают, что это не так - иногда (с большими объемами выходных данных) это все равно будет обрабатывать только части оставшихся данных.
Итак, мне нужно условие, которое я могу проверить, чтобы убедиться, что все буферизованные выходные данные были доставлены.
Ниже приведен мой тестовый код, которыйможет быть вставлен в файл AppDelegate.m
нового проекта Xcode. Сначала он ожидает завершения инструмента с waitUntilExit
. Если бы он немедленно удалил outputFileHandleReadCompletionObserver
, большая часть вывода инструмента была бы пропущена. При добавлении вызова runMode:
в течение секунды, все выходные данные инструмента принимаются - конечно, этот временной цикл меньше оптимального. Как мне сделать это правильно?
И я хотел бы сохранить функцию runModal
синхронной, то есть она не должна возвращаться, пока не получит все выходные данные инструмента. Он работает в своей собственной программе, если это имеет значение (я видел комментарий Питера Хоси, предупреждающий, что waitUntilExit
заблокирует пользовательский интерфейс, но в моем случае это не будет проблемой).
- (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);
}