1 /*
   2  * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 package com.oracle.java.testlibrary.dcmd;
  25 
  26 import com.oracle.java.testlibrary.OutputAnalyzer;
  27 
  28 import javax.management.*;
  29 import javax.management.remote.JMXConnector;
  30 import javax.management.remote.JMXConnectorFactory;
  31 import javax.management.remote.JMXServiceURL;
  32 
  33 import java.io.IOException;
  34 import java.io.PrintWriter;
  35 import java.io.StringWriter;
  36 
  37 import java.lang.management.ManagementFactory;
  38 
  39 import java.util.HashMap;
  40 
  41 /**
  42  * Executes Diagnostic Commands on the target VM (specified by a host/port combination or a full JMX Service URL) using
  43  * the JMX interface. If the target is not the current VM, the JMX Remote interface must be enabled beforehand.
  44  */
  45 public class JMXExecutor extends CommandExecutor {
  46 
  47     private final MBeanServerConnection mbs;
  48 
  49     /**
  50      * Instantiates a new JMXExecutor targeting the current VM
  51      */
  52     public JMXExecutor() {
  53         super();
  54         mbs = ManagementFactory.getPlatformMBeanServer();
  55     }
  56 
  57     /**
  58      * Instantiates a new JMXExecutor targeting the VM indicated by the given host/port combination or a full JMX
  59      * Service URL
  60      *
  61      * @param target a host/port combination on the format "host:port" or a full JMX Service URL of the target VM
  62      */
  63     public JMXExecutor(String target) {
  64         String urlStr;
  65 
  66         if (target.matches("^\\w[\\w\\-]*(\\.[\\w\\-]+)*:\\d+$")) {
  67             /* Matches "hostname:port" */
  68             urlStr = String.format("service:jmx:rmi:///jndi/rmi://%s/jmxrmi", target);
  69         } else if (target.startsWith("service:")) {
  70             urlStr = target;
  71         } else {
  72             throw new IllegalArgumentException("Could not recognize target string: " + target);
  73         }
  74 
  75         try {
  76             JMXServiceURL url = new JMXServiceURL(urlStr);
  77             JMXConnector c = JMXConnectorFactory.connect(url, new HashMap<>());
  78             mbs = c.getMBeanServerConnection();
  79         } catch (IOException e) {
  80             throw new CommandExecutorException("Could not initiate connection to target: " + target, e);
  81         }
  82     }
  83 
  84     protected OutputAnalyzer executeImpl(String cmd) throws CommandExecutorException {
  85         String stdout = "";
  86         String stderr = "";
  87 
  88         String[] cmdParts = cmd.split(" ", 2);
  89         String operation = commandToMethodName(cmdParts[0]);
  90         Object[] dcmdArgs = produceArguments(cmdParts);
  91         String[] signature = {String[].class.getName()};
  92 
  93         ObjectName beanName = getMBeanName();
  94 
  95         try {
  96             stdout = (String) mbs.invoke(beanName, operation, dcmdArgs, signature);
  97         }
  98 
  99         /* Failures on the "local" side, the one invoking the command. */
 100         catch (ReflectionException e) {
 101             Throwable cause = e.getCause();
 102             if (cause instanceof NoSuchMethodException) {
 103                 /* We want JMXExecutor to match the behavior of the other CommandExecutors */
 104                 String message = "Unknown diagnostic command: " + operation;
 105                 stderr = exceptionTraceAsString(new IllegalArgumentException(message, e));
 106             } else {
 107                 rethrowExecutorException(operation, dcmdArgs, e);
 108             }
 109         }
 110 
 111         /* Failures on the "local" side, the one invoking the command. */
 112         catch (InstanceNotFoundException | IOException e) {
 113             rethrowExecutorException(operation, dcmdArgs, e);
 114         }
 115 
 116         /* Failures on the remote side, the one executing the invoked command. */
 117         catch (MBeanException e) {
 118             stdout = exceptionTraceAsString(e);
 119         }
 120 
 121         return new OutputAnalyzer(stdout, stderr);
 122     }
 123 
 124     private void rethrowExecutorException(String operation, Object[] dcmdArgs,
 125                                           Exception e) throws CommandExecutorException {
 126         String message = String.format("Could not invoke: %s %s", operation,
 127                 String.join(" ", (String[]) dcmdArgs[0]));
 128         throw new CommandExecutorException(message, e);
 129     }
 130 
 131     private ObjectName getMBeanName() throws CommandExecutorException {
 132         String MBeanName = "com.sun.management:type=DiagnosticCommand";
 133 
 134         try {
 135             return new ObjectName(MBeanName);
 136         } catch (MalformedObjectNameException e) {
 137             String message = "MBean not found: " + MBeanName;
 138             throw new CommandExecutorException(message, e);
 139         }
 140     }
 141 
 142     private Object[] produceArguments(String[] cmdParts) {
 143         Object[] dcmdArgs = {new String[0]}; /* Default: No arguments */
 144 
 145         if (cmdParts.length == 2) {
 146             dcmdArgs[0] = cmdParts[1].split(" ");
 147         }
 148         return dcmdArgs;
 149     }
 150 
 151     /**
 152      * Convert from diagnostic command to MBean method name
 153      *
 154      * Examples:
 155      * help            --> help
 156      * VM.version      --> vmVersion
 157      * VM.command_line --> vmCommandLine
 158      */
 159     private static String commandToMethodName(String cmd) {
 160         String operation = "";
 161         boolean up = false; /* First letter is to be lower case */
 162 
 163         /*
 164          * If a '.' or '_' is encountered it is not copied,
 165          * instead the next character will be converted to upper case
 166          */
 167         for (char c : cmd.toCharArray()) {
 168             if (('.' == c) || ('_' == c)) {
 169                 up = true;
 170             } else if (up) {
 171                 operation = operation.concat(Character.toString(c).toUpperCase());
 172                 up = false;
 173             } else {
 174                 operation = operation.concat(Character.toString(c).toLowerCase());
 175             }
 176         }
 177 
 178         return operation;
 179     }
 180 
 181     private static String exceptionTraceAsString(Throwable cause) {
 182         StringWriter sw = new StringWriter();
 183         cause.printStackTrace(new PrintWriter(sw));
 184         return sw.toString();
 185     }
 186 
 187 }