When a Pipe isn't enough: TTYs in Java
I needed to launch Docker containers in Java and pipe the result pack to some other place. This included interacting with that container directly via standard in/out. Nothing easier than that, you can do this with docker like this:
docker run -it ubuntu:20.04
Ok, then lets launch it via Java:
var processCreation = new ProcessBuilder("docker", "run", "-it", "ubuntu:20.04");
processCreation.redirectErrorStream();
processCreation.redirectOutput();
processCreation.redirectInput();
var process = processCreation.start();
redirectToRightPlace(process.getInputStream());
redirectToRightPlace(process.getErrorStream());
redirectToRightPlace(process.getOutputStream());
int exitCode = process.waitFor();
Unfortunately, you get this error back from the process:
the input device is not a TTY
Event when you ignore this error, interactive apps like a shell, text editors etc won’t work properly. So, what is this TTY anyway?
TTY: Telewriters and Pseudo Terminals PTYs
The TTY comes from typewriters. In the early days, unix computers where connected to actual typewriters via some (telephone)-wires. The computer sent characters down that wire to print out things on paper and receive typed in commands. Later on the type writer got replaced by terminals. The same approach was used, but a terminal started to support more complex control characters like (VT100, ANSI X3.64).
These approach is still in use today, but we connect with a pseudo terminal (PTS). A pseudo terminal is a software emulation of a terminal. Your graphical terminal application, ssh, tmux, etc open pseudo terminals and then show the output.
You can use the tty
utility to what terminal you are connected to.
Here is an example of different tmux panels.
Each panel has a different /dev/pts/<number>
.
The /dev/pts/<number> are the pseudo terminal 'devices'.
If you run the tty command with a redirected input stream,
then the standard input is not a tty
and you will get this instead:
$ echo '' | tty > not a tty
Unsuccessful Search for Pseudo Terminal Utility
Ok, back to my original problem.
I’m launching Docker from a Java process and I need a tty for it, because I want the interactive behavior.
In the documentation you find the -i
option for keeping stdin open
and -t
option to allocate a pseudo terminal, so that sound promising.
However, I already used these flags. The -t
opens a pseudo terminal in the container,
but it needs a terminal on the docker client side to work.
I needed to launch the docker
command with a tty
in Java process.
My though was, that if there are commands like ssh
and tmux
which create pseude terminals,
then surely there is some tiny utility which can do one thing:
Launch a process with a pseudo terminal as standard input.
Unfortunately, I couldn’t find a standard tool to that.
[1]
The tools I found where not popular enough to be packaged in most Linux distros.
I even started to look into the syscalls to create a pseudoterminal, but I didn’t want to go down to nitty gritty details for my small use case.
Python Pty leads to pty4j
After searching in circles, I stumbled upon the Python pty package. It had a nice wrapper around the syscalls and seemed to have all the things I need. So, in the worst case I could write a wrapper Python script which opens a pseudo terminal to launch Docker and feed the results to my Java process.
But wait! If there’s a Python library, there might be a Java library as well? Search for Java pty and voila, the first result is Pty4j.
It solves exactly my issue. It allows launching processes with a pseudo-terminal allocated. Problem solved!
var cmd = new String[]{"docker", "run", "-it", "ubuntu:20.04"};
PtyProcess process = PtyProcess.exec(cmd);
// Now the process runs under a pseudo terminal,
// And the results are redirected back like the regular Java's Process class
redirectToRightPlace(process.getInputStream());
redirectToRightPlace(process.getErrorStream());
redirectToRightPlace(process.getOutputStream());
// There is some extra stuff available, like setting windows size etc.
process.setWinSize(new WinSize(120,100));
int exitCode = process.waitFor();