Package com.cloudbees.jmx

Source Code of com.cloudbees.jmx.JmxInvoker$JmxInvokerArguments

/*
* Copyright 2010-2013, CloudBees Inc.
*
* Licensed 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 com.cloudbees.jmx;

import com.cloudbees.util.Strings2;
import com.cloudbees.util.nio.Files2;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.VirtualMachine;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import org.kohsuke.args4j.spi.ObjectNameOptionHandler;
import org.kohsuke.args4j.spi.StringArrayOptionHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.impl.SimpleLogger;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.management.*;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.*;

/**
* Invoke arbitrary JMX operation or attribute connecting to the target local JVM thanks to the
* <a href="http://docs.oracle.com/javase/6/docs/jdk/api/attach/spec/index.html">Attach API</a>
* (see <a href="https://blogs.oracle.com/CoreJavaTechTips/entry/the_attach_api">The Attach API</a>).
*
* @author <a href="mailto:cleclerc@cloudbees.com">Cyrille Le Clerc</a>
*/
public class JmxInvoker {
    public static final String NULL_VALUE = "##NULL##";
    private Logger logger = LoggerFactory.getLogger(JmxInvoker.class);

    public static void main(String[] args) throws Exception {

        JmxInvokerArguments arguments = new JmxInvokerArguments();
        CmdLineParser parser = new CmdLineParser(arguments);
        try {
            parser.parseArgument(args);
            arguments.cmdLineParser = parser;
            if (Strings2.isEmpty(arguments.pid) && arguments.pidFile == null) {
                throw new CmdLineException(parser, "Options --pid and --pid-file can NOT be both null");
            } else if (!Strings2.isEmpty(arguments.pid) && arguments.pidFile != null) {
                throw new CmdLineException(parser, "Options --pid and --pid-file can NOT be both defined");
            } else if (
                    (arguments.attribute == null || arguments.attribute.length == 0) &&
                            (arguments.operation == null || arguments.operation.length == 0)) {
                throw new CmdLineException(parser, "Option --attribute or --operation must be defined");
            } else if (
                    (arguments.attribute != null && arguments.attribute.length > 0) &&
                            (arguments.operation != null && arguments.operation.length > 0)) {
                throw new CmdLineException(parser, "Options --attribute and --operation can NOT be both defined");
            }


            String logLevel;
            if (arguments.superVerbose) {
                logLevel = "TRACE";
            } else if (arguments.verbose) {
                logLevel = "DEBUG";
            } else {
                logLevel = "INFO";
            }
            System.setProperty(SimpleLogger.DEFAULT_LOG_LEVEL_KEY, logLevel);

            Map<ObjectName, Object> results = new JmxInvoker().process(arguments);
        } catch (CmdLineException e) {
            System.err.println("INVALID INVOCATION: " + e.getMessage());
            parser.printUsage(System.err);
            return;
        }
    }

    public Map<ObjectName, Object> process(JmxInvokerArguments arguments) throws IOException {

        String pid = (arguments.pid == null) ? Files2.readFile(arguments.pidFile, "US-ASCII") : arguments.pid;
        pid = pid.replace("\n", "").trim();

        ObjectName on = arguments.objectName;

        String[] op = arguments.operation;

        String operationName = op == null || op.length == 0 ? null : op[0];
        String[] operationArguments = op == null || op.length < 2 ? new String[0] : Arrays.copyOfRange(op, 1, op.length);

        String[] attr = arguments.attribute;
        String attributeName = attr == null || attr.length == 0 ? null : attr[0];
        String attributeValue = attr == null || attr.length < 2 ? null : attr[1];

        MBeanServerConnection mbeanServer = connectToMbeanServer(pid);

        Map<ObjectName, Object> results = new HashMap<ObjectName, Object>();

        Set<ObjectName> objectNames = mbeanServer.queryNames(on, null);
        if (objectNames.isEmpty()) {
            logger.warn("No mbean found for ObjectName {}", on);
        }

        for (ObjectName objectName : objectNames) {
            Object result;
            try {
                if (operationName != null) {
                    result = invokeOperation(mbeanServer, objectName, operationName, operationArguments);
                } else if (attributeName != null) {
                    result = invokeAttribute(mbeanServer, objectName, attributeName, attributeValue);
                } else {
                    throw new CmdLineException("NOT OPERATION OR ATTRIBUTE DEFINED");
                }
            } catch (Exception e) {
                StringWriter sw = new StringWriter();
                e.printStackTrace(new PrintWriter(sw));
                result = "## EXCEPTION ##\n" + sw.toString();
            }

            results.put(objectName, result);

        }

        logger.info("INVOCATION RESULT");
        logger.info("#################");
        logger.info("pid: {}", pid);
        logger.info("object-name: {}", on);
        if (operationName != null) {
            logger.info("invoke operation {}{}", operationName, Arrays.asList(operationArguments));
        } else if (attributeValue == null) {
            logger.info("get attribute {}", attributeName);
        } else {
            logger.info("set attribute {}: {}", attributeName, attributeValue);
        }
        for (Map.Entry<ObjectName, Object> entry : results.entrySet()) {
            logger.info("{}", entry.getKey());
            logger.info("\t{}", entry.getValue());
        }

        return results;

    }

