/* * 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.flink.runtime.testutils; import org.apache.commons.lang3.ArrayUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.StringWriter; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Arrays; import static org.apache.flink.runtime.testutils.CommonTestUtils.createTemporaryLog4JProperties; import static org.apache.flink.runtime.testutils.CommonTestUtils.getCurrentClasspath; import static org.apache.flink.runtime.testutils.CommonTestUtils.getJavaCommandPath; import static org.apache.flink.util.Preconditions.checkArgument; import static org.apache.flink.util.Preconditions.checkNotNull; import static org.apache.flink.util.Preconditions.checkState; import static org.junit.Assert.fail; /** * A {@link Process} running a separate JVM. */ public abstract class TestJvmProcess { private static final Logger LOG = LoggerFactory.getLogger(TestJvmProcess.class); /** Lock to guard {@link #startProcess()} and {@link #destroy()} calls. */ private final Object createDestroyLock = new Object(); /** The java command path */ private final String javaCommandPath; /** The log4j configuration path. */ private final String log4jConfigFilePath; /** Shutdown hook for resource cleanup */ private final Thread shutdownHook; /** JVM process memory (set for both '-Xms' and '-Xmx'). */ private int jvmMemoryInMb = 80; /** The JVM process */ private volatile Process process; /** Writer for the process output */ private volatile StringWriter processOutput; /** flag to mark the process as already destroyed */ private volatile boolean destroyed; public TestJvmProcess() throws Exception { this(getJavaCommandPath(), createTemporaryLog4JProperties().getPath()); } public TestJvmProcess(String javaCommandPath, String log4jConfigFilePath) { this.javaCommandPath = checkNotNull(javaCommandPath, "Java command path"); this.log4jConfigFilePath = checkNotNull(log4jConfigFilePath, "log4j config file path"); this.shutdownHook = new Thread(new Runnable() { @Override public void run() { try { destroy(); } catch (Throwable t) { LOG.error("Error during process cleanup shutdown hook.", t); } } }); } /** * Returns the name of the process. */ public abstract String getName(); /** * Returns the arguments to the JVM. * * <p>These can be parsed by the main method of the entry point class. */ public abstract String[] getJvmArgs(); /** * Returns the name of the class to run. * * <p>Arguments to the main method can be specified via {@link #getJvmArgs()}. */ public abstract String getEntryPointClassName(); // --------------------------------------------------------------------------------------------- /** * Sets the memory for the process (<code>-Xms</code> and <code>-Xmx</code> flags) (>= 80). * * @param jvmMemoryInMb Amount of memory in Megabytes for the JVM (>= 80). */ public void setJVMMemory(int jvmMemoryInMb) { checkArgument(jvmMemoryInMb >= 80, "Process JVM Requires at least 80 MBs of memory."); checkState(process == null, "Cannot set memory after process was started"); this.jvmMemoryInMb = jvmMemoryInMb; } /** * Creates and starts the {@link Process}. * * <strong>Important:</strong> Don't forget to call {@link #destroy()} to prevent * resource leaks. The created process will be child process and is not guaranteed to * terminate when the parent process terminates. */ public void startProcess() throws IOException { String[] cmd = new String[] { javaCommandPath, "-Dlog.level=DEBUG", "-Dlog4j.configuration=file:" + log4jConfigFilePath, "-Xms" + jvmMemoryInMb + "m", "-Xmx" + jvmMemoryInMb + "m", "-classpath", getCurrentClasspath(), getEntryPointClassName() }; String[] jvmArgs = getJvmArgs(); if (jvmArgs != null && jvmArgs.length > 0) { cmd = ArrayUtils.addAll(cmd, jvmArgs); } synchronized (createDestroyLock) { checkState(process == null, "process already started"); LOG.debug("Running command '{}'.", Arrays.toString(cmd)); this.process = new ProcessBuilder(cmd).start(); // Forward output this.processOutput = new StringWriter(); new CommonTestUtils.PipeForwarder(process.getErrorStream(), processOutput); try { // Add JVM shutdown hook to call shutdown of service Runtime.getRuntime().addShutdownHook(shutdownHook); } catch (IllegalStateException ignored) { // JVM is already shutting down. No need to do this. } catch (Throwable t) { LOG.error("Cannot register process cleanup shutdown hook.", t); } } } public void printProcessLog() { checkState(processOutput != null, "not started"); System.out.println("-----------------------------------------"); System.out.println(" BEGIN SPAWNED PROCESS LOG FOR " + getName()); System.out.println("-----------------------------------------"); String out = processOutput.toString(); if (out == null || out.length() == 0) { System.out.println("(EMPTY)"); } else { System.out.println(out); } System.out.println("-----------------------------------------"); System.out.println(" END SPAWNED PROCESS LOG " + getName()); System.out.println("-----------------------------------------"); } public void destroy() { synchronized (createDestroyLock) { checkState(process != null, "process not started"); if (destroyed) { // already done return; } LOG.info("Destroying " + getName() + " process."); try { // try to call "destroyForcibly()" on Java 8 boolean destroyed = false; try { Method m = process.getClass().getMethod("destroyForcibly"); m.setAccessible(true); m.invoke(process); destroyed = true; } catch (NoSuchMethodException ignored) { // happens on Java 7 } catch (Throwable t) { LOG.error("Failed to forcibly destroy process", t); } // if it was not destroyed, call the regular destroy method if (!destroyed) { try { process.destroy(); } catch (Throwable t) { LOG.error("Error while trying to destroy process.", t); } } } finally { destroyed = true; if (shutdownHook != null && shutdownHook != Thread.currentThread()) { try { Runtime.getRuntime().removeShutdownHook(shutdownHook); } catch (IllegalStateException ignored) { // JVM is in shutdown already, we can safely ignore this. } catch (Throwable t) { LOG.warn("Exception while unregistering process cleanup shutdown hook."); } } } } } public String getProcessOutput() { if (processOutput != null) { return processOutput.toString(); } else { return null; } } /** * Gets the process ID, if possible. This method currently only work on UNIX-based * operating systems. On others, it returns {@code -1}. * * @return The process ID, or -1, if the ID cannot be determined. */ public long getProcessId() { checkState(process != null, "process not started"); try { Class<? extends Process> clazz = process.getClass(); if (clazz.getName().equals("java.lang.UNIXProcess")) { Field pidField = clazz.getDeclaredField("pid"); pidField.setAccessible(true); return pidField.getLong(process); } else { return -1; } } catch (Throwable ignored) { return -1; } } public boolean isAlive() { if (destroyed) { return false; } else { try { // the method throws an exception as long as the // process is alive process.exitValue(); return false; } catch (IllegalThreadStateException ignored) { // thi return true; } } } public void waitFor() throws InterruptedException { Process process = this.process; if (process != null) { process.waitFor(); } else { throw new IllegalStateException("process not started"); } } // --------------------------------------------------------------------------------------------- // File based synchronization utilities // --------------------------------------------------------------------------------------------- public static void touchFile(File file) throws IOException { if (!file.exists()) { new FileOutputStream(file).close(); } if (!file.setLastModified(System.currentTimeMillis())) { throw new IOException("Could not touch the file."); } } public static void waitForMarkerFile(File file, long timeoutMillis) throws InterruptedException { final long deadline = System.nanoTime() + timeoutMillis * 1_000_000; boolean exists; while (!(exists = file.exists()) && System.nanoTime() < deadline) { Thread.sleep(10); } if (!exists) { fail("The marker file was not found within " + timeoutMillis + " msecs"); } } public static void waitForMarkerFiles(File basedir, String prefix, int num, long timeout) { long now = System.currentTimeMillis(); final long deadline = now + timeout; while (now < deadline) { boolean allFound = true; for (int i = 0; i < num; i++) { File nextToCheck = new File(basedir, prefix + i); if (!nextToCheck.exists()) { allFound = false; break; } } if (allFound) { return; } else { // not all found, wait for a bit try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } now = System.currentTimeMillis(); } } fail("The tasks were not started within time (" + timeout + "msecs)"); } }