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