    /**
     * @param mbeanServer
     * @param objectName
     * @param attributeName
     * @param attributeValue if <code>null</code>, this is a read access.
     * @return if this is a read access, the returned value (may be null); if it is a write access, return <code>void.class</code>.
     * @throws IOException
     * @throws JMException
     */
    public Object invokeAttribute(@Nonnull MBeanServerConnection mbeanServer, @Nonnull ObjectName objectName, @Nonnull String attributeName, @Nullable String attributeValue) throws IOException, JMException {
        MBeanInfo mbeanInfo = mbeanServer.getMBeanInfo(objectName);
        MBeanAttributeInfo attributeInfo = null;
        for (MBeanAttributeInfo mai : mbeanInfo.getAttributes()) {
            if (mai.getName().equals(attributeName)) {
                attributeInfo = mai;
                break;
            }
        }
        if (attributeInfo == null) {
            throw new IllegalArgumentException("No attribute '" + attributeName + "' found on '" + objectName + "'. Existing attributes: " + Arrays.asList(mbeanInfo.getAttributes()));
        }

        if (attributeValue == null) {
            if (attributeInfo.isReadable()) {
                Object attribute = mbeanServer.getAttribute(objectName, attributeName);
                logger.info("get attribute value {}:{}:{}", objectName, attributeName, attribute);
                return attribute;
            } else {
                throw new IllegalArgumentException("Attribute '" + attributeName + "' is not readable on '" + objectName + "': " + attributeInfo);
            }
        } else {
            if (attributeInfo.isWritable()) {
                Object value = convertValue(attributeValue, attributeInfo.getType());
                mbeanServer.setAttribute(objectName, new Attribute(attributeName, value));
                logger.info("set attribute value {}:{}:{}", objectName, attributeName, value);
                return void.class;
            } else {
                throw new IllegalArgumentException("Attribute '" + attributeName + "' is not writable on '" + objectName + "': " + attributeInfo);
            }
        }
    }

    @Nullable
    public Object invokeOperation(@Nonnull MBeanServerConnection mBeanServer, @Nonnull ObjectName on, @Nonnull String operationName, @Nonnull String... arguments) throws JMException, IOException {

        logger.debug("invokeOperation({},{}, {}, {})...", on, operationName, Arrays.asList(arguments));
        MBeanInfo mbeanInfo = mBeanServer.getMBeanInfo(on);

        List<MBeanOperationInfo> candidates = new ArrayList<MBeanOperationInfo>();
        for (MBeanOperationInfo mbeanOperationInfo : mbeanInfo.getOperations()) {
            if (mbeanOperationInfo.getName().equals(operationName) && mbeanOperationInfo.getSignature().length == arguments.length) {
                candidates.add(mbeanOperationInfo);
                logger.debug("select matching operation {}", mbeanOperationInfo);
            } else {
                logger.trace("Ignore non matching operation {}", mbeanOperationInfo);
            }
        }
        if (candidates.isEmpty()) {
            throw new IllegalArgumentException("Operation '" + operationName + "" + Arrays.asList(arguments) + "' NOT found on " + on);
        } else if (candidates.size() > 1) {
            throw new IllegalArgumentException("More than 1 (" + candidates.size() + ", operation '" + operationName + "' found on '" + on + "': " + candidates);
        }
        MBeanOperationInfo beanOperationInfo = candidates.get(0);

        MBeanParameterInfo[] mbeanParameterInfos = beanOperationInfo.getSignature();

        List<String> signature = new ArrayList<String>();
        for (MBeanParameterInfo mbeanParameterInfo : mbeanParameterInfos) {
            signature.add(mbeanParameterInfo.getType());
        }

        Object[] convertedArguments = convertValues(arguments, signature);

        logger.debug("Invoke {}:{}({}) ...", on, operationName, Arrays.asList(convertedArguments));

        Object result = mBeanServer.invoke(on, operationName, convertedArguments, signature.toArray(new String[0]));

        logger.info("Invoke {}:{}({}): {}", on, operationName, Arrays.asList(convertedArguments), result);

        return result;
    }

    /**
     * Convert given values
     *
     * @return
     */
    @Nonnull
    protected Object[] convertValues(@Nonnull String[] arguments, @Nonnull List<String> signature) {
        if (arguments.length != signature.size())
            throw new IllegalArgumentException("arguments and signature must have the same length (" + arguments.length + " vs. " + signature.size() + "");

        Object[] results = new Object[arguments.length];
        for (int i = 0; i < arguments.length; i++) {
            results[i] = convertValue(arguments[i], signature.get(i));
        }
        return results;
    }

