May 29, 2021

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?

File Isn’t a TTY
Figure 1. File Is Not a TTY
Pipe Isn’t a TTY
Figure 2. Pipe Is Not a TTY

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'.

TTYs in Tmux
Figure 3. TTYs in Tmux

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:

Files and Pipes are not TTYs:
$ 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();
Pseudo Terminal as TTY
Figure 4. Pseudo Terminal as TTY

Conclusion

Interactive programs often need a tty as standard input. A redirected pipe is not a tty. Luckily we can allocate a pseudo-terminal pty for such programs. There are nice libraries to that. For Java there is Pty4j and for Python pty. Worst case you can use the low-level syscalls.


1. I couldn’t reproduce my search results from memory. When I wrote this blog post I now found a way which might work on Stackoverflow using the script utility.
Tags: Unix Java Development