Search code examples
javagwttomcat8processbuilder

ProcessBuilder failing when calling a system command where Runtime exec works


I'm having trouble with ProcessBuilder not running a command on the server.

Early in my project I use Runtime.exec() just to retrieve output from a program which works fine:

private List<SatelliteCode> getSatelliteCodes() {
    List<SatelliteCode> codes = new ArrayList<>();

    Runtime runtime = Runtime.getRuntime();
    String[] commands = { "w_scan", "-s?" };
    Process process;

    try {
        process = runtime.exec(commands);

        BufferedReader error = new BufferedReader(new InputStreamReader(process.getErrorStream()));

        String s = error.readLine(); // discard first line

        while ((s = error.readLine()) != null) {
            s = s.trim();
            int i = s.indexOf('\t'); // separated by a tab!?!?
            codes.add(new SatelliteCode(s.substring(0, i), s.substring(i)));
        }
    } catch (IOException e) {
        e.printStackTrace();
    }

    return codes;
}

Running this in the terminal works fine and I get all the output I need:

w_scan -fs -cGB -sS19E2 > channels.conf

However, the server needs to grab the ongoing output from the 'process.getErrorStream()' to display in the web interface. What is actually happening is the ProcessBuilder is failing and returning an exit code of 1.

The function that initialises the ProcessBuilder and to start the scan running is [EDIT 1]:

private static StringBuilder scan_error_output = null;

@Override
public boolean startSatelliteScan(String user, String country_code, String satellite_code) {
    UserAccountPermissions perm = validateUserEdit(user);
    if (perm == null) return false;

    Shared.writeUserLog(user, Shared.getTimeStamp() + 
            ": DVB satellite scan started " + 
            country_code + " - " + satellite_code + 
            System.lineSeparator() + System.lineSeparator());

    scan_error_output = new StringBuilder();
    new ScanThread(country_code, satellite_code).start();

    // write out country code and satellite code to prefs file

    BufferedWriter bw = null;
    try {
        bw = new BufferedWriter(new FileWriter(satellite_last_scan_codes));

        bw.write(country_code); bw.newLine();
        bw.write(satellite_code); bw.newLine();

        bw.close();
    } catch (IOException e) {
        e.printStackTrace();
    }

    return true;
}

That will then run two other threads on the server, one that will run the scan itself and wait for it to finish so that it can get the final scan data. And the other which constantly updates the output from the std error stream which is then polled at intervals from the clients browser. This is much like showing the ongoing output from the terminal.

The scan thread (which fails to start the process) [EDIT 1]:

private static class ScanThread extends Thread {
    private String cc, sc;

    public ScanThread(String country_code, String satellite_code) {
        cc = country_code;
        sc = satellite_code;
    }

    public void run() {
        ProcessBuilder pb = new ProcessBuilder("/usr/bin/w_scan", 
                "-fs", "-c" + cc, "-s" + sc);
        pb.redirectOutput(new File(satellite_scan_file));

        Process process;
        try {
            System.out.println("Scan thread started");

            process = pb.start();

            IOScanErrorOutputHandler error_output_handler = new IOScanErrorOutputHandler(process.getErrorStream());
            error_output_handler.start();

            int result = process.waitFor();
            System.out.println(cc + " - " + sc + " - " + 
                    "Process.waitFor() result " + result);

        } catch (IOException e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        } catch (InterruptedException e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        }

        System.out.println("Scan thread finished");
    }
}

The error output stream thread which captures the output which obviously doesn't start due to the scan thread failing:

private static class IOScanErrorOutputHandler extends Thread {
    private InputStream inputStream;

    IOScanErrorOutputHandler(InputStream inputStream) {
        this.inputStream = inputStream;
    }

    public void run() {
        Scanner br = null;
        try {
            System.out.println("Scan thread Error IO capture running");
            br = new Scanner(new InputStreamReader(inputStream));
            String line = null;
            while (br.hasNextLine()) {
                line = br.nextLine();
                scan_error_output.append(line + System.getProperty("line.separator"));
            }
        } finally {
            br.close();
        }
        System.out.println("Scan thread Error IO capture finished");

        scan_error_output = null;
    }
}

And the server function which returns the std error output progress:

@Override
public String pollScanResult(String user) {
    if (validateUserEdit(user) == null) return null;
    StringBuilder sb = scan_error_output; // grab instance
    if (sb == null) return null;
    return sb.toString();
}

As mentioned above, Runtime.exec() works fine, but the ProcessBuilder is failing.

NB: I'm on Linux Mint 18.1, using Apache Tomcat 8 as the server, linux default JDK 8 and GWT 2.7 [Correction from 2.8] in Eclipse Neon.

Can anyone see what I am doing wrong?

Many thanks in advance...

[EDIT 1]

Whilst developing this on another machine, Linux Mint 17.2, JDK 8 and Apache Tomcat 7, for DVB-T, this method worked fine and polling for the scan output showed up in the client's browser.

The ProcessBuilder.start still returns 1 and an empty file is created for the output scan file.

[EDIT 2]

It appears that the reason the ProcessBuilder is failing is because the user 'tomcat8' doesn't have permissions to run 'w_scan'. 'w_scan' works from the terminal, but not from the tomcat server. Somehow I've got to fix that now.

[SOLUTIONS]

After being put in the right direction by VGR for getting the error stream from the ProcessBuilder, I started digging further and found I was getting:

main:3909: FATAL: failed to open '/dev/dvb/adapter0/frontend0': 13 Permission denied

Apache tomcat 8 didn't have permission to access the DVB-S frontend to run a scan. This was fixed in two ways:

1 - 03catalina.policy I added the extra permissions (whether they made a difference I do not know).

grant codeBase "file:/dev/dvb/-" {
    permission java.io.FilePermission "file:/dev/dvb/-", "read, write";
    permission java.security.AllPermission;
};

2 - The dvb frontends belong to the 'video' group. So I needed to add the user tomcat8 to that group.

usermod -a -G video tomcat8

All works for now...


Solution

  • You are not doing the same thing with ProcessBuilder that you’re doing with Runtime.exec, so I don't know why you think ProcessBuilder is the problem.

    You have a few problems with how you’re writing the command’s output to a file.

    First, the presence of ">", satellite_scan_temp_file in your ProcessBuilder command is incorrect. Output redirection is not part of any command; it is handled by a shell. But when you run with Runtime.exec or ProcessBuilder, you are not running in a shell, you are executing the process directly. Neither w_scan nor any other command considers > a special character.

    The correct way to redirect to a file is with the redirectOutput method:

    ProcessBuilder pb = new ProcessBuilder(
        "/usr/bin/w_scan", "-fs", "-s" + satellite_code, "-c" + country_code);
    
    pb.redirectOutput(new File(satellite_scan_temp_file));
    

    Second, your ScanThread code is ignoring the (currently incorrect) redirect, and is attempting to read the command’s output. But there is no output, because you are redirecting it all to a file.

    Once you are properly redirecting output to a file, you can remove your BufferedReader and BufferedWriter loops completely.

    Finally, it is worth noting that the error output you captured probably told you that > is not a valid argument to the w_scan process.