2014-01-21 3 views
10

Я пишу a simple terminal используя openpty, NSTask и NSTextView. Как работают CtrlC и CtrlD Должен быть реализован?Как реализовать Ctrl-C и Ctrl-D с помощью openpty?

Я начинаю оболочки, как это:

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]; 

Тогда я перехватывать CtrlC и отправить -[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]; 
    } 
} 

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

Я понятия не имею, как реализовать CtrlD.

ответ

4

Я прошел через 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]];
+0

К сожалению, по какой-то причине Ctrl-C не работает для меня 10.11, когда я запускаю ваш проект: [coolterm] (https://github.com/alltom/coolterm). У вас есть идеи, почему это может случиться? –

+0

Ctrl-C не запускает новую строку и не убивает дочерний процесс (например, ping). Две дополнительные детали: Ctrl-D работает так, как ожидалось, Ctrl-C не печатает^C. Похож на то, что некоторые дополнительные фрагменты кода отсутствуют, чтобы заставить его работать с 10.11. –

1

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

См. this или this ответ на возможные способы отслеживания 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); 

Source

+1

Спасибо за все советы и ссылки (особенно последний), но это не похоже на правильный путь , Я уверен, что другие терминальные приложения не делают ничего подобного, потому что они работают с любой оболочкой, а не только с bash. – alltom

+0

(Теперь я ищу способ получить детей процесса.) – alltom

4

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

Его реализация основана на том, что предложил Поки McPokerson, но более прост: он использует GetBSDProcessList() из Technical Q&A QA1123 Getting List of All Processes on 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); 
} 

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

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

Ниже полной версии кода Dmitriy в следующем:

- (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; 
} 
Смежные вопросы