/*
* The MIT License
*
* Copyright (c) 2014 Red Hat, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.github.olivergondza.dumpling.factory;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import javax.annotation.Nonnull;
import com.github.olivergondza.dumpling.model.ProcessRuntime;
import com.github.olivergondza.dumpling.model.dump.ThreadDumpRuntime;
/**
* Create {@link ProcessRuntime} from running local process.
*
* Process ID is used as a locator.
*
* This implementations invokes jstack binary and delegates to {@link ThreadDumpFactory} so it shares its features
* and limitations.
*
* @author ogondza
*/
public class PidRuntimeFactory {
private final @Nonnull String javaHome;
private final @Nonnull ThreadDumpFactory threadDumpFactory = new ThreadDumpFactory();
public PidRuntimeFactory() {
this(System.getProperty("java.home"));
}
public PidRuntimeFactory(@Nonnull String javaHome) {
this.javaHome = javaHome;
}
/**
* Historically, dumpling tolerates some of the errors silently.
*
* Turning this on will replace log records for failures to parse the threaddump.
*/
public PidRuntimeFactory failOnErrors(boolean failOnErrors) {
threadDumpFactory.failOnErrors(failOnErrors);
return this;
}
/**
* @param pid Process id to examine.
* @throws IOException When jstack invocation failed.
* @throws InterruptedException When jstack invocation was interrupted.
*/
public @Nonnull ThreadDumpRuntime fromProcess(long pid) throws IOException, InterruptedException {
ProcessBuilder pb = new ProcessBuilder(jstackBinary(), "-l", Long.toString(pid));
Process process = pb.start();
// Start consuming the output without waiting for process completion not to block both processes.
ThreadDumpRuntime runtime = null;
RuntimeException runtimeEx = null;
try {
runtime = createRuntime(process);
} catch (IllegalRuntimeStateException ex) {
// Do not throw the exception right away so #validateResult can diagnose more severe problem first.
runtimeEx = ex;
}
int ret = process.waitFor();
validateResult(process, ret);
if (runtimeEx != null) throw runtimeEx;
return runtime;
}
// Kept for binary compatibility.
public @Nonnull ThreadDumpRuntime fromProcess(int pid) throws IOException, InterruptedException {
return fromProcess((long) pid);
}
/**
* Extract runtime from running process.
* @param process Jvm process.
* @throws UnsupportedOperationException Process implementation is not supported.
* @throws IllegalStateException Process has already terminated.
* @throws IOException Unable to extract runtime from the process.
*/
public @Nonnull ThreadDumpRuntime fromProcess(@Nonnull Process process) throws IOException, InterruptedException, UnsupportedOperationException {
try {
int exitValue = process.exitValue();
throw new IllegalStateException("Process terminated with " + exitValue);
} catch (IllegalThreadStateException expected) {
// Process alive
}
long pid = extractPid(process);
return fromProcess(pid);
}
private long extractPid(@Nonnull Process process) {
Throwable problem = null;
try {
Method toHandle = Process.class.getMethod("toHandle");
Object handle = toHandle.invoke(process);
return (Long) Class.forName("java.lang.ProcessHandle").getMethod("getPid").invoke(handle);
} catch (NoSuchMethodException e) {
// Not Java 9 - Fallback
} catch (IllegalAccessException e) {
throw new AssertionError(e);
} catch (ClassNotFoundException e) {
throw new AssertionError(e);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof UnsupportedOperationException) {
// Process impl might not support management - Fallback
problem = cause;
} else if (cause instanceof SecurityException) {
// Access to monitoring is rejected - Fallback
problem = cause;
} else {
throw new AssertionError(e);
}
}
// Fallback for Java6+ on unix. This is known not to work for Java8 on Windows.
if (!"java.lang.UNIXProcess".equals(process.getClass().getName())) throw new UnsupportedOperationException(
"Unknown java.lang.Process implementation: " + process.getClass().getName(),
problem
);
try {
// Protected class
Class<?> clazz = Class.forName("java.lang.UNIXProcess");
Field pidField = clazz.getDeclaredField("pid");
pidField.setAccessible(true);
return pidField.getLong(process);
} catch (ClassNotFoundException e) {
throw new UnsupportedOperationException("Unable to find java.lang.UNIXProcess", e);
} catch (NoSuchFieldException e) {
throw new UnsupportedOperationException("Unable to find java.lang.UNIXProcess.pid", e);
} catch (IllegalAccessException e) {
throw new UnsupportedOperationException("Unable to access java.lang.UNIXProcess.pid", e);
}
}
/**
* Extract runtime from current process.
*
* This approach is somewhat external and {@link JvmRuntimeFactory} should be preferred.
*/
public @Nonnull ThreadDumpRuntime fromCurrentProcess() throws IOException, InterruptedException {
final String jvmName = ManagementFactory.getRuntimeMXBean().getName();
final int index = jvmName.indexOf('@');
if (index < 1) throw new IOException("Unable to extract PID from " + jvmName);
long pid;
try {
pid = Long.parseLong(jvmName.substring(0, index));
} catch (NumberFormatException e) {
throw new IOException("Unable to extract PID from " + jvmName);
}
return fromProcess(pid);
}
protected ThreadDumpRuntime createRuntime(Process process) {
return threadDumpFactory.fromStream(process.getInputStream());
}
private void validateResult(Process process, int ret) throws IOException {
if (ret == 0) return;
StringBuilder sb = new StringBuilder();
byte[] buffer = new byte[1024];
while (process.getErrorStream().read(buffer) != -1) {
sb.append(new String(buffer));
}
throw new IOException("jstack failed with code " + ret + ": " + sb.toString().trim());
}
private String jstackBinary() {
String suffix = ";".equals(File.pathSeparator) ? ".exe" : "";
File jstack = new File(javaHome + "/bin/jstack" + suffix);
if (!jstack.exists()) {
// This is the more common variant when java.home points to the jre/ (or other) subdirectory
jstack = new File(javaHome + "/../bin/jstack" + suffix);
}
if (jstack.exists()) return jstack.getAbsolutePath();
// Chances are there is 'jstack' on PATH that happens to be compatible
try {
Process p = new ProcessBuilder("jstack", "-h").start();
if (p.waitFor() == 0) {
return "jstack";
}
} catch (IOException e) {
// Likely does not exist
} catch (InterruptedException e) {
// Likely does not exist
}
throw new UnsupportedJdk(javaHome);
}
public static final class UnsupportedJdk extends RuntimeException {
private UnsupportedJdk(String jdk) {
super("Unable to capture runtime as the JDK is missing jstack utility: " + jdk);
}
}
}