How to Fake Terminal Input with Termios.Tiocsti

Unable to fake terminal input with termios.TIOCSTI

TIOCSTI is an ioctl (documented in tty_ioctl(4)), not a terminal setting, so you can't use tcsetattr() -- you need to feed each character of the fake input to ioctl() instead. Never had to do ioctl's from Python before, but the following seems to work for running an ls in a different terminal (specified as the argument, e.g. /dev/pts/13) that's running Bash:

import fcntl
import sys
import termios

with open(sys.argv[1], 'w') as fd:
for c in "ls\n":
fcntl.ioctl(fd, termios.TIOCSTI, c)

TIOCSTI requires root privileges (or CAP_SYS_ADMIN to be more specific, but that's usually the same in practice) by the way -- see capabilities(7).

Run Emacs on startup on system with no XServer

Thanks to Mark Plotnick, who answered below in comments. Using ioctl you can write to own tty.
c program:

#include "unistd.h"
#include "stdlib.h"
#include "stdio.h"
#include "sys/stat.h"
#include "sys/types.h"
#include "fcntl.h"
#include "termios.h"
#include "sys/ioctl.h"

int main(int argc, char ** argv)
{
if (argc >= 3)
{
int fd = open (argv[1], O_RDWR);

if (fd)
{
char * cmd = argv[2];
while(*cmd)
ioctl(fd, TIOCSTI, cmd++);

if (argc >= 4)
ioctl(fd, TIOCSTI, "\r");

return 0;
}
else
printf("could'n open file\n");

}
else
printf("wrong args\n");
return -1;
}

compile:
gcc my_ioctl.c -o my_ioctl

very end of .profile:

~/my_ioctl $(tty) emacs rr

(my c program does not care about what 3rd arg's actually is).

Writing to File descriptor 0 (STDIN) only affects terminal. Program doesn't read

Thanks to @Ian Aboot's answers I could find some explanation here:

https://unix.stackexchange.com/questions/385771/writing-to-stdin-of-a-process/385782

According to the answer of the post above:

Accessing /proc/PID/fd/0 doesn't access file descriptor 0 of process PID, it accesses the file which PID has open on file descriptor 0. This is a subtle distinction, but it matters. A file descriptor is a connection that a process has to a file. Writing to a file descriptor writes to the file regardless of how the file has been opened.

and

If /proc/PID/fd/0 is a terminal, then writing to it outputs the data on a terminal. A terminal file is bidirectional: writing to it outputs the data, i.e. the terminal displays the text; reading from a terminal inputs the data, i.e. the terminal transmits user input.

Basically I had to control the terminal process to get the input be forwarded into my process. Writing directly to the /dev/pts* didn't work.

Redirecting the input to a fifo, for example, worked as expected. Maybe there is a way to simulate something between the terminal process and the running program itself so I'll keep the research

EDIT

Finally I found a solution:

I was using echo command, so it was just writing text to the FD, instead we need to properly make the correct simulation as a device input, fake the input.

How to get it working? We need to simulate the input in the FD.

In the linux there is a way to simulate the terminal input, using the iocontrols (ioctl). One of the argument options is the TIOCSTI (Terminal input/output control - Simulate terminal input) that inserts a character in the input queue. Basically it simplifies the locking/input management of a given character.

We need the CAP_SYS_ADMIN capability to be able to execute tiocsti() so I started a Python docker container with this linux capability turned on (see reference 4).

#app/echo.py
import sys
from os import getpid

print(f'Hello world! Process: { getpid() }')

for line in sys.stdin:
print(f'Echoing: {line}')

#app/writer.py

from fcntl import ioctl
from termios import TIOCSTI
import sys

with open(f'/proc/{sys.argv[1]}/fd/0', 'w') as fd:
for char in f'{sys.argv[2]}\n':
ioctl(fd, TIOCSTI, char)
version: '3'

services:
python:
container_name: python_fd
image: python:3.11-rc-bullseye
cap_add:
- CAP_SYS_ADMIN
command:
- /bin/sh
- -c
- |
sleep 10000
volumes:
- ./app:/home/app
working_dir: /home/app/

Terminal 1:

$ docker-compose up -d 
$ docker exec -it python_fd sh
# python echo.py
Hello world! Process: <pid>

Terminal 2:

$ docker exec -it python_fd sh
# python writer.py <process pid returned in the previous command> "Hello Lais"

Output of Terminal 1:

Hello Lais
Echoing: Hello Lais

References:

https://unix.stackexchange.com/a/345572

https://manpages.debian.org/bullseye/manpages-dev/ioctl.2.en.html

https://man7.org/linux/man-pages/man7/capabilities.7.html

https://github.com/torvalds/linux/blob/master/drivers/tty/tty_io.c#L2278

Open terminal, run command, return to SAME terminal later and execute another command

In order to write commands to a terminal from another program or terminal you must use a system input-output control system call (ioctl). (This may not always be the case but is is the solution I have found). I will also be presenting a solution in Python but I have cited other resources including a method in c below.

First, you need the process identifier (PID) of the terminal instance you wish to send commands to for it to execute. This can be determined in a few ways but the easiest way I found was via the following command:

ps -A | grep bash --color=always

This will output a list of open terminals and their PIDs and pts numbers. The easiest way I find to know which is the one you want is to open a terminal via your program, run the aforementioned command and the recently opened terminal will be the last on the list. I'm sure you can get more fancy with it if you need to be certain but that isn't the point of this question. You will see something like this, where the pts/# is what you're after

108514 pts/2    00:00:00 bash

Next use the following code and simply save it to a .py file of your choice, (credit for this code goes to the answer in the first link below, the Python one). Note that the example below is hard coded to send the "ls" command. Again, either change the hard coded command or make it not hard coded depending on your own preference and use case.

import fcntl
import sys
import termios

with open(sys.argv[1], 'w') as fd:
for c in "ls\n":
fcntl.ioctl(fd, termios.TIOCSTI, c)

Then, simply call the new function and pass it the following path based on the pts number found previously like so:

python <your_fcn_name_here).py /dev/pts/#

Worked fine for me on Ubuntu 14.04. I'll be trying it on CentOS soon. Didn't have to install any python libraries to do it.

Other Resources

This question has been posed differently here:

  • In Python: https://stackoverflow.com/a/29615101/7590133
  • In C: https://stackoverflow.com/a/7370822/7590133

For more good information regarding IOCTLs:

  • IOCTL Linux device driver


Related Topics



Leave a reply



Submit