org.cloudcoder.submitsvc.oop.builder.ProcessRunner.java Source code

Java tutorial

Introduction

Here is the source code for org.cloudcoder.submitsvc.oop.builder.ProcessRunner.java

Source

/*
 * Web C programming environment
 * Copyright (c) 2010-2011, David H. Hovemeyer <david.hovemeyer@gmail.com>
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.cloudcoder.submitsvc.oop.builder;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.io.IOUtils;
import org.cloudcoder.daemon.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Run a subprocess, capturing its stdout and stderr as text.
 * Optionally, send text to the stdin of the process.
 * 
 * @author David Hovemeyer
 * @author Jaime Spacco
 */
public class ProcessRunner implements ITestOutput {
    private static final Logger logger = LoggerFactory.getLogger(ProcessRunner.class);

    private static String RUN_PROCESS_SCRIPT;
    static {
        // "Externalize" the runProcess.sh script.
        // If we're running out of a directory, then we can directly access the file
        // in the classpath.  If we're running out of a jarfile, then this will copy
        // runProcess.sh into a temporary file in the filesystem.
        try {
            String runProcessPath = ProcessRunner.class.getPackage().getName().replace('.', '/')
                    + "/res/runProcess.sh";
            RUN_PROCESS_SCRIPT = Util.getExternalizedFileName(ProcessRunner.class.getClassLoader(), runProcessPath);
        } catch (IOException e) {
            throw new IllegalStateException("Couldn't get externalized path for runProcess.sh", e);
        }
    }

    private String statusMessage = "";

    private boolean exitStatusKnown;
    private boolean processStarted;
    private int exitCode;
    private boolean killedBySignal;

    private volatile Process process;
    private Thread exitValueMonitor;
    private String stdin;
    private IOutputCollector stdoutCollector;
    private IOutputCollector stderrCollector;
    private InputSender stdinSender;

    private Map<String, String> env = new HashMap<String, String>();

    /**
     * Constructor.
     */
    public ProcessRunner() {
        for (Entry<String, String> entry : System.getenv().entrySet()) {
            env.put(entry.getKey(), entry.getValue());
        }
    }

    /**
     * Set text to send to the process as its standard input.
     * 
     * @param stdin text to send to the process as its standard input
     */
    public void setStdin(String stdin) {
        this.stdin = stdin;
    }

    /**
     * Create the environment array defining the environment
     * variables for the process.  The environment will
     * contain all of the environment variables in the parent process,
     * plus any extra ones specified by the extraVars parameter.
     * 
     * @param extraVars extra environment variables to define for the process,
     *                  in the form VAR=value
     * @return enviroment array
     */
    protected String[] getEnvp(String... extraVars) {
        String[] envp = new String[env.size() + extraVars.length];
        int i = 0;
        for (Entry<String, String> entry : env.entrySet()) {
            envp[i] = entry.getKey() + "=" + entry.getValue();
            i += 1;
        }
        for (String s : extraVars) {
            envp[i++] = s;
        }
        return envp;
    }

    public void addDirToPath(String dir) {
        String path = env.get("PATH");
        path += File.separatorChar + dir;
        env.put("PATH", path);
    }

    public String getStatusMessage() {
        return statusMessage;
    }

    public boolean runSynchronous(File workingDir, String[] command) {
        // wrap command (by default, using the runProcess.sh script)
        command = wrapCommand(command);

        // exec command
        logger.info("Running in {} the command: {}", workingDir.toString(), CUtil.mergeOneLine(command));
        try {
            // Create a temp file in which the runProcess.sh script can save
            // the exit status of the process.
            File exitStatusFile = File.createTempFile("ccxs", ".txt", workingDir);
            //logger.debug("Creating exit status file " + exitStatusFile.getPath());
            exitStatusFile.deleteOnExit();

            // Start process, setting CC_PROC_STAT_FILE env var
            // to indicate where runProcess.sh should write the process's
            // exit status information
            process = Runtime.getRuntime().exec(command, getEnvp("CC_PROC_STAT_FILE=" + exitStatusFile.getPath()),
                    workingDir);

            // Collect process output
            stdoutCollector = createOutputCollector(process.getInputStream());
            stderrCollector = createOutputCollector(process.getErrorStream());
            stdoutCollector.start();
            stderrCollector.start();

            // If stdin was provided, send it
            if (stdin != null) {
                stdinSender = new InputSender(process.getOutputStream(), stdin);
                stdinSender.start();
            }

            // wait for process and output collector threads to finish
            exitCode = process.waitFor();
            stdoutCollector.join();
            stderrCollector.join();
            if (stdinSender != null) {
                stdinSender.join();
            }

            // Read the process's exit status information
            readProcessExitStatus(exitStatusFile);
            return true;
        } catch (IOException e) {
            statusMessage = "Could not execute process: " + e.getMessage();
        } catch (InterruptedException e) {
            statusMessage = "Process was interrupted (infinite loop killed?)";
        }
        return false;
    }

    protected String[] wrapCommand(String[] command) {
        List<String> cmd = new ArrayList<String>();
        cmd.add("/bin/bash");
        cmd.add(RUN_PROCESS_SCRIPT);
        cmd.addAll(Arrays.asList(command));
        return cmd.toArray(new String[cmd.size()]);
    }

    /**
     * Create an IOutputCollector to be used to collect the stdout/stderr
     * of the process.  Subclasses may override to precisely control how
     * output is collected (for example, to limit the number of bytes/lines
     * that will be collected.)
     * 
     * Default implementation returns an {@link OutputCollector}, which
     * reads an unlimited amount of output.
     * 
     * @param inputStream the InputStream for the process's stdout or stderr
     * @return an IOutputCollector to collect the process's stdout or stderr
     */
    protected IOutputCollector createOutputCollector(InputStream inputStream) {
        return new OutputCollector(inputStream);
    }

