Java tutorial
/* * Copyright (c) 2009-2011, Martijn Brinkers, Djigzo. * * This file is part of Djigzo email encryption. * * Djigzo is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License * version 3, 19 November 2007 as published by the Free Software * Foundation. * * Djigzo 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 Djigzo. If not, see <http://www.gnu.org/licenses/> * * Additional permission under GNU AGPL version 3 section 7 * * If you modify this Program, or any covered work, by linking or * combining it with aspectjrt.jar, aspectjweaver.jar, tyrex-1.0.3.jar, * freemarker.jar, dom4j.jar, mx4j-jmx.jar, mx4j-tools.jar, * spice-classman-1.0.jar, spice-loggerstore-0.5.jar, spice-salt-0.8.jar, * spice-xmlpolicy-1.0.jar, saaj-api-1.3.jar, saaj-impl-1.3.jar, * wsdl4j-1.6.1.jar (or modified versions of these libraries), * containing parts covered by the terms of Eclipse Public License, * tyrex license, freemarker license, dom4j license, mx4j license, * Spice Software License, Common Development and Distribution License * (CDDL), Common Public License (CPL) the licensors of this Program grant * you additional permission to convey the resulting work. */ package mitm.common.util; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.List; import mitm.common.scheduler.DestroyProcessTimeoutTask; import mitm.common.scheduler.Task; import mitm.common.scheduler.TaskScheduler; import mitm.common.scheduler.ThreadInterruptTimeoutTask; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Helper class for running external applications. * * @author Martijn Brinkers * */ public class ProcessRunner { private final static Logger logger = LoggerFactory.getLogger(ProcessRunner.class); /* * Thread class which will be used to copy and input stream to an output * stream in it's own class. This is used to handle standard output and error * without blocking. */ private static class StreamCopier extends Thread { private final InputStream input; private final OutputStream output; private final String identifier; private final boolean closeInput; private final boolean closeOutput; public StreamCopier(InputStream input, OutputStream output, String identifier, boolean closeInput, boolean closeOutput) { this.input = input; this.output = output; this.identifier = identifier; this.closeInput = closeInput; this.closeOutput = closeOutput; } @Override public void run() { logger.debug("Start thread " + identifier); try { IOUtils.copy(input, output); } catch (IOException e) { // ignore } finally { if (closeInput) { IOUtils.closeQuietly(input); } if (closeOutput) { IOUtils.closeQuietly(output); } } logger.debug("end thread " + identifier); } } /* * The input will be written to the standard input stream of the process */ private InputStream input; /* * The standard output of the process will be written to this stream. */ private OutputStream output; /* * The standard error of the process will be written to this stream. */ private OutputStream error; /* * The process exit code */ private int exitCode; /* * The process exit code for success */ private int successExitCode; /* * If true and the exit code of the process is not equal to successExitCode and * exception will be thrown. */ private boolean throwExceptionOnErrorExitCode = true; /* * The number of milliseconds a process may run before it's terminated. */ private long timeout = -1; /* * If true the complete cmd line will be logged (in exception etc.). You might want to * set this to false when one of the arguments contain sensitive data */ private boolean logArguments = true; public InputStream getInput() { return input; } public void setInput(InputStream input) { this.input = input; } public OutputStream getOutput() { return output; } public void setOutput(OutputStream output) { this.output = output; } public OutputStream getError() { return error; } public void setError(OutputStream error) { this.error = error; } public int getExitCode() { return exitCode; } public void setExitCode(int exitCode) { this.exitCode = exitCode; } public int getSuccessExitCode() { return successExitCode; } public void setSuccessExitCode(int successExitCode) { this.successExitCode = successExitCode; } public boolean isThrowExceptionOnErrorExitCode() { return throwExceptionOnErrorExitCode; } public void setThrowExceptionOnErrorExitCode(boolean throwExceptionOnErrorExitCode) { this.throwExceptionOnErrorExitCode = throwExceptionOnErrorExitCode; } public long getTimeout() { return timeout; } public void setTimeout(long timeout) { this.timeout = timeout; } public void setLogArguments(boolean logArguments) { this.logArguments = logArguments; } public boolean getLogArguments() { return logArguments; } public int run(List<String> cmd) throws IOException { if (cmd == null || cmd.size() == 0) { throw new IllegalArgumentException("cmd is invalid."); } /* * Used to show which command was executed */ String cmdLine = logArguments ? StringUtils.join(cmd, ",") : cmd.get(0); logger.debug("Starting application. cmdLine: " + cmdLine); /* * Watchdog that will be used to destroy the process on a timeout */ TaskScheduler watchdog = new TaskScheduler(ProcessRunner.class.getCanonicalName() + "#" + cmdLine); try { ProcessBuilder processBuilder = new ProcessBuilder(cmd); Process process = processBuilder.start(); if (timeout > 0) { /* * Task that will destroy the process on a timeout */ Task processWatchdogTask = new DestroyProcessTimeoutTask(process, watchdog.getName()); watchdog.addTask(processWatchdogTask, timeout); /* * Task that will interrupt the current thread on a timeout */ Task threadInterruptTimeoutTask = new ThreadInterruptTimeoutTask(Thread.currentThread(), watchdog.getName()); watchdog.addTask(threadInterruptTimeoutTask, timeout); } Thread inputThread = null; Thread outputThread = null; Thread errorThread = null; if (input != null) { inputThread = new StreamCopier(input, process.getOutputStream(), "input", false, true); inputThread.start(); } if (output != null) { outputThread = new StreamCopier(process.getInputStream(), output, "output", true, false); outputThread.start(); } if (error != null) { errorThread = new StreamCopier(process.getErrorStream(), error, "error", true, false); errorThread.start(); } try { exitCode = process.waitFor(); /* * We need to wait for the threads to finish because otherwise there is * a chance we will start using the output before all the output has been * written/read. */ if (inputThread != null) { inputThread.join(); } if (outputThread != null) { outputThread.join(); } if (errorThread != null) { errorThread.join(); } /* * We MUST close the standard streams otherwise some "FIFO pipes" won't be closed until * the garbage collector is run. If there are too many open "FIFO pipes", the following * exception might occur: * * java.io.IOException: error=24, Too many open files * * Closing the standard streams makes sure that the pipes are closed. * * Note: The Javadoc for Process does not mention that you should close the standard streams * even if not used. Perhaps they rely on the GC? * * Note2: The number of FIFI pipes can be counted with: * * lsof | grep java | grep pipe | wc -l */ IOUtils.closeQuietly(process.getInputStream()); IOUtils.closeQuietly(process.getOutputStream()); IOUtils.closeQuietly(process.getErrorStream()); } catch (InterruptedException e) { throw new IOException("Error running [" + cmdLine + "]", e); } if (exitCode != successExitCode) { throw new ProcessException("Error running [" + cmdLine + "]. exit value: " + exitCode, exitCode); } } finally { logger.debug("Application finished. cmdLine: " + cmdLine); /* * Need to cancel any pending watchdog tasks */ watchdog.cancel(); } return exitCode; } }