Как реализовать Ctrl-C и Ctrl-D с помощью openpty?

Я пишу простой терминал с помощью openpty, NSTask и NSTextView. Как предполагается выполнение Ctrl C и Ctrl D?

Я запускаю оболочку следующим образом:

int amaster = 0, aslave = 0;
if (openpty(&amaster, &aslave, NULL, NULL, NULL) == -1) {
    NSLog(@"openpty failed");
    return;
}

masterHandle = [[NSFileHandle alloc] initWithFileDescriptor:amaster closeOnDealloc:YES];
NSFileHandle *slaveHandle = [[NSFileHandle alloc] initWithFileDescriptor:aslave closeOnDealloc:YES];

NSTask *task = [NSTask new];
task.launchPath = @"/bin/bash";
task.arguments = @[@"-i", @"-l"];
task.standardInput = slaveHandle;
task.standardOutput = slaveHandle;
task.standardError = errorOutputPipe = [NSPipe pipe];
[task launch];

Затем я перехватываю Ctrl C и отправлю -[interrupt] в NSTask следующим образом:

- (void)keyDown:(NSEvent *)theEvent
{
    NSUInteger flags = theEvent.modifierFlags;
    unsigned short keyCode = theEvent.keyCode;

    if ((flags & NSControlKeyMask) && keyCode == 8) { // ctrl-c
        [task interrupt]; // ???
    } else if ((flags & NSControlKeyMask) && keyCode == 2) { // ctrl-d
        // ???
    } else {
        [super keyDown:theEvent];
    }
}

Однако прерывание, похоже, не убивает любую программу, выполняемую оболочкой. Если оболочка не имеет подпроцесса, прерывание отменяет текущую строку ввода.

Я не знаю, как реализовать Ctrl D.

Ответ 1

Я прошел через st (терминал suckless, код которого на самом деле мал и достаточно прост для понимания) в gdb на Linux, чтобы найти что когда вы нажимаете Ctrl-C и Ctrl-D, он записывает \003 и \004 в процесс, соответственно. Я пробовал это на OS X в своем проекте, и он работал так же хорошо.

Итак, в контексте моего кода выше, решение для обработки каждой из горячих клавиш таково:

  • Ctrl-C: [masterHandle writeData:[NSData dataWithBytes:"\003" length:1]];
  • Ctrl-D: [masterHandle writeData:[NSData dataWithBytes:"\004" length:1]];

Ответ 2

Я также задал этот вопрос на русском языке Cocoa Developers Slack channel и получил ответ от Дмитрий Родионов. Он ответил по-русски на эту тему: ctrlc-ptty-nstask.markdown и дал мне разрешение опубликовать здесь английскую версию.

Его реализация основана на том, что предложил Поки Мак-Покерсон, но более прост: он использует GetBSDProcessList() из Технический Q & A QA1123 Получение списка всех процессов в Mac OS X, чтобы получить список дочерних процессов и отправить SIGINT каждому из них:

kinfo_proc *procs = NULL;
size_t count;
if (0 != GetBSDProcessList(&procs, &count)) {
    return;
}
BOOL hasChildren = NO;
for (size_t i = 0; i < count; i++) {
    // If the process if a child of our bash process we send SIGINT to it
    if (procs[i].kp_eproc.e_ppid == task.processIdentifier) {
        hasChildren = YES;

        kill(procs[i].kp_proc.p_pid, SIGINT);
    }
}
free(procs);

Если процесс не имеет дочерних процессов, он отправляет SIGINT непосредственно этому процессу:

if (hasChildren == NO) {
    kill(task.processIdentifier, SIGINT);
}

Этот подход работает отлично, однако есть две возможные проблемы (которые мне лично не нравятся в настоящий момент, когда я пишу свой собственный игровой терминал):

  • Исключительно перечислить все процессы при каждом нажатии Ctrl-C. Возможно, есть лучший способ найти дочерние процессы.
  • Я и Дмитрий мы оба не уверены, что убить ВСЕ дочерние процессы - это способ, которым работает Ctrl-C в реальных терминалах.

Ниже приведена полная версия кода Дмитрия:

- (void)keyDown:(NSEvent *)theEvent
{
    NSUInteger flags = theEvent.modifierFlags;
    unsigned short keyCode = theEvent.keyCode;

    if ((flags & NSControlKeyMask) && keyCode == 8) {

        [self sendCtrlC];

    } else if ((flags & NSControlKeyMask) && keyCode == 2) {
        [masterHandle writeData:[NSData dataWithBytes: "\004" length:1]];
    } else if ((flags & NSDeviceIndependentModifierFlagsMask) == 0 && keyCode == 126) {
        NSLog(@"up");
    } else if ((flags & NSDeviceIndependentModifierFlagsMask) == 0 && keyCode == 125) {
        NSLog(@"down");
    } else {
        [super keyDown:theEvent];
    }
}

