如何用openpty实现Ctrl-C和Ctrl-D?

时间:2022-07-16 21:03:58

I am writing a simple terminal using openpty, NSTask and NSTextView. How are CtrlC and CtrlD supposed to be implemented?

我正在使用openpty,NSTask和NSTextView编写一个简单的终端。如何实现CtrlC和CtrlD?

I start a shell like this:

我开始像这样的shell:

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

Then I intercept CtrlC and send -[interrupt] to the NSTask like this:

然后我拦截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];
    }
}

However, the interrupt doesn't seem to kill whatever program is being executed by the shell. If the shell has no sub-process, the interrupt does cancel the current input line.

但是,中断似乎不会杀死shell正在执行的任何程序。如果shell没有子进程,则中断会取消当前输入行。

I have no idea how to implement CtrlD.

我不知道如何实现CtrlD。

3 个解决方案

#1


4  

I stepped through st (the suckless terminal, whose code is actually small and simple enough to understand) in gdb on Linux to find that when you press Ctrl-C and Ctrl-D, it writes \003 and \004 to the process, respectively. I tried this on OS X in my project and it worked just as well.

我在Linux上的gdb中逐步执行了st(这个无用的终端,其代码实际上很小而且非常简单易懂),当你按下Ctrl-C和Ctrl-D时,它分别将\ 003和\ 004写入进程。我在我的项目中在OS X上尝试了这个,它也很有用。

So in the context of my code above, the solution for handling each of the hotkeys is this:

因此,在我上面的代码的上下文中,处理每个热键的解决方案是这样的:

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

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

#2


4  

I have also asked about this question in Russian Cocoa Developers Slack channel and received the answer from Dmitry Rodionov. He answered in Russian with this gist: ctrlc-ptty-nstask.markdown and gave me approval to post English version of it here.

我还在俄罗斯Cocoa Developers Slack频道询问了这个问题,并得到了Dmitry Rodionov的回答。他用俄语回答了这个要点:ctrlc-ptty-nstask.markdown,并允许我在这里发布英文版本。

His implementation is based on what Pokey McPokerson suggested but is more straightforward: he uses GetBSDProcessList() from Technical Q&A QA1123 Getting List of All Processes on Mac OS X to get the list of the child processes and to send SIGINT to each of them:

他的实现基于Pokey McPokerson的建议,但更直接:他使用来自技术问答的GetBSDProcessList()来获取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);

In case if a process has no child processes he sends SIGINT to that process directly:

如果进程没有子进程,则直接将SIGINT发送到该进程:

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

This approach works perfectly however there are two possible concerns (which I personally don't care about at the moment I'm writing my own toy terminal):

这种方法很有效,但有两个可能的问题(我个人并不关心我现在正在编写自己的玩具终端):

  1. It is exhaustive to enumerate through all the processes every time Ctrl-C is pressed. Maybe there is a better way of finding child processes.
  2. 每按一次Ctrl-C,枚举所有进程都是详尽无遗的。也许有更好的方法来寻找子流程。

  3. I and Dmitriy we are both not sure if killing ALL child processes is the way how Ctrl-C works in real terminals.
  4. 我和Dmitriy我们都不确定是否杀死所有子进程是Ctrl-C在真实终端中的工作方式。


Below the full version of Dmitriy's code follows:

下面是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;
}

#3


1  

The NSTask refers to the actual bash, not the commands it runs. So when you call terminate on it, it's sending that signal to the bash process. You can check this by printing [task processIdentifier], and having a look at the PID in Activity Manager. Unless you find a way to track the PID of any new created processes, you're going to struggle to kill them.

NSTask是指实际的bash,而不是它运行的命令。因此,当您在其上调用terminate时,它会将该信号发送到bash进程。您可以通过打印[task processIdentifier]来查看,并查看活动管理器中的PID。除非你找到一种方法来跟踪任何新创建的进程的PID,否则你将很难杀死它们。

See this or this answer for possible ways to track the PIDs. I had a look at your project and you could implement something similar by changing your didChangeText method. For example:

有关跟踪PID的可能方法,请参阅此答案或此答案。我看了一下你的项目,你可以通过改变你的didChangeText方法来实现类似的东西。例如:

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

and then read from the childpid file whenever you want to kill the children. The extras will appear in the terminal though, which isn't great.

然后每当你想杀死孩子时从childpid文件中读取。额外的东西会出现在终端,但这并不好。

