/* * RHQ Management Platform * Copyright (C) 2005-2008 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2, as * published by the Free Software Foundation, and/or the GNU Lesser * General Public License, version 2.1, also as published by the Free * Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License and the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU General Public License * and the GNU Lesser General Public License along with this program; * if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.rhq.core.system.pquery; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.rhq.core.system.NativeSystemInfo; import org.rhq.core.system.ProcessInfo; import org.rhq.core.system.pquery.Conditional.Qualifier; /** * Performs a query over a set of {@link ProcessInfo#getCommandLine() command line strings}. The query strings are * written in the <i>Process Info Query Language</i> (PIQL, pronounced <i>pickle</i>).In effect, your PIQL will examine * a list of running processes whose command lines match a certain set of criteria. PIQL statements are formatted by a * series of <i>criteria</i>, with each criteria separated with a comma: * * <pre>CRITERIA[,CRITERIA]*</pre> * * <p>Criteria are formatted in the following manner:</p> * * <pre>CONDITIONAL=VALUE</pre> * * <p><i>VALUE</i> is either a regular expression or a pid filename to compare a value obtained using the <i> * CONDITIONAL</i>. See the class javadoc for <code>java.util.regex.Pattern</code> to learn the syntax of valid regular * expressions. A <i>CONDITIONAL</i> is defined as:</p> * * <pre>CATEGORY|ATTRIBUTE|OPERATOR[|QUALIFIER]</pre> * * <p>where:</p> * * <ul> * <li><i>CATEGORY</i> is either <b>process</b> or <b>arg</b></li> * <li><i>ATTRIBUTE</i> declares what is to be matched; value depends on the category - see below</li> * <li><i>OPERATOR</i> is the conditional check made against the given <i>VALUE</i> - see below</li> * <li><i>QUALIFIER</i> is an optional query flag; <code>parent</code> is the only value currently allowed</li> * </ul> * * <p>The <i>ATTRIBUTE</i> can be one of the following:</p> * * <ul> * <li>If <i>CATEGORY</i> is <b>process</b>: * * <ul> * <li><b>name</b> - the full path of the executable (i.e. the full string of the first command line argument) * </li> * <li><b>basename</b> - just the executable filename (not including any path information)</li> * <li><b>pidfile</b> - the contents of a file, assumed to be a single number representing a pid</li> * <li><b>pid</b> - the pid itself (allows you to match a process in which you already know its pid)</li> * </ul> * </li> * <li>If <i>CATEGORY</i> is <b>arg</b>: * * <ul> * <li><b><#></b> - a specific argument; this is an index in the process' command line arguments array (where * argument 0 maps to the process name, -1 maps to the last argument)</li> * <li><b>*</b> - a literal asterisk means any argument can match</li> * <li><b><argname></b> - the name of the argument (e.g. "-b", "--port", "verbose")</li> * </ul> * </li> * </ul> * * <p>The <i>OPERATOR</i> can be one of the following:</p> * * <ul> * <li><b>match</b> - the <i>VALUE</i> regular expression must match</li> * <li><b>nomatch</b> - the <i>VALUE</i> regular expression must not match</li> * </ul> * * <p>Some examples of PIQL are:</p> * * <table border="1"> * <tr> * <th>PIQL</th> * <th>What is matched</th> * </tr> * <tr> * <td><code>process|pidfile|match=/etc/product/lock.pid</code></td> * <td>the process whose pid matches the number found in the lock.pid file</td> * </tr> * <tr> * <td><code>process|pidfile|match|parent=/etc/product/lock.pid</code></td> * <td>child processes of the parent process whose pid matches the number found in the lock.pid file</td> * </tr> * <tr> * <td><code>process|name|match=^/foo.*</code></td> * <td>all processes whose executables are found under the root "foo" directory</td> * </tr> * <tr> * <td><code>process|basename|match=^java.*</code></td> * <td>all processes whose executable file has "java" at the start of it</td> * </tr> * <tr> * <td><code>process|basename|match=(?i)^java.*</code></td> * <td>all processes whose executable file has "java" at the start of it (case insensitive, so "JAVA" would also * match)</td> * </tr> * <tr> * <td><code>process|name|match=.*(product|java).*</code></td> * <td>all processes whose executable paths have either "product" or "java" in them</td> * </tr> * <tr> * <td><code>process|name|match=^C:.*,process|basename|nomatch=java.exe</code></td> * <td>all processes whose executables are found on the Windows C: drive but is not a "java.exe" process</td> * </tr> * <tr> * <td><code>arg|1|match=org\.jboss\.Main</code></td> * <td>all processes whose command line argument #1 has a value of "org.jboss.Main". This will NOT match a process * that does not have a command line argument at the given index.</td> * </tr> * <tr> * <td><code>arg|*|match=.*daemon.*</code></td> * <td>all processes whose command lines have any argument with the substring "daemon" in them</td> * </tr> * <tr> * <td><code>arg|-b|nomatch=127\.0\.0\.1</code></td> * <td>all processes whose command lines have any argument named "-b" whose value is not "127.0.0.1" (e.g. "-b * 192.168.0.5"). This will NOT match a process that does not have that argument at all.</td> * </tr> * <tr> * <td><code>arg|-Dbind.address|match=127.0.0.1</code></td> * <td>all processes whose command lines have any argument named "bind.address" whose value is "127.0.0.1" (e.g. * "-Dbind.address=127.0.0.1"). This will NOT match a process that does not have that argument at all.</td> * </tr> * <tr> * <td><code>arg|-cp|match=.*org\.abc\.Class.*</code></td> * <td>all processes whose command lines have any argument named "-cp" whose value contains "org.abc.Class". This * will NOT match a process that does not have that argument at all.</td> * </tr> * <tr> * <td><code>arg|org.jboss.Main|match=.*</code></td> * <td>all processes whose command lines have any argument named "org.jboss.Main"</td> * </tr> * <tr> * <td><code>process|basename|match=(?i)Apache.exe,arg|-k|match|parent=runservice</code></td> * <td>all Apache processes that are running as child processes to the main Apache service.</td> * </tr> * <tr> * <td><code>process|basename|nomatch|parent=exec</code></td> * <td>all processes that have a parent whose basename is not exec. This will match all processes that do not have a * parent.</td> * </tr> * <tr> * * <td> * <code>process|basename|match=^(https?d.*|[Aa]pache)$,process|name|nomatch|parent=^(https?d.*|[Aa]pache)$</code> * </td> * <td>all Apache processes that do not have a parent process that is also an Apache process (i.e. this eliminates * all of the httpd child processes and only returns the main Apache servers). This will match a process that does * not have a parent but has a basename of Apache.</td> * </tr> * <tr> * <td><code>process|pid|match=1016</code></td> * <td>The process whose pid is 1016.</td> * </tr> * </table> * * @author John Mazzitelli */ public class ProcessInfoQuery { private static final Log log = LogFactory.getLog(ProcessInfoQuery.class); /** * The map of all processes keyed on their pids. */ private final Map<Long, ProcessInfo> allProcesses; /** * Constructor for {@link ProcessInfoQuery} given an collection of process information that represents the processes * currently running. Think of the <code>processes</code> data as coming from part of the output you see in the * typical UNIX "ps" command. * * @param processes * * @see NativeSystemInfo#getAllProcesses() */ public ProcessInfoQuery(List<ProcessInfo> processes) { this.allProcesses = new HashMap<Long, ProcessInfo>(processes.size()); for (ProcessInfo process : processes) { this.allProcesses.put(process.getPid(), process); } } /** * Returns the list of all the {@link ProcessInfo processes} that this object will {@link #query(String) query} * against. * * @return all processes this object knows about */ public List<ProcessInfo> getProcesses() { return new ArrayList<ProcessInfo>(allProcesses.values()); } /** * Performs a query on the set of known processes where <code>query</code> defines the criteria. * * @param query the query string containing the criteria to match * * @return the matches processes' command lines * * @throws IllegalArgumentException if the query was invalid */ public List<ProcessInfo> query(String query) { List<Criteria> criteriaList = getCriteriaList(query); // if we got an empty query - it means we match nothing so return an empty list immediately if (criteriaList.size() == 0) { return new ArrayList<ProcessInfo>(); } // keyed on pid so we automatically avoid dups (in case more than one criteria matches) Map<Long, ProcessInfo> queryResults = new HashMap<Long, ProcessInfo>(this.allProcesses); Map<Long, ProcessInfo> criteriaResults; for (Criteria criteria : criteriaList) { if (criteria.getConditional().getCategory().equals(Conditional.Category.process)) { criteriaResults = doProcessCriteriaQuery(criteria); } else if (criteria.getConditional().getCategory().equals(Conditional.Category.arg)) { criteriaResults = doArgCriteriaQuery(criteria); } else { throw new IllegalArgumentException("Unknown category: " + criteria); // should never happen } // multiple criteria results are ANDed together // only retain those previously matched processes that were also matched in the latest criteria Set<Long> pids = new HashSet<Long>(queryResults.keySet()); // new set to avoid concurrent mod exceptions for (Long pid : pids) { if (!criteriaResults.containsKey(pid)) { // a previously matched process was not matched in the latest criteria, so removed it queryResults.remove(pid); } } if (queryResults.size() == 0) { // we've eliminated every possible process - don't bother running any more criteria break; } } List<ProcessInfo> results = new ArrayList<ProcessInfo>(queryResults.size()); results.addAll(queryResults.values()); return results; } /** * Runs the given criteria with the arg conditional and returns the processes that match. * * @param criteria the criteria with the arg conditional * * @return the matched processes keyed on the pids * * @throws IllegalArgumentException */ private Map<Long, ProcessInfo> doArgCriteriaQuery(Criteria criteria) { Map<Long, ProcessInfo> matches = new HashMap<Long, ProcessInfo>(); Attribute attribute = criteria.getConditional().getAttribute(); Operation op = new Operation(criteria.getConditional().getOperator()); Qualifier qualifier = criteria.getConditional().getQualifier(); String operand1 = null; String operand2 = criteria.getValue(); for (ProcessInfo process : getProcesses()) { ProcessInfo processToMatch; // will be the same as process unless the parent qualifier was provided if (qualifier.equals(Qualifier.parent)) { processToMatch = getParentProcess(process); } else { processToMatch = process; } String[] cmdline = (processToMatch != null) ? processToMatch.getCommandLine() : null; if ((cmdline == null) || (cmdline.length == 0)) { continue; // no sense continuing with this process - there are no command line arguments } if (attribute.getAttributeValue().equals("*")) { // * means see if any arg matches for (String arg : cmdline) { operand1 = arg; if (op.doOperation(operand1, operand2)) { matches.put(process.getPid(), process); break; // we got a match, don't bother looking at more args } } } else if (attribute.getAttributeValueAsInteger() != null) { // if we get here, it means the argument specified was a specific argument index number int attributeIndex = attribute.getAttributeValueAsInteger().intValue(); // an arg of -1 means the query wants to obtain the last argument in the command line if (attributeIndex < 0) { attributeIndex = cmdline.length - 1; } if ((cmdline.length - 1) < attributeIndex) { continue; // process doesn't have enough args - there is no command line argument with that index } operand1 = cmdline[attributeIndex]; if (op.doOperation(operand1, operand2)) { matches.put(process.getPid(), process); } } else { // if we get here, it means the attribute specified was the name of an argument String attributeName = attribute.getAttributeValue(); for (int i = 0; i < cmdline.length; i++) { String arg = cmdline[i]; // if the arg name doesn't even start with our attribute, then we continue on to the next if (arg.startsWith(attributeName)) { if (arg.equals(attributeName)) { // the full argument name is the attribute name, the command line was something like: // "exec.exe -arg value" or "exec.exe -arg" so the value is the next argument operand1 = ((i + 1) < cmdline.length) ? cmdline[i + 1] : ""; } else { // the command line was something like: "exec.exe -arg=value" so the value is after the equals side within the arg int equals = arg.indexOf('='); if (equals == -1) { continue; // the argument looked like what we were trying to find, but it really wasn't } operand1 = (arg.length() > (equals + 1)) ? arg.substring(equals + 1) : ""; } if (op.doOperation(operand1, operand2)) { matches.put(process.getPid(), process); break; // no need to continue, we've got the match we are looking for } } } } } return matches; } /** * Runs the given criteria with the process conditional and returns the processes that match. * * @param criteria the criteria with the process conditional * * @return the matched processes keyed on the pids * * @throws IllegalArgumentException */ private Map<Long, ProcessInfo> doProcessCriteriaQuery(Criteria criteria) { Map<Long, ProcessInfo> matches = new HashMap<Long, ProcessInfo>(); Attribute attribute = criteria.getConditional().getAttribute(); Operation op = new Operation(criteria.getConditional().getOperator()); Qualifier qualifier = criteria.getConditional().getQualifier(); String operand1; String operand2; String pidfileContentsCache = null; // so we avoid reading the file over and over again for (ProcessInfo process : getProcesses()) { ProcessInfo processToMatch; // will be the same as process unless the parent qualifier was provided if (qualifier.equals(Qualifier.parent)) { processToMatch = getParentProcess(process); } else { processToMatch = process; } if (attribute.getAttributeValue().equals(Attribute.ProcessCategoryAttributes.name.toString())) { operand1 = (processToMatch != null) ? processToMatch.getName() : ""; operand2 = criteria.getValue(); } else if (attribute.getAttributeValue().equals(Attribute.ProcessCategoryAttributes.basename.toString())) { operand1 = (processToMatch != null) ? processToMatch.getBaseName() : ""; operand2 = criteria.getValue(); } else if (attribute.getAttributeValue().equals(Attribute.ProcessCategoryAttributes.pid.toString())) { operand1 = (processToMatch != null) ? Long.toString(processToMatch.getPid()) : ""; operand2 = criteria.getValue(); } else if (attribute.getAttributeValue().equals(Attribute.ProcessCategoryAttributes.pidfile.toString())) { if (pidfileContentsCache == null) { pidfileContentsCache = getPidfileContents(criteria.getValue()); } operand1 = (processToMatch != null) ? String.valueOf(processToMatch.getPid()) : null; operand2 = pidfileContentsCache; } else { throw new IllegalArgumentException( "Criteria with 'process' category must have an attribute of either 'name' or 'basename': " + criteria); } if (op.doOperation(operand1, operand2)) { matches.put(process.getPid(), process); } } return matches; } /** * Gets the parent process for the given process. The parent will be searched for within the {@link #getProcesses()} * list. * * @param child * * @return the child's parent process or <code>null</code> if the child has no parent */ private ProcessInfo getParentProcess(ProcessInfo child) { ProcessInfo parent = null; if (child != null) { parent = this.allProcesses.get(child.getParentPid()); } return parent; } private List<Criteria> getCriteriaList(String query) { List<Criteria> criteria = new ArrayList<Criteria>(); if (query != null) { String[] tokens = query.split(","); for (String criteriaString : tokens) { Criteria c = new Criteria(criteriaString); criteria.add(c); } } return criteria; } private String getPidfileContents(String pidfileName) { String contents; try { FileInputStream fis = new FileInputStream(pidfileName); BufferedReader br = new BufferedReader(new InputStreamReader(fis)); try { contents = br.readLine(); if (contents == null) { throw new IOException("empty pid"); } } finally { fis.close(); } } catch (FileNotFoundException e) { log.trace("pid not found"); return ""; } catch (IOException e) { log.warn("unable to read pid file " + pidfileName, e); return ""; } return contents.trim(); } }