// #include <sys/sysctl.h>
// typedef struct kinfo_proc kinfo_proc;

- (void)sendCtrlC
{
    [masterHandle writeData:[NSData dataWithBytes: "\003" length:1]];

    kinfo_proc *procs = NULL;
    size_t count;
    if (0 != GetBSDProcessList(&procs, &count)) {
        return;
    }
    BOOL hasChildren = NO;
    for (size_t i = 0; i < count; i++) {
        if (procs[i].kp_eproc.e_ppid == task.processIdentifier) {
            hasChildren = YES;
            kill(procs[i].kp_proc.p_pid, SIGINT);
        }
    }
    free(procs);

    if (hasChildren == NO) {
        kill(task.processIdentifier, SIGINT);
    }
}

static int GetBSDProcessList(kinfo_proc **procList, size_t *procCount)
// Returns a list of all BSD processes on the system.  This routine
// allocates the list and puts it in *procList and a count of the
// number of entries in *procCount.  You are responsible for freeing
// this list (use "free" from System framework).
// On success, the function returns 0.
// On error, the function returns a BSD errno value.
{
    int                 err;
    kinfo_proc *        result;
    bool                done;
    static const int    name[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0 };
    // Declaring name as const requires us to cast it when passing it to
    // sysctl because the prototype doesn't include the const modifier.
    size_t              length;

    assert( procList != NULL);
    assert(*procList == NULL);
    assert(procCount != NULL);

    *procCount = 0;

    // We start by calling sysctl with result == NULL and length == 0.
    // That will succeed, and set length to the appropriate length.
    // We then allocate a buffer of that size and call sysctl again
    // with that buffer.  If that succeeds, we're done.  If that fails
    // with ENOMEM, we have to throw away our buffer and loop.  Note
    // that the loop causes use to call sysctl with NULL again; this
    // is necessary because the ENOMEM failure case sets length to
    // the amount of data returned, not the amount of data that
    // could have been returned.

    result = NULL;
    done = false;
    do {
        assert(result == NULL);

        // Call sysctl with a NULL buffer.

        length = 0;
        err = sysctl( (int *) name, (sizeof(name) / sizeof(*name)) - 1,
                     NULL, &length,
                     NULL, 0);
        if (err == -1) {
            err = errno;
        }

        // Allocate an appropriately sized buffer based on the results
        // from the previous call.

        if (err == 0) {
            result = malloc(length);
            if (result == NULL) {
                err = ENOMEM;
            }
        }

        // Call sysctl again with the new buffer.  If we get an ENOMEM
        // error, toss away our buffer and start again.

        if (err == 0) {
            err = sysctl( (int *) name, (sizeof(name) / sizeof(*name)) - 1,
                         result, &length,
                         NULL, 0);
            if (err == -1) {
                err = errno;
            }
            if (err == 0) {
                done = true;
            } else if (err == ENOMEM) {
                assert(result != NULL);
                free(result);
                result = NULL;
                err = 0;
            }
        }
    } while (err == 0 && ! done);

    // Clean up and establish post conditions.

    if (err != 0 && result != NULL) {
        free(result);
        result = NULL;
    }
    *procList = result;
    if (err == 0) {
        *procCount = length / sizeof(kinfo_proc);
    }
    assert( (err == 0) == (*procList != NULL) );
    return err;
}

Ответ 3

NSTask ссылается на фактический bash, а не на команды, которые он запускает. Поэтому, когда вы вызываете terminate на нем, он отправляет этот сигнал в процесс bash. Вы можете проверить это, распечатав [task processIdentifier] и взглянув на PID в диспетчере действий. Если вы не найдете способ отслеживать PID любых новых созданных процессов, вы будете пытаться их убить.

См. этот или этот для возможных путей отслеживания PID. Я просмотрел ваш проект, и вы можете реализовать что-то подобное, изменив метод didChangeText. Например:

// [self writeCommand:input]; Take this out
[self writeCommand:[NSString stringWithFormat:@"%@ & echo $! > /tmp/childpid\n", [input substringToIndex:[input length] - 2]]];

а затем прочитайте из файла childpid всякий раз, когда вы хотите убить детей. Экстра появится в терминале, хотя это и не очень удобно.

Лучшим вариантом может быть создание новых NSTasks для каждой входящей команды (т.е. не подключайте вход пользователя прямо к bash) и отправляйте свои выходы одному и тому же обработчику. Затем вы можете вызвать terminate непосредственно на них.

Когда вы будете работать ctrl-c, вы можете реализовать ctrl-d так:

kill([task processIdentifier], SIGQUIT);

Источник