/* * RHQ Management Platform * Copyright (C) 2005-2008 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2, as * published by the Free Software Foundation, and/or the GNU Lesser * General Public License, version 2.1, also as published by the Free * Software Foundation. * * 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 and the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU General Public License * and the GNU Lesser General Public License along with this program; * if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.rhq.core.util.exec; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.rhq.core.util.UtilI18NResourceKeys; /** * Executes processes using the Java API. * This instance is thread safe and can be reused. * * <p><b>Warning: caution should be exercised when using this class - it allows any process to be started with no * security restrictions.</b></p> * * @author John Mazzitelli */ public class ProcessExecutor { /** * A thread pool for executing processes. * Although it would make sense to have this 'static', it mucks up RHQ Agent cleanup. * Far better is to keep a reference to this instance. */ private final ExecutorService threadPool; /** * Constructs with a thread pool which executes tasks. */ public ProcessExecutor(ExecutorService threadPool) { this.threadPool = threadPool; } /** * Constructs using a new, cached thread pool. */ public ProcessExecutor() { this(Executors.newCachedThreadPool()); } /** * This executes any operating system process as described in the given start command. When this method returns, it * can be assumed that the process was launched but not necessarily finished. The caller can ask this method to * block until process exits by setting {@link ProcessToStart#setWaitForExit(Long)} to a positive, non-zero timeout * in milliseconds. On error, the exception will be returned in the returned results. * * @param processToStart the information on what process to start * * @return the results of the process execution */ public ProcessExecutorResults execute(ProcessToStart processToStart) { ProcessExecutorResults results = new ProcessExecutorResults(); try { Integer exitCode = startProgram(processToStart); results.setExitCode(exitCode); } catch (Throwable t) { results.setError(t); } return results; } /** * Starts a child process. When this method returns, it can be assumed that the process was launched. On error, an * exception is thrown. Note that this method will also wait for the process to exit if * {@link ProcessToStart#getWaitForExit()} is positive, non-zero. In that case, the returned value will be the exit * code of the process. If this method is told not to wait, the returned value will be <code>null</code>. * * @param process provides the information necessary to start the child process * * @return process exit code (if the method waited for it to exit) or <code>null</code> if this method was to only * start the process but not wait or was to wait and the wait time expired before the process exited * @throws Exception if any error occurs while trying to start the child process */ protected Integer startProgram(final ProcessToStart process) throws Exception { // prepare the process comand line and environment String[] cmdline = getCommandLine(process); File workingDir = getWorkingDirectory(process); String[] environment = process.getEnvironment(); // execute the program final Process childProcess = Runtime.getRuntime().exec(cmdline, environment, workingDir); // redirect the program's streams final RedirectThreads redirect = redirectAllStreams(process, childProcess); Integer exitCode = null; // wait if told to - note that the default is not to wait if (process.getWaitForExit().intValue() > 0) { Callable<Integer> call = new Callable<Integer>() { @Override public Integer call() throws Exception { Thread.currentThread().setName("ExecuteProcess-" + process.getProgramTitle()); try { return childProcess.waitFor(); } finally { // wait for I/O to finish redirect.join(); } } }; Future<Integer> future = threadPool.submit(call); try { exitCode = future.get(process.getWaitForExit().intValue(), TimeUnit.MILLISECONDS); } catch (InterruptedException ie) { // this might happen if the launching thread got interrupted Thread.currentThread().interrupt(); } catch (TimeoutException e) { // the documentation requires we return null } finally { future.cancel(true); } if (exitCode == null) { // never got the exit code so the wait time must have expired, kill the process if configured to do so if (process.isKillOnTimeout().booleanValue()) { childProcess.destroy(); } // cancel the output threads redirect.interrupt(); } } return exitCode; } /** * Wrapper for threads used for capturing output. * Call {@link #join} to wait for output to be fully captured. */ protected static class RedirectThreads { private final Future<?> stdout; private final Future<?> stderr; private RedirectThreads(Future<?> stdout, Future<?> stderr) { this.stdout = stdout; this.stderr = stderr; } /** * Waits for output to be fully captured. */ public void join() throws InterruptedException, ExecutionException { stderr.get(); stdout.get(); } /** * Interrupts these threads. */ public void interrupt() { stderr.cancel(true); stdout.cancel(true); } } @Deprecated protected void redirectStreams(ProcessToStart process, Process childProcess) throws IOException { redirectAllStreams(process, childProcess); } /** * This method redirects the stdout/stderr streams of the child process to the output log file and pipes the * contents of the input file (if one was specified) to the stdin stream of the child process. * * <p>This is done asynchronously so as to avoid deadlocking and to allow the main thread to continue its work.</p> * * <p>Once the child process dies, so do the piping threads.</p> * * @param process used to configure the process * @param childProcess the newly spawned child process * * @throws IOException if failed to pipe data to/from stdin/stdout * @return RedirectThreads containing a handle to the threads redirecting output */ protected RedirectThreads redirectAllStreams(ProcessToStart process, Process childProcess) throws IOException { // Process.getInputStream is actually the process's stdout output // Process.getOutputStream is actually the process's stdin intput // Process.getErrorStream is the process's stderr output InputStream stdout = childProcess.getInputStream(); InputStream stderr = childProcess.getErrorStream(); OutputStream stdin = childProcess.getOutputStream(); // pipe both stderr and stdout to the output file asynchronously so we don't hang collecting output data infinitely String threadNamePrefix = process.getProgramTitle(); OutputStream fileOutputStream = null; if (process.isCaptureOutput().booleanValue()) { fileOutputStream = process.getOutputStream(); // override the file if given a stream already if (fileOutputStream == null) { File outputFile = createOutputFile(process); if (outputFile != null) { fileOutputStream = new BufferedOutputStream(new FileOutputStream(outputFile)); } } } if (threadNamePrefix == null) { threadNamePrefix = process.getProgramExecutable(); } StreamRedirectorRunnable stdoutThread = new StreamRedirectorRunnable(threadNamePrefix + "-stdout", stdout, fileOutputStream); StreamRedirectorRunnable stderrThread = new StreamRedirectorRunnable(threadNamePrefix + "-stderr", stderr, fileOutputStream); Future<?> out = threadPool.submit(stdoutThread); Future<?> err = threadPool.submit(stderrThread); // if an input file was specified, take the file's data and write it to the process' stdin File inputFile = getInputFile(process); if (inputFile != null) { BufferedInputStream fileInputStream = new BufferedInputStream(new FileInputStream(inputFile)); byte[] fileBytes = new byte[4096]; int fileBytesRead = 1; // prime the pump while (fileBytesRead > 0) { fileBytesRead = fileInputStream.read(fileBytes); if (fileBytesRead > 0) { stdin.write(fileBytes, 0, fileBytesRead); } } fileInputStream.close(); } stdin.close(); return new RedirectThreads(out, err); } /** * Creates the output file and returns its <code>File</code> representation. This is the location where the child * process' stdout/stderr output streams get redirected to. Note that if the file does not exist, it will be * created; if it does exist, the original will be renamed with a timestamp before a new file is created only if the * {@link ProcessToStart#isBackupOutputFile() backup output file flag} is <code>Boolean.TRUE</code>; otherwise it * will be overwritten. * * <p>If the {@link ProcessToStart#getOutputDirectory() output directory} was not specified, a temporary location * will be used (i.e. the System property <code>java.io.tmpdir</code>). If the * {@link ProcessToStart#getOutputFile() output file name} was not specified, one will be generated automatically - * using the {@link ProcessToStart#getProgramTitle() title} if one was specified.</p> * * <p>If we are not to {@link ProcessToStart#isCaptureOutput() capture the output}, this method returns <code> * null</code>.</p> * * @param process the process to start * * @return output log file (may be <code>null</code>) * * @throws IOException if the output file could not be created * @throws FileNotFoundException the output directory does not exist or is not a valid directory */ protected File createOutputFile(ProcessToStart process) throws IOException { // return immediately if the client does want us to capture the output if (!process.isCaptureOutput().booleanValue()) { return null; } String directoryStr = process.getOutputDirectory(); String filenameStr = process.getOutputFile(); // determine the valid output directory to use if ((directoryStr == null) || (directoryStr.length() == 0)) { directoryStr = System.getProperty("java.io.tmpdir"); } File directoryFile = new File(directoryStr); if (!directoryFile.exists()) { throw new FileNotFoundException(UtilI18NResourceKeys.MSG.getMsg( UtilI18NResourceKeys.PROCESS_EXEC_OUTPUT_DIR_DOES_NOT_EXIST, directoryFile)); } if (!directoryFile.isDirectory()) { throw new IOException(UtilI18NResourceKeys.MSG.getMsg(UtilI18NResourceKeys.PROCESS_EXEC_OUTPUT_DIR_INVALID, directoryFile)); } // determine the valid filename to use and create the file if necessary // IF the filename was not specified // Create a file with a name that has the title or executable as part of the filename // ELSE // Use the filename given, renaming any existing file if we were told to back it up // END IF File retOutputFile; if ((filenameStr == null) || (filenameStr.length() == 0)) { String prefix = process.getProgramTitle(); if (prefix == null) { prefix = process.getProgramExecutable(); } prefix += "__"; // ensures that we follow createTempFile requirement that it be at least 3 characters retOutputFile = File.createTempFile(prefix, ".out", directoryFile); } else { retOutputFile = new File(directoryFile, filenameStr); if (retOutputFile.isDirectory()) { throw new IOException(UtilI18NResourceKeys.MSG.getMsg( UtilI18NResourceKeys.PROCESS_EXEC_OUTPUT_FILE_IS_DIR, retOutputFile)); } if (retOutputFile.exists()) { if ((process.isBackupOutputFile() != null) && process.isBackupOutputFile().booleanValue()) { renameFile(retOutputFile); } else { retOutputFile.delete(); } } if (!retOutputFile.createNewFile()) { throw new IOException(UtilI18NResourceKeys.MSG.getMsg( UtilI18NResourceKeys.PROCESS_EXEC_OUTPUT_FILE_CREATION_FAILURE, retOutputFile)); } } return retOutputFile; } /** * Gets the input file and returns its <code>File</code> representation. This is the file whose data will be sent to * the child process' stdin input stream. Note that if the input file that was specified in the command does not * exist, an exception is thrown. If the command does not specify an input file, <code>null</code> is returned. * * @param process the start command * * @return input file (may be <code>null</code>) * * @throws IOException if the input file could not be found * @throws FileNotFoundException the input directory does not exist or is not a valid directory */ protected File getInputFile(ProcessToStart process) throws IOException { String directoryStr = process.getInputDirectory(); String filenameStr = process.getInputFile(); boolean filenameSpecified = (filenameStr != null) && (filenameStr.length() > 0); boolean directorySpecified = (directoryStr != null) && (directoryStr.length() > 0); if (directorySpecified ^ filenameSpecified) { throw new IOException(UtilI18NResourceKeys.MSG.getMsg( UtilI18NResourceKeys.PROCESS_EXEC_INPUT_PARAMS_INVALID, process)); } if (!directorySpecified) { return null; } // determine the valid input directory to use File directoryFile = new File(directoryStr); if (!directoryFile.exists()) { throw new FileNotFoundException(UtilI18NResourceKeys.MSG.getMsg( UtilI18NResourceKeys.PROCESS_EXEC_INPUT_DIR_DOES_NOT_EXIST, directoryFile)); } if (!directoryFile.isDirectory()) { throw new IOException(UtilI18NResourceKeys.MSG.getMsg(UtilI18NResourceKeys.PROCESS_EXEC_INPUT_DIR_INVALID, directoryFile)); } // determine the valid input filename to use File retInputFile = new File(directoryFile, filenameStr); if (!retInputFile.exists()) { throw new FileNotFoundException(UtilI18NResourceKeys.MSG.getMsg( UtilI18NResourceKeys.PROCESS_EXEC_INPUT_FILE_DOES_NOT_EXIST, retInputFile)); } if (!retInputFile.canRead()) { throw new IOException(UtilI18NResourceKeys.MSG.getMsg( UtilI18NResourceKeys.PROCESS_EXEC_INPUT_FILE_UNREADABLE, retInputFile)); } if (retInputFile.isDirectory()) { throw new IOException(UtilI18NResourceKeys.MSG.getMsg(UtilI18NResourceKeys.PROCESS_EXEC_INPUT_FILE_IS_DIR, retInputFile)); } return retInputFile; } /** * Returns the full pathname to the program executable. If the program executable does not exist, an exception is * thrown. * * @param process the process to start * * @return full path name to the program executable file * * @throws FileNotFoundException if the program executable does not exist */ protected String getFullProgramExecutablePath(ProcessToStart process) throws FileNotFoundException { File progFile = new File(process.getProgramDirectory(), process.getProgramExecutable()); String result = progFile.getPath(); // If executable verification has been turned off then assume the caller wants his executable "as-is". // Otherwise, validate and ensure a full path. if (Boolean.TRUE.equals(process.isCheckExecutableExists())) { if (!progFile.exists()) { throw new FileNotFoundException(UtilI18NResourceKeys.MSG.getMsg( UtilI18NResourceKeys.PROCESS_EXEC_PROGRAM_DOES_NOT_EXIST, progFile)); } result = progFile.getAbsolutePath(); } return result; } /** * Returns the full pathname to the working directory. An exception is thrown if the directory does not exist. If * the working directory is <code>null</code>, child process inherits the parent process's current working * directory. * * @param process the process to start * * @return the working directory where the program should "start in" - its starting or current directory in other * words * * @throws FileNotFoundException if the working directory does not exist */ protected File getWorkingDirectory(ProcessToStart process) throws FileNotFoundException { File retWorkingDir = null; String workingDirString = process.getWorkingDirectory(); if (workingDirString != null) { retWorkingDir = new File(workingDirString); if (!retWorkingDir.exists()) { throw new FileNotFoundException(UtilI18NResourceKeys.MSG.getMsg( UtilI18NResourceKeys.PROCESS_EXEC_WORKING_DIR_DOES_NOT_EXIST, retWorkingDir)); } } return retWorkingDir; } /** * Builds the command line containing the full path to the program executable and any arguments that are to be * passed to the program. * * @param process the process to start * * @return array of command line arguments (the first of which is the full path to the program executable file) * * @throws FileNotFoundException if the program executable file does not exist */ protected String[] getCommandLine(ProcessToStart process) throws FileNotFoundException { // determine where the executable is String fullProgramPath = getFullProgramExecutablePath(process); // build the command line String[] args = process.getArguments(); int numArgs = (args != null) ? args.length : 0; String[] retCmdline = new String[numArgs + 1]; // +1 for the program executable path retCmdline[0] = fullProgramPath; if (numArgs > 0) { System.arraycopy(args, 0, retCmdline, 1, numArgs); } return retCmdline; } /** * Renames the given file by appending to its name a date/time stamp. * * @param file the file to be renamed * * @throws IOException if failed to rename the file */ private void renameFile(File file) throws IOException { SimpleDateFormat formatter = new SimpleDateFormat("-yyyy-MM-dd--HH-mm-ss"); String timestamp = formatter.format(new Date()); String newFileName = file.getCanonicalPath() + timestamp; file.renameTo(new File(newFileName)); } }