Skip to content
Advertisement

How to get fully interactive bash terminal in C with shift + ctrl keys?

I want to know how to create a fully interactive terminal. I am creating an interactive bash terminal like this:

fds = open(ptsname(fdm), O_RDWR);

if (fork())
{
     ....
}
else
{
    ...

    ioctl(0, TIOCSCTTY, 1);

    // Execution of the program
    {

            char *child_av[] = {"/bin/sh", "-i", NULL};


            rc = execvp(child_av[0], child_av);
    }

    // if Error...
    return -1;
}

I can then go on to use read()/write() to send commands and receive output. My problem is how do I automate CTRL & SHIFT keys? Python’s subprocess does this so it’s definitely possible.

  1. How to send ctrl-c to kill foreground process? In python it would be the follow and I want to know what it looks like in C:
process = subprocess.Popen(..) ...
process.send_signal(signal.SIGINT)
  1. How to send shift, ctrl, esc keys? For example, how to open nano, then send ctrl-a-esc? In python it would be the following & I want to know what it looks like in C:
from subprocess import Popen, PIPE

shift_a_sequence = '''keydown Shift_L
key A
keyup Shift_L
'''

def keypress(sequence):
    p = Popen(['xte'], stdin=PIPE)
    p.communicate(input=sequence)`

Advertisement

Answer

There are four separate issues here:

1. How to trigger a Ctrl-C on the PTY

The sigint is triggered by the terminal’s line discipline upon receiving Ctrl-C as input (in cooked mode). You can do this by sending 0x03, which is equivalent to uppercase ASCII ‘C’ with the 7th bit cleared:

write(fdm, "x03", 1);

2. The C equivalent of process.send_signal

process = subprocess.Popen(..) ...
process.send_signal(signal.SIGINT)

This simply does a fork+kill, so it’s unrelated to anything you’d do to accomplish #1. You can see this in strace:

$ cat foo.py
import signal
import subprocess
process = subprocess.Popen(["sleep", "10"])
process.send_signal(signal.SIGINT)

$ strace -f -eclone,kill,execve  python foo.py
execve("/usr/bin/python", ["python", "foo.py"], 0x7ffe4d179458 /* 30 vars */) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fa0f6e14a10) = 12917
strace: Process 12917 attached
[pid 12917] execve("/usr/bin/sleep", ["sleep", "10"], 0x7ffe6a45efc0 /* 30 vars */) = -1 ENOENT (No such file or directory)
[pid 12917] execve("/bin/sleep", ["sleep", "10"], 0x7ffe6a45efc0 /* 30 vars */ <unfinished ...>
[pid 12916] kill(12917, SIGINT)         = 0
[pid 12917] <... execve resumed> )      = 0
[pid 12917] --- SIGINT {si_signo=SIGINT, si_code=SI_USER, si_pid=12916, si_uid=1000} ---
[pid 12917] +++ killed by SIGINT +++

3. How to send Shift, Ctrl and Esc

Esc is simple: you just send its ASCII value. Here’s man ascii:

033   27    1B    ESC (escape) 

Shift and Control are modifiers, so you don’t actually send them. You just modify the character you send. #1 already covered how to do this for Ctrl on simple ascii characters.

For shift, you should simply uppercase the character you’re interested in using appropriate string functions. The traditional hardware logic of clearing/setting bit 6 doesn’t work well for unicode characters, but 'c' == 'C'+0x20 if you feel like it.

Note that this does not apply to things like arrow keys, where you need to send a different ANSI escape code corresponding to the terminal you’re trying to emulate.

For completeness, Alt/Meta has two forms: Traditionally setting bit 8 (mostly deprecated for Unicode reasons), or Meta-As-Escape where you simply send the two bytes ESC x for Alt+x.


4. The C equivalent of the piping to xte

from subprocess import Popen, PIPE

shift_a_sequence = '''keydown Shift_L
key A
keyup Shift_L
'''

 def keypress(sequence):
    p = Popen(['xte'], stdin=PIPE)
    p.communicate(input=sequence)`

This opens an X11 utility that simulates an X11 key sequence. If you are running the program in an XTerm and don’t switch focus, this will hopefully end up in the terminal where you started, but this is not at all a guarantee. It is definitely nothing like #3.

If you wanted to do this though, you can use popen from C in much the same way, but it won’t help you do anything of what you describe textually.


You can find a complete example for sending Ctrl+C adapted from your code here:

#define _XOPEN_SOURCE 600
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#define __USE_BSD
#include <termios.h>

#define guard(x) do { if((x)<0) { perror(#x); exit(1); } } while(0);
int main(void)
{
int fdm, fds, rc;
char input[150];
fdm = posix_openpt(O_RDWR);
guard(grantpt(fdm));
guard(unlockpt(fdm));

if (fork())
{
  char* output = "sleep 60n";
  // Start a long sleep
  write(fdm, output, strlen(output));
  // Wait and send Ctrl-C to abort it after 1 second
  sleep(1); write(fdm, "x03", 1);
  // Make sure shell is still alive
  output = "echo 'Shell is still alive'n";
  write(fdm, output, strlen(output));
  // Wait and send Ctrl-D to exit
  sleep(1); write(fdm, "x04", 1);

  while((rc = read(fdm, input, sizeof(input)-1)) > 0) {
    input[rc] = 0;
    printf("From PTY:n%sn", input);
  }
  close(fdm);
  wait(NULL);
}
else
{
  setsid();
  guard(fds = open(ptsname(fdm), O_RDWR));
  close(fdm);
  dup2(fds, 0);
  dup2(fds, 1);
  dup2(fds, 2);
  close(fds);
  execlp("sh", "sh", "-i", NULL);
}

return 0;
} // main

Execution shows that sleep is interrupted, the shell continues, and is finally exited:

$ ./foo
From PTY:
sleep 60
$ ^C
echo 'Shell is still alive'
$ Shell is still alive
$
Advertisement