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