NSTask требует сброса при чтении из стандартного вывода процесса, Терминал этого не делает.

У меня есть простой скрипт Python, который запрашивает ваше имя, а затем выдает его обратно:

def main():
    print('Enter your name: ')
    for line in sys.stdin:
        print 'You entered: ' + line

Довольно простые вещи! При запуске этого в терминале OS X он отлично работает:

$ python nameTest.py 
Enter your name: 
Craig^D
You entered: Craig

Но при попытке запустить этот процесс через NSTask стандартный вывод появляется только в том случае, если в сценарий Python добавлены дополнительные вызовы flush().

Вот как я настроил NSTask и трубопровод:

NSTask *_currentTask = [[NSTask alloc] init];
_currentTask.launchPath = @"/usr/bin/python";
_currentTask.arguments = [NSArray arrayWithObject:@"nameTest.py"];

NSPipe *pipe = [[NSPipe alloc] init];
_currentTask.standardOutput = pipe;
_currentTask.standardError = pipe;

dispatch_queue_t stdout_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

__block dispatch_block_t checkBlock;

checkBlock = ^{
    NSData *readData = [[pipe fileHandleForReading] availableData];
    NSString *consoleOutput = [[NSString alloc] initWithData:readData encoding:NSUTF8StringEncoding];
    dispatch_sync(dispatch_get_main_queue(), ^{
        [self.consoleView appendString:consoleOutput];
    });
    if ([_currentTask isRunning]) {
        [NSThread sleepForTimeInterval:0.1];
        checkBlock();
    } else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSData *readData = [[pipe fileHandleForReading] readDataToEndOfFile];
            NSString *consoleOutput = [[NSString alloc] initWithData:readData encoding:NSUTF8StringEncoding];
            [self.consoleView appendString:consoleOutput];
        });
    }
};

dispatch_async(stdout_queue, checkBlock);

[_currentTask launch];

Но при запуске NSTask это выглядит так (сначала оно пустое, но после ввода моего имени и нажатия CTRL+D все сразу заканчивается):

Craig^DEnter your name: 
You entered: Craig

Итак, мой вопрос: как я могу прочитать stdout из моего NSTask, не требуя дополнительных операторов flush() в моем скрипте Python? Почему приглашение Введите свое имя: не появляется сразу при запуске от имени NSTask?


person Craig Otis    schedule 13.11.2012    source источник
comment
У вас есть вопрос?   -  person kindall    schedule 13.11.2012
comment
@kindall Извините, если ОП был неясен - добавил конкретный вопрос внизу.   -  person Craig Otis    schedule 13.11.2012
comment
Я надеюсь, что все, кто борется с тем, почему скрипты Python не выводят промежуточных результатов, найдут этот. Вы спасли меня еще от 5 часов хлопот! Спасибо!   -  person Dr.Kameleon    schedule 02.12.2014


Ответы (1)


Когда Python видит, что его стандартный вывод является терминалом, он автоматически сбрасывает sys.stdout, когда скрипт читает из sys.stdin. Когда вы запускаете сценарий с помощью NSTask, стандартным выводом сценария является канал, а не терминал.

ОБНОВИТЬ

Для этого есть специфичное для Python решение. Вы можете передать флаг -u интерпретатору Python (например, _currentTask.arguments = @[ @"-u", @"nameTest.py"];), который сообщает Python вообще не буферизовать стандартный ввод, стандартный вывод или стандартную ошибку. Вы также можете установить PYTHONUNBUFFERED=1 в среде процесса для достижения того же эффекта.

ОРИГИНАЛ

Более общее решение, которое применимо к любой программе, использует так называемый «псевдотерминал» (или, исторически, «псевдотелетайп»), который мы сокращаем до просто «pty». (На самом деле это то, что делает само приложение «Терминал». Редкий Mac имеет физический терминал или телетайп, подключенный к последовательному порту!)

Каждый pty на самом деле представляет собой пару виртуальных устройств: ведомое устройство и ведущее устройство. Байты, которые вы пишете мастеру, вы можете прочитать с ведомого, и наоборот. Таким образом, эти устройства больше похожи на сокеты (двунаправленные), чем на трубы (однонаправленные). Кроме того, pty также позволяет вам устанавливать флаги ввода-вывода терминала (или «termios»), которые контролируют, повторяет ли ведомое устройство свой ввод, передает ли оно на свой ввод строку за раз или символ за раз и многое другое.

В любом случае, вы можете легко открыть пару ведущий/ведомый с помощью функции openpty. Вот небольшая категория, которую вы можете использовать, чтобы объект NSTask использовал подчиненную сторону для стандартного ввода и вывода задачи.

NSTask+PTY.h

@interface NSTask (PTY)

- (NSFileHandle *)masterSideOfPTYOrError:(NSError **)error;

@end

NSTask+PTY.m

#import "NSTask+PTY.h"
#import <util.h>

@implementation NSTask (PTY)

- (NSFileHandle *)masterSideOfPTYOrError:(NSError *__autoreleasing *)error {
    int fdMaster, fdSlave;
    int rc = openpty(&fdMaster, &fdSlave, NULL, NULL, NULL);
    if (rc != 0) {
        if (error) {
            *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil];
        }
        return NULL;
    }
    fcntl(fdMaster, F_SETFD, FD_CLOEXEC);
    fcntl(fdSlave, F_SETFD, FD_CLOEXEC);
    NSFileHandle *masterHandle = [[NSFileHandle alloc] initWithFileDescriptor:fdMaster closeOnDealloc:YES];
    NSFileHandle *slaveHandle = [[NSFileHandle alloc] initWithFileDescriptor:fdSlave closeOnDealloc:YES];
    self.standardInput = slaveHandle;
    self.standardOutput = slaveHandle;
    return masterHandle;
}

@end

Вы можете использовать его следующим образом:

NSTask *_currentTask = [[NSTask alloc] init];
_currentTask.launchPath = @"/usr/bin/python";
_currentTask.arguments = @[[[NSBundle mainBundle] pathForResource:@"nameTest" ofType:@"py"]];

NSError *error;
NSFileHandle *masterHandle = [_currentTask masterSideOfPTYOrError:&error];
if (!masterHandle) {
    NSLog(@"error: could not set up PTY for task: %@", error);
    return;
}

Затем вы можете читать из задачи и записывать в задачу, используя masterHandle.

person rob mayoff    schedule 13.11.2012
comment
Спасибо, Роб, фантастический ответ, работает почти идеально. Одна проблема заключается в том, что я не могу закрыть NSFileHandle, возвращаемый методом категории, он просто зависает. (Вызов closeFile был тем, как я имитировал CTRL+D) Кроме того, поскольку ввод/вывод происходит на одном и том же дескрипторе, каждый символ, который я записываю на стандартный ввод, возвращается обратно на стандартный вывод. - person Craig Otis; 13.11.2012
comment
Для проблемы с эхом: попробуйте использовать tcgetattr, чтобы получить termios ведомого устройства. Очистите бит ECHO в поле c_lflag и поместите измененный termios обратно на ведомое устройство с помощью tcsetattr. Вы можете прочитать справочные страницы tcgetattr и termios. - person rob mayoff; 13.11.2012
comment
Для проблемы с зависанием: я не знаю навскидку, что происходит. Возможно, вам придется найти некоторые другие биты termios для настройки. - person rob mayoff; 13.11.2012