【问题标题】:How to implement Ctrl-C and Ctrl-D with openpty?如何用 openpty 实现 Ctrl-C 和 Ctrl-D?
【发布时间】:2014-02-11 19:21:33
【问题描述】:

我正在使用 openpty、NSTask 和 NSTextView 编写 a simple terminalCtrlCCtrlD应该如何实现?

我这样启动一个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];

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

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

我不知道如何实现 CtrlD

【问题讨论】:

    标签: objective-c shell cocoa nstask pty


    【解决方案1】:

    我也曾在俄罗斯 Cocoa Developers Slack 频道问过这个问题,并收到了来自Dmitry Rodionov 的回答。他用俄语回答了这个要点:ctrlc-ptty-nstask.markdown,并同意我在这里发布它的英文版。

    他的实现基于 Pokey McPokerson 的建议,但更直接:他使用来自 Technical Q&A QA1123 Getting List of All Processes on Mac OS XGetBSDProcessList() 来获取子进程的列表并向每个子进程发送 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. 我和 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;
    }
    

    【讨论】:

      【解决方案2】:

      我在 Linux 上的 gdb 中浏览了st(这个烂终端,它的代码实际上很小且简单到可以理解),发现当你按下Ctrl-CCtrl-D 时,它会写出\003 和@ 987654326@ 分别给进程。我在我的项目中在 OS X 上尝试过这个,效果也一样。

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

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

      【讨论】:

      • 不幸的是,由于某种原因,当我在 10.11 运行您的项目时,Ctrl-C 对我不起作用:coolterm。你有什么想法为什么会发生?
      • Ctrl-C 既不开始新行也不终止子进程(如 ping)。两个附加细节:Ctrl-D 按预期工作,Ctrl-C 不打印 ^C。看起来缺少一些额外的代码来使它在 10.11 上工作。
      【解决方案3】:

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

      请参阅thisthis 答案以了解跟踪 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

      【讨论】:

      • 感谢所有提示和参考资料(尤其是最后一个),但这似乎不是正确的方法。我确信其他终端应用程序不会做这样的事情,因为它们可以与任何 shell 一起工作,而不仅仅是 bash。
      • (我现在正在寻找一种方法来获取进程的子进程。)
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-03-17
      • 2012-06-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多