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     */
017    package org.apache.activemq.console.command;
018    
019    import java.io.File;
020    import java.io.IOException;
021    import java.lang.management.ManagementFactory;
022    import java.lang.reflect.Method;
023    import java.net.MalformedURLException;
024    import java.net.URL;
025    import java.net.URLClassLoader;
026    import java.util.HashMap;
027    import java.util.List;
028    import java.util.Map;
029    import java.util.Properties;
030    
031    import javax.management.MBeanServerConnection;
032    import javax.management.remote.JMXConnector;
033    import javax.management.remote.JMXConnectorFactory;
034    import javax.management.remote.JMXServiceURL;
035    
036    public 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    }