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
How to Specify the Function Type in My Type Hints
What Does a Python Process Return Code -9 Mean
Conda Reports Packagesnotfounderror: Python=3.1 for Reticulate Environment
"Getaddrinfo Failed", What Does That Mean
Display Image as Grayscale Using Matplotlib
Difference Between Subprocess.Popen and Os.System
How to Allow or Deny Notification Geo-Location Microphone Camera Pop Up
Plot Different Color for Different Categorical Levels Using Matplotlib
Why Does Foo.Append(Bar) Affect All Elements in a List of Lists
Extending Setuptools Extension to Use Cmake in Setup.Py
Popen.Communicate() Throws Oserror: "[Errno 10] No Child Processes"
Computing Cross-Correlation Function
How to Dereference Variable Id'S
Create a .CSV File with Values from a Python List
Python: Problem with Raw_Input Reading a Number
Some Unix Commands Fail with "<Command> Not Found", When Executed Using Python Paramiko Exec_Command