Utility to Send Commands or Data to Other Terminals (tty/pts)

I have been working on a solution to mirror the PWD of multiple terminals. As part of that I need a way to execute commands on other tty/pts. A simple echo won’t work because echo writes to the output buffer, while I need to push these commands to the input buffer of the tty/pts. I found this forum post after some googling. I adapted the code from the forum post for my purpose.

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <string.h>
#include <unistd.h>

void print_help(char *prog_name) {
        printf("Usage: %s [-n] DEVNAME COMMAND\n", prog_name);
        printf("Usage: '-n' is an optional argument if you want to push a new line at the end of the text\n");
        printf("Usage: Will require 'sudo' to run if the executable is not setuid root\n");
        exit(1);
}

int main (int argc, char *argv[]) {
    char *cmd, *nl = "\n";
    int i, fd;
    int devno, commandno, newline;
    int mem_len;
    devno = 1; commandno = 2; newline = 0;
    if (argc < 3) {
        print_help(argv[0]);
    }
    if (argc > 3 && argv[1][0] == '-' && argv[1][1] == 'n') {
        devno = 2; commandno = 3; newline=1;
    } else if (argc > 3 && argv[1][0] == '-' && argv[1][1] != 'n') {
        printf("Invalid Option\n");
        print_help(argv[0]);
    }
    fd = open(argv[devno],O_RDWR);
    if(fd == -1) {
        perror("open DEVICE");
        exit(1);
    }
    mem_len = 0;
    for ( i = commandno; i < argc; i++ ) {
        mem_len += strlen(argv[i]) + 2;
        if ( i > commandno ) {
            cmd = (char *)realloc((void *)cmd, mem_len);
        } else { //i == commandno
            cmd = (char *)malloc(mem_len);
        }

        strcat(cmd, argv[i]);
        strcat(cmd, " ");
    }
  if (newline == 0)
        usleep(225000);
    for (i = 0; cmd[i]; i++)
        ioctl (fd, TIOCSTI, cmd+i);
    if (newline == 1)
        ioctl (fd, TIOCSTI, nl);
    close(fd);
    free((void *)cmd);
    exit (0);
}

Copy the above code to some C file (For eg. ttyecho.c). Run the following command in the directory you have created the C file in to compile the code.

make ttyecho

If you named the file as abc.c then the command would be make abc.

Copy this file to the bin directory under your Home Directory. In my case it is /home/pratik/bin. Create the directory if it doesn’t exist. Its a good practice to keep all custom binaries/executables in this bin directory.

Start another terminal or switch to any other open terminal that you wish to control and execute the command tty. You can see a sample output below.

@~$ tty
/dev/pts/5

Now to execute a command on /dev/pts/5, run the following command in the controlling/original terminal.

sudo ttyecho -n /dev/pts/5 ls

You will see that the ls command is executed in /dev/pts/5. The -n option makes ttyecho send a newline after the command, so that the command gets executed and not just inserted. This utility can infact be used to send any data to other terminals For eg, you could open vim in /dev/pts/5 and then run the following command in the controlling terminal to cause vim to exit in /dev/pts/5.

sudo ttyecho -n /dev/pts/5 :q

To avoid using sudo all the time, so that the command is easily scriptable, change the owners/permissions of this executable using the following commands.

sudo chown root:root ttyecho
sudo chmod u+s ttyecho

What we did was change the owner/group to root and set the setuid bit for the executable which will allow you to run the utility with root permissions.

Setting the setuid bit can become a security risk.
The code for the utility can be improved to make it more secure.

