001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.activemq.console.command;
018
019import java.io.File;
020import java.io.IOException;
021import java.lang.management.ManagementFactory;
022import java.lang.reflect.Method;
023import java.net.ConnectException;
024import java.net.MalformedURLException;
025import java.net.URL;
026import java.net.URLClassLoader;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.Properties;
031
032import javax.management.MBeanServerConnection;
033import javax.management.remote.JMXConnector;
034import javax.management.remote.JMXConnectorFactory;
035import javax.management.remote.JMXServiceURL;
036
037public abstract class AbstractJmxCommand extends AbstractCommand {
038    public static String DEFAULT_JMX_URL;
039    private static String jmxUser;
040    private static String jmxPassword;
041    private static boolean jmxUseLocal;
042    private static final String CONNECTOR_ADDRESS =
043        "com.sun.management.jmxremote.localConnectorAddress";
044
045    private JMXServiceURL jmxServiceUrl;
046    private JMXConnector jmxConnector;
047    private MBeanServerConnection jmxConnection;
048
049    static {
050        DEFAULT_JMX_URL = System.getProperty("activemq.jmx.url", "service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi");
051        jmxUser = System.getProperty("activemq.jmx.user");
052        jmxPassword = System.getProperty("activemq.jmx.password");
053        jmxUseLocal = Boolean.parseBoolean(System.getProperty("activemq.jmx.useLocal", "false"));
054    }
055
056    /**
057     * Get the current specified JMX service url.
058     * @return JMX service url
059     */
060    protected JMXServiceURL getJmxServiceUrl() {
061        return jmxServiceUrl;
062    }
063
064    public static String getJVM() {
065        return System.getProperty("java.vm.specification.vendor");
066    }
067
068    public static boolean isSunJVM() {
069        // need to check for Oracle as that is the name for Java7 onwards.
070        return getJVM().equals("Sun Microsystems Inc.") || getJVM().startsWith("Oracle");
071    }
072
073    /**
074     * Finds the JMX Url for a VM by its process id
075     *
076     * @param pid
077     *          The process id value of the VM to search for.
078     *
079     * @return the JMX Url of the VM with the given pid or null if not found.
080     */
081    @SuppressWarnings({ "rawtypes", "unchecked" })
082    protected String findJMXUrlByProcessId(int pid) {
083
084        if (isSunJVM()) {
085            try {
086                // Classes are all dynamically loaded, since they are specific to Sun VM
087                // if it fails for any reason default jmx url will be used
088
089                // tools.jar are not always included used by default class loader, so we
090                // will try to use custom loader that will try to load tools.jar
091
092                String javaHome = System.getProperty("java.home");
093                String tools = javaHome + File.separator +
094                        ".." + File.separator + "lib" + File.separator + "tools.jar";
095                URLClassLoader loader = new URLClassLoader(new URL[]{new File(tools).toURI().toURL()});
096
097                Class virtualMachine = Class.forName("com.sun.tools.attach.VirtualMachine", true, loader);
098                Class virtualMachineDescriptor = Class.forName("com.sun.tools.attach.VirtualMachineDescriptor", true, loader);
099
100                Method getVMList = virtualMachine.getMethod("list", (Class[])null);
101                Method attachToVM = virtualMachine.getMethod("attach", String.class);
102                Method getAgentProperties = virtualMachine.getMethod("getAgentProperties", (Class[])null);
103                Method getVMId = virtualMachineDescriptor.getMethod("id",  (Class[])null);
104
105                List allVMs = (List)getVMList.invoke(null, (Object[])null);
106
107                for(Object vmInstance : allVMs) {
108                    String id = (String)getVMId.invoke(vmInstance, (Object[])null);
109                    if (id.equals(Integer.toString(pid))) {
110
111                        Object vm = attachToVM.invoke(null, id);
112
113                        Properties agentProperties = (Properties)getAgentProperties.invoke(vm, (Object[])null);
114                        String connectorAddress = agentProperties.getProperty(CONNECTOR_ADDRESS);
115
116                        if (connectorAddress != null) {
117                            return connectorAddress;
118                        } else {
119                            break;
120                        }
121                    }
122                }
123            } catch (Exception ignore) {
124            }
125        }
126
127        return null;
128    }
129
130    /**
131     * Get the current JMX service url being used, or create a default one if no JMX service url has been specified.
132     * @return JMX service url
133     * @throws MalformedURLException
134     */
135    @SuppressWarnings({ "rawtypes", "unchecked" })
136    protected JMXServiceURL useJmxServiceUrl() throws MalformedURLException {
137        if (getJmxServiceUrl() == null) {
138            String jmxUrl = DEFAULT_JMX_URL;
139            int connectingPid = -1;
140            if (isSunJVM()) {
141                try {
142                    // Classes are all dynamically loaded, since they are specific to Sun VM
143                    // if it fails for any reason default jmx url will be used
144
145                    // tools.jar are not always included used by default class loader, so we
146                    // will try to use custom loader that will try to load tools.jar
147
148                    String javaHome = System.getProperty("java.home");
149                    String tools = javaHome + File.separator +
150                            ".." + File.separator + "lib" + File.separator + "tools.jar";
151                    URLClassLoader loader = new URLClassLoader(new URL[]{new File(tools).toURI().toURL()});
152
153                    Class virtualMachine = Class.forName("com.sun.tools.attach.VirtualMachine", true, loader);
154                    Class virtualMachineDescriptor = Class.forName("com.sun.tools.attach.VirtualMachineDescriptor", true, loader);
155
156                    Method getVMList = virtualMachine.getMethod("list", (Class[])null);
157                    Method attachToVM = virtualMachine.getMethod("attach", String.class);
158                    Method getAgentProperties = virtualMachine.getMethod("getAgentProperties", (Class[])null);
159                    Method getVMDescriptor = virtualMachineDescriptor.getMethod("displayName",  (Class[])null);
160                    Method getVMId = virtualMachineDescriptor.getMethod("id",  (Class[])null);
161
162                    List allVMs = (List)getVMList.invoke(null, (Object[])null);
163
164                    for(Object vmInstance : allVMs) {
165                        String displayName = (String)getVMDescriptor.invoke(vmInstance, (Object[])null);
166                        if (displayName.contains("activemq.jar start")) {
167                            String id = (String)getVMId.invoke(vmInstance, (Object[])null);
168
169                            Object vm = attachToVM.invoke(null, id);
170
171                            Properties agentProperties = (Properties)getAgentProperties.invoke(vm, (Object[])null);
172                            String connectorAddress = agentProperties.getProperty(CONNECTOR_ADDRESS);
173
174                            if (connectorAddress != null) {
175                                jmxUrl = connectorAddress;
176                                connectingPid = Integer.parseInt(id);
177                                context.print("useJmxServiceUrl Found JMS Url: " + jmxUrl);
178                                break;
179                            }
180                        }
181                    }
182                } catch (Exception ignore) {
183                }
184            }
185
186            if (connectingPid != -1) {
187                context.print("Connecting to pid: " + connectingPid);
188            } else {
189                context.print("Connecting to JMX URL: " + jmxUrl);
190            }
191            setJmxServiceUrl(jmxUrl);
192        }
193
194        return getJmxServiceUrl();
195    }
196
197    /**
198     * Sets the JMX service url to use.
199     * @param jmxServiceUrl - new JMX service url to use
200     */
201    protected void setJmxServiceUrl(JMXServiceURL jmxServiceUrl) {
202        this.jmxServiceUrl = jmxServiceUrl;
203    }
204
205    /**
206     * Sets the JMX service url to use.
207     * @param jmxServiceUrl - new JMX service url to use
208     * @throws MalformedURLException
209     */
210    protected void setJmxServiceUrl(String jmxServiceUrl) throws MalformedURLException {
211        setJmxServiceUrl(new JMXServiceURL(jmxServiceUrl));
212    }
213
214    /**
215     * Get the JMX user name to be used when authenticating.
216     * @return the JMX user name
217     */
218    public String getJmxUser() {
219        return jmxUser;
220    }
221
222    /**
223     * Sets the JMS user name to use
224     * @param jmxUser - the jmx
225     */
226    public void setJmxUser(String jmxUser) {
227        AbstractJmxCommand.jmxUser = jmxUser;
228    }
229
230    /**
231     * Get the password used when authenticating
232     * @return the password used for JMX authentication
233     */
234    public String getJmxPassword() {
235        return jmxPassword;
236    }
237
238    /**
239     * Sets the password to use when authenticating
240     * @param jmxPassword - the password used for JMX authentication
241     */
242    public void setJmxPassword(String jmxPassword) {
243        AbstractJmxCommand.jmxPassword = jmxPassword;
244    }
245
246    /**
247     * Get whether the default mbean server for this JVM should be used instead of the jmx url
248     * @return <code>true</code> if the mbean server from this JVM should be used, <code>false<code> if the jmx url should be used
249     */
250    public boolean isJmxUseLocal() {
251        return jmxUseLocal;
252    }
253
254    /**
255     * Sets whether the the default mbean server for this JVM should be used instead of the jmx url
256     * @param jmxUseLocal - <code>true</code> if the mbean server from this JVM should be used, <code>false<code> if the jmx url should be used
257     */
258    public void setJmxUseLocal(boolean jmxUseLocal) {
259        AbstractJmxCommand.jmxUseLocal = jmxUseLocal;
260    }
261
262    /**
263     * Create a JMX connector using the current specified JMX service url. If there is an existing connection,
264     * it tries to reuse this connection.
265     * @return created JMX connector
266     * @throws IOException
267     */
268    private JMXConnector createJmxConnector() throws IOException {
269        // Reuse the previous connection
270        if (jmxConnector != null) {
271            jmxConnector.connect();
272            return jmxConnector;
273        }
274
275        // Create a new JMX connector
276        if (jmxUser != null && jmxPassword != null) {
277            Map<String,Object> props = new HashMap<String,Object>();
278            props.put(JMXConnector.CREDENTIALS, new String[] { jmxUser, jmxPassword });
279            jmxConnector = JMXConnectorFactory.connect(useJmxServiceUrl(), props);
280        } else {
281            jmxConnector = JMXConnectorFactory.connect(useJmxServiceUrl());
282        }
283        return jmxConnector;
284    }
285
286    /**
287     * Close the current JMX connector
288     */
289    protected void closeJmxConnection() {
290        try {
291            if (jmxConnector != null) {
292                jmxConnector.close();
293                jmxConnector = null;
294            }
295        } catch (IOException e) {
296        }
297    }
298
299    protected MBeanServerConnection createJmxConnection() throws IOException {
300        if (jmxConnection == null) {
301            if (isJmxUseLocal()) {
302                jmxConnection = ManagementFactory.getPlatformMBeanServer();
303            } else {
304                jmxConnection = createJmxConnector().getMBeanServerConnection();
305            }
306        }
307        return jmxConnection;
308    }
309
310    /**
311     * Handle the --jmxurl option.
312     * @param token - option token to handle
313     * @param tokens - succeeding command arguments
314     * @throws Exception
315     */
316    @Override
317    protected void handleOption(String token, List<String> tokens) throws Exception {
318        // Try to handle the options first
319        if (token.equals("--jmxurl")) {
320            // If no jmx url specified, or next token is a new option
321            if (tokens.isEmpty() || tokens.get(0).startsWith("-")) {
322                context.printException(new IllegalArgumentException("JMX URL not specified."));
323            }
324
325            // If jmx url already specified
326            if (getJmxServiceUrl() != null) {
327                context.printException(new IllegalArgumentException("Multiple JMX URL cannot be specified."));
328                tokens.clear();
329            }
330
331            String strJmxUrl = tokens.remove(0);
332            try {
333                this.setJmxServiceUrl(new JMXServiceURL(strJmxUrl));
334            } catch (MalformedURLException e) {
335                context.printException(e);
336                tokens.clear();
337            }
338        } else if(token.equals("--pid")) {
339           if (isSunJVM()) {
340               if (tokens.isEmpty() || tokens.get(0).startsWith("-")) {
341                   context.printException(new IllegalArgumentException("pid not specified"));
342                   return;
343               }
344               int pid = Integer.parseInt(tokens.remove(0));
345               context.print("Connecting to pid: " + pid);
346
347               String jmxUrl = findJMXUrlByProcessId(pid);
348               if (jmxUrl != null) {
349                   // If jmx url already specified
350                   if (getJmxServiceUrl() != null) {
351                       context.printException(new IllegalArgumentException("JMX URL already specified."));
352                       tokens.clear();
353                   }
354                   try {
355                       this.setJmxServiceUrl(new JMXServiceURL(jmxUrl));
356                   } catch (MalformedURLException e) {
357                       context.printException(e);
358                       tokens.clear();
359                   }
360               } else {
361                   context.printInfo("failed to resolve jmxUrl for pid:" + pid + ", using default JMX url");
362               }
363           }  else {
364              context.printInfo("--pid option is not available for this VM, using default JMX url");
365           }
366        } else if (token.equals("--jmxuser")) {
367            // If no jmx user specified, or next token is a new option
368            if (tokens.isEmpty() || tokens.get(0).startsWith("-")) {
369                context.printException(new IllegalArgumentException("JMX user not specified."));
370            }
371            this.setJmxUser(tokens.remove(0));
372        } else if (token.equals("--jmxpassword")) {
373            // If no jmx password specified, or next token is a new option
374            if (tokens.isEmpty() || tokens.get(0).startsWith("-")) {
375                context.printException(new IllegalArgumentException("JMX password not specified."));
376            }
377            this.setJmxPassword(tokens.remove(0));
378        } else if (token.equals("--jmxlocal")) {
379            this.setJmxUseLocal(true);
380        } else {
381            // Let the super class handle the option
382            super.handleOption(token, tokens);
383        }
384    }
385
386    @Override
387    public void execute(List<String> tokens) throws Exception {
388        try {
389            super.execute(tokens);
390        } catch (Exception exception) {
391            handleException(exception, jmxServiceUrl.toString());
392            return;
393        }finally {
394            closeJmxConnection();
395        }
396    }
397}