blob: e2fa146108e5c567a75009fe36b2d568014c8ae9 [file] [log] [blame]
/*
* Copyright (c) 1997, 2021 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package com.sun.enterprise.util;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.util.ArrayList;
/**
* @author Kedar
* @version 1.0
* @deprecated Use ProcessManager instead
*/
@Deprecated
public class ProcessExecutor {
public static final long kDefaultTimeoutMillis = 600000;
public static final long kSleepTime = 2000;
private static final long DEFAULT_TIMEOUT_SEC = 600;
private static final String NEWLINE = System.getProperty("line.separator");
private long mTimeoutMilliseconds = 0;
protected String[] mCmdStrings = null;
protected File mOutFile = null;
protected File mErrFile = null;
private OutputStream mOutStream = null;
private OutputStream mErrStream = null;
private File mWorkingDir = null; //working directory
private String[] mEnv = null; //environment
private String[] mInputLines = null; // strings to set in process's InputStream (like from redirection)
private int mExitValue = -1;
private Process mSubProcess = null; // used to get handle to child process for ProcessManager funtionality
private boolean mVerboseMode = false;
private boolean retainExecutionLogs = false;
private String lastExecutionOutputString = null;
private String lastExecutionErrorString = null;
private boolean bDebug = false;
/**
* Creates new ProcessExecutor
*/
public ProcessExecutor(String[] cmd) {
this(cmd, DEFAULT_TIMEOUT_SEC, null);
}
/**
* Creates new ProcessExecutor
*/
public ProcessExecutor(String[] cmd, String[] inputLines) {
this(cmd, DEFAULT_TIMEOUT_SEC, inputLines);
}
/**
* Creates new ProcessExecutor
*/
public ProcessExecutor(String[] cmd, long timeoutSeconds) {
this(cmd, timeoutSeconds, null);
}
public ProcessExecutor(String[] cmd, long timeoutSeconds, String[] inputLines) {
this(cmd, timeoutSeconds, inputLines, null, null);
}
/**
* Creates a new
* <code> ProcessExecutor </code> that executes the given command.
*
* @param cmd String that has command name and its command line arguments
* @param timeoutSeconds long integer timeout to be applied in seconds.
* After this time if the process to execute does not end, it will be
* destroyed.
*/
public ProcessExecutor(String[] cmd, long timeoutSeconds, String[] inputLines,
String[] env, File workingDir) {
mCmdStrings = cmd;
mInputLines = inputLines;
mEnv = env;
mWorkingDir = workingDir;
char fwdSlashChar = '/';
char backSlashChar = '\\';
if (System.getProperty("Debug") != null) {
// turn on debug, this option was added to help developers
// debug the their code
bDebug = true;
}
for (int i = 0; i < mCmdStrings.length; i++) {
if (OS.isUnix()) {
mCmdStrings[i] = mCmdStrings[i].replace(backSlashChar, fwdSlashChar);
}
else {
mCmdStrings[i] = mCmdStrings[i].replace(fwdSlashChar, backSlashChar);
}
}
mTimeoutMilliseconds = timeoutSeconds * 1000;
}
/**
* This is the setting after the fact that an instance of ProcessExecutor is
* created. This is to be used in case the output and error of the last
* execute call has to be retained for latter analysis.
*
* @param s boolean representing whether to retain, true means the buffers
* will be retained, false otherwise.
*/
public void setExecutionRetentionFlag(final boolean s) {
this.retainExecutionLogs = s;
}
public boolean getExecutionRetentionFlag() {
return (this.retainExecutionLogs);
}
/**
* Returns the last LAST_BYTES bytes in the error stream of last execution
* as a String, if the ProcessExecutor was configured properly. It may
* return null if the retentionFlag is set to false.
*/
public String getLastExecutionError() {
return (this.lastExecutionErrorString);
}
/**
* Returns the last LAST_BYTES bytes in the output stream of last execution
* as a String, if the ProcessExecutor was configured properly. It may
* return null if the retentionFlag is set to false.
*/
public String getLastExecutionOutput() {
return (this.lastExecutionOutputString);
}
private void init() throws ExecException {
try {
mOutFile = File.createTempFile("stdout", null);
mOutFile.deleteOnExit();
mErrFile = File.createTempFile("stderr", null);
mErrFile.deleteOnExit();
}
catch (IllegalArgumentException iae) {
deleteTempFiles();
throw new ExecException("Internal error (util.ProcessExecutor.init()): " + iae.getMessage());
}
catch (IOException ioe) {
deleteTempFiles();
throw new ExecException(cannotCreateTempFiles());
}
}
private final static String cannotCreateTempFiles() {
return "Could not create temporary files - check "
+ System.getProperty("java.io.tmpdir")
+ " to see if its writeable and not-full";
}
private void deleteTempFiles() {
if (mOutStream != null) {
try {
mOutStream.flush();
mOutStream.close();
}
catch (IOException ioe) {
// Ignore
}
}
if (mErrStream != null) {
try {
mErrStream.flush();
mErrStream.close();
}
catch (IOException ioe) {
// Ignore
}
}
if (mOutFile != null)
delete(mOutFile);
if (mErrFile != null)
delete(mErrFile);
}
public void execute() throws ExecException {
execute(false);
}
/*
* Executes the command. Redirects the standard output and error streams
* safely to files. This makes the subprocess NOT block or wait on buffers
* getting flushed. This is done in a threaded manner. Note that the
* subprocess will be killed if it does not end in given timeout.
*
* @throws ExecException if anything goes wrong in subprocess, or subprocess
* terminates abruptly.
*/
public String[] execute(boolean bReturnOutputLines) throws ExecException {
return execute(bReturnOutputLines, true);
}
/**
* Allows a subclass to control the error message returned when a non-zero
* exit code is returned from a failed execution
*
* @return
*/
protected String getExceptionMessage() {
/*
* read the error message from error file
*/
String errorMessage = getFileBuffer(mErrFile);
if (errorMessage.length() == 0) {
errorMessage = "The Process Output: " + getLatestOutput(mOutFile);
}
return "abnormal subprocess termination: Detailed Message:" + errorMessage;
}
/*
* Executes the command. Redirects the standard output and error streams
* safely to files. This makes the subprocess NOT block or wait on buffers
* getting flushed. This is done in a threaded manner. Note that the
* subprocess will be killed if it does not end in given timeout.
*
* @throws ExecException if anything goes wrong in subprocess, or subprocess
* terminates abruptly.
*/
public String[] execute(boolean bReturnOutputLines, boolean bStartUpTimeLimit) throws ExecException {
init();
InputStream inputStream = null;
try {
if (bDebug) {
System.out.println("\n**** Executing command:");
for (int ii = 0; ii < mCmdStrings.length; ii++) {
System.out.println(mCmdStrings[ii]);
}
}
mSubProcess = Runtime.getRuntime().exec(mCmdStrings, mEnv, mWorkingDir);
if (mInputLines != null)
addInputLinesToProcessInput(mSubProcess);
if (!bReturnOutputLines)
mOutStream = redirectProcessOutput(mSubProcess);
else
inputStream = mSubProcess.getInputStream(); //attach to input stream for later reading
mErrStream = redirectProcessError(mSubProcess);
// see if process should startup in a limited ammount of time
// processes used by ProcessManager don't return
if (bStartUpTimeLimit) {
long timeBefore = System.currentTimeMillis();
boolean timeoutReached = false;
boolean isSubProcessFinished = false;
boolean shouldBeDone = false;
while (!shouldBeDone) {
sleep(kSleepTime);
long timeAfter = System.currentTimeMillis();
timeoutReached = (timeAfter - timeBefore) >= mTimeoutMilliseconds;
try {
mExitValue = mSubProcess.exitValue();
isSubProcessFinished = true;
}
catch (IllegalThreadStateException itse) {
isSubProcessFinished = false;
//ignore exception
}
shouldBeDone = timeoutReached || isSubProcessFinished;
}
if (!isSubProcessFinished) {
mSubProcess.destroy();
mExitValue = -255;
throw new ExecException("Subprocess timed out after " + mTimeoutMilliseconds + "mS");
}
else {
mExitValue = mSubProcess.exitValue();
if (debug()) {
System.out.println("Subprocess command line = " + a2s(mCmdStrings));
System.out.println("Subprocess exit value = " + mExitValue);
}
if (mExitValue != 0) {
mExitValue = mSubProcess.exitValue();
if (mExitValue != 0) {
throw new ExecException(getExceptionMessage());
}
}
}
}
}
catch (SecurityException se) {
throw new ExecException(se.getMessage());
}
catch (IOException ioe) {
throw new ExecException(ioe.getMessage());
}
finally {
// retain buffers before deleting them
retainBuffers();
// only delete files if the time is limited
// for processes that don't return, the temp files will remain
if (bStartUpTimeLimit) {
deleteTempFiles();
}
}
if (bReturnOutputLines) {
return getInputStrings(inputStream);
}
else {
return null;
}
}
/**
* Get the exit value of the process executed. If this method is called
* before process execution is complete (i.e. before execute() method has
* returned, it will return -1. If sub process is terminated at timeout, the
* method will return -255
*/
public int getProcessExitValue() {
return mExitValue;
}
private void addInputLinesToProcessInput(Process subProcess) throws ExecException {
if (mInputLines == null)
return;
PrintWriter out = null;
try {
out = new PrintWriter(new BufferedWriter(
new OutputStreamWriter(subProcess.getOutputStream())));
for (int i = 0; i < mInputLines.length; i++) {
if (bDebug) {
System.out.println("InputLine ->" + mInputLines[i] + "<-");
}
out.println(mInputLines[i]);
}
out.flush();
}
catch (Exception e) {
throw new ExecException(e.getMessage());
}
finally {
try {
out.close();
}
catch (Throwable t) {
}
}
}
private String[] getInputStrings(InputStream inputStream) throws ExecException {
if (inputStream == null)
return null;
BufferedReader in = null;
ArrayList list = new ArrayList();
String str;
try {
in = new BufferedReader(new InputStreamReader(inputStream));
while ((str = in.readLine()) != null)
list.add(str);
if (list.size() < 1)
return null;
return (String[]) list.toArray(new String[list.size()]);
}
catch (Exception e) {
throw new ExecException(e.getMessage());
}
finally {
try {
in.close();
}
catch (Throwable t) {
}
}
}
private OutputStream redirectProcessOutput(Process subProcess) throws ExecException {
OutputStream out = null;
try {
InputStream in = subProcess.getInputStream();
// Redirect stderr for verbose mode
if (mVerboseMode) {
// send output to stderr
out = System.err;
}
else {
// send to temp file
out = new FileOutputStream(mOutFile);
}
new FlusherThread(in, out).start();
}
catch (Exception e) {
throw new ExecException(e.getMessage());
}
return out;
}
private OutputStream redirectProcessError(Process subProcess) throws ExecException {
OutputStream out = null;
try {
InputStream in = subProcess.getErrorStream();
// Redirect stderr for verbose mode
if (mVerboseMode) {
// send output to stderr
out = System.err;
}
else {
// send to temp file
out = new FileOutputStream(mErrFile);
}
new FlusherThread(in, out).start();
}
catch (Exception e) {
throw new ExecException(e.getMessage());
}
return out;
}
public void setVerbose(boolean verbose) {
mVerboseMode = verbose;
}
private void sleep(long millis) {
try {
Thread.sleep(millis);
}
catch (InterruptedException ie) {
//ignore exception
}
}
/**
* Returns the contents of a file as a String. It never returns a null. If
* the file is empty, an empty string is returned.
*
* @param file the file to read
*/
protected String getFileBuffer(File file) {
final StringBuffer sb = new StringBuffer();
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(file));
String line = null;
while ((line = reader.readLine()) != null) {
sb.append(line);
sb.append(NEWLINE);
}
}
catch (Exception e) {
//squelch the exception
}
finally {
try {
reader.close();
}
catch (Exception e) {
}
}
return (sb.toString());
}
protected String getLatestOutput(final File f) {
return (new RAFileReader(f).readLastBytesAsString());
}
public void retainBuffers() {
if (this.retainExecutionLogs) {
this.lastExecutionErrorString = this.getLatestOutput(this.mErrFile);
this.lastExecutionOutputString = this.getLatestOutput(this.mOutFile);
}
}
private boolean debug() {
final String td = System.getProperty("java.io.tmpdir");
final String n = "as_debug_process_executor"; // a debug hook
final File f = new File(td, n);
return (f.exists());
}
private String a2s(String[] a) {
final StringBuffer s = new StringBuffer();
if (a != null) {
for (int i = 0; i < a.length; i++) {
s.append(a[i]);
s.append(" ");
}
}
return (s.toString());
}
/*
* bnevins, April 2012
* I added this method to solve a FindBugs low-level issue about ignoring the
* return value.
*/
private static void delete(File f) {
if(f != null && !f.delete()) {
f.deleteOnExit();
}
}
private static class RAFileReader {
final File file;
final static int LAST_BYTES = 16384;
final static String RMODE = "r"; //read
RAFileReader(final File file) {
this.file = file;
}
String readLastBytesAsString() {
final int n = getNumberOfBytes(LAST_BYTES);
final StringBuffer sb = new StringBuffer();
final long ln = file.length(); //if SecurityManager is not present, this is safe.
if (ln == 0)
return (sb.toString()); //nothing to read, file may not exist, is protected, is a directory etc.
assert (n <= ln) : ("Asked to read number of bytes more than size of file");
final long s = ln - n;
return (readWithoutCheck(s));
}
private String readWithoutCheck(final long seekPos) {
final StringBuffer sb = new StringBuffer();
RandomAccessFile rf = null;
int lines = 0;
try {
rf = new RandomAccessFile(file, RMODE);
rf.seek(seekPos);
String tmp = rf.readLine();
while (tmp != null) {
lines++;
sb.append(tmp);
//sb.append(Character.LINE_SEPARATOR);
sb.append('\n'); // adding a newline character is going to add one extra byte
tmp = rf.readLine();
}
}
catch (Exception e) {
//e.printStackTrace(); //ignore
}
finally {
try {
if (rf != null)
rf.close();
}
catch (Exception e) {
}//ignore;
}
//System.out.println("ln-seekPos = " + (ln - seekPos) );
//System.out.println("bytes = " + sb.toString().getBytes().length);
//System.out.println("lines = " + lines);
//assert ((ln - seekPos) == (sb.toString().getBytes().length + lines)) : "Wrong number of bytes read";
return (sb.toString());
}
private int getNumberOfBytes(final int max) {
final long ln = file.length();
return (max >= ln ? (int) ln : max);
}
}
// used for ProcessManager to watchdog subProcess
public Process getSubProcess() {
return mSubProcess;
}
public static void main(String args[]) {
testProcessError();
}
/*
* This method tests the condition of process throwing an error. On Unixlike
* systems this throws an error in error file. On non-unixlike Systems it
* will throw IOException for CreateProcess, which is desired
*/
private static void testProcessError() {
ProcessExecutor executor = new ProcessExecutor(
new String[]{"/usr/bin/ls", "-wrongPARAMS123"});
try {
executor.execute();
}
catch (ExecException ee) {
System.out.println(ee.getMessage());
}
}
}
/**
* inner class to flush runtime.exec process so it doesn't hang
*/
class FlusherThread extends Thread {
InputStream mInStream = null;
OutputStream mOutStream = null;
public static final int kSize = 1024;
FlusherThread(InputStream in, OutputStream out) {
mInStream = in;
mOutStream = out;
}
@Override
public void run() {
// check for null stream
if (mInStream == null)
return;
// transfer bytes from input to output stream
try {
int byteCnt = 0;
byte[] buffer = new byte[4096];
while ((byteCnt = mInStream.read(buffer)) != -1) {
if (mOutStream != null && byteCnt > 0) {
mOutStream.write(buffer, 0, byteCnt);
mOutStream.flush();
}
Thread.yield();
}
}
catch (IOException e) {
// ignore
}
finally {
try {
mOutStream.close();
}
catch (IOException ioe) {
// ignore
}
}
}
}