Tuesday, April 21, 2009

Detect Local Processes for JMX access

     People who dealt JMX must know about jconsole -- a very useful JMX-compliant monitoring tool. So when I was asked to create a web application that monitors JMX enabled remote servers, I tried to mimic the same functionalities that jconsole offers. One thing in particular, is that how jconsole detects local processes that are available for viewing.

     First of all, jconsole from Java5 and Java6 has different behavior on detection. Java5 only lists local processes that are JMX enabled while Java6 lists all local processes and greys out the ones that is not available for viewing and has the ability to enable JMX agent on a process that can be viewed.

     I think this is because of the attach API introduced in Java6. By using the VirtualMachine class from attach API, I'm able to tell whether a process is running under Java6. As long as a process is running using Java6, VirtualMachine can load an agent to it and thus enable the JMX viewing in jconsole. Knowing where the difference comes from, I created a simple class to mimic the very same function using attach API in conjunction with jps runtime command.


package net.katiewang.jmx.local;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import sun.tools.jps.Jps;

import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

/**
 * @author KWang
 *
 */
public class DetectLocalProcess {

   protected static enum STATUS{enabled, notEnalbed, willBeEnabled};

    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        // first round: get all process info.
        // process id is the key, name of the process is the value
        // name includes full package name for the application's main class
        // or the full path name to the application's JAR file
        // along with the arguments passed to the main method
        Map processes = getProcesses();

        // second round: detect if jmx is enabled
        // process id is the key, whether the process is jmx enabled is the
        // value
        Map jmx = detectJMXAgent(processes.keySet());

        printProcessInfo(processes, jmx);
    }

    private static void printProcessInfo(Map processes, Map jmx){
        for(Map.Entry process : jmx.entrySet()){
        System.out.println("******************************");
        System.out.println("PID: " + process.getKey());
        System.out.println("Name: " + processes.get(process.getKey()));
        System.out.println("Status: " + getString(process.getValue()));
        }
    }

    private static String getString(STATUS value) {
        switch (value) {
            case enabled:
                return "Enabled";
            case willBeEnabled:
                return "Will Be Enabled";
            case notEnalbed:
            default:
                return "Not Enabled";
        }
        }

    private static Map detectJMXAgent(Set processids) throws IOException {
        Map jmx = new TreeMap();

        Runtime rt = Runtime.getRuntime();

        BufferedReader br = new BufferedReader(new InputStreamReader(rt.exec("jps -l -v")
            .getInputStream()));
        String line = new String();
        while ((line = br.readLine()) != null) {
            String[] proc = line.split(" ");
            if (processids.contains(proc[0])){
                STATUS isJmx = STATUS.notEnalbed;
                for(String process : proc){
                    if(process.startsWith("-Dcom.sun.management.jmxremote") ||
                        process.startsWith("-Dcom.sun.management.config.file")){
                        isJmx = STATUS.enabled;
                        break;
                    }
                }
                if(!STATUS.enabled.equals(isJmx)){
                    try {
                        VirtualMachine.attach(proc[0]);
                        isJmx = STATUS.willBeEnabled;
                    } catch (AttachNotSupportedException e) {
                        //do nothing
                    }
                }
                jmx.put(proc[0], isJmx);
            }
        }
        return jmx;
    }

    private static Map getProcesses() throws IOException {
        Map processes = new TreeMap();
        Runtime rt = Runtime.getRuntime();

        BufferedReader br = new BufferedReader(new InputStreamReader(rt.exec(
            "jps -l -m").getInputStream()));
        String line = new String();
        while ((line = br.readLine()) != null) {
            String[] proc = line.split(" ");
            if (proc[1].equals(Jps.class.getName())
                || proc[1].equals(DetectLocalProcess.class.getName()))
                continue; // ignore jps command process and this process

            processes.put(proc[0], line.substring(proc[0].length()).trim());
        }
        return processes;
    }

}


     The result will be something like:

******************************
PID: 2476
Name: org.apache.catalina.startup.Bootstrap start
Status: Not Enabled
******************************
PID: 3872
Name: my_jmx_enabled_app.jar client
Status: Enabled
******************************
PID: 5164
Name: sun.tools.jconsole.JConsole
Status: Will Be Enabled


     With viewable process dectected. Now I can simply using the attach API to create a JMX monitoring service connection.

     Attach API is not included in Java classpath by default. So when running my little test class, you need to either make sure to include tools.jar in your classpath or comment out the if statement which calls VirtualMachine.attach(String processId) method (Of course, in this case, you'll only know processes that are JMX enable at start -- same behavior as jconsole in Java5).

No comments: