org.cloudcoder.builder2.process.ProcessRunner.java Source code

Java tutorial

Introduction

Here is the source code for org.cloudcoder.builder2.process.ProcessRunner.java

Source

// CloudCoder - a web-based pedagogical programming environment
// Copyright (C) 2011-2014, Jaime Spacco <jspacco@knox.edu>
// Copyright (C) 2010-2014, David H. Hovemeyer <david.hovemeyer@gmail.com>
// Copyright (C) 2013, York College of Pennsylvania
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package org.cloudcoder.builder2.process;

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.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;

import org.apache.commons.io.IOUtils;
import org.cloudcoder.builder2.model.ProcessStatus;
import org.cloudcoder.builder2.model.WrapperMode;
import org.cloudcoder.builder2.util.ProcessUtil;
import org.cloudcoder.builder2.util.StringUtil;
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 {
    private static final Logger logger = LoggerFactory.getLogger(ProcessRunner.class);

    private Properties config;
    private WrapperMode wrapperMode;

    private String statusMessage = "";

    private boolean processStarted;
    private int exitCode;
    private ProcessStatus status;

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

    private Map<String, String> env;

    /**
     * Constructor.
     * 
     * @param config builder configuration properties
     */
    public ProcessRunner(Properties config) {
        this.config = config;
        this.wrapperMode = WrapperMode.SCRIPT;
        env = new HashMap<String, String>(System.getenv());
        status = ProcessStatus.UNKNOWN;
    }

    /**
     * Get the builder configuration properties.
     * 
     * @return the builder configuration properties.
     */
    public Properties getConfig() {
        return config;
    }

    /**
     * Set the {@link WrapperMode}.
     * 
     * @param wrapperMode the {@link WrapperMode}
     */
    public void setWrapperMode(WrapperMode wrapperMode) {
        this.wrapperMode = wrapperMode;
    }

    /**
     * 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[] curEnvp = ProcessUtil.getEnvArray(env);
        String[] envp = new String[curEnvp.length + extraVars.length];
        System.arraycopy(curEnvp, 0, envp, 0, curEnvp.length);
        System.arraycopy(extraVars, 0, envp, curEnvp.length, extraVars.length);
        return envp;
    }

    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(), StringUtil.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) {
                //System.out.println("Creating InputSender for input: " + stdin);
                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;
    }

    private String[] wrapCommand(String[] command) {
        List<String> cmd = new ArrayList<String>();

        switch (wrapperMode) {
        case NATIVE_EXE:
            RunProcessNativeExe runProc = RunProcessNativeExe.getInstance(config);
            if (runProc.getNativeExePath() != null) {
                // Native exe process wrapper exists, so use it.
                cmd.add(runProc.getNativeExePath());
                break;
            }

            // There was a problem compiling the native exe wrapper.
            // Fall through!

        case SCRIPT:
            cmd.add("/bin/bash");
            cmd.add(RunProcessScript.getInstance(config));
            break;
        }

        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";
                    this.status = ProcessStatus.COULD_NOT_START;

                    logger.debug("process stderr is {}",
                            StringUtil.mergeOneLine(stderrCollector.getCollectedOutput()));
                } else if (status.equals("exited")) {
                    // The process exited normally.
                    this.processStarted = true;
                    this.statusMessage = "Process exited";
                    this.status = ProcessStatus.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.processStarted = true;
                    this.statusMessage = "Process crashed (terminated by signal " + this.exitCode + ")";
                    this.status = ProcessStatus.KILLED_BY_SIGNAL;
                } else {
                    // Should not happen.
                    logger.warn("Unknown process exit status " + status);
                    this.statusMessage = "Process status could not be determined";
                    this.status = ProcessStatus.COULD_NOT_START;
                }
            }
        } catch (IOException e) {
            logger.warn("IOException trying to read process status file");
            this.statusMessage = "Process status could not be determined";
            this.status = ProcessStatus.COULD_NOT_START;
        } catch (NumberFormatException e) {
            logger.warn("NumberFormatException trying to read process status file");
            this.statusMessage = "Process status could not be determined";
            this.status = ProcessStatus.COULD_NOT_START;
        } 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 status != ProcessStatus.COULD_NOT_START;
    }

    /**
     * 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 {@link ProcessStatus}.
     * <b>Important:</b>: don't call this unless the process is definitely not running.
     * 
     * @return the {@link ProcessStatus}
     */
    public ProcessStatus getStatus() {
        if (status == ProcessStatus.KILLED_BY_SIGNAL) {
            if (exitCode == 9 || exitCode == 24) {
                // Special case: if the process was killed by signals 9 (KILL) or 24 (XCPU),
                // treat as a timeout.
                return ProcessStatus.TIMED_OUT;
            } else if (exitCode == 25) {
                // Special case: process killed with SIGXFSZ, file size limit exceeded.
                return ProcessStatus.FILE_SIZE_LIMIT_EXCEEDED;
            }
        }
        return status;
    }

    /**
     * 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;
    }

    /**
     * @return stdout as a single string
     */
    public String getStdout() {
        return StringUtil.merge(getStdoutAsList());
    }

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

    /**
     * @return stderr as a single string
     */
    public String getStderr() {
        return StringUtil.merge(getStderrAsList());
    }

    /**
     * @return the standard error written by the process as a List of strings
     */
    public List<String> getStderrAsList() {
        // Special case: if the process was killed because it exceeded
        // a resource limit, its stderr is probably not useful.
        ProcessStatus status = getStatus();
        if (status == ProcessStatus.TIMED_OUT || status == ProcessStatus.FILE_SIZE_LIMIT_EXCEEDED) {
            return Collections.emptyList();
        } else {
            return stderrCollector.getCollectedOutput();
        }
    }

    /**
     * Check whether or not the process is still running.
     * 
     * @return true if the process is still running, false if it has completed
     */
    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;
        }
    }

    /**
     * Forcibly kill the process.
     */
    public void killProcess() {
        logger.info("Killing process");
        process.destroy();

        // Important: wait for the process, otherwise we will probably create
        // a zombie process.
        boolean exited = false;
        do {
            try {
                process.waitFor();
                process.exitValue();
                exited = true;
            } catch (InterruptedException e) {
                logger.warn("Interrupted waiting for destroyed process to exit");
            } catch (IllegalThreadStateException e) {
                logger.warn("Trouble getting exit status of destroyed process", e);
            }
        } while (!exited);

        stdoutCollector.interrupt();
        stderrCollector.interrupt();
        if (stdinSender != null) {
            stdinSender.interrupt();
        }
    }
}