27 comments

  1. Hey man, thanks A LOT for this code, i would never write it by myself, i don’t know why this function doesn’t have in linux by default, but you really help me a lot! Other stuff doesn’t work like i need, busybox and other shit have some problems with comfortable using. Your code work like a charm! Thanks man!

  2. Lovely and elegant and way beyond my understanding!

    I wanted to point out a somewhat viable alternative. The busybox edition of nc allows one to do this to the same effect:

    busybox nc -f /dev/pts/2 -e id

  3. It’s now 2019 and this doesn’t work (except for own tty) — running directly as root. Any ideas? I’m on CentOS 7.6 and its default 3.10 kernel.

      1. Nope. In the meanwhile, I started using ‘reptyr’ for my purposes (which is different than that of the article’s author purpose — I needed to cleanly end disconnected sessions, like sending :wq to vi in a pty that got disconnected in an ssh session due to network issues) — reptyr takes over the control of the target process and attaches it to your current pty so that you can continue where your ssh got disconnected and pty lost. reptyr works *very well* for that purpose.

  4. There is one bug in your code.
    “malloc” may return memory not set to zero.
    In this case “strcat” will fail causing “Segmentation fault” error.
    The fix is very easy – use “calloc” instead.
    – cmd = (char *)malloc(mem_len);
    + cmd = (char *)calloc(mem_len, 1);

  5. while true; do echo print this; sleep 5; done;
    this command works standalone but when combined with ttyecho throws an error? Can anyone help me out with that?

    ttyecho -n /dev/pts/2 while true; do echo print this; sleep 5; done; – GIVES ERROR
    bash: syntax error near unexpected token `do’

  6. Can this login to a getty process?
    When I run it with the user name it appears to accept it and show up the request for the password, but then sending the password always returns “Login incorrect”.
    Is a security feature of Linux or is the characters ttyecho ends not compatible with getty?

    1. Linux password entry fields are secured and protected; this utility cannot enter them. Essentially, Linux binds the password entry field directly to the keyboard. So it only registers physical keyboard presses directly from the hardware.

      However, there is another way, I think, to do what you want with the getty terminals. I, for example, use it to help manage my server. It runs non-graphical, and is set up close to my desk: close enough I can see the screen, too far to interact with without getting up. So, I use this program to interact with the physical terminal. The trick is that you have to allocate a new console and attach a login session. This can be achieved with the openvt command.

      # openvt -ls
      will create a new virtual terminal, attach an authenticated bash session, and make that terminal active on the display. You can use ‘chvt ‘ to switch to TTY. ‘deallocvt ‘ removes that TTY.

      Not as straightforward as I would like, but it works.

  7. I do as this page said , but when I run the command, I got the error info :

    [user@host ~/bin]$ sudo ttyecho -n /dev/pts/16 ls
    sudo: ignoring `ttyecho’ found in ‘.’
    Use `sudo ./ttyecho’ if this is the `ttyecho’ you wish to run.

    and then I changed the command , the error info exists also:

    [user@host ~/bin]$ sudo ./ttyecho -n /dev/pts/16 ls
    Sorry, user user is not allowed to execute ‘./ttyecho -n /dev/pts/16 ls’ as root on host.

    why?

  8. Thanks a lot! Saved my day!

    I had a disconnected SSH connection to a server, and in that SSH connection, the mail tool was running with an open overfilled mbox with more than 900.000 mails. Just opening this big mbox took quite a while. Killing the mail process and re-opening that mbox again would’ve been a pain in the a**.

    As I only needed the last ~~ 100 of these mails, I used ttyecho to send “d 1-900000” and “q”, so the mail utility deleted most of the mails and finished nicely, leaving only a few hundred messages for later reading & filtering.

    This worked perfectly, it took just a minute, and that includes the time needed to create ttyecho.c, copy&paste and compiling. 😀

    1. I do as this page said , but when I run the command, I got the error info :

      [user@host ~/bin]$ sudo ttyecho -n /dev/pts/16 ls
      sudo: ignoring `ttyecho’ found in ‘.’
      Use `sudo ./ttyecho’ if this is the `ttyecho’ you wish to run.

      and then I changed the command , the error info exists also:

      [user@host ~/bin]$ sudo ./ttyecho -n /dev/pts/16 ls
      Sorry, user user is not allowed to execute ‘./ttyecho -n /dev/pts/16 ls’ as root on host.

      why?

  9. Wow! Like others said, this tool is truly a gem. So many people states that writing to the stdin of an unrelated process isn’t possible…

  10. Thanks to your need util, I didn’t had to restart a long taking process, that had already finished, but asked me to answer 17272 times “1”. I’m going to include it on all my installations from now on, it is so useful.
    Thanks a lot.

  11. Fantastic! I was poking around all day trying to figure out how to do with tee. Saved me a lot of frustration! Thank you kind sir.

  12. Great tool. I have been searching for a solution to this for a while and always running into dead ends. Nice clean solution to executing commands in other terminals. This is a real gem piece of code.

    Thanks!

  13. that tool just rocks dynamite!

    I’ve written an initramfs generator (http://github.com/r1k0/kigen) supporting dropbear (amongst others) to rescue LUKS systems remotly and I had a very hard time firing up the new init from a remote dropbear console (/dev/ttyp0).

    Thanks to your tool and I can redirect my commands to /dev/console (as if it was local).

    very very nice

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.