A better option might be to create new NSTasks for each command coming in (i.e. don't pipe the user input straight to bash), and send their outputs to the same handler. Then you can call terminate directly on them.

更好的选择可能是为每个进入的命令创建新的NSTasks(即不要将用户输入直接传递给bash),并将它们的输出发送到同一个处理程序。然后你可以直接调用它们终止。

When you get ctrl-c working, you can implement ctrl-d like so:

当你得到ctrl-c工作时,你可以像这样实现ctrl-d:

kill([task processIdentifier], SIGQUIT);

Source

#1


4  

I stepped through st (the suckless terminal, whose code is actually small and simple enough to understand) in gdb on Linux to find that when you press Ctrl-C and Ctrl-D, it writes \003 and \004 to the process, respectively. I tried this on OS X in my project and it worked just as well.

我在Linux上的gdb中逐步执行了st(这个无用的终端,其代码实际上很小而且非常简单易懂),当你按下Ctrl-C和Ctrl-D时,它分别将\ 003和\ 004写入进程。我在我的项目中在OS X上尝试了这个,它也很有用。

So in the context of my code above, the solution for handling each of the hotkeys is this:

因此,在我上面的代码的上下文中,处理每个热键的解决方案是这样的:

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

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

#2


4  

I have also asked about this question in Russian Cocoa Developers Slack channel and received the answer from Dmitry Rodionov. He answered in Russian with this gist: ctrlc-ptty-nstask.markdown and gave me approval to post English version of it here.

我还在俄罗斯Cocoa Developers Slack频道询问了这个问题,并得到了Dmitry Rodionov的回答。他用俄语回答了这个要点:ctrlc-ptty-nstask.markdown,并允许我在这里发布英文版本。

His implementation is based on what Pokey McPokerson suggested but is more straightforward: he uses GetBSDProcessList() from Technical Q&A QA1123 Getting List of All Processes on Mac OS X to get the list of the child processes and to send SIGINT to each of them:

他的实现基于Pokey McPokerson的建议,但更直接:他使用来自技术问答的GetBSDProcessList()来获取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);

In case if a process has no child processes he sends SIGINT to that process directly:

如果进程没有子进程,则直接将SIGINT发送到该进程:

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

This approach works perfectly however there are two possible concerns (which I personally don't care about at the moment I'm writing my own toy terminal):

这种方法很有效,但有两个可能的问题(我个人并不关心我现在正在编写自己的玩具终端):

  1. It is exhaustive to enumerate through all the processes every time Ctrl-C is pressed. Maybe there is a better way of finding child processes.
  2. 每按一次Ctrl-C,枚举所有进程都是详尽无遗的。也许有更好的方法来寻找子流程。

  3. I and Dmitriy we are both not sure if killing ALL child processes is the way how Ctrl-C works in real terminals.
  4. 我和Dmitriy我们都不确定是否杀死所有子进程是Ctrl-C在真实终端中的工作方式。


Below the full version of Dmitriy's code follows:

下面是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;
}

#3


1  

The NSTask refers to the actual bash, not the commands it runs. So when you call terminate on it, it's sending that signal to the bash process. You can check this by printing [task processIdentifier], and having a look at the PID in Activity Manager. Unless you find a way to track the PID of any new created processes, you're going to struggle to kill them.

NSTask是指实际的bash,而不是它运行的命令。因此,当您在其上调用terminate时,它会将该信号发送到bash进程。您可以通过打印[task processIdentifier]来查看,并查看活动管理器中的PID。除非你找到一种方法来跟踪任何新创建的进程的PID,否则你将很难杀死它们。

See this or this answer for possible ways to track the PIDs. I had a look at your project and you could implement something similar by changing your didChangeText method. For example:

有关跟踪PID的可能方法,请参阅此答案或此答案。我看了一下你的项目,你可以通过改变你的didChangeText方法来实现类似的东西。例如:

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

and then read from the childpid file whenever you want to kill the children. The extras will appear in the terminal though, which isn't great.

然后每当你想杀死孩子时从childpid文件中读取。额外的东西会出现在终端,但这并不好。

A better option might be to create new NSTasks for each command coming in (i.e. don't pipe the user input straight to bash), and send their outputs to the same handler. Then you can call terminate directly on them.

更好的选择可能是为每个进入的命令创建新的NSTasks(即不要将用户输入直接传递给bash),并将它们的输出发送到同一个处理程序。然后你可以直接调用它们终止。

When you get ctrl-c working, you can implement ctrl-d like so:

当你得到ctrl-c工作时,你可以像这样实现ctrl-d:

kill([task processIdentifier], SIGQUIT);

Source