    /**
     * Convert the given <code>value</code> to the given <code>targetType</code>.
     * </p>
     * Special value '{@value #NULL_VALUE}' is seen as <code>null</code>.
     *
     * @param value
     * @param targetType The value {@link Class#getName()} such as <code>java.lang.String</code> if <code>null</code>, target type is assumed to be String.
     * @return the converted value
     * @see javax.management.MBeanParameterInfo#getType()
     */
    @Nullable
    private Object convertValue(@Nullable String value, @Nullable String targetType) {
        if (NULL_VALUE.equals(value) || null == value) {
            return null;
        } else if (String.class.getName().equals(targetType)) {
            return value;
        } else if (int.class.getName().equals(targetType) || Integer.class.getName().equals(targetType)) {
            return new Integer(value);
        } else if (float.class.getName().equals(targetType) || Float.class.getName().equals(targetType)) {
            return new Float(value);
        } else if (boolean.class.getName().equals(targetType) || Boolean.class.getName().equals(targetType)) {
            return Boolean.valueOf(value);
        } else {
            logger.warn("Unexpected type {} for value {}, return String", targetType, value);
            return value;
        }
    }

    @Nonnull
    protected MBeanServerConnection connectToMbeanServer(@Nonnull String pid) throws IOException {
        VirtualMachine vm;
        try {
            Integer.parseInt(pid);
        } catch (Exception e) {
            logger.warn("Exception parsing PID '{}'", pid, e);
        }
        try {
            vm = VirtualMachine.attach(pid);
        } catch (Exception e) {
            throw new IllegalStateException("Exception attaching VM with PID '" + pid + "'", e);
        }


        logger.trace("VM Agent Properties");
        for (String key : new TreeSet<String>(vm.getAgentProperties().stringPropertyNames())) {
            logger.trace("\t {}: {}", key, vm.getAgentProperties().get(key));
        }
        logger.trace("VM System Properties");
        for (String key : new TreeSet<String>(vm.getSystemProperties().stringPropertyNames())) {
            logger.trace("\t {}: {}", key, vm.getSystemProperties().get(key));
        }


        String connectorAddress =
                vm.getAgentProperties().getProperty("com.sun.management.jmxremote.localConnectorAddress");
        if (connectorAddress == null) {
            String agent = vm.getSystemProperties().getProperty(
                    "java.home") + File.separator + "lib" + File.separator +
                    "management-agent.jar";
            try {
                vm.loadAgent(agent);
            } catch (Exception e) {
                throw new IllegalStateException("Exception loading agent " + agent);
            }
            connectorAddress = vm.getAgentProperties().getProperty(
                    "com.sun.management.jmxremote.localConnectorAddress");
        }

        if (connectorAddress == null) {
            throw new IllegalStateException("Could not attach to pid: " + pid + ". No connector available");
        }
        logger.trace("Connect to {} ...", connectorAddress);

        JMXConnector connector = JMXConnectorFactory.connect(new JMXServiceURL(connectorAddress));

        return connector.getMBeanServerConnection();
    }

    static {
        // configure Log4j SimpleLogger
        if (!System.getProperties().contains(SimpleLogger.DATE_TIME_FORMAT_KEY))
            System.setProperty(SimpleLogger.DATE_TIME_FORMAT_KEY, "yyyy-MM-dd'T'HH:mm:ss.SSSZ");
        if (!System.getProperties().contains(SimpleLogger.LOG_FILE_KEY))
            System.setProperty(SimpleLogger.LOG_FILE_KEY, "System.out");


    }

    static class JmxInvokerArguments {

        @Option(name = "-p", aliases = "--pid", required = false, metaVar = "PID", usage = "PID of the JVM to attach to. " +
                "--pid or --pid-file required")
        public String pid;
        @Option(name = "--pid-file", required = false, metaVar = "PID_FILE", usage = "PID FILE of the JVM to attach to. " +
                "--pid or --pid-file required")
        public File pidFile;
        @Option(name = "-on", aliases = "--object-name", required = true, handler = ObjectNameOptionHandler.class, metaVar = "OBJECT_NAME", usage = "ObjectName of the MBean(s) to invoke, can contain wildcards (*). If more than one MBean match, all MBeans are invoked")
        public ObjectName objectName;
        @Option(name = "-attr", aliases = "--attribute", required = false, handler = StringArrayOptionHandler.class, metaVar = "ATTRIBUTE_NAME [VAL]", usage = "attribute to read or to update. " +
                "If a VAL is passed, then it is a write action, otherwise, it is a read action")
        public String[] attribute;
        @Option(name = "-op", aliases = "--operation", required = false, handler = StringArrayOptionHandler.class, metaVar = "OPERATION_NAME [ARG1 [ARG2  ...]]", usage = "operation to invoke with arguments")
        public String[] operation;
        @Option(name = "-v", aliases = {"-x", "--verbose"}, usage = "print debug info ")
        public boolean verbose;
        @Option(name = "-vvv", aliases = "-xxx", usage = "print super verbose debug info ")
        public boolean superVerbose;
        @Option(name = "-h", aliases = "--help", usage = "print help")
        public boolean showHelp;
        public CmdLineParser cmdLineParser;


    }
}
TOP

Related Classes of com.cloudbees.jmx.JmxInvoker$JmxInvokerArguments

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.