Tuesday, September 11, 2018

Java's Process with Linux Shells

Intro

Today's post highlights some challenges of using Java's Process with Linux interactive shells and provides solutions to these challenges. 
The goal is more complex than being able to execute a Linux command and print a result. The idea is to be able to execute the shell in an interactive fashion. To be able to pass a command to the shell and print the output. And while java.lang.Process provides the I/O streams to do that there are some challenges involved. 
In my post I'll describe these challenges and show possible solutions. 

Interactive Output

Problem

The problem is that the process may print some data to the output over irregular intervals. The time it takes for a command to execute is not predefined. And it is preferable that the user be able to see the output in real-time.

Solution

Use a thread to read the output from Process.inputStream. The Thread wakes up every n milliseconds, reads what is available from the input stream and send to the appropriate destination. When the user wants to enter a command enter it immediately and return. 

Reading From Streams

This problem may sound silly, I understand. Java has all those great classes like InputStream, InputStreamReader, Reader etc. What is the problem with reading from the InputStream?
I thought along similar lines when began developing this code. But it turned out some issues were not obvious at all. 
Most of us are used to reading from a file. Practically everyone knows how to read from a File. But the important thing is that the file streams do not block at read. The input stream that is returned from Process.getInputStream() can actually block in this code (in bold):

InputStream is = process.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
char[] isContents = null;
isr.read(isContents, totalCharsRead, isContents.length - totalCharsRead);

One has to use the isr.ready() method to find out if the next read will block like this:

while (isr.ready() && (totalCharsRead < isContents.length)) {
    currCharsRead = isr.read(isContents, totalCharsRead,      
        isContents.length - totalCharsRead);
    totalCharsRead += currCharsRead;
}

It is also important to understand that InputStream.available() may give you more bytes than is actually available! Yes, that is true but the reason for this unknown. If the char array is declared like this:
char[] isContents = new char[is.available()];
one has to keep track of the actual number of chars read by adding the returned value of InputStreamReader.read to some total value and then using this value.
Here is the complete code:

private String readFromInputStream(InputStream is) {
    try {
        if (is.available() > 0) {

            InputStreamReader isr = new InputStreamReader(is);
            char[] isContents = new char[is.available()];
            int currCharsRead = 0;
            int totalCharsRead = 0;
        
            while (isr.ready() && (totalCharsRead < isContents.length)) {
                currCharsRead = isr.read(isContents, totalCharsRead, isContents.length - totalCharsRead);
                totalCharsRead += currCharsRead;
            }
            return new String(isContents, 0, totalCharsRead);
        }
    } catch (IOException e1) {
        LOG.error("", e1);
    }
    return "";
}

Web UI

If a Web UI is necessary then WebSockets is the likely solution. Spring Boot has a good tutorial on how to use the Stomp client. 

No comments:

Post a Comment