Как реализовать Ctrl-C и Ctrl-D с помощью openpty?
Я пишу простой терминал, используя openpty
, NSTask и NSTextView. Как должны быть реализованы CtrlC и CtrlD?
Я запускаю оболочку, как это:
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.
3 ответа
Я прошел через st (бесполезный терминал, чей код на самом деле небольшой и достаточно простой для понимания) в 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]];
Я также спросил об этом вопросе на российском канале разработчиков какао Slack и получил ответ от Дмитрия Родионова. Он ответил по-русски с этой сущностью: ctrlc-ptty-nstask.markdown и дал мне разрешение опубликовать здесь английскую версию.
Его реализация основана на том, что предложил Pokey McPokerson, но более проста: он использует GetBSDProcessList()
из Технических вопросов и ответов 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;
}
NSTask относится к фактическому bash, а не к командам, которые он запускает. Поэтому, когда вы звоните terminate
он посылает этот сигнал процессу bash. Вы можете проверить это, распечатав [task processIdentifier]
и взглянуть на PID в Activity Manager. Если вы не найдете способ отследить 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);