diff --git a/common/source/java/ch/systemsx/cisd/common/process/ProcessExecutionHelper.java b/common/source/java/ch/systemsx/cisd/common/process/ProcessExecutionHelper.java index 631e12f5859b9d3057b19171ba15cd8466d217a6..7ebf724961587731baecdbc8616aea6cc8b8c979 100644 --- a/common/source/java/ch/systemsx/cisd/common/process/ProcessExecutionHelper.java +++ b/common/source/java/ch/systemsx/cisd/common/process/ProcessExecutionHelper.java @@ -17,9 +17,12 @@ package ch.systemsx.cisd.common.process; import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -32,10 +35,10 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; +import ch.systemsx.cisd.base.exceptions.CheckedExceptionTunnel; import ch.systemsx.cisd.base.exceptions.InterruptedExceptionUnchecked; import ch.systemsx.cisd.base.namedthread.NamedCallable; import ch.systemsx.cisd.base.namedthread.NamingThreadPoolExecutor; -import ch.systemsx.cisd.base.utilities.OSUtilities; import ch.systemsx.cisd.common.concurrent.ConcurrencyUtilities; import ch.systemsx.cisd.common.concurrent.ExecutionResult; import ch.systemsx.cisd.common.concurrent.ExecutionStatus; @@ -49,6 +52,8 @@ import ch.systemsx.cisd.common.utilities.ITerminable; public final class ProcessExecutionHelper { + private static final int BUFFER_SIZE = 4096; + /** * Strategy on whether to read the process output or not. */ @@ -67,6 +72,19 @@ public final class ProcessExecutionHelper ALWAYS; } + /** + * Role for handling the process I/O. + */ + public interface IProcessIOHandler + { + /** + * Method that gets the process' <code>stdin</code>, <code>stdout</code> and + * <code>stderr</code> and is expected to handlt the I/O of the process. + */ + public void handle(OutputStream stdin, InputStream stdout, InputStream stderr) + throws IOException; + } + /** * Record to store process and its output. */ @@ -74,12 +92,26 @@ public final class ProcessExecutionHelper { private final Process process; - private final List<String> processOutput; + private final List<String> processTextOutput; + + private final List<String> processErrorOutput; + + private final ByteArrayOutputStream processBinaryOutput; ProcessRecord(final Process process) { this.process = process; - this.processOutput = Collections.synchronizedList(new ArrayList<String>()); + if (binaryOutput) + { + this.processTextOutput = null; + this.processErrorOutput = new ArrayList<String>(); + this.processBinaryOutput = new ByteArrayOutputStream(); + } else + { + this.processTextOutput = new ArrayList<String>(); + this.processErrorOutput = null; + this.processBinaryOutput = null; + } } Process getProcess() @@ -87,9 +119,19 @@ public final class ProcessExecutionHelper return process; } - List<String> getProcessOutput() + List<String> getTextProcessOutput() { - return processOutput; + return processTextOutput; + } + + ByteArrayOutputStream getBinaryProcessOutput() + { + return processBinaryOutput; + } + + List<String> getErrorProcessOutput() + { + return processErrorOutput; } } @@ -123,6 +165,10 @@ public final class ProcessExecutionHelper private final OutputReadingStrategy outputReadingStrategy; + private final boolean binaryOutput; + + private final IProcessIOHandler processIOHandlerOrNull; + /** The number used in thread names to distinguish the process. */ private final int processNumber; @@ -165,7 +211,8 @@ public final class ProcessExecutionHelper final Logger machineLog, boolean stopOnInterrupt) throws InterruptedExceptionUnchecked { return new ProcessExecutionHelper(cmd, ConcurrencyUtilities.NO_TIMEOUT, - DEFAULT_OUTPUT_READING_STRATEGY, operationLog, machineLog).run(stopOnInterrupt); + DEFAULT_OUTPUT_READING_STRATEGY, null, false, operationLog, machineLog) + .run(stopOnInterrupt); } /** @@ -241,7 +288,8 @@ public final class ProcessExecutionHelper final long millisToWaitForCompletion) throws InterruptedExceptionUnchecked { return new ProcessExecutionHelper(cmd, millisToWaitForCompletion, - DEFAULT_OUTPUT_READING_STRATEGY, operationLog, machineLog).run(stopOnInterrupt); + DEFAULT_OUTPUT_READING_STRATEGY, null, false, operationLog, machineLog) + .run(stopOnInterrupt); } /** @@ -309,7 +357,65 @@ public final class ProcessExecutionHelper throws InterruptedExceptionUnchecked { return new ProcessExecutionHelper(cmd, millisToWaitForCompletion, outputReadingStrategy, - operationLog, machineLog).run(stopOnInterrupt); + null, false, operationLog, machineLog).run(stopOnInterrupt); + } + + /** + * Runs an Operating System process, specified by <var>cmd</var>. + * + * @param cmd The command line to run. + * @param operationLog The {@link Logger} to use for all message on the higher level. + * @param machineLog The {@link Logger} to use for all message on the lower (machine) level. + * @param millisToWaitForCompletion The time to wait for the process to complete in milli + * seconds. If the process is not finished after that time, it will be terminated by + * a watch dog. + * @param outputReadingStrategy The strategy for when to read the output (both + * <code>stdout</code> and <code>sterr</code>) of the process. + * @param binaryOutput If <code>true</code>, the process is expected to produce binary output on + * <code>stdout</code>. + * @param stopOnInterrupt If <code>true</code>, throw a {@link InterruptedExceptionUnchecked} if + * the thread gets interrupted while waiting on the future. + * @return The process result. + * @throws InterruptedExceptionUnchecked If the thread got interrupted and + * <var>stopOnInterrupt</var> is <code>true</code>. + */ + public static ProcessResult run(final List<String> cmd, final Logger operationLog, + final Logger machineLog, final long millisToWaitForCompletion, + final OutputReadingStrategy outputReadingStrategy, final boolean binaryOutput, + final boolean stopOnInterrupt) throws InterruptedExceptionUnchecked + { + return new ProcessExecutionHelper(cmd, millisToWaitForCompletion, outputReadingStrategy, + null, binaryOutput, operationLog, machineLog).run(stopOnInterrupt); + } + + /** + * Runs an Operating System process, specified by <var>cmd</var>. + * + * @param cmd The command line to run. + * @param operationLog The {@link Logger} to use for all message on the higher level. + * @param machineLog The {@link Logger} to use for all message on the lower (machine) level. + * @param millisToWaitForCompletion The time to wait for the process to complete in milli + * seconds. If the process is not finished after that time, it will be terminated by + * a watch dog. + * @param outputReadingStrategy The strategy for when to read the output (both + * <code>stdout</code> and <code>sterr</code>) of the process. + * @param processIOHandler The handler in charge of dealing with the process input and output. + * Beware that if the process reaches the I/O buffer limit and this handler doesn't + * read the output, the process may hang indefinitely. + * @param stopOnInterrupt If <code>true</code>, throw a {@link InterruptedExceptionUnchecked} if + * the thread gets interrupted while waiting on the future. + * @return The process result. + * @throws InterruptedExceptionUnchecked If the thread got interrupted and + * <var>stopOnInterrupt</var> is <code>true</code>. + */ + public static ProcessResult run(final List<String> cmd, final Logger operationLog, + final Logger machineLog, final long millisToWaitForCompletion, + final OutputReadingStrategy outputReadingStrategy, + final IProcessIOHandler processIOHandler, final boolean stopOnInterrupt) + throws InterruptedExceptionUnchecked + { + return new ProcessExecutionHelper(cmd, millisToWaitForCompletion, outputReadingStrategy, + processIOHandler, false, operationLog, machineLog).run(stopOnInterrupt); } /** Handler to a running process. Allows to wait for the result and stop the process. */ @@ -346,7 +452,7 @@ public final class ProcessExecutionHelper final Logger machineLog) { return new ProcessExecutionHelper(cmd, ConcurrencyUtilities.NO_TIMEOUT, - outputReadingStrategy, operationLog, machineLog).runUnblocking(); + outputReadingStrategy, null, false, operationLog, machineLog).runUnblocking(); } /** @@ -380,6 +486,67 @@ public final class ProcessExecutionHelper } } + /** + * Reads the <code>stdout</code> and <code>stderr</code> of <var>process</var>. + */ + private final void readProcessOutput(final ProcessRecord processRecord, final boolean discard) + { + readProcessOutput(processRecord, -1, discard); + } + + /** + * Reads the <code>stdout</code> and <code>stderr</code> of <var>process</var>. If + * <code>maxBytes > 0</code>, read not more than so many bytes. + */ + private final void readProcessOutput(final ProcessRecord processRecord, final long maxBytes, + final boolean discard) + { + assert processRecord != null; + assert machineLog != null; + + final Process process = processRecord.getProcess(); + if (binaryOutput) + { + try + { + copy(process, processRecord, maxBytes, discard); + } catch (final IOException e) + { + machineLog.warn(String.format("IOException when reading stdout/stderr, msg='%s'.", + e.getMessage())); + } + final List<String> errorOutput = processRecord.getErrorProcessOutput(); + final BufferedReader errorReader = + new BufferedReader(new InputStreamReader(process.getErrorStream())); + readProcessOutputLines(errorOutput, errorReader, discard); + } else + { + final List<String> processOutput = processRecord.getTextProcessOutput(); + final BufferedReader reader = + new BufferedReader(new InputStreamReader(process.getInputStream())); + readProcessOutputLines(processOutput, reader, discard); + } + } + + protected void copy(final Process process, final ProcessRecord processRecord, + final long maxBytes, final boolean discard) throws IOException + { + final InputStream input = process.getInputStream(); + final OutputStream output = processRecord.getBinaryProcessOutput(); + final byte[] buffer = new byte[BUFFER_SIZE]; + long count = 0; + int n = 0; + while ((maxBytes <= 0 || count < maxBytes) && input.available() > 0 + && -1 != (n = input.read(buffer))) + { + if (discard == false) + { + output.write(buffer, 0, n); + } + count += n; + } + } + /** * Returns the <code>stdout</code> and <code>stderr</code> of the <var>process</var> in * <var>processRecord.getProcessOutput()</var>. @@ -413,13 +580,11 @@ public final class ProcessExecutionHelper } /** - * On Windows it is necessary to read the output stream during the process is running in order - * not to get dead-locked. This is less efficient (due to the limited interface of - * {@link Process}), so we don't do it if we don't have to. + * Returns <code>true</code>, if an I/O handler for the process I/O is available. */ - private boolean hasLimitedOutputBuffer() + private boolean hasIOHandler() { - return OSUtilities.isWindows(); + return processIOHandlerOrNull != null; } /** @@ -432,7 +597,10 @@ public final class ProcessExecutionHelper private final Process launch() throws IOException { final ProcessBuilder processBuilder = new ProcessBuilder(commandLine); - processBuilder.redirectErrorStream(true); + if (binaryOutput == false && processIOHandlerOrNull == null) + { + processBuilder.redirectErrorStream(true); + } if (operationLog.isDebugEnabled()) { operationLog.debug("Running command: " + getCommand(commandLine)); @@ -453,18 +621,15 @@ public final class ProcessExecutionHelper { ProcessRecord processRecord = new ProcessRecord(process); processWrapper.set(processRecord); - final List<String> processOutput = processRecord.getProcessOutput(); - final BufferedReader reader = - new BufferedReader(new InputStreamReader(process.getInputStream())); int exitValue = ProcessResult.NO_EXIT_VALUE; - if (hasLimitedOutputBuffer()) + if (hasIOHandler() == false) { final boolean discardOutput = (outputReadingStrategy == OutputReadingStrategy.NEVER); while (exitValue == ProcessResult.NO_EXIT_VALUE) { - readProcessOutputLines(processOutput, reader, discardOutput); + readProcessOutput(processRecord, discardOutput); exitValue = getExitValue(process); if (exitValue == ProcessResult.NO_EXIT_VALUE) { @@ -472,23 +637,67 @@ public final class ProcessExecutionHelper } } processWrapper.set(null); - readProcessOutputLines(processOutput, reader, discardOutput); + readProcessOutput(processRecord, discardOutput); + if (binaryOutput) + { + return new ProcessResult(commandLine, processNumber, + ExecutionStatus.COMPLETE, "", exitValue, processRecord + .getBinaryProcessOutput().toByteArray(), + processRecord.getErrorProcessOutput(), operationLog, machineLog); + } else + { + return new ProcessResult(commandLine, processNumber, + ExecutionStatus.COMPLETE, "", exitValue, + processRecord.getTextProcessOutput(), operationLog, machineLog); + } } else { + final Future<?> future = executor.submit(new Runnable() + { + public void run() + { + try + { + processIOHandlerOrNull.handle(process.getOutputStream(), + process.getInputStream(), process.getErrorStream()); + } catch (IOException ex) + { + throw CheckedExceptionTunnel.wrapIfNecessary(ex); + } + } + }); exitValue = process.waitFor(); - final boolean stillInCharge = (processWrapper.getAndSet(null) != null); - - if (stillInCharge - && (OutputReadingStrategy.ALWAYS.equals(outputReadingStrategy) || (OutputReadingStrategy.ON_ERROR - .equals(outputReadingStrategy) && ProcessResult - .isProcessOK(exitValue) == false))) + processWrapper.set(null); + final ExecutionResult<?> result = + ConcurrencyUtilities.getResult(future, SHORT_TIMEOUT); + switch (result.getStatus()) { - readProcessOutputLines(processOutput, reader, false); + case COMPLETE: + break; + case EXCEPTION: + final Throwable th = result.tryGetException(); + final Throwable cause = + (th == null) ? new RuntimeException("Unknown exception.") + : (th instanceof Error) ? (Error) th + : CheckedExceptionTunnel + .unwrapIfNecessary((Exception) th); + machineLog + .warn(String + .format("Exception when reading stdout/stderr, type='%s', msg='%s'.", + cause.getClass().getSimpleName(), + cause.getMessage())); + break; + case INTERRUPTED: + machineLog.warn("Interrupted when reading stdout/stderr."); + break; + case TIMED_OUT: + machineLog.warn("Timeout when reading stdout/stderr."); + break; } + return new ProcessResult(commandLine, processNumber, + ExecutionStatus.COMPLETE, "", exitValue, null, operationLog, + machineLog); } - return new ProcessResult(commandLine, processNumber, ExecutionStatus.COMPLETE, - "", exitValue, processRecord.getProcessOutput(), operationLog, - machineLog); } finally { IOUtils.closeQuietly(process.getErrorStream()); @@ -536,22 +745,22 @@ public final class ProcessExecutionHelper if (processRecord != null) { final Process process = processRecord.getProcess(); - if (hasLimitedOutputBuffer() == false - && OutputReadingStrategy.NEVER.equals(outputReadingStrategy) == false) - { - final List<String> processOutput = processRecord.getProcessOutput(); - final BufferedReader reader = - new BufferedReader(new InputStreamReader(process.getInputStream())); - readProcessOutputLines(processOutput, reader, false); - } process.destroy(); // Note: this also closes the I/O streams. if (machineLog.isInfoEnabled()) { machineLog.info(String.format("Killed '%s'.", getCommand(commandLine))); } final int exitValue = getExitValue(processRecord.getProcess()); - return new ProcessResult(commandLine, processNumber, status, "", exitValue, - processRecord.getProcessOutput(), operationLog, machineLog); + if (binaryOutput) + { + return new ProcessResult(commandLine, processNumber, status, "", exitValue, + processRecord.getBinaryProcessOutput().toByteArray(), + processRecord.getErrorProcessOutput(), operationLog, machineLog); + } else + { + return new ProcessResult(commandLine, processNumber, status, "", exitValue, + processRecord.getTextProcessOutput(), operationLog, machineLog); + } } else { return null; // Value signals that the ProcessRunner got us. @@ -567,8 +776,9 @@ public final class ProcessExecutionHelper private ProcessExecutionHelper(final List<String> commandLine, final long millisToWaitForCompletion, - final OutputReadingStrategy outputReadingStrategy, final Logger operationLog, - final Logger machineLog) + final OutputReadingStrategy outputReadingStrategy, + final IProcessIOHandler processIOHandlerOrNull, final boolean binaryOutput, + final Logger operationLog, final Logger machineLog) { this.processNumber = processCounter.getAndIncrement(); this.callingThreadName = Thread.currentThread().getName(); @@ -583,6 +793,8 @@ public final class ProcessExecutionHelper this.millisToWaitForCompletion = millisToWaitForCompletion; } this.outputReadingStrategy = outputReadingStrategy; + this.processIOHandlerOrNull = processIOHandlerOrNull; + this.binaryOutput = binaryOutput; this.commandLine = Collections.unmodifiableList(commandLine); this.processWrapper = new AtomicReference<ProcessRecord>(); } @@ -656,9 +868,17 @@ public final class ProcessExecutionHelper // see the note above about termination from other thread status = ExecutionStatus.INTERRUPTED; } - return new ProcessResult(commandLine, processNumber, status, - tryGetStartupFailureMessage(executionResult.tryGetException()), - ProcessResult.NO_EXIT_VALUE, null, operationLog, machineLog); + if (binaryOutput) + { + return new ProcessResult(commandLine, processNumber, status, + tryGetStartupFailureMessage(executionResult.tryGetException()), + ProcessResult.NO_EXIT_VALUE, null, null, operationLog, machineLog); + } else + { + return new ProcessResult(commandLine, processNumber, status, + tryGetStartupFailureMessage(executionResult.tryGetException()), + ProcessResult.NO_EXIT_VALUE, null, operationLog, machineLog); + } } } diff --git a/common/source/java/ch/systemsx/cisd/common/process/ProcessResult.java b/common/source/java/ch/systemsx/cisd/common/process/ProcessResult.java index 4d8adb63b3f22b90ef948c0c2ae330f4e4edd9b3..604e8249799730495a25caebb0ae10f4beeae056 100644 --- a/common/source/java/ch/systemsx/cisd/common/process/ProcessResult.java +++ b/common/source/java/ch/systemsx/cisd/common/process/ProcessResult.java @@ -76,6 +76,12 @@ public final class ProcessResult private final List<String> output; + private final byte[] binaryOutput; + + private final List<String> errorOutput; + + private final boolean isBinaryOutput; + /** * Returns <code>true</code> if the <var>exitValue</var> indicates that the process has been * terminated on the Operating System level. @@ -108,6 +114,7 @@ public final class ProcessResult this.startupFailureMessage = (startupFailureMessageOrNull == null) ? "" : startupFailureMessageOrNull; this.exitValue = exitValue; + this.isBinaryOutput = false; this.outputAvailable = (processOutputOrNull != null); if (outputAvailable) { @@ -118,6 +125,41 @@ public final class ProcessResult this.output = Collections.emptyList(); } + this.errorOutput = null; + this.binaryOutput = null; + this.operationLog = operationLog; + this.machineLog = machineLog; + } + + public ProcessResult(final List<String> commandLine, final int processNumber, + final ExecutionStatus status, final String startupFailureMessageOrNull, + final int exitValue, final byte[] processBinaryOutputOrNull, + final List<String> processErrorOutputOrNull, final Logger operationLog, + final Logger machineLog) + { + this.commandLine = commandLine; + this.commandName = ProcessExecutionHelper.getCommandName(commandLine); + this.processNumber = processNumber; + this.status = status; + this.startupFailureMessage = + (startupFailureMessageOrNull == null) ? "" : startupFailureMessageOrNull; + this.exitValue = exitValue; + this.isBinaryOutput = true; + this.outputAvailable = (processBinaryOutputOrNull != null); + if (outputAvailable) + { + this.errorOutput = + (processErrorOutputOrNull == null) ? Collections.<String> emptyList() + : Collections.unmodifiableList(processErrorOutputOrNull); + this.binaryOutput = processBinaryOutputOrNull; + + } else + { + this.errorOutput = Collections.emptyList(); + binaryOutput = new byte[0]; + + } + this.output = null; this.operationLog = operationLog; this.machineLog = machineLog; } @@ -156,8 +198,49 @@ public final class ProcessResult } /** - * Returns the output of the process (<code>stdout</code> and <code>stderr</code>). If it not - * available (see {@link #isOutputAvailable()}, an empty list is returned. + * Returns <code>true</code>, if the output of the method is binary and <code>false</code>, if + * it is text. If the output is binary, the error output is available separately, otherwise it + * is merged with the regular output. + * <p> + * If this method returns <code>true</code>, you are supposed to call {@link #getBinaryOutput()} + * and {@link #getErrorOutput()}. If it is <code>false</code>, you are supposed to call + * {@link #getOutput()}. + */ + public boolean isBinaryOutput() + { + return isBinaryOutput; + } + + /** + * Returns the binary output of the process (<code>stdout</code>). If it not available (see + * {@link #isOutputAvailable()}, an empty array is returned. + * <p> + * <i>Only call this method, if {@link #isBinaryOutput()} is <code>true</code>. Otherwise this + * method will return <code>null</code></i>. + */ + public byte[] getBinaryOutput() + { + return binaryOutput; + } + + /** + * Returns the text error output of the process (<code>stderr</code>). If it not available (see + * {@link #isOutputAvailable()}, an empty array is returned. + * <p> + * <i>Only call this method, if {@link #isBinaryOutput()} is <code>true</code>. Otherwise this + * method will return <code>null</code></i>. + */ + public List<String> getErrorOutput() + { + return errorOutput; + } + + /** + * Returns the text output of the process (<code>stdout</code> and <code>stderr</code>). If it + * not available (see {@link #isOutputAvailable()}, an empty list is returned. + * <p> + * <i>Only call this method, if {@link #isBinaryOutput()} is <code>false</code>. Otherwise this + * method will return <code>null</code></i>. */ public List<String> getOutput() { @@ -255,16 +338,16 @@ public final class ProcessResult processNumber, commandName, startupFailureMessage)); } else if (isTimedOut()) { - operationLog.log(logLevel, String.format("P%d-{%s} process has timed out.", - processNumber, commandName)); + operationLog.log(logLevel, + String.format("P%d-{%s} process has timed out.", processNumber, commandName)); } else if (isInterruped()) { - operationLog.log(logLevel, String.format("P%d-{%s} thread was interrupted.", - processNumber, commandName)); + operationLog.log(logLevel, + String.format("P%d-{%s} thread was interrupted.", processNumber, commandName)); } else if (isTerminated()) { - operationLog.log(logLevel, String.format("P%d-{%s} process was terminated.", - processNumber, commandName)); + operationLog.log(logLevel, + String.format("P%d-{%s} process was terminated.", processNumber, commandName)); } else { operationLog.log(logLevel, String.format( @@ -277,17 +360,40 @@ public final class ProcessResult { assert logLevel != null; - final List<String> processOutputLines = getOutput(); - if (processOutputLines.size() == 0) + if (isBinaryOutput) { - return; - } - machineLog.log(logLevel, String.format("[%s] output:", commandName)); - for (final String ln : processOutputLines) + if (getBinaryOutput().length != 0) + { + machineLog.log(logLevel, String.format("[%s] output: %d bytes", commandName, + getBinaryOutput().length)); + } + final List<String> processErrorOutputLines = getErrorOutput(); + if (processErrorOutputLines.size() > 0) + { + machineLog.log(logLevel, String.format("[%s] error output:", commandName)); + for (final String ln : processErrorOutputLines) + { + if (ln.trim().length() > 0) + { + machineLog.log(logLevel, String.format("\"%s\"", ln)); + } + } + } + + } else { - if (ln.trim().length() > 0) + final List<String> processOutputLines = getOutput(); + if (processOutputLines.size() == 0) + { + return; + } + machineLog.log(logLevel, String.format("[%s] output:", commandName)); + for (final String ln : processOutputLines) { - machineLog.log(logLevel, String.format("\"%s\"", ln)); + if (ln.trim().length() > 0) + { + machineLog.log(logLevel, String.format("\"%s\"", ln)); + } } } } diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/process/ProcessExecutionHelperTest.java b/common/sourceTest/java/ch/systemsx/cisd/common/process/ProcessExecutionHelperTest.java index ddb10a18313af0089b6bb0cf97765042b60b165e..7c6e726348453577da280d186d6ea17e9ca47a4f 100644 --- a/common/sourceTest/java/ch/systemsx/cisd/common/process/ProcessExecutionHelperTest.java +++ b/common/sourceTest/java/ch/systemsx/cisd/common/process/ProcessExecutionHelperTest.java @@ -98,6 +98,12 @@ public class ProcessExecutionHelperTest "n=1; while [ 1 ]; do echo $n; n=$(($n+1)); done"); } + private File createExecutableEndlessDevURandom(String name) throws IOException, + InterruptedException + { + return createExecutable(name, "#! /bin/sh", "cat < /dev/urandom"); + } + @BeforeClass public void init() { @@ -294,7 +300,8 @@ public class ProcessExecutionHelperTest operationLog, machineLog, WATCHDOG_WAIT_MILLIS); assertTrue(result.isTimedOut()); assertFalse(result.isOK()); - assertTrue(Integer.toString(result.getOutput().size()), result.getOutput().size() > 100); + assertTrue(Integer.toString(result.getOutput().size()), + result.getOutput().size() > 100); assertEquals(Integer.toString(result.getOutput().size()), result.getOutput().get(result.getOutput().size() - 1)); } @@ -308,8 +315,8 @@ public class ProcessExecutionHelperTest final String stderr1 = "This goes to stderr, 1"; final String stderr2 = "This goes to stderr, 2"; final File dummyExec = - createExecutable("dummy.sh", "echo " + stdout1, "echo " + stderr1, "echo " - + stdout2, "echo " + stderr2); + createExecutable("dummy.sh", "echo " + stdout1, "echo " + stderr1 + + " > /dev/stderr", "echo " + stdout2, "echo " + stderr2 + " > /dev/stderr"); final ProcessResult result = ProcessExecutionHelper.run(Arrays.asList(dummyExec.getAbsolutePath()), operationLog, machineLog, ConcurrencyUtilities.NO_TIMEOUT, @@ -318,6 +325,7 @@ public class ProcessExecutionHelperTest assertEquals(0, exitValue); result.log(); assertTrue(result.isOutputAvailable()); + assertFalse(result.isBinaryOutput()); assertEquals(4, result.getOutput().size()); assertEquals(stdout1, result.getOutput().get(0)); assertEquals(stderr1, result.getOutput().get(1)); @@ -325,6 +333,47 @@ public class ProcessExecutionHelperTest assertEquals(stderr2, result.getOutput().get(3)); } + @Test(groups = + { "requires_unix", "slow" }) + public void testTryExecutionReadBinaryProcessOutput() throws Exception + { + final String stdout1 = "This goes to stdout, 1"; + final String stdout2 = "This goes to stdout, 2"; + final String stderr1 = "This goes to stderr, 1"; + final String stderr2 = "This goes to stderr, 2"; + final File dummyExec = + createExecutable("dummy.sh", "echo " + stdout1, "echo " + stderr1 + + " > /dev/stderr", "echo " + stdout2, "echo " + stderr2 + " > /dev/stderr"); + final ProcessResult result = + ProcessExecutionHelper.run(Arrays.asList(dummyExec.getAbsolutePath()), + operationLog, machineLog, ConcurrencyUtilities.NO_TIMEOUT, + OutputReadingStrategy.ALWAYS, true, false); + final int exitValue = result.getExitValue(); + assertEquals(0, exitValue); + result.log(); + assertTrue(result.isOutputAvailable()); + assertTrue(result.isBinaryOutput()); + assertEquals(2, result.getErrorOutput().size()); + assertEquals(stderr1, result.getErrorOutput().get(0)); + assertEquals(stderr2, result.getErrorOutput().get(1)); + final String stdout = new String(result.getBinaryOutput()); + assertEquals(stdout1 + "\n" + stdout2 + "\n", stdout); + } + + @Test(groups = + { "requires_unix", "slow" }) + public void testHangingExecLotsOfBinaryOutputOnStdOut() throws Exception + { + final File dummyExec = createExecutableEndlessDevURandom("iHangOnUrandom.sh"); + final ProcessResult result = + ProcessExecutionHelper.run(Arrays.asList(dummyExec.getAbsolutePath()), + operationLog, machineLog, WATCHDOG_WAIT_MILLIS, + OutputReadingStrategy.ON_ERROR, true, false); + assertTrue(result.isTimedOut()); + assertFalse(result.isOK()); + assertTrue(result.getBinaryOutput().length > 1000); + } + @Test(groups = "requires_unix") public void testStartupFailed() {