/* * 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 static com.github.olivergondza.dumpling.factory.MXBeanFactoryUtils.fillThreadInfoData; import static com.github.olivergondza.dumpling.factory.MXBeanFactoryUtils.getSynchronizer; import java.io.File; import java.io.IOException; import java.lang.management.LockInfo; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import javax.annotation.Nonnegative; import javax.annotation.Nonnull; import javax.management.JMX; import javax.management.MBeanServerConnection; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import com.github.olivergondza.dumpling.model.ThreadStatus; import com.github.olivergondza.dumpling.model.jmx.JmxRuntime; import com.github.olivergondza.dumpling.model.jmx.JmxThread; /** * Create runtime from running process via JMX interface. * * A process can be identified by process ID or by host and port combination. * * @author ogondza */ public final class JmxRuntimeFactory { private static final ObjectName THREADING_MBEAN; private static final ObjectName RUNTIME_MBEAN; static { try { THREADING_MBEAN = new ObjectName(ManagementFactory.THREAD_MXBEAN_NAME); RUNTIME_MBEAN = new ObjectName(ManagementFactory.RUNTIME_MXBEAN_NAME); } catch (MalformedObjectNameException ex) { throw new AssertionError(ex); } } public @Nonnull JmxRuntime forConnectionString(@Nonnull String locator) throws FailedToInitializeJmxConnection { try { int pid = Integer.parseInt(locator); return forLocalProcess(pid); } catch (NumberFormatException ex) { // No a PID - remote process } String username = null; String password = null; List<String> chunks = Arrays.asList(locator.split("[:@]")); Collections.reverse(chunks); int port = Integer.parseInt(chunks.get(0)); String host = chunks.get(1); if (chunks.size() == 4) { password = chunks.get(2); username = chunks.get(3); } return forRemoteProcess(host, port, username, password); } public @Nonnull JmxRuntime forRemoteProcess(@Nonnull String host, int port) throws FailedToInitializeJmxConnection { return forRemoteProcess(host, port, null, null); } public @Nonnull JmxRuntime forRemoteProcess(@Nonnull String host, int port, String username, String password) throws FailedToInitializeJmxConnection { return fromConnection(new RemoteConnector(host, port, username, password).getServerConnection()); } public @Nonnull JmxRuntime forLocalProcess(int pid) throws FailedToInitializeJmxConnection { return fromConnection(new LocalConnector(pid).getServerConnection()); } private @Nonnull JmxRuntime fromConnection(@Nonnull MBeanServerConnection connection) { return extractRuntime(connection); } private @Nonnull JmxRuntime extractRuntime(@Nonnull MBeanServerConnection connection) { final List<ThreadInfo> threads = getRemoteThreads(connection); HashSet<JmxThread.Builder> builders = new HashSet<JmxThread.Builder>(threads.size()); for (ThreadInfo thread: threads) { JmxThread.Builder builder = new JmxThread.Builder(); final ThreadStatus status = fillThreadInfoData(thread, builder); final LockInfo lockInfo = thread.getLockInfo(); if (lockInfo != null) { builder.setWaitingToLock(getSynchronizer(lockInfo)); } builders.add(builder); } return new JmxRuntime(builders, new Date(), getVmName(connection)); } private List<ThreadInfo> getRemoteThreads(@Nonnull MBeanServerConnection connection) { ThreadMXBean proxy = JMX.newMXBeanProxy(connection, THREADING_MBEAN, ThreadMXBean.class); return Arrays.asList(proxy.dumpAllThreads(true, true)); } @SuppressWarnings("null") private @Nonnull String getVmName(@Nonnull MBeanServerConnection connection) { RuntimeMXBean proxy = JMX.newMXBeanProxy(connection, RUNTIME_MBEAN, RuntimeMXBean.class); Map<String, String> props = proxy.getSystemProperties(); return String.format( "Dumpling JMX thread dump %s (%s):", props.get("java.vm.name"), props.get("java.vm.version") ); } private static final class LocalConnector { private static final String CONNECTOR_CLASS_NAME = "com.github.olivergondza.dumpling.factory.jmx.JmxLocalProcessConnector"; private final @Nonnegative int pid; private LocalConnector(@Nonnegative int pid) { this.pid = pid; } /* Delegate to JmxLocalProcessConnector in separated classloader */ private @Nonnull MBeanServerConnection getServerConnection() { ClassLoader classLoader = loadToolsJarClasses(); try { final Class<?> type = classLoader.loadClass(CONNECTOR_CLASS_NAME); final Method method = type.getDeclaredMethod("getServerConnection", int.class); method.setAccessible(true); return (MBeanServerConnection) method.invoke(null, pid); } catch (InvocationTargetException ex) { Throwable cause = ex.getCause(); // Unwrap and rethrow as FailedToInitializeJmxConnection if necessary if (cause instanceof FailedToInitializeJmxConnection) throw (FailedToInitializeJmxConnection) cause; throw new FailedToInitializeJmxConnection(cause); } catch (ClassNotFoundException ex) { throw assertionError("Unable to invoke " + CONNECTOR_CLASS_NAME, ex); } catch (NoSuchMethodException ex) { throw assertionError("Unable to invoke " + CONNECTOR_CLASS_NAME, ex); } catch (SecurityException ex) { throw assertionError("Unable to invoke " + CONNECTOR_CLASS_NAME, ex); } catch (IllegalAccessException ex) { throw assertionError("Unable to invoke " + CONNECTOR_CLASS_NAME, ex); } catch (IllegalArgumentException ex) { throw assertionError("Unable to invoke " + CONNECTOR_CLASS_NAME, ex); } } private AssertionError assertionError(String msg, Throwable cause) { AssertionError ex = new AssertionError(msg); ex.initCause(cause); return ex; } private ClassLoader loadToolsJarClasses() { final ClassLoader currentClassLoader = this.getClass().getClassLoader(); try { Class.forName("com.sun.tools.attach.VirtualMachine"); return currentClassLoader; } catch (ClassNotFoundException ex) { // Using null as parent classloader to baypass parent-first policy return new URLClassLoader(locateJars(), null); } } private URL[] locateJars() { final String dumplingJar = getClass().getProtectionDomain().getCodeSource().getLocation().getPath(); final String javaHome = System.getProperty("java.home"); try { return jarUrlArray(dumplingJar, javaHome + "/lib/tools.jar", javaHome + "/../lib/tools.jar"); } catch (MalformedURLException ex) { throw new FailedToInitializeJmxConnection(ex); } } private URL[] jarUrlArray(@Nonnull String... jars) throws MalformedURLException { ArrayList<URL> out = new ArrayList<URL>(jars.length); for (String jar: jars) { File file = new File(jar); if (file.isFile()) { out.add(file.toURI().toURL()); } } return out.toArray(new URL[out.size()]); } } /*package*/ static final class RemoteConnector { /*package*/ final @Nonnull String host; /*package*/ final @Nonnegative int port; /*package*/ String username; /*package*/ String password; /*package*/ RemoteConnector(@Nonnull String host, int port, String username, String password) { this.host = host; this.port = port; this.username = username; this.password = password; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(host).append(':').append(port); if (username != null) { sb.insert(0, '@').insert(0, password).insert(0, ':').insert(0, username); } return sb.toString(); } private @Nonnull MBeanServerConnection getServerConnection() { HashMap<String, String[]> map = new HashMap<String, String[]>(); if (username != null) { map.put(JMXConnector.CREDENTIALS, new String[] {username, password}); } JMXServiceURL serviceUrl = getServiceUrl(); try { return JMXConnectorFactory.connect(serviceUrl, map).getMBeanServerConnection(); } catch (SecurityException ex) { throw new FailedToInitializeJmxConnection("Failed to initialize connection to " + serviceUrl + ": " + ex.getMessage(), ex); } catch (IOException ex) { throw new FailedToInitializeJmxConnection("Failed to initialize connection to " + serviceUrl + ": " + ex.getMessage(), ex); } } private JMXServiceURL getServiceUrl() { try { return new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + host + ":" + port + "/jmxrmi"); } catch (MalformedURLException ex) { throw new FailedToInitializeJmxConnection(ex); } } } public static final class FailedToInitializeJmxConnection extends RuntimeException { private static final long serialVersionUID = 1L; public FailedToInitializeJmxConnection(Throwable ex) { super(ex.getMessage(), ex); } public FailedToInitializeJmxConnection(String string, Throwable ex) { super(string, ex); } public FailedToInitializeJmxConnection(String message) { super(message); } } }