How to implement Ctrl-C and Ctrl-D with openpty?
Asked Answered
T

3

11

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

I start a shell like this:

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:

- (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.

I have no idea how to implement CtrlD.

Trackandfield answered 21/1, 2014 at 23:10 Comment(0)
T
5

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.

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-D: [masterHandle writeData:[NSData dataWithBytes:"\004" length:1]];
Trackandfield answered 21/8, 2014 at 19:18 Comment(2)
Unfortunately, for some reason Ctrl-C does not work for me on 10.11 when I'm running your project: coolterm. Do you have any ideas why it can happen?Macready
Ctrl-C neither starts new line nor kills the child process (like ping). Two additional details: Ctrl-D works as expected, Ctrl-C does not print ^C. Looks like some additional piece of code is missing to make it work on 10.11.Macready
M
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.

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:

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:

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. I and Dmitriy we are both not sure if killing ALL child processes is the way how Ctrl-C works in real terminals.

Below the full version of Dmitriy's code follows:

- (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;
}
Macready answered 25/10, 2015 at 14:52 Comment(0)
S
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.

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:

// [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.

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.

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

kill([task processIdentifier], SIGQUIT);

Source

Snowshoe answered 10/7, 2014 at 13:46 Comment(2)
Thanks for all the tips and references (particularly the last one), but this doesn't seem like the right way to go. I'm sure other terminal applications don't do anything like this because they work with any shell, not just bash.Trackandfield
(I'm now looking for a way to get the children of a process.)Trackandfield

© 2022 - 2024 — McMap. All rights reserved.