/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package org.apache.geode.test.process;
import static org.apache.geode.distributed.ConfigurationProperties.*;
import org.apache.geode.internal.logging.LogService;
import org.apache.logging.log4j.Logger;
import java.io.File;
import java.io.PrintStream;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import static org.junit.Assert.fail;
/**
* Wraps spawned {@link java.lang.Process} to capture output and provide interaction with the
* process.
*
* @since GemFire 4.1.1
*/
public class ProcessWrapper {
private static final Logger logger = LogService.getLogger();
private static final long PROCESS_TIMEOUT_MILLIS = 10 * 60 * 1000L; // 10 minutes
private static final long DELAY = 10;
private final boolean headless;
private final long timeoutMillis;
private final String[] jvmArguments;
private final Class<?> mainClass;
private final String[] mainArguments;
private volatile Process process;
private volatile Throwable processException;
private volatile ProcessOutputReader outputReader;
private final boolean useMainLauncher;
private final List<String> allLines;
private final BlockingQueue<String> lineBuffer;
private final AtomicInteger exitValue = new AtomicInteger(-1);
private boolean starting = false;
private boolean started = false;
private boolean stopped = false;
private boolean interrupted = false;
private Thread processThread;
private ProcessStreamReader stdout;
private ProcessStreamReader stderr;
private ProcessWrapper(final String[] jvmArguments, final Class<?> mainClass,
final String[] mainArguments, final boolean useMainLauncher, final boolean headless,
final long timeoutMillis) {
this.jvmArguments = jvmArguments;
this.mainClass = mainClass;
this.mainArguments = mainArguments;
this.useMainLauncher = useMainLauncher;
this.headless = headless;
this.timeoutMillis = timeoutMillis;
this.lineBuffer = new LinkedBlockingQueue<String>();
this.allLines = Collections.synchronizedList(new ArrayList<String>());
}
public ProcessStreamReader getStandardOutReader() {
synchronized (this.exitValue) {
return stdout;
}
}
public ProcessStreamReader getStandardErrorReader() {
synchronized (this.exitValue) {
return stderr;
}
}
private void waitForProcessStart() throws InterruptedException, TimeoutException {
final long start = System.currentTimeMillis();
boolean done = false;
while (!done) {
synchronized (this.exitValue) {
done = (this.process != null || this.processException != null)
&& (this.started || this.exitValue.get() > -1 || this.interrupted);
}
if (!done && System.currentTimeMillis() > start + timeoutMillis) {
throw new TimeoutException("Timed out launching process");
}
Thread.sleep(DELAY);
}
}
public boolean isAlive() throws InterruptedException, TimeoutException {
checkStarting();
waitForProcessStart();
synchronized (this.exitValue) {
if (this.interrupted) { // TODO: do we want to do this?
throw new InterruptedException("Process was interrupted");
}
return this.exitValue.get() == -1 && this.started && !this.stopped && !this.interrupted
&& this.processThread.isAlive();
}
}
public ProcessWrapper destroy() {
if (this.process != null) {
this.process.destroy();
}
return this;
}
public int waitFor(final long timeout, final boolean throwOnTimeout) throws InterruptedException {
checkStarting();
final Thread thread = getThread();
thread.join(timeout);
synchronized (this.exitValue) {
if (throwOnTimeout) {
checkStopped();
}
return this.exitValue.get();
}
}
public int waitFor(final long timeout) throws InterruptedException {
return waitFor(timeout, false);
}
public int waitFor(final boolean throwOnTimeout) throws InterruptedException {
return waitFor(timeoutMillis, throwOnTimeout);
}
public int waitFor() throws InterruptedException {
return waitFor(timeoutMillis, false);
}
public String getOutput() {
return getOutput(false);
}
public String getOutput(final boolean ignoreStopped) {
checkStarting();
if (!ignoreStopped) {
checkStopped();
}
final StringBuffer sb = new StringBuffer();
final Iterator<String> iterator = this.allLines.iterator();
while (iterator.hasNext()) {
sb.append(iterator.next() + "\n");
}
return sb.toString();
}
public ProcessWrapper sendInput() {
checkStarting();
sendInput("");
return this;
}
public ProcessWrapper sendInput(final String input) {
checkStarting();
final PrintStream ps = new PrintStream(this.process.getOutputStream());
ps.println(input);
ps.flush();
return this;
}
public ProcessWrapper failIfOutputMatches(final String patternString, final long timeoutMillis)
throws InterruptedException {
checkStarting();
checkOk();
final Pattern pattern = Pattern.compile(patternString);
logger.debug("failIfOutputMatches waiting for \"{}\"...", patternString);
final long start = System.currentTimeMillis();
while (System.currentTimeMillis() <= start + timeoutMillis) {
final String line = lineBuffer.poll(timeoutMillis, TimeUnit.MILLISECONDS);
if (line != null && pattern.matcher(line).matches()) {
fail("failIfOutputMatches Matched pattern \"" + patternString + "\" against output \""
+ line + "\". Output: " + this.allLines);
}
}
return this;
}
/*
* Waits for the process stdout or stderr stream to contain the specified text. Uses the specified
* timeout for debugging purposes.
*/
public ProcessWrapper waitForOutputToMatch(final String patternString, final long timeoutMillis)
throws InterruptedException {
checkStarting();
checkOk();
final Pattern pattern = Pattern.compile(patternString);
logger.debug("ProcessWrapper:waitForOutputToMatch waiting for \"{}\"...", patternString);
while (true) {
final String line = this.lineBuffer.poll(timeoutMillis, TimeUnit.MILLISECONDS);
if (line == null) {
fail("Timed out waiting for output \"" + patternString + "\" after " + timeoutMillis
+ " ms. Output: " + new OutputFormatter(this.allLines));
}
if (pattern.matcher(line).matches()) {
logger.debug(
"ProcessWrapper:waitForOutputToMatch Matched pattern \"{}\" against output \"{}\"",
patternString, line);
break;
} else {
logger.debug(
"ProcessWrapper:waitForOutputToMatch Did not match pattern \"{}\" against output \"{}\"",
patternString, line);
}
}
return this;
}
/*
* Waits for the process stdout or stderr stream to contain the specified text. Uses the default
* timeout.
*/
public ProcessWrapper waitForOutputToMatch(final String patternString)
throws InterruptedException {
return waitForOutputToMatch(patternString, timeoutMillis);
}
public ProcessWrapper execute() throws InterruptedException, TimeoutException {
return execute(null, new File(System.getProperty("user.dir")));
}
public ProcessWrapper execute(final Properties properties)
throws InterruptedException, TimeoutException {
return execute(properties, new File(System.getProperty("user.dir")));
}
public ProcessWrapper execute(final Properties properties, final File workingDirectory)
throws InterruptedException, TimeoutException {
synchronized (this.exitValue) {
if (this.starting) {
throw new IllegalStateException("ProcessWrapper can only be executed once");
}
this.starting = true;
this.processThread = new Thread(new Runnable() {
public void run() {
start(properties, workingDirectory);
}
}, "ProcessWrapper Process Thread");
}
this.processThread.start();
waitForProcessStart();
synchronized (this.exitValue) {
if (this.processException != null) {
logger.error("ProcessWrapper:execute failed with " + this.processException);
this.processException.printStackTrace();
}
}
if (this.useMainLauncher) {
sendInput(); // to trigger MainLauncher delegation to inner main
}
return this;
}
private void start(final Properties properties, final File workingDirectory) {
final List<String> jvmArgumentsList = new ArrayList<String>();
if (properties != null) {
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
if (!entry.getKey().equals(LOG_FILE)) {
jvmArgumentsList.add("-D" + entry.getKey() + "=" + entry.getValue());
}
}
}
if (this.headless) {
jvmArgumentsList.add("-Djava.awt.headless=true");
}
if (this.jvmArguments != null) {
for (String jvmArgument : this.jvmArguments) {
jvmArgumentsList.add(jvmArgument);
}
}
try {
synchronized (this.exitValue) {
final String[] command =
defineCommand(jvmArgumentsList.toArray(new String[jvmArgumentsList.size()]));
this.process = new ProcessBuilder(command).directory(workingDirectory).start();
final StringBuilder processCommand = new StringBuilder();
boolean addSpace = false;
for (String string : command) {
if (addSpace) {
processCommand.append(" ");
}
processCommand.append(string);
addSpace = true;
}
final String commandString = processCommand.toString();
logger.debug("Starting " + commandString);
final ProcessStreamReader stdOut = new ProcessStreamReader(commandString,
this.process.getInputStream(), this.lineBuffer, this.allLines);
final ProcessStreamReader stdErr = new ProcessStreamReader(commandString,
this.process.getErrorStream(), this.lineBuffer, this.allLines);
this.stdout = stdOut;
this.stderr = stdErr;
this.outputReader = new ProcessOutputReader(this.process, stdOut, stdErr);
this.started = true;
}
this.outputReader.start();
this.outputReader.waitFor(PROCESS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
int code = this.process.waitFor();
synchronized (this.exitValue) {
this.exitValue.set(code);
this.stopped = true;
}
} catch (InterruptedException e) {
synchronized (this.exitValue) {
this.interrupted = true;
this.processException = e;
}
} catch (Throwable t) {
synchronized (this.exitValue) {
this.processException = t;
}
}
}
private String[] defineCommand(final String[] jvmArguments) {
final File javaBinDir = new File(System.getProperty("java.home"), "bin");
final File javaExe = new File(javaBinDir, "java");
final List<String> argumentList = new ArrayList<String>();
argumentList.add(javaExe.getPath());
argumentList.add("-classpath");
argumentList.add(System.getProperty("java.class.path"));
// -d64 is not a valid option for windows and results in failure
final int bits = Integer.getInteger("sun.arch.data.model", 0).intValue();
if (bits == 64 && !(System.getProperty("os.name").toLowerCase().contains("windows"))) {
argumentList.add("-d64");
}
argumentList.add("-Djava.library.path=" + System.getProperty("java.library.path"));
if (jvmArguments != null) {
argumentList.addAll(Arrays.asList(jvmArguments));
}
if (this.useMainLauncher) {
argumentList.add(MainLauncher.class.getName());
}
argumentList.add(mainClass.getName());
if (mainArguments != null) {
argumentList.addAll(Arrays.asList(mainArguments));
}
final String[] command = argumentList.toArray(new String[argumentList.size()]);
return command;
}
private void checkStarting() throws IllegalStateException {
synchronized (this.exitValue) {
if (!this.starting) {
throw new IllegalStateException("Process has not been launched");
}
}
}
private void checkStopped() throws IllegalStateException {
synchronized (this.exitValue) {
if (!this.stopped) {
throw new IllegalStateException("Process has not stopped");
}
}
}
private void checkOk() throws RuntimeException {
if (this.processException != null) {
throw new RuntimeException("Failed to launch process", this.processException);
}
}
private Thread getThread() {
synchronized (this.exitValue) {
return this.processThread;
}
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder(getClass().getSimpleName());
sb.append("@").append(System.identityHashCode(this)).append("{");
sb.append(this.mainClass);
sb.append("}");
return sb.toString();
}
public static class Builder {
private String[] jvmArguments = null;
private Class<?> mainClass;
private String[] mainArguments = null;
private boolean useMainLauncher = true;
private boolean headless = true;
private long timeoutMillis = PROCESS_TIMEOUT_MILLIS;
private boolean inline = false;
public Builder() {
// nothing
}
public Builder jvmArguments(final String[] jvmArguments) {
this.jvmArguments = jvmArguments;
return this;
}
public Builder mainClass(final Class<?> mainClass) {
this.mainClass = mainClass;
return this;
}
public Builder mainArguments(final String[] mainArguments) {
this.mainArguments = mainArguments;
return this;
}
public Builder useMainLauncher(final boolean useMainLauncher) {
this.useMainLauncher = useMainLauncher;
return this;
}
public Builder headless(final boolean headless) {
this.headless = headless;
return this;
}
public Builder timeoutMillis(final long timeoutMillis) {
this.timeoutMillis = timeoutMillis;
return this;
}
public Builder inline(final boolean inline) {
this.inline = inline;
return this;
}
public ProcessWrapper build() {
return new ProcessWrapper(jvmArguments, mainClass, mainArguments, useMainLauncher, headless,
timeoutMillis);
}
}
}