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