1 /*
   2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
   3  * 
   4  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   5  *
   6  * The contents of this file are subject to the terms of either the Universal Permissive License
   7  * v 1.0 as shown at http://oss.oracle.com/licenses/upl
   8  *
   9  * or the following license:
  10  *
  11  * Redistribution and use in source and binary forms, with or without modification, are permitted
  12  * provided that the following conditions are met:
  13  * 
  14  * 1. Redistributions of source code must retain the above copyright notice, this list of conditions
  15  * and the following disclaimer.
  16  * 
  17  * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
  18  * conditions and the following disclaimer in the documentation and/or other materials provided with
  19  * the distribution.
  20  * 
  21  * 3. Neither the name of the copyright holder nor the names of its contributors may be used to
  22  * endorse or promote products derived from this software without specific prior written permission.
  23  * 
  24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
  25  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
  26  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  27  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  28  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  29  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  30  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
  31  * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  32  */
  33 package org.openjdk.jmc.rjmx.services.internal;
  34 
  35 import java.io.IOException;
  36 import java.util.ArrayList;
  37 import java.util.Collection;
  38 import java.util.Collections;
  39 import java.util.HashMap;
  40 import java.util.List;
  41 import java.util.Map;
  42 import java.util.concurrent.Callable;
  43 
  44 import javax.management.Descriptor;
  45 import javax.management.InstanceNotFoundException;
  46 import javax.management.MBeanException;
  47 import javax.management.MBeanInfo;
  48 import javax.management.MBeanOperationInfo;
  49 import javax.management.MBeanServerConnection;
  50 import javax.management.ObjectName;
  51 import javax.management.ReflectionException;
  52 
  53 import org.openjdk.jmc.rjmx.ConnectionToolkit;
  54 import org.openjdk.jmc.rjmx.RJMXPlugin;
  55 import org.openjdk.jmc.rjmx.ServiceNotAvailableException;
  56 import org.openjdk.jmc.rjmx.services.IDiagnosticCommandService;
  57 import org.openjdk.jmc.rjmx.services.IOperation.OperationImpact;
  58 import org.openjdk.jmc.rjmx.services.IllegalOperandException;
  59 import org.openjdk.jmc.rjmx.util.internal.SimpleAttributeInfo;
  60 
  61 public class HotSpot24DiagnosticCommandService implements IDiagnosticCommandService {
  62 
  63         private static final ObjectName DIAGNOSTIC_BEAN = ConnectionToolkit
  64                         .createObjectName("com.sun.management:type=DiagnosticCommand"); //$NON-NLS-1$
  65         private static final String OPERATION_UPDATE = "update"; //$NON-NLS-1$
  66         private final MBeanServerConnection m_mbeanServer;
  67         private final Map<String, String> commandNameToOperation = new HashMap<>();
  68         private Collection<DiagnosticCommand> operations;
  69 
  70         private static final String IMPACT = "dcmd.vmImpact"; //$NON-NLS-1$
  71         private static final String NAME = "dcmd.name"; //$NON-NLS-1$
  72         private static final String DESCRIPTION = "dcmd.description"; //$NON-NLS-1$
  73 //      private final static String HELP = "dcmd.help"; //$NON-NLS-1$
  74         private static final String ARGUMENTS = "dcmd.arguments"; //$NON-NLS-1$
  75         private static final String ARGUMENT_NAME = "dcmd.arg.name"; //$NON-NLS-1$
  76         private static final String ARGUMENT_DESCRIPTION = "dcmd.arg.description"; //$NON-NLS-1$
  77         private static final String ARGUMENT_MANDATORY = "dcmd.arg.isMandatory"; //$NON-NLS-1$
  78         private static final String ARGUMENT_TYPE = "dcmd.arg.type"; //$NON-NLS-1$
  79         private static final String ARGUMENT_OPTION = "dcmd.arg.isOption"; //$NON-NLS-1$
  80         private static final String ARGUMENT_MULITPLE = "dcmd.arg.isMultiple"; //$NON-NLS-1$
  81 
  82         private static List<DiagnosticCommandParameter> extractSignature(Descriptor args) {
  83                 if (args != null) {
  84                         String[] argNames = args.getFieldNames();
  85                         List<DiagnosticCommandParameter> parameters = new ArrayList<>(argNames.length);
  86                         for (String argName : argNames) {
  87                                 Descriptor arg = (Descriptor) args.getFieldValue(argName);
  88                                 parameters.add(new DiagnosticCommandParameter(arg));
  89                         }
  90                         return parameters;
  91                 } else {
  92                         return Collections.emptyList();
  93                 }
  94         }
  95 
  96         private static OperationImpact extractImpact(Descriptor d) {
  97                 String impact = d.getFieldValue(IMPACT).toString();
  98                 if (impact.startsWith("Low")) { //$NON-NLS-1$
  99                         return OperationImpact.IMPACT_LOW;
 100                 }
 101                 if (impact.startsWith("Medium")) { //$NON-NLS-1$
 102                         return OperationImpact.IMPACT_MEDIUM;
 103                 }
 104                 if (impact.startsWith("High")) { //$NON-NLS-1$
 105                         return OperationImpact.IMPACT_HIGH;
 106                 }
 107                 return OperationImpact.IMPACT_UNKNOWN;
 108         }
 109 
 110         private static String extractType(Descriptor d) {
 111                 boolean isMultiple = Boolean.parseBoolean(d.getFieldValue(ARGUMENT_MULITPLE).toString());
 112                 String typeName = d.getFieldValue(ARGUMENT_TYPE).toString();
 113                 if (isMultiple) {
 114                         if (typeName.equals("STRING SET")) { //$NON-NLS-1$
 115                                 return String[].class.getName();
 116                         } else {
 117                                 return typeName.toLowerCase().replace(' ', '_') + '*';
 118                         }
 119                 }
 120                 if (typeName.equals("BOOLEAN")) { //$NON-NLS-1$
 121                         return Boolean.class.getName();
 122                 } else if (typeName.equals("STRING") || typeName.equals("NANOTIME") || typeName.equals("MEMORY SIZE")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
 123                         return String.class.getName();
 124                 } else if (typeName.equals("JLONG")) { //$NON-NLS-1$
 125                         return Long.class.getName();
 126                 } else {
 127                         return typeName.toLowerCase().replace(' ', '_');
 128                 }
 129         }
 130 
 131         private static String extractDescription(Descriptor d) {
 132                 // FIXME: Argument descriptions for JFR operations contains \" that should be ". Workaround for now.
 133                 String desc = d.getFieldValue(ARGUMENT_DESCRIPTION).toString().trim().replaceAll("\\\\\"", "\""); //$NON-NLS-1$ //$NON-NLS-2$
 134                 return desc.length() > 0 ? desc : d.getFieldValue(ARGUMENT_NAME).toString().trim();
 135         }
 136 
 137         private static class ArgumentBuilder {
 138                 private final List<String> arguments = new ArrayList<>();
 139 
 140                 public void appendArgument(Object value, DiagnosticCommandParameter parameterInfo)
 141                                 throws IllegalOperandException {
 142                         if (parameterInfo.isMultiple) {
 143                                 if (value.getClass().isArray()) {
 144                                         for (Object o : ((Object[]) value)) {
 145                                                 appendValue(o, parameterInfo);
 146                                         }
 147                                 } else {
 148                                         throw new IllegalOperandException(parameterInfo);
 149                                 }
 150                         } else {
 151                                 appendValue(value, parameterInfo);
 152                         }
 153                 }
 154 
 155                 private void appendValue(Object value, DiagnosticCommandParameter parameterInfo)
 156                                 throws IllegalOperandException {
 157                         StringBuilder sb = new StringBuilder();
 158                         if (parameterInfo.isOption) {
 159                                 sb.append(parameterInfo.parameterName).append('=');
 160                         }
 161                         String stringValue = String.valueOf(value);
 162                         if (stringValue.indexOf('"') >= 0) {
 163                                 throw new IllegalOperandException(parameterInfo);
 164                         } else if (stringValue.indexOf(' ') >= 0) {
 165                                 sb.append('"').append(stringValue).append('"');
 166                         } else {
 167                                 sb.append(stringValue);
 168                         }
 169                         arguments.add(sb.toString());
 170                 }
 171 
 172                 public String[] asArray() {
 173                         return arguments.toArray(new String[arguments.size()]);
 174                 }
 175         }
 176 
 177         private static class DiagnosticCommandParameter extends SimpleAttributeInfo {
 178                 private final boolean isOption;
 179                 private final boolean isMultiple;
 180                 private final boolean isRequired;
 181                 private final String parameterName;
 182 
 183                 public DiagnosticCommandParameter(Descriptor d) {
 184                         super(d.getFieldValue(ARGUMENT_NAME).toString(), extractType(d), extractDescription(d));
 185                         parameterName = d.getFieldValue(ARGUMENT_NAME).toString();
 186                         isOption = Boolean.parseBoolean(d.getFieldValue(ARGUMENT_OPTION).toString());
 187                         isMultiple = Boolean.parseBoolean(d.getFieldValue(ARGUMENT_MULITPLE).toString());
 188                         isRequired = Boolean.parseBoolean(d.getFieldValue(ARGUMENT_MANDATORY).toString());
 189                         RJMXPlugin.getDefault().getLogger()
 190                                         .finest("DiagnosticCommandArg created: " + getType() + ' ' + getName() + ' ' + getDescription() //$NON-NLS-1$
 191                                                         + (isRequired ? " isRequired" : "") //$NON-NLS-1$ //$NON-NLS-2$
 192                                                         + (isOption ? " isOption" : "") + (isMultiple ? " isMultiple" : "")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
 193                 }
 194 
 195         }
 196 
 197         private class DiagnosticCommand extends AbstractOperation<DiagnosticCommandParameter> {
 198 
 199                 public DiagnosticCommand(Descriptor d, String returnType) {
 200                         super(d.getFieldValue(NAME).toString(), d.getFieldValue(DESCRIPTION).toString(), returnType,
 201                                         extractSignature((Descriptor) d.getFieldValue(ARGUMENTS)), extractImpact(d));
 202                         RJMXPlugin.getDefault().getLogger()
 203                                         .finest("DiagnosticCommand created: " + getName() + ' ' + getReturnType() + ' ' + getImpact()); //$NON-NLS-1$
 204                 }
 205 
 206                 @Override
 207                 public Callable<?> getInvocator(Object ... argValues) throws IllegalOperandException {
 208                         ArgumentBuilder ab = new ArgumentBuilder();
 209                         List<DiagnosticCommandParameter> args = getSignature();
 210                         for (int i = 0; i < args.size(); i++) {
 211                                 if (i >= argValues.length || argValues[i] == null) {
 212                                         if (args.get(i).isRequired) {
 213                                                 // Argument value is required but not provided
 214                                                 IllegalOperandException ex = new IllegalOperandException(args.get(i));
 215                                                 while (++i < args.size()) {
 216                                                         // Check for other attributes with the same error
 217                                                         if (args.get(i).isRequired) {
 218                                                                 ex.addInvalidValue(args.get(i));
 219                                                         }
 220                                                 }
 221                                                 throw ex;
 222                                         } else {
 223                                                 continue;
 224                                         }
 225                                 }
 226                                 ab.appendArgument(argValues[i], args.get(i));
 227                         }
 228                         final String[] arguments = ab.asArray();
 229                         return new Callable<Object>() {
 230 
 231                                 @Override
 232                                 public Object call() throws Exception {
 233                                         return execute(arguments);
 234                                 }
 235 
 236                                 @Override
 237                                 public String toString() {
 238                                         return getName() + ' ' + asString(arguments);
 239                                 }
 240                         };
 241                 }
 242 
 243                 private String asString(String[] array) {
 244                         StringBuilder sb = new StringBuilder();
 245                         if (array != null) {
 246                                 for (int i = 0; i < array.length; i += 1) {
 247                                         sb.append(array[i]);
 248                                         if (i + 1 < array.length) {
 249                                                 sb.append(' ');
 250                                         }
 251                                 }
 252                         }
 253                         return sb.toString();
 254                 }
 255 
 256                 private String execute(String[] arguments)
 257                                 throws InstanceNotFoundException, MBeanException, ReflectionException, IOException {
 258                         String operation = commandNameToOperation.get(getName());
 259                         RJMXPlugin.getDefault().getLogger()
 260                                         .fine("Running " + getName() + " |" + operation + '|' + asString(arguments) + '|'); //$NON-NLS-1$ //$NON-NLS-2$
 261                         if (operation == null) {
 262                                 throw new RuntimeException("Unavailable diagnostic command " + getName() + '!'); //$NON-NLS-1$
 263                         }
 264                         if (getSignature().size() > 0) {
 265                                 return (String) m_mbeanServer.invoke(DIAGNOSTIC_BEAN, operation, new Object[] {arguments},
 266                                                 new String[] {String[].class.getName()});
 267                         } else {
 268                                 return (String) m_mbeanServer.invoke(DIAGNOSTIC_BEAN, operation, new Object[0], new String[0]);
 269                         }
 270                 }
 271 
 272         }
 273 
 274         public HotSpot24DiagnosticCommandService(MBeanServerConnection server) throws ServiceNotAvailableException {
 275                 m_mbeanServer = server;
 276                 try {
 277                         refreshOperations();
 278                 } catch (Exception e) {
 279                         throw new ServiceNotAvailableException("Unable to retrieve diagnostic commands!"); //$NON-NLS-1$
 280                 }
 281         }
 282 
 283         @Override
 284         public synchronized Collection<DiagnosticCommand> getOperations() throws Exception {
 285                 refreshOperations();
 286                 return operations;
 287         }
 288 
 289         private void refreshOperations() throws Exception {
 290                 RJMXPlugin.getDefault().getLogger().finer("Refreshing diagnostic operations"); //$NON-NLS-1$
 291                 MBeanInfo info = m_mbeanServer.getMBeanInfo(DIAGNOSTIC_BEAN);
 292                 operations = new ArrayList<>(info.getOperations().length);
 293                 commandNameToOperation.clear();
 294                 for (MBeanOperationInfo oper : info.getOperations()) {
 295                         if (!oper.getName().equals(OPERATION_UPDATE)) {
 296                                 Descriptor descriptor = oper.getDescriptor();
 297                                 DiagnosticCommand c = new DiagnosticCommand(descriptor, oper.getReturnType());
 298                                 operations.add(c);
 299                                 commandNameToOperation.put(c.getName(), oper.getName());
 300                         }
 301                 }
 302         }
 303 
 304         @Override
 305         public String runCtrlBreakHandlerWithResult(String command) throws Exception {
 306                 int index = command.indexOf(' ');
 307                 if (index > 0) {
 308                         String operationName = command.substring(0, index);
 309                         return findDiagnosticCommand(operationName).execute(splitArguments(command.substring(index + 1).trim()));
 310                 } else {
 311                         return findDiagnosticCommand(command).execute(null);
 312                 }
 313         }
 314 
 315         private String[] splitArguments(String commandArguments) {
 316                 List<String> arguments = new ArrayList<>();
 317                 StringBuilder argument = new StringBuilder();
 318                 boolean inCitation = false;
 319                 for (char c : commandArguments.toCharArray()) {
 320                         if (inCitation) {
 321                                 if (c == '"') {
 322                                         inCitation = false;
 323                                 }
 324                                 argument.append(c);
 325                         } else {
 326                                 if (Character.isWhitespace(c)) {
 327                                         if (argument.length() > 0) {
 328                                                 arguments.add(argument.toString());
 329                                                 argument = new StringBuilder();
 330                                         }
 331                                 } else {
 332                                         if (c == '"') {
 333                                                 inCitation = true;
 334                                         }
 335                                         argument.append(c);
 336                                 }
 337                         }
 338                 }
 339                 if (argument.length() > 0) {
 340                         arguments.add(argument.toString());
 341                 }
 342                 return arguments.toArray(new String[arguments.size()]);
 343         }
 344 
 345         private synchronized DiagnosticCommand findDiagnosticCommand(String operationName) throws Exception {
 346                 for (DiagnosticCommand op : operations) {
 347                         if (op.getName().equals(operationName)) {
 348                                 return op;
 349                         }
 350                 }
 351                 throw new IllegalArgumentException("Unavailable diagnostic command " + operationName + '!'); //$NON-NLS-1$
 352         }
 353 
 354 }