    /**
     * Read the file written by the runProcess.sh script
     * which contains information about the process's exit status.
     * 
     * @param exitStatusFile file containing information about the process's exit status
     */
    private void readProcessExitStatus(File exitStatusFile) {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader(exitStatusFile));
            String status = reader.readLine();
            String exitCode = reader.readLine();
            if (status != null && exitCode != null) {
                logger.debug("Read process exit status file: status={}, exitCode={}", status, exitCode);

                // Second line of file should be the exit code
                this.exitCode = Integer.parseInt(exitCode);

                if (status.equals("failed_to_execute")) {
                    // The process could not be started
                    this.processStarted = false;
                    this.statusMessage = "Process could not be started";

                    logger.debug("process stderr is {}", CUtil.mergeOneLine(stderrCollector.getCollectedOutput()));
                } else if (status.equals("exited")) {
                    // The process exited normally.
                    this.exitStatusKnown = true;
                    this.processStarted = true;
                    this.statusMessage = "Process exited";
                } else if (status.equals("terminated_by_signal")) {
                    // The process was killed by a signal.
                    // The exit code is the signal that terminated the process.
                    this.exitStatusKnown = true;
                    this.processStarted = true;
                    this.killedBySignal = true;
                    this.statusMessage = "Process crashed (terminated by signal " + this.exitCode + ")";
                } else {
                    // Should not happen.
                    logger.warn("Unknown process exit status " + status);
                    this.exitStatusKnown = false;
                    this.statusMessage = "Process status could not be determined";
                }
            }
        } catch (IOException e) {
            logger.warn("IOException trying to read process status file");
            this.exitStatusKnown = false;
            this.statusMessage = "Process status could not be determined";
        } catch (NumberFormatException e) {
            logger.warn("NumberFormatException trying to read process status file");
            this.exitStatusKnown = false;
            this.statusMessage = "Process status could not be determined";
        } finally {
            IOUtils.closeQuietly(reader);
            exitStatusFile.delete();
        }
    }

    public void runAsynchronous(final File workingDir, final String[] command) {
        exitValueMonitor = new Thread() {
            public void run() {
                runSynchronous(workingDir, command);
            }
        };
        exitValueMonitor.start();
    }

    /**
     * Find out whether or not the exit status of this process is known.
     * Because in Java it's not directly possible to find out things about
     * how a process was terminated (such as whether it was killed by
     * a signal), we use a wrapper script to collect information about the
     * process's status and write this to a file that this class can read.
     * However, we can't rule out the possibility that this file wasn't
     * written or was corrupted in some way.  If this method returns true,
     * then the process's exit status information is definitely known.
     * <bImportant:</b> don't call this unless the process is definitely not running.
     * 
     * @return true if the process's exit status is definitely known,
     *         false otherwise
     */
    public boolean isExitStatusKnown() {
        return exitStatusKnown;
    }

    /**
     * Find out whether or not the process was actually started.
     * <b>Important:</b>: don't call this unless the process is definitely not running.
     * 
     * @return the processStarted
     */
    public boolean isProcessStarted() {
        return processStarted;
    }

    /**
     * Get the terminated process's exit code.
     * If the process was killed by a signal, then the exit code is
     * the number of the signal that killed the process.
     * <b>Important:</b>: don't call this unless the process is definitely not running.
     * 
     * @return process's exit code, or the number of the signal that
     *         killed the process
     */
    public int getExitCode() {
        return exitCode;
    }

    /**
     * Find out whether the process was killed by a signal.
     * <b>Important:</b>: don't call this unless the process is definitely not running.
     * 
     * @return true if the process was killed by a signal, false otherwise
     */
    public boolean isKilledBySignal() {
        return killedBySignal;
    }

    /* (non-Javadoc)
     * @see org.cloudcoder.submitsvc.oop.builder.IHasStdoutAndStderr#getStdout()
     */
    @Override
    public String getStdout() {
        return CUtil.merge(getStdoutAsList());
    }

    /**
     * @return the standard output written by the process as a List of strings
     */
    public List<String> getStdoutAsList() {
        return stdoutCollector.getCollectedOutput();
    }

    /* (non-Javadoc)
     * @see org.cloudcoder.submitsvc.oop.builder.IHasStdoutAndStderr#getStderr()
     */
    @Override
    public String getStderr() {
        return CUtil.merge(getStderrAsList());
    }

    /**
     * @return the standard error written by the process as a List of strings
     */
    public List<String> getStderrAsList() {
        return stderrCollector.getCollectedOutput();
    }

    public boolean isRunning() {
        Process p = process;

        if (p == null) {
            // Process hasn't started yet.
            // Count that as "running".
            return true;
        }

        try {
            p.exitValue();
            return false;
        } catch (IllegalThreadStateException e) {
            return true;
        }
    }

    public void killProcess() {
        logger.info("Killing process");
        process.destroy();
        stdoutCollector.interrupt();
        stderrCollector.interrupt();
        if (stdinSender != null) {
            stdinSender.interrupt();
        }
    }

    /**
     * Determine whether or not the process ended with a fatal signal.
     * <b>Important:</b>: don't call this unless the process is definitely not running.
     * 
     * @return true if the process ended with a fatal signal, false if
     *         it exited normally
     */
    public boolean isCoreDump() {
        return isKilledBySignal();
    }
}