/* * RHQ Management Platform * Copyright (C) 2005-2013 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; import java.io.File; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hyperic.sigar.OperatingSystem; import org.hyperic.sigar.ProcCpu; import org.hyperic.sigar.ProcCred; import org.hyperic.sigar.ProcCredName; import org.hyperic.sigar.ProcExe; import org.hyperic.sigar.ProcFd; import org.hyperic.sigar.ProcMem; import org.hyperic.sigar.ProcStat; import org.hyperic.sigar.ProcState; import org.hyperic.sigar.ProcTime; import org.hyperic.sigar.Sigar; import org.hyperic.sigar.SigarException; import org.hyperic.sigar.SigarNotImplementedException; import org.hyperic.sigar.SigarPermissionDeniedException; import org.hyperic.sigar.SigarProxy; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * <p> * Encapsulates information about a known process. * </p> * <p> * A few process properties (i.e. PID, command line) will never change during the lifetime of the process and can be * read directly with this class accessors. Other process properties (i.e. state, cpu usage) will vary and their values * are grouped in {@link ProcessInfoSnapshot} instances. * </p> * <p> * Operations on static properties of the process must be implemented in the {@link ProcessInfo} type. Operations on * non static properties must be implemented in the {@link ProcessInfoSnapshot} type. The {@link ProcessInfoSnapshot} * subtype has been created to remind users of the class that they are working with cached data. * </p> * <p>For example, if you want to be sure a process is still alive, you should use this code:<br> * <code>processInfo.freshSnapshot().isRunning()</code> * </p> * <p>Rather than:<br> * <code>processInfo.priorSnapshot().isRunning()</code> * </p> * * @author John Mazzitelli * @author Ian Springer * @author Thomas Segismont */ public class ProcessInfo { /** * <p> * Exposes non static process properties and operations computed on them (like {@link #isRunning()} method). * Operations on non static process properties should all be implemented here. * </p> * <p> * New snapshots are created when {@link ProcessInfo#refresh()} or {@link ProcessInfo#freshSnapshot()} are * called. * </p> * <p> * Note the current implementation does not actually encapsulate these properties for backward compatibility * reasons (we have to keep the write access to {@link ProcessInfo} protected properties). * </p> */ public final class ProcessInfoSnapshot { public long getParentPid() throws SystemInfoException { return (ProcessInfo.this.procState != null) ? ProcessInfo.this.procState.getPpid() : 0L; } public ProcState getState() throws SystemInfoException { return ProcessInfo.this.procState; } public ProcExe getExecutable() throws SystemInfoException { return ProcessInfo.this.procExe; } public ProcTime getTime() throws SystemInfoException { return ProcessInfo.this.procTime; } public ProcMem getMemory() throws SystemInfoException { return ProcessInfo.this.procMem; } public ProcCpu getCpu() throws SystemInfoException { return ProcessInfo.this.procCpu; } public ProcFd getFileDescriptor() throws SystemInfoException { return ProcessInfo.this.procFd; } public ProcCred getCredentials() throws SystemInfoException { return ProcessInfo.this.procCred; } public ProcCredName getCredentialsName() throws SystemInfoException { return ProcessInfo.this.procCredName; } /** * @return null if process executable or cwd is unavailable. Otherwise the Cwd as returned from the * process executable. * @throws SystemInfoException */ public String getCurrentWorkingDirectory() throws SystemInfoException { String result = null; try { if (null != ProcessInfo.this.procExe) { result = ProcessInfo.this.procExe.getCwd(); } } catch (Exception e) { ProcessInfo.this.handleSigarCallException(e, "procExe.getCwd()"); } return result; } /** * Checks if the process is alive. * * @return true if the process is running, sleeping or idle * @throws SystemInfoException */ public boolean isRunning() throws SystemInfoException { boolean running = false; if (ProcessInfo.this.procState != null) { running = (ProcessInfo.this.procState.getState() == ProcState.RUN || ProcessInfo.this.procState.getState() == ProcState.SLEEP || ProcessInfo.this.procState .getState() == ProcState.IDLE); } return running; } } private static final Log LOG = LogFactory.getLog(ProcessInfo.class); private static final int REFRESH_LOCK_ACQUIRE_TIMEOUT_SECONDS = 5; private static final String UNKNOWN_PROCESS_NAME = "?"; private static final Set<String> MS_WINDOWS_TERMINATE_SIGNAL_NAMES = new HashSet<String>(); static { MS_WINDOWS_TERMINATE_SIGNAL_NAMES.add("INT"); MS_WINDOWS_TERMINATE_SIGNAL_NAMES.add("KILL"); MS_WINDOWS_TERMINATE_SIGNAL_NAMES.add("QUIT"); MS_WINDOWS_TERMINATE_SIGNAL_NAMES.add("TERM"); } protected boolean initialized; protected SigarProxy sigar; // these are static - values remain for the life of this object protected long pid; protected String name; protected String[] commandLine; protected Map<String, String> procEnv; // these are computed once with static data (purposely lazy in order to speed up discovery process) protected Map<String, String> environmentVariables; protected String baseName; // this one is computed once with non static data protected ProcessInfo parentProcess; // these are refreshed and may change during the life of the process /** * @deprecated as of 4.6. To read this property call {@link #priorSnaphot()} and then corresponding method * from the returned {@link ProcessInfoSnapshot}. */ @Deprecated protected ProcState procState; /** * @deprecated as of 4.6. To read this property call {@link #priorSnaphot()} and then corresponding method * from the returned {@link ProcessInfoSnapshot}. */ @Deprecated protected ProcExe procExe; /** * @deprecated as of 4.6. To read this property call {@link #priorSnaphot()} and then corresponding method * from the returned {@link ProcessInfoSnapshot}. */ @Deprecated protected ProcTime procTime; /** * @deprecated as of 4.6. To read this property call {@link #priorSnaphot()} and then corresponding method * from the returned {@link ProcessInfoSnapshot}. */ @Deprecated protected ProcMem procMem; /** * @deprecated as of 4.6. To read this property call {@link #priorSnaphot()} and then corresponding method * from the returned {@link ProcessInfoSnapshot}. */ @Deprecated protected ProcCpu procCpu; /** * @deprecated as of 4.6. To read this property call {@link #priorSnaphot()} and then corresponding method * from the returned {@link ProcessInfoSnapshot}. */ @Deprecated protected ProcFd procFd; /** * @deprecated as of 4.6. To read this property call {@link #priorSnaphot()} and then corresponding method * from the returned {@link ProcessInfoSnapshot}. */ @Deprecated protected ProcCred procCred; /** * @deprecated as of 4.6. To read this property call {@link #priorSnaphot()} and then corresponding method * from the returned {@link ProcessInfoSnapshot}. */ @Deprecated protected ProcCredName procCredName; // Set to true in handleSigarCallException protected boolean processDied; // Set to true if a SIGAR permission error has already been logged private boolean loggedPermissionsError = false; // The last snasphot of non static process properties // In a future implementation a new snapshot will be created on each call to refresh private ProcessInfoSnapshot snapshot = new ProcessInfoSnapshot(); // A lock to serialize calls to refresh method private ReentrantLock refreshLock = new ReentrantLock(); // useful for mocking this object, this is purposely not public protected ProcessInfo() { } public ProcessInfo(long pid) throws SystemInfoException { this(pid, SigarAccess.getSigar()); } public ProcessInfo(long pid, SigarProxy sigar) throws SystemInfoException { this.pid = pid; this.sigar = sigar; update(pid); } /** * Takes a fresh snapshot of non static properties of the underlying process. This method internally serializes * calls so that it maintains a consistent view of the various Sigar call results. * * @throws SystemInfoException */ public void refresh() throws SystemInfoException { // Serializing is also important as in somes cases, the process could be reported up while being down. // See this thread on VMWare forum: http://communities.vmware.com/message/2187972#2187972 boolean acquiredLock = false; try { acquiredLock = refreshLock.tryLock(REFRESH_LOCK_ACQUIRE_TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (InterruptedException e) { LOG.error("Thread interrupted while trying to acquire ProcessInfo[" + this.pid + "] refresh lock", e); } if (!acquiredLock) { throw new RuntimeException("Could not acquire ProcessInfo[" + this.pid + "] refresh lock"); } try { // No need to update if the process has already been reported down, Sigar will only throw exceptions... if (priorSnaphot().isRunning()) { update(this.pid); } } finally { refreshLock.unlock(); } } // Refresh and update methods cannot be merged because subclasses may override refresh behavior // and we can't be sure that instances will already be properly initialized. private void update(long pid) throws SystemInfoException { long startTime = System.currentTimeMillis(); try { this.processDied = false; // Get ProcState and ProcExe before static data as they can help to determine the name field in some cases. ProcState procState = null; try { procState = sigar.getProcState(pid); } catch (Exception e) { handleSigarCallException(e, "getProcState"); } ProcExe procExe = null; try { procExe = sigar.getProcExe(pid); } catch (Exception e) { handleSigarCallException(e, "getProcExe"); } // If this is the first time we are refreshing this object, initialize the static data. // We only have to do this once, since this data never changes during the life of the process. if (!this.initialized) { String[] procArgs = null; try { procArgs = sigar.getProcArgs(pid); } catch (Exception e) { handleSigarCallException(e, "getProcArgs"); } this.name = determineName(procArgs, procExe, procState); // NOTE: for the sake of efficiency, this.baseName is lazily initialized by its getter. this.commandLine = (procArgs != null) ? procArgs : new String[0]; this.procEnv = null; try { this.procEnv = sigar.getProcEnv(pid); if (this.procEnv == null) { LOG.debug("SIGAR returned a null environment for [" + getBaseName() + "] process with pid [" + this.pid + "]."); } } catch (Exception e) { handleSigarCallException(e, "getProcEnv"); } this.initialized = true; } // now refresh the process data this.procState = procState; this.procExe = procExe; try { this.procTime = sigar.getProcTime(pid); } catch (Exception e) { handleSigarCallException(e, "getProcTime"); } try { this.procMem = sigar.getProcMem(pid); } catch (Exception e) { handleSigarCallException(e, "getProcMem"); } try { this.procCpu = sigar.getProcCpu(pid); } catch (Exception e) { handleSigarCallException(e, "getProcCpu"); } try { this.procFd = sigar.getProcFd(pid); } catch (Exception e) { handleSigarCallException(e, "getProcFd"); } try { this.procCred = sigar.getProcCred(pid); } catch (Exception e) { handleSigarCallException(e, "getProcCred"); } try { this.procCredName = sigar.getProcCredName(pid); } catch (Exception e) { handleSigarCallException(e, "getProcCredName"); } } catch (Exception e) { throw new SystemInfoException(e); } if (LOG.isTraceEnabled()) { long elapsedTime = System.currentTimeMillis() - startTime; LOG.trace("Retrieval of process info for pid " + pid + " took " + elapsedTime + " ms."); } this.processDied = false; } /** * <p> * Returns the last snasphot of the non static process properties. * </p> * <p> * Caveat the returned may hold stale data has it was taken with previous SIGAR calls. * Calling {@link #freshSnapshot()} instead is almost always a better idea. * </p> * * @return a {@link ProcessInfoSnapshot} possibly holding stale data */ public ProcessInfoSnapshot priorSnaphot() { return snapshot; } /** * Takes a fresh snapshot of the non static process properties. * * @return a fresh {@link ProcessInfoSnapshot} */ public ProcessInfoSnapshot freshSnapshot() { refresh(); return snapshot; } public void destroy() throws SystemInfoException { if (this.sigar instanceof Sigar) { try { ((Sigar) this.sigar).close(); } catch (RuntimeException e) { throw new SystemInfoException(e); } } } private void handleSigarCallException(Exception e, String methodName) { if (this.processDied) { // We already figured out the process died on a previous call to this method, so just return rather than // flooding the log with debug messages. return; } if (OperatingSystem.IS_WIN32 && (this.pid == 0 || this.pid == 4)) { // On Windows, Pid 0 and Pid 4 are special Kernel processes (Pid 0 is the "System Idle Process" and Pid 4 is // the "System" process). For these processes, it's normal for many of the Sigar.getProc calls to fail, so // there's no need to log anything. return; } String procName = (this.baseName != null) ? this.baseName : "<unknown>"; if (e instanceof SigarPermissionDeniedException) { if (!this.loggedPermissionsError) { // Only log permissions errors once per process. String currentUserName = System.getProperty("user.name"); LOG.trace("Unable to obtain all info for [" + procName + "] process with pid [" + this.pid + "] - call to " + methodName + "failed. " + "The process is most likely owned by a user other than the user that owns the RHQ plugin container's process (" + currentUserName + ")."); this.loggedPermissionsError = true; } } else if (e instanceof SigarNotImplementedException) { LOG.trace("Unable to obtain all info for [" + procName + "] process with pid [" + this.pid + "] - call to " + methodName + "failed. Cause: " + e); } else { if (!exists()) { LOG.debug("Attempt to refresh info for process with pid [" + this.pid + "] failed, because the process is no longer running."); this.processDied = true; } LOG.debug("Unexpected error occurred while looking up info for [" + procName + "] process with pid [" + this.pid + "] - call to " + methodName + " failed. Did the process die? Cause: " + e); } } private boolean exists() { long[] pids; try { pids = sigar.getProcList(); } catch (SigarException e1) { // TODO (ips, 04/30/12): It probably makes more sense to let this exception bubble up. LOG.error("Failed to obtain process list.", e1); return true; } boolean foundProcess = false; for (long pid : pids) { if (pid == this.pid) { foundProcess = true; break; } } return foundProcess; } private String determineName(String[] procArgs, ProcExe procExe, ProcState procState) { String name; if ((procArgs != null) && (procArgs.length != 0)) { name = procArgs[0]; } else if ((procExe != null) && (procExe.getName() != null)) { name = procExe.getName(); } else if ((procState != null) && (procState.getName() != null)) { name = procState.getName(); } else { name = UNKNOWN_PROCESS_NAME; } return name; } public long getPid() { return pid; } /** * Convenience method that returns the first command line argument, which is the name of the program that the * process is executing. * * @return full name of program that is executing * * @see #getBaseName() * @see #getCommandLine() */ public String getName() { return name; } /** * Similar to {@link #getName()}, this is a convenience method that returns the first command line argument, which * is the name of the program that the process is executing. However, this is only the relative filename of the * program, which does not include the full path to the program (e.g. this would return "sh" if the name of the * process is "/usr/bin/sh"). * * @return filename of program that is executing * * @see #getName() * @see #getCommandLine() */ public String getBaseName() { if (baseName == null) { baseName = (getName() != null) ? new File(getName()).getName() : UNKNOWN_PROCESS_NAME; } return baseName; } public String[] getCommandLine() { return commandLine; } public Map<String, String> getEnvironmentVariables() { if (this.procEnv == null) { return Collections.emptyMap(); } if (this.environmentVariables == null) { this.environmentVariables = new HashMap<String, String>(this.procEnv.size()); SystemInfo systemInfo = SystemInfoFactory.createJavaSystemInfo(); boolean isWindows = systemInfo.getOperatingSystemType() == OperatingSystemType.WINDOWS; if (isWindows) { // Windows environment is case-insensitive so convert variable names to all-caps, // this way we will be able to do case-insensitive lookups from the map later for (Map.Entry<String, String> env : this.procEnv.entrySet()) { this.environmentVariables.put(env.getKey().toUpperCase(), env.getValue()); } } else { this.environmentVariables.putAll(procEnv); } } return this.environmentVariables; } /** * Retrieves a specific environment property if it exists, <code>null</code> otherwise. * * @param name the name of the property to find * * @return the environment value */ @Nullable public String getEnvironmentVariable(@NotNull String name) { if (this.procEnv == null) { return null; } SystemInfo systemInfo = SystemInfoFactory.createJavaSystemInfo(); boolean isWindows = systemInfo.getOperatingSystemType() == OperatingSystemType.WINDOWS; // Windows env names are case insensitive, so convert the specified name to all-caps before doing the lookup. return getEnvironmentVariables().get((isWindows) ? name.toUpperCase() : name); } /** * @deprecated as of 4.6. For similar purpose, call {@link #priorSnaphot()} and then corresponding method * from the returned {@link ProcessInfoSnapshot}. */ @Deprecated public long getParentPid() throws SystemInfoException { return priorSnaphot().getParentPid(); } /** * @deprecated as of 4.6. For similar purpose, call {@link #priorSnaphot()} and then corresponding method * from the returned {@link ProcessInfoSnapshot}. */ @Deprecated public ProcState getState() throws SystemInfoException { return priorSnaphot().getState(); } /** * @deprecated as of 4.6. For similar purpose, call {@link #priorSnaphot()} and then corresponding method * from the returned {@link ProcessInfoSnapshot}. */ @Deprecated public ProcExe getExecutable() throws SystemInfoException { return priorSnaphot().getExecutable(); } /** * @deprecated as of 4.6. For similar purpose, call {@link #priorSnaphot()} and then corresponding method * from the returned {@link ProcessInfoSnapshot}. */ @Deprecated public ProcTime getTime() throws SystemInfoException { return priorSnaphot().getTime(); } /** * @deprecated as of 4.6. For similar purpose, call {@link #priorSnaphot()} and then corresponding method * from the returned {@link ProcessInfoSnapshot}. */ @Deprecated public ProcMem getMemory() throws SystemInfoException { return priorSnaphot().getMemory(); } /** * @deprecated as of 4.6. For similar purpose, call {@link #priorSnaphot()} and then corresponding method * from the returned {@link ProcessInfoSnapshot}. */ @Deprecated public ProcCpu getCpu() throws SystemInfoException { return priorSnaphot().getCpu(); } /** * @deprecated as of 4.6. For similar purpose, call {@link #priorSnaphot()} and then corresponding method * from the returned {@link ProcessInfoSnapshot}. */ @Deprecated public ProcFd getFileDescriptor() throws SystemInfoException { return priorSnaphot().getFileDescriptor(); } /** * @deprecated as of 4.6. For similar purpose, call {@link #priorSnaphot()} and then corresponding method * from the returned {@link ProcessInfoSnapshot}. */ @Deprecated public ProcCred getCredentials() throws SystemInfoException { return priorSnaphot().getCredentials(); } /** * @deprecated as of 4.6. For similar purpose, call {@link #priorSnaphot()} and then corresponding method * from the returned {@link ProcessInfoSnapshot}. */ @Deprecated public ProcCredName getCredentialsName() throws SystemInfoException { return priorSnaphot().getCredentialsName(); } /** * @deprecated as of 4.6. For similar purpose, call {@link #priorSnaphot()} and then corresponding method * from the returned {@link ProcessInfoSnapshot}. */ @Deprecated public String getCurrentWorkingDirectory() throws SystemInfoException { return priorSnaphot().getCurrentWorkingDirectory(); } /** * @deprecated as of 4.6. For similar purpose, call {@link #priorSnaphot()} and then corresponding method * from the returned {@link ProcessInfoSnapshot}. */ @Deprecated public boolean isRunning() throws SystemInfoException { return priorSnaphot().isRunning(); } /** * Returns an object that provides aggregate information on this process and all its children. * * @return aggregate information on this process and its children */ public AggregateProcessInfo getAggregateProcessTree() { AggregateProcessInfo root = new AggregateProcessInfo(this.pid); return root; } /** * Returns a {@link ProcessInfo} instance for the parent of this process. * * This method uses the parent process id which is not static (it can change if the parent process dies before its * child). So in theory it should be moved to the {@link ProcessInfoSnapshot} type. * * In practice, it stays here because the parent {@link ProcessInfo} instance is cached after creation. * * @since 4.4 */ public ProcessInfo getParentProcess() throws SystemInfoException { if (this.parentProcess == null) { this.parentProcess = new ProcessInfo(priorSnaphot().getParentPid(), sigar); } else { this.parentProcess.refresh(); } return this.parentProcess; } /** * Send the signal with the specified name to this process. * * @param signalName the name of the signal to send * * @throws IllegalArgumentException if the signal name is not valid * @throws SigarException if the native kill() call fails * * @since 4.4 */ public void kill(String signalName) throws SigarException { int signalNumber = getSignalNumber(signalName); // TODO: Should we check if the process is even running and throw a special exception if it's not? Sigar fullSigar = new Sigar(); try { fullSigar.kill(pid, signalNumber); } finally { fullSigar.close(); } } /** * A process' pid makes it unique - this returns the {@link #getPid()} itself. * * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return Long.valueOf(this.pid).intValue(); } /** * Two {@link ProcessInfo} objects are equal if their {@link #getPid() pids} are the same. * * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if ((obj == null) || !(obj instanceof ProcessInfo)) { return false; } return this.pid == ((ProcessInfo) obj).pid; } @Override public String toString() { StringBuilder s = new StringBuilder("process: "); s.append("pid=["); s.append(getPid()); s.append("], name=["); s.append((!getName().equals(UNKNOWN_PROCESS_NAME)) ? getName() : getBaseName()); s.append("], ppid=["); try { s.append(priorSnaphot().getParentPid()); } catch (Exception e) { s.append(e); } s.append("]"); return s.toString(); } private static int getSignalNumber(String signalName) { if (signalName == null) { throw new IllegalArgumentException("Signal name is null."); } int signalNumber; if (OperatingSystem.IS_WIN32) { if (MS_WINDOWS_TERMINATE_SIGNAL_NAMES.contains(signalName)) { signalNumber = 1; } else { throw new IllegalArgumentException("Unsupported signal name: " + signalName + " - on Windows, the only supported signal names are " + MS_WINDOWS_TERMINATE_SIGNAL_NAMES + ", all of which return 1."); } } else { signalNumber = Sigar.getSigNum(signalName); if (signalNumber == -1) { throw new IllegalArgumentException("Unknown signal name: " + signalName); } } return signalNumber; } }