| /* |
| * 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 java.util.concurrent.TimeUnit.MILLISECONDS; |
| import static org.apache.geode.distributed.ConfigurationProperties.LOG_FILE; |
| import static org.junit.Assert.fail; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.PrintStream; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.UUID; |
| import java.util.concurrent.BlockingQueue; |
| import java.util.concurrent.LinkedBlockingQueue; |
| import java.util.concurrent.TimeoutException; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.function.Consumer; |
| import java.util.jar.Attributes; |
| import java.util.jar.JarOutputStream; |
| import java.util.jar.Manifest; |
| import java.util.regex.Pattern; |
| |
| import org.apache.commons.lang3.JavaVersion; |
| import org.apache.commons.lang3.SystemUtils; |
| import org.apache.logging.log4j.Logger; |
| |
| import org.apache.geode.internal.logging.LogService; |
| import org.apache.geode.test.awaitility.GeodeAwaitility; |
| |
| /** |
| * Wraps spawned {@link Process} to capture output and provide interaction with the process. |
| * |
| * @since GemFire 4.1.1 |
| */ |
| public class ProcessWrapper implements Consumer<String> { |
| private static final Logger logger = LogService.getLogger(); |
| |
| private static final long PROCESS_TIMEOUT_MILLIS = GeodeAwaitility.getTimeout().getValueInMS(); |
| private static final long DELAY = 10; |
| |
| private final boolean headless; |
| private final long timeoutMillis; |
| private final File directory; |
| |
| 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; |
| private boolean started; |
| private boolean stopped; |
| private boolean interrupted; |
| private Thread processThread; |
| private ProcessStreamReader stdout; |
| private ProcessStreamReader stderr; |
| private Consumer<String> consumer; |
| |
| private ProcessWrapper(final String[] jvmArguments, final Class<?> mainClass, |
| final String[] mainArguments, final boolean useMainLauncher, final boolean headless, |
| final long timeoutMillis, final File directory) { |
| this.jvmArguments = jvmArguments; |
| this.mainClass = mainClass; |
| this.mainArguments = mainArguments; |
| this.useMainLauncher = useMainLauncher; |
| this.headless = headless; |
| this.timeoutMillis = timeoutMillis; |
| this.directory = directory; |
| |
| lineBuffer = new LinkedBlockingQueue<>(); |
| allLines = Collections.synchronizedList(new ArrayList<>()); |
| } |
| |
| public void setConsumer(Consumer<String> consumer) { |
| this.consumer = consumer; |
| } |
| |
| @Override |
| public void accept(String line) { |
| allLines.add(line); |
| lineBuffer.offer(line); |
| |
| if (consumer != null) { |
| consumer.accept(line); |
| } |
| } |
| |
| public ProcessStreamReader getStandardOutReader() { |
| synchronized (exitValue) { |
| return stdout; |
| } |
| } |
| |
| public ProcessStreamReader getStandardErrorReader() { |
| synchronized (exitValue) { |
| return stderr; |
| } |
| } |
| |
| private void waitForProcessStart() throws InterruptedException, TimeoutException { |
| final long start = System.currentTimeMillis(); |
| boolean done = false; |
| while (!done) { |
| synchronized (exitValue) { |
| done = (process != null || processException != null) |
| && (started || exitValue.get() > -1 || 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 (exitValue) { |
| if (interrupted) { |
| throw new InterruptedException("Process was interrupted"); |
| } |
| return exitValue.get() == -1 && started && !stopped && !interrupted |
| && processThread.isAlive(); |
| } |
| } |
| |
| public ProcessWrapper destroy() { |
| if (process != null) { |
| process.destroy(); |
| } |
| return this; |
| } |
| |
| public int waitFor(final long timeout, final boolean throwOnTimeout) throws InterruptedException { |
| checkStarting(); |
| final Thread thread = getThread(); |
| thread.join(timeout); |
| synchronized (exitValue) { |
| if (throwOnTimeout) { |
| checkStopped(); |
| } |
| return 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 = allLines.iterator(); |
| while (iterator.hasNext()) { |
| sb.append(iterator.next() + System.lineSeparator()); |
| } |
| return sb.toString(); |
| } |
| |
| public ProcessWrapper sendInput() { |
| checkStarting(); |
| sendInput(""); |
| return this; |
| } |
| |
| public ProcessWrapper sendInput(final String input) { |
| checkStarting(); |
| final PrintStream ps = new PrintStream(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, MILLISECONDS); |
| if (line != null && pattern.matcher(line).matches()) { |
| fail("failIfOutputMatches Matched pattern \"" + patternString + "\" against output \"" |
| + line + "\". Output: " + 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(); |
| |
| logger.debug("ProcessWrapper:waitForOutputToMatch waiting for \"{}\"...", patternString); |
| final Pattern pattern = Pattern.compile(patternString); |
| |
| while (true) { |
| final String line = lineBuffer.poll(timeoutMillis, MILLISECONDS); |
| if (line == null) { |
| fail("Timed out waiting for output \"" + patternString + "\" after " + timeoutMillis + |
| " ms from process \"" + toString(process) + "\" in \"" + this + "\". Output: " + |
| new OutputFormatter(allLines)); |
| } |
| |
| if (pattern.matcher(line).matches()) { |
| logger.debug( |
| "ProcessWrapper:waitForOutputToMatch Matched pattern \"{}\" against output \"{}\"", |
| patternString, line); |
| break; |
| } |
| logger.debug( |
| "ProcessWrapper:waitForOutputToMatch Did not match pattern \"{}\" against output \"{}\"", |
| patternString, line); |
| } |
| return this; |
| } |
| |
| private String toString(Process process) { |
| StringBuilder sb = new StringBuilder(process.getClass().getSimpleName()); |
| sb.append("@").append(System.identityHashCode(this)).append("{"); |
| sb.append("alive=").append(process.isAlive()); |
| sb.append("}"); |
| return sb.toString(); |
| } |
| |
| /* |
| * 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, directory); |
| } |
| |
| public ProcessWrapper execute(final Properties properties) |
| throws InterruptedException, TimeoutException { |
| return execute(properties, directory); |
| } |
| |
| public ProcessWrapper execute(final Properties properties, final File workingDirectory) |
| throws InterruptedException, TimeoutException { |
| synchronized (exitValue) { |
| if (starting) { |
| throw new IllegalStateException("ProcessWrapper can only be executed once"); |
| } |
| starting = true; |
| processThread = |
| new Thread(() -> start(properties, workingDirectory), "ProcessWrapper Process Thread"); |
| } |
| processThread.start(); |
| |
| waitForProcessStart(); |
| |
| synchronized (exitValue) { |
| if (processException != null) { |
| logger.error("ProcessWrapper:execute failed with " + processException); |
| processException.printStackTrace(); |
| } |
| } |
| |
| if (useMainLauncher) { |
| // to trigger MainLauncher delegation to inner main |
| sendInput(); |
| } |
| return this; |
| } |
| |
| private void start(final Properties properties, final File workingDirectory) { |
| final List<String> jvmArgumentsList = new ArrayList<>(); |
| |
| 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 (headless) { |
| jvmArgumentsList.add("-Djava.awt.headless=true"); |
| } |
| |
| if (jvmArguments != null) { |
| Collections.addAll(jvmArgumentsList, jvmArguments); |
| } |
| |
| try { |
| synchronized (exitValue) { |
| final String[] command = |
| defineCommand(jvmArgumentsList.toArray(new String[jvmArgumentsList.size()]), |
| workingDirectory.getCanonicalPath()); |
| 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.info("Starting " + commandString); |
| |
| final ProcessStreamReader stdOut = new ProcessStreamReader(commandString, |
| process.getInputStream(), this); |
| final ProcessStreamReader stdErr = new ProcessStreamReader(commandString, |
| process.getErrorStream(), this); |
| |
| stdout = stdOut; |
| stderr = stdErr; |
| outputReader = new ProcessOutputReader(process, stdOut, stdErr); |
| started = true; |
| } |
| |
| outputReader.start(); |
| outputReader.waitFor(PROCESS_TIMEOUT_MILLIS, MILLISECONDS); |
| boolean exited = process.waitFor(PROCESS_TIMEOUT_MILLIS, MILLISECONDS); |
| |
| synchronized (exitValue) { |
| exitValue.set(exited ? process.exitValue() : 0); |
| stopped = exited; |
| } |
| |
| } catch (InterruptedException e) { |
| synchronized (exitValue) { |
| interrupted = true; |
| processException = e; |
| } |
| } catch (Throwable t) { |
| synchronized (exitValue) { |
| processException = t; |
| } |
| } |
| } |
| |
| private String[] defineCommand(final String[] jvmArguments, String workingDir) |
| throws IOException { |
| final File javaBinDir = new File(System.getProperty("java.home"), "bin"); |
| final File javaExe = new File(javaBinDir, "java"); |
| |
| String classPath = System.getProperty("java.class.path"); |
| List<String> parts = Arrays.asList(classPath.split(File.pathSeparator)); |
| String manifestJar = createManifestJar(parts, workingDir); |
| |
| final List<String> argumentList = new ArrayList<>(); |
| argumentList.add(javaExe.getPath()); |
| argumentList.add("-classpath"); |
| argumentList.add(manifestJar); |
| |
| // -d64 is not a valid option for windows and results in failure |
| // -d64 is not a valid option for java 9 and above |
| final int bits = Integer.getInteger("sun.arch.data.model", 0); |
| if (bits == 64 && !System.getProperty("os.name").toLowerCase().contains("windows") |
| && !SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_9)) { |
| argumentList.add("-d64"); |
| } |
| |
| argumentList.add("-Djava.library.path=" + System.getProperty("java.library.path")); |
| |
| if (jvmArguments != null) { |
| argumentList.addAll(Arrays.asList(jvmArguments)); |
| } |
| |
| if (useMainLauncher) { |
| argumentList.add(MainLauncher.class.getName()); |
| } |
| argumentList.add(mainClass.getName()); |
| |
| if (mainArguments != null) { |
| argumentList.addAll(Arrays.asList(mainArguments)); |
| } |
| |
| return argumentList.toArray(new String[0]); |
| } |
| |
| private void checkStarting() throws IllegalStateException { |
| synchronized (exitValue) { |
| if (!starting) { |
| throw new IllegalStateException("Process has not been launched"); |
| } |
| } |
| } |
| |
| private void checkStopped() throws IllegalStateException { |
| synchronized (exitValue) { |
| if (!stopped) { |
| throw new IllegalStateException("Process has not stopped"); |
| } |
| } |
| } |
| |
| private void checkOk() throws RuntimeException { |
| if (processException != null) { |
| throw new RuntimeException("Failed to launch process", processException); |
| } |
| } |
| |
| private Thread getThread() { |
| synchronized (exitValue) { |
| return processThread; |
| } |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(getClass().getSimpleName()); |
| sb.append("@").append(System.identityHashCode(this)).append("{"); |
| sb.append("mainClass=").append(mainClass).append(", "); |
| sb.append("jvmArguments=").append(Arrays.toString(jvmArguments)).append(", "); |
| sb.append("mainArguments=").append(Arrays.toString(mainArguments)); |
| sb.append("}"); |
| return sb.toString(); |
| } |
| |
| public Process getProcess() { |
| return process; |
| } |
| |
| /** |
| * Method to create a manifest jar from a list of jars or directories. The provided entries are |
| * first converted to absolute paths and then converted to relative paths, relative to the |
| * location provided. This is to support the Manifest's requirement that class-paths only be |
| * relative. For example, if a jar is given as /a/b/c/foo.jar and the location is /tmp/app, the |
| * following will happen: |
| * - the manifest jar will be created as /tmp/app/manifest.jar |
| * - the class-path attribute will be ../../a/b/c/foo.jar |
| * |
| * @return the path to the created manifest jar |
| */ |
| public static String createManifestJar(List<String> entries, String location) throws IOException { |
| // Must use the canonical path so that symbolic links are resolved correctly |
| Path locationPath = new File(location).getCanonicalFile().toPath(); |
| Files.createDirectories(locationPath); |
| |
| List<String> manifestEntries = new ArrayList<>(); |
| for (String jarEntry : entries) { |
| Path jarEntryAbsolutePath = Paths.get(jarEntry).toAbsolutePath(); |
| Path jarEntryRelativizedPath = locationPath.relativize(jarEntryAbsolutePath); |
| if (jarEntryAbsolutePath.toFile().isDirectory()) { |
| manifestEntries.add(jarEntryRelativizedPath + File.separator); |
| } else { |
| manifestEntries.add(jarEntryRelativizedPath.toString()); |
| } |
| } |
| |
| Manifest manifest = new Manifest(); |
| Attributes attributes = manifest.getMainAttributes(); |
| attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0.0"); |
| attributes.put(new Attributes.Name("Class-Path"), String.join(" ", manifestEntries)); |
| |
| // Generate a 'unique' 8 char name |
| String uuid = UUID.randomUUID().toString().substring(0, 8); |
| Path manifestJarPath = Paths.get(location, "manifest-" + uuid + ".jar"); |
| File manifestJarFile = manifestJarPath.toFile(); |
| manifestJarFile.deleteOnExit(); |
| |
| try (JarOutputStream jos = |
| new JarOutputStream(new FileOutputStream(manifestJarFile), manifest)) { |
| // the above try-with-resource writes the manifest to the manifestJarFile |
| } |
| |
| return manifestJarPath.toFile().getAbsolutePath(); |
| } |
| |
| public static class Builder { |
| |
| private String[] jvmArguments; |
| private Class<?> mainClass; |
| private String[] mainArguments; |
| private boolean useMainLauncher = true; |
| private boolean headless = true; |
| private long timeoutMillis = PROCESS_TIMEOUT_MILLIS; |
| private boolean inline; |
| private File directory = new File(System.getProperty("user.dir")); |
| |
| 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 Builder directory(final File directory) { |
| this.directory = directory; |
| return this; |
| } |
| |
| public ProcessWrapper build() { |
| return new ProcessWrapper(jvmArguments, mainClass, mainArguments, useMainLauncher, headless, |
| timeoutMillis, directory); |
| } |
| } |
| } |