Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement Ctrl-C and Ctrl-D with openpty?

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.

like image 499
alltom Avatar asked Jan 21 '14 23:01

alltom


People also ask

Which signal is Ctrl D?

CTRL + D – SIGQUIT Many programs won't respond to a SIGQUIT (some might, it's up to them) but bash itself will. If you're in a bash prompt and it want it to exit (like if you're remotely connected to a bash server for example), if you hit CTRL + D it'll tell the bash session to end.

What does Ctrl d do in shell?

Ctrl+D. This shortcut will effectively log you out of any terminal and close it, or get you back to the original user when used after su or sudo commands. It sends an EOF (End-of-file) marker to bash. Bash exits when it receives this marker.

Why is Ctrl d EOF?

the “end-of-file” (EOF) key combination can be used to quickly log out of any terminal. CTRL-D is also used in programs such as “at” to signal that you have finished typing your commands (the EOF command). key combination is used to stop a process. It can be used to put something in the background temporarily.

What does Ctrl d return in C?

brewbuck said: 02-26-2010. Ctrl+D is a key combination which is recognized by the terminal device. The terminal responds to it by generating an end of file. The program never sees the character Ctrl+D. It just sees "end of file" and terminates.


2 Answers

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]];
like image 151
alltom Avatar answered Oct 26 '22 18:10

alltom


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;
}
like image 42
Stanislav Pankevich Avatar answered Oct 26 '22 19:10

Stanislav Pankevich