All Downloads are FREE. Search and download functionalities are using the official Maven repository.

hudson.util.ProcessTreeKiller Maven / Gradle / Ivy

package hudson.util;

import hudson.EnvVars;
import hudson.Util;
import org.apache.commons.io.FileUtils;
import org.jvnet.winp.WinProcess;
import org.jvnet.winp.WinpException;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileReader;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Kills a process tree to clean up the mess left by a build.
 *
 * @author Kohsuke Kawaguchi
 * @since 1.201
 */
public abstract class ProcessTreeKiller {
    /**
     * Kills the given process (like {@link Process#destroy()}
     * but also attempts to kill descendant processes created from the given
     * process.
     *
     * 

* The implementation is obviously OS-dependent. * *

* The execution doesn't have to be blocking; the method may return * before processes are actually killed. */ public abstract void kill(Process proc); /** * In addition to what {@link #kill(Process)} does, also tries to * kill all the daemon processes launched. * *

* Daemon processes are hard to find because they normally detach themselves * from the parent process. In this method, the method is given a * "model environment variables", which is a list of environment variables * and their values that are characteristic to the launched process. * The implementation is expected to find processes * in the system that inherit these environment variables, and kill * them even if the {@code proc} and such processes do not have direct * ancestor/descendant relationship. */ public abstract void kill(Process proc, Map modelEnvVars); /** * Creates a magic cookie that can be used as the model environment variable * when we later kill the processes. */ public static EnvVars createCookie() { EnvVars r = new EnvVars(); r.put("HUDSON_COOKIE",UUID.randomUUID().toString()); return r; } /** * Gets the {@link ProcessTreeKiller} suitable for the current system * that JVM runs in, or in the worst case return the default one * that's not capable of killing descendants at all. */ public static ProcessTreeKiller get() { if(!enabled) return DEFAULT; if(File.pathSeparatorChar==';') return new Windows(); String os = Util.fixNull(System.getProperty("os.name")); if(os.equals("Linux")) return new Linux(); if(os.equals("SunOS")) return new Solaris(); return DEFAULT; } /** * Given the environment variable of a process and the "model environment variable" that Hudson * used for launching the build, returns true if there's a match (which means the process should * be considered a descendant of a build.) */ protected boolean hasMatchingEnvVars(Map envVar, Map modelEnvVar) { if(modelEnvVar.isEmpty()) // sanity check so that we don't start rampage. return false; for (Entry e : modelEnvVar.entrySet()) { String v = envVar.get(e.getKey()); if(v==null || !v.equals(e.getValue())) return false; // no match } return true; } /** * Fallback implementation that doesn't do anything clever. */ private static final ProcessTreeKiller DEFAULT = new ProcessTreeKiller() { public void kill(Process proc) { proc.destroy(); } public void kill(Process proc, Map modelEnvVars) { proc.destroy(); } }; /** * Implementation for Windows. * *

* Not a singleton pattern because loading this class requires Windows specific library. */ private static final class Windows extends ProcessTreeKiller { public void kill(Process proc) { new WinProcess(proc).killRecursively(); } public void kill(Process proc, Map modelEnvVars) { kill(proc); for( WinProcess p : WinProcess.all() ) { if(p.getPid()<10) continue; // ignore system processes like "idle process" boolean matched; try { matched = hasMatchingEnvVars(p.getEnvironmentVariables(), modelEnvVars); } catch (WinpException e) { // likely a missing privilege continue; } if(matched) p.killRecursively(); } } static { WinProcess.enableDebugPrivilege(); } } /** * Implementation for Unix that supports reasonably powerful /proc FS. */ private static abstract class Unix> extends ProcessTreeKiller { public void kill(Process proc) { kill(proc,null); } protected abstract S createSystem(); public void kill(Process proc, Map modelEnvVars) { S system = createSystem(); UnixProcess p; try { p = system.get((Integer) PID_FIELD.get(proc)); } catch (IllegalAccessException e) { // impossible IllegalAccessError x = new IllegalAccessError(); x.initCause(e); throw x; } if(p==null) { // process already dead? proc.destroy(); return; } if(modelEnvVars ==null) p.killRecursively(); else { for (UnixProcess lp : system) { if(hasMatchingEnvVars(lp.getEnvVars(),modelEnvVars)) lp.kill(); } } } /** * Field to access the PID of the process. */ private static final Field PID_FIELD; /** * Method to destroy a process, given pid. */ private static final Method DESTROY_PROCESS; static { try { Class clazz = Class.forName("java.lang.UNIXProcess"); PID_FIELD = clazz.getDeclaredField("pid"); PID_FIELD.setAccessible(true); DESTROY_PROCESS = clazz.getDeclaredMethod("destroyProcess",int.class); DESTROY_PROCESS.setAccessible(true); } catch (ClassNotFoundException e) { LinkageError x = new LinkageError(); x.initCause(e); throw x; } catch (NoSuchFieldException e) { LinkageError x = new LinkageError(); x.initCause(e); throw x; } catch (NoSuchMethodException e) { LinkageError x = new LinkageError(); x.initCause(e); throw x; } } /** * Represents a single Unix system, which hosts multiple processes. * *

* The object represents a snapshot of the system state. */ static abstract class UnixSystem

> implements Iterable

{ private final Map processes = new HashMap(); UnixSystem() { File[] processes = new File("/proc").listFiles(new FileFilter() { public boolean accept(File f) { return f.isDirectory(); } }); if(processes==null) { LOGGER.info("No /proc"); return; } for (File p : processes) { int pid; try { pid = Integer.parseInt(p.getName()); } catch (NumberFormatException e) { // other sub-directories continue; } try { this.processes.put(pid,createProcess(pid)); } catch (IOException e) { // perhaps the process status has changed since we obtained a directory listing } } } protected abstract P createProcess(int pid) throws IOException; public P get(int pid) { return processes.get(pid); } public Iterator

iterator() { return processes.values().iterator(); } } /** * A process. */ public static abstract class UnixProcess

> { public final UnixSystem

system; protected UnixProcess(UnixSystem

system) { this.system = system; } public abstract int getPid(); /** * Gets the parent process. This method may return null, because * there's no guarantee that we are getting a consistent snapshot * of the whole system state. */ public abstract P getParent(); protected final File getFile(String relativePath) { return new File(new File("/proc/"+getPid()),relativePath); } /** * Immediate child processes. */ public List

getChildren() { List

r = new ArrayList

(); for (P p : system) if(p.getParent()==this) r.add(p); return r; } /** * Tries to kill this process. */ public void kill() { try { DESTROY_PROCESS.invoke(null,getPid()); } catch (IllegalAccessException e) { // this is impossible IllegalAccessError x = new IllegalAccessError(); x.initCause(e); throw x; } catch (InvocationTargetException e) { // tunnel serious errors if(e.getTargetException() instanceof Error) throw (Error)e.getTargetException(); // otherwise log and let go. I need to see when this happens LOGGER.log(Level.INFO, "Failed to terminate pid="+getPid(),e); } } public void killRecursively() { for (P p : getChildren()) p.killRecursively(); kill(); } /** * Obtains the environment variables of this process. * * @return * empty map if failed (for example because the process is already dead, * or the permission was denied.) */ public abstract EnvVars getEnvVars(); } } /** * Implementation for Linux that uses /proc. */ private static final class Linux extends Unix { protected LinuxSystem createSystem() { return new LinuxSystem(); } static class LinuxSystem extends Unix.UnixSystem { protected LinuxProcess createProcess(int pid) throws IOException { return new LinuxProcess(this,pid); } } static class LinuxProcess extends Unix.UnixProcess { private final int pid; private int ppid = -1; private EnvVars envVars; LinuxProcess(LinuxSystem system, int pid) throws IOException { super(system); this.pid = pid; BufferedReader r = new BufferedReader(new FileReader(getFile("status"))); try { String line; while((line=r.readLine())!=null) { line=line.toLowerCase(Locale.ENGLISH); if(line.startsWith("ppid:")) { ppid = Integer.parseInt(line.substring(5).trim()); break; } } } finally { r.close(); } if(ppid==-1) throw new IOException("Failed to parse PPID from /proc/"+pid+"/status"); } public int getPid() { return pid; } public LinuxProcess getParent() { return system.get(ppid); } public synchronized EnvVars getEnvVars() { if(envVars !=null) return envVars; envVars = new EnvVars(); try { byte[] environ = FileUtils.readFileToByteArray(getFile("environ")); int pos=0; for (int i = 0; i < environ.length; i++) { byte b = environ[i]; if(b==0) { envVars.addLine(new String(environ,pos,i-pos)); pos=i+1; } } } catch (IOException e) { // failed to read. this can happen under normal circumstances (most notably permission denied) // so don't report this as an error. } return envVars; } } } /** * Implementation for Solaris that uses /proc. * * Amazingly, this single code works for both 32bit and 64bit Solaris, despite the fact * that does a lot of pointer manipulation and what not. */ private static final class Solaris extends Unix { protected SolarisSystem createSystem() { return new SolarisSystem(); } static class SolarisSystem extends Unix.UnixSystem { protected SolarisProcess createProcess(int pid) throws IOException { return new SolarisProcess(this,pid); } } static class SolarisProcess extends Unix.UnixProcess { private final int pid; private final int ppid; /** * Address of the environment vector. Even on 64bit Solaris this is still 32bit pointer. */ private final int envp; private EnvVars envVars; SolarisProcess(SolarisSystem system, int pid) throws IOException { super(system); this.pid = pid; RandomAccessFile psinfo = new RandomAccessFile(getFile("psinfo"),"r"); try { //typedef struct psinfo { // int pr_flag; /* process flags */ // int pr_nlwp; /* number of lwps in the process */ // pid_t pr_pid; /* process id */ // pid_t pr_ppid; /* process id of parent */ // pid_t pr_pgid; /* process id of process group leader */ // pid_t pr_sid; /* session id */ // uid_t pr_uid; /* real user id */ // uid_t pr_euid; /* effective user id */ // gid_t pr_gid; /* real group id */ // gid_t pr_egid; /* effective group id */ // uintptr_t pr_addr; /* address of process */ // size_t pr_size; /* size of process image in Kbytes */ // size_t pr_rssize; /* resident set size in Kbytes */ // dev_t pr_ttydev; /* controlling tty device (or PRNODEV) */ // ushort_t pr_pctcpu; /* % of recent cpu time used by all lwps */ // ushort_t pr_pctmem; /* % of system memory used by process */ // timestruc_t pr_start; /* process start time, from the epoch */ // timestruc_t pr_time; /* cpu time for this process */ // timestruc_t pr_ctime; /* cpu time for reaped children */ // char pr_fname[PRFNSZ]; /* name of exec'ed file */ // char pr_psargs[PRARGSZ]; /* initial characters of arg list */ // int pr_wstat; /* if zombie, the wait() status */ // int pr_argc; /* initial argument count */ // uintptr_t pr_argv; /* address of initial argument vector */ // uintptr_t pr_envp; /* address of initial environment vector */ // char pr_dmodel; /* data model of the process */ // lwpsinfo_t pr_lwp; /* information for representative lwp */ //} psinfo_t; // see http://cvs.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/common/sys/types.h // for the size of the various datatype. psinfo.seek(8); if(adjust(psinfo.readInt())!=pid) throw new IOException("psinfo PID mismatch"); // sanity check ppid = adjust(psinfo.readInt()); psinfo.seek(196); // now jump to pr_envp envp = adjust(psinfo.readInt()); } finally { psinfo.close(); } if(ppid==-1) throw new IOException("Failed to parse PPID from /proc/"+pid+"/status"); } /** * {@link DataInputStream} reads a value in big-endian, so * convert it to the correct value on little-endian systems. */ private int adjust(int i) { if(IS_LITTLE_ENDIAN) return (i<<24) |((i<<8) & 0x00FF0000) | ((i>>8) & 0x0000FF00) | (i>>>24); else return i; } public int getPid() { return pid; } public SolarisProcess getParent() { return system.get(ppid); } public synchronized EnvVars getEnvVars() { if(envVars !=null) return envVars; envVars = new EnvVars(); try { RandomAccessFile as = new RandomAccessFile(getFile("as"),"r"); try { for( int n=0; ; n++ ) { // read a pointer to one entry as.seek(to64(envp+n*4)); int p = as.readInt(); if(p==0) break; // completed the walk // now read the null-terminated string as.seek(to64(p)); ByteArrayOutputStream buf = new ByteArrayOutputStream(); int ch; while((ch=as.read())!=0) buf.write(ch); envVars.addLine(buf.toString()); } } finally { // failed to read. this can happen under normal circumstances, // so don't report this as an error. as.close(); } } catch (IOException e) { // failed to read. this can happen under normal circumstances (most notably permission denied) // so don't report this as an error. } return envVars; } /** * int to long conversion with zero-padding. */ private static long to64(int i) { return i&0xFFFFFFFFL; } } } /* On MacOS X, there's no procfs instead you'd do it with the sysctl There's CLI but that doesn't seem to offer the access to per-process info On HP-UX, pstat_getcommandline get you command line, but I'm not seeing any environment variables. */ private static final boolean IS_LITTLE_ENDIAN = "little".equals(System.getProperty("sun.cpu.endian")); private static final Logger LOGGER = Logger.getLogger(ProcessTreeKiller.class.getName()); /** * Flag to control this feature. * *

* This feature is still experimental, so it is disabled by default. * In distributed environment, this flag needs to be enabled per slave. */ public static boolean enabled = Boolean.getBoolean(ProcessTreeKiller.class.getName()); }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy