cycle) throws CycleDetectedException {
+ throw new CycleDetectedException(cycle);
+ }
+
+ public static final class CycleDetectedException extends Exception {
+ public final List cycle;
+
+ public CycleDetectedException(List cycle) {
+ super("Cycle detected: " + Util.join(cycle, " -> "));
+ this.cycle = cycle;
+ }
+ }
+}
diff --git a/src/main/java/org/ow2/proactive/process_tree_killer/EnvVars.java b/src/main/java/org/ow2/proactive/process_tree_killer/EnvVars.java
new file mode 100644
index 0000000..42ed5b5
--- /dev/null
+++ b/src/main/java/org/ow2/proactive/process_tree_killer/EnvVars.java
@@ -0,0 +1,441 @@
+/*
+ * ProActive Parallel Suite(TM):
+ * The Open Source library for parallel and distributed
+ * Workflows & Scheduling, Orchestration, Cloud Automation
+ * and Big Data Analysis on Enterprise Grids & Clouds.
+ *
+ * Copyright (c) 2007 - 2017 ActiveEon
+ * Contact: contact@activeeon.com
+ *
+ * This library is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation: version 3 of
+ * the License.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * If needed, contact us to obtain a release under GPL Version 2 or 3
+ * or a different license than the AGPL.
+ */
+package org.ow2.proactive.process_tree_killer;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.UUID;
+import java.util.logging.Logger;
+
+
+/**
+ * Environment variables.
+ *
+ *
+ * While all the platforms I tested (Linux 2.6, Solaris, and Windows XP) have the case sensitive
+ * environment variable table, Windows batch script handles environment variable in the case preserving
+ * but case insensitive way (that is, cmd.exe can get both FOO and foo as environment variables
+ * when it's launched, and the "set" command will display it accordingly, but "echo %foo%" results in
+ * echoing the value of "FOO", not "foo" — this is presumably caused by the behavior of the underlying
+ * Win32 API GetEnvironmentVariable acting in case insensitive way.) Windows users are also
+ * used to write environment variable case-insensitively (like %Path% vs %PATH%), and you can see many
+ * documents on the web that claims Windows environment variables are case insensitive.
+ *
+ *
+ * So for a consistent cross platform behavior, it creates the least confusion to make the table
+ * case insensitive but case preserving.
+ *
+ *
+ * In Jenkins, often we need to build up "environment variable overrides"
+ * on master, then to execute the process on slaves. This causes a problem
+ * when working with variables like PATH. So to make this work,
+ * we introduce a special convention PATH+FOO — all entries
+ * that starts with PATH+ are merged and prepended to the inherited
+ * PATH variable, on the process where a new process is executed.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+@SuppressWarnings("all")
+public class EnvVars extends TreeMap {
+ private static Logger LOGGER = Logger.getLogger(EnvVars.class.getName());
+
+ /**
+ * If this {@link EnvVars} object represents the whole environment variable set,
+ * not just a partial list used for overriding later, then we need to know
+ * the platform for which this env vars are targeted for, or else we won't know
+ * how to merge variables properly.
+ *
+ *
+ * So this property remembers that information.
+ */
+ private Platform platform;
+
+ public EnvVars() {
+ super(CaseInsensitiveComparator.INSTANCE);
+ }
+
+ public EnvVars(Map m) {
+ this();
+ putAll(m);
+
+ // because of the backward compatibility, some parts of Jenkins passes
+ // EnvVars as Map so downcasting is safer.
+ if (m instanceof EnvVars) {
+ EnvVars lhs = (EnvVars) m;
+ this.platform = lhs.platform;
+ }
+ }
+
+ public EnvVars(EnvVars m) {
+ // this constructor is so that in future we can get rid of the downcasting.
+ this((Map) m);
+ }
+
+ /**
+ * Builds an environment variables from an array of the form "key","value","key","value"...
+ */
+ public EnvVars(String... keyValuePairs) {
+ this();
+ if (keyValuePairs.length % 2 != 0)
+ throw new IllegalArgumentException(Arrays.asList(keyValuePairs).toString());
+ for (int i = 0; i < keyValuePairs.length; i += 2)
+ put(keyValuePairs[i], keyValuePairs[i + 1]);
+ }
+
+ /**
+ * Overrides the current entry by the given entry.
+ *
+ *
+ * Handles PATH+XYZ notation.
+ */
+ public void override(String key, String value) {
+ if (value == null || value.length() == 0) {
+ remove(key);
+ return;
+ }
+
+ int idx = key.indexOf('+');
+ if (idx > 0) {
+ String realKey = key.substring(0, idx);
+ String v = get(realKey);
+ if (v == null)
+ v = value;
+ else {
+ // we might be handling environment variables for a slave that can have different path separator
+ // than the master, so the following is an attempt to get it right.
+ // it's still more error prone that I'd like.
+ char ch = platform == null ? File.pathSeparatorChar : platform.pathSeparator;
+ v = value + ch + v;
+ }
+ put(realKey, v);
+ return;
+ }
+
+ put(key, value);
+ }
+
+ /**
+ * Overrides all values in the map by the given map.
+ * See {@link #override(String, String)}.
+ * @return this
+ */
+ public EnvVars overrideAll(Map all) {
+ for (Map.Entry e : all.entrySet()) {
+ override(e.getKey(), e.getValue());
+ }
+ return this;
+ }
+
+ /**
+ * Calculates the order to override variables.
+ *
+ * Sort variables with topological sort with their reference graph.
+ *
+ * This is package accessible for testing purpose.
+ */
+ static class OverrideOrderCalculator {
+ /**
+ * Extract variables referred directly from a variable.
+ */
+ private static class TraceResolver implements VariableResolver {
+ private final Comparator super String> comparator;
+
+ public Set referredVariables;
+
+ public TraceResolver(Comparator super String> comparator) {
+ this.comparator = comparator;
+ clear();
+ }
+
+ public void clear() {
+ referredVariables = new TreeSet(comparator);
+ }
+
+ public String resolve(String name) {
+ referredVariables.add(name);
+ return "";
+ }
+ }
+
+ private static class VariableReferenceSorter extends CyclicGraphDetector {
+ // map from a variable to a set of variables that variable refers.
+ private final Map> refereeSetMap;
+
+ public VariableReferenceSorter(Map> refereeSetMap) {
+ this.refereeSetMap = refereeSetMap;
+ }
+
+ @Override
+ protected Iterable extends String> getEdges(String n) {
+ // return variables referred from the variable.
+ if (!refereeSetMap.containsKey(n)) {
+ // there is a case a non-existing variable is referred...
+ return Collections.emptySet();
+ }
+ return refereeSetMap.get(n);
+ }
+ };
+
+ private final Comparator super String> comparator;
+
+ private final EnvVars target;
+
+ private final Map overrides;
+
+ private Map> refereeSetMap;
+
+ private List orderedVariableNames;
+
+ public OverrideOrderCalculator(EnvVars target, Map overrides) {
+ comparator = target.comparator();
+ this.target = target;
+ this.overrides = overrides;
+ scan();
+ }
+
+ public List getOrderedVariableNames() {
+ return orderedVariableNames;
+ }
+
+ // Cut the reference to the variable in a cycle.
+ private void cutCycleAt(String referee, List cycle) {
+ // cycle contains variables in referrer-to-referee order.
+ // This should not be negative, for the first and last one is same.
+ int refererIndex = cycle.lastIndexOf(referee) - 1;
+
+ assert (refererIndex >= 0);
+ String referrer = cycle.get(refererIndex);
+ boolean removed = refereeSetMap.get(referrer).remove(referee);
+ assert (removed);
+ LOGGER.warning(String.format("Cyclic reference detected: %s", Util.join(cycle, " -> ")));
+ LOGGER.warning(String.format("Cut the reference %s -> %s", referrer, referee));
+ }
+
+ // Cut the variable reference in a cycle.
+ private void cutCycle(List cycle) {
+ // if an existing variable is contained in that cycle,
+ // cut the cycle with that variable:
+ // existing:
+ // PATH=/usr/bin
+ // overriding:
+ // PATH1=/usr/local/bin:${PATH}
+ // PATH=/opt/something/bin:${PATH1}
+ // then consider reference PATH1 -> PATH can be ignored.
+ for (String referee : cycle) {
+ if (target.containsKey(referee)) {
+ cutCycleAt(referee, cycle);
+ return;
+ }
+ }
+
+ // if not, cut the reference to the first one.
+ cutCycleAt(cycle.get(0), cycle);
+ }
+
+ /**
+ * Scan all variables and list all referring variables.
+ */
+ public void scan() {
+ refereeSetMap = new TreeMap>(comparator);
+ List extendingVariableNames = new ArrayList();
+
+ TraceResolver resolver = new TraceResolver(comparator);
+
+ for (Map.Entry entry : overrides.entrySet()) {
+ if (entry.getKey().indexOf('+') > 0) {
+ // XYZ+AAA variables should be always processed in last.
+ extendingVariableNames.add(entry.getKey());
+ continue;
+ }
+ resolver.clear();
+ Util.replaceMacro(entry.getValue(), resolver);
+
+ // Variables directly referred from the current scanning variable.
+ Set refereeSet = resolver.referredVariables;
+ // Ignore self reference.
+ refereeSet.remove(entry.getKey());
+ refereeSetMap.put(entry.getKey(), refereeSet);
+ }
+
+ VariableReferenceSorter sorter;
+ while (true) {
+ sorter = new VariableReferenceSorter(refereeSetMap);
+ try {
+ sorter.run(refereeSetMap.keySet());
+ } catch (CyclicGraphDetector.CycleDetectedException e) {
+ // cyclic reference found.
+ // cut the cycle and retry.
+ @SuppressWarnings("unchecked")
+ List cycle = e.cycle;
+ cutCycle(cycle);
+ continue;
+ }
+ break;
+ }
+
+ // When A refers B, the last appearance of B always comes after
+ // the last appearance of A.
+ List reversedDuplicatedOrder = new ArrayList(sorter.getSorted());
+ Collections.reverse(reversedDuplicatedOrder);
+
+ orderedVariableNames = new ArrayList(overrides.size());
+ for (String key : reversedDuplicatedOrder) {
+ if (overrides.containsKey(key) && !orderedVariableNames.contains(key)) {
+ orderedVariableNames.add(key);
+ }
+ }
+ Collections.reverse(orderedVariableNames);
+ orderedVariableNames.addAll(extendingVariableNames);
+ }
+ }
+
+ /**
+ * Overrides all values in the map by the given map. Expressions in values will be expanded.
+ * See {@link #override(String, String)}.
+ * @return this
+ */
+ public EnvVars overrideExpandingAll(Map all) {
+ for (String key : new OverrideOrderCalculator(this, all).getOrderedVariableNames()) {
+ override(key, expand(all.get(key)));
+ }
+ return this;
+ }
+
+ /**
+ * Resolves environment variables against each other.
+ */
+ public static void resolve(Map env) {
+ for (Map.Entry entry : env.entrySet()) {
+ entry.setValue(Util.replaceMacro(entry.getValue(), env));
+ }
+ }
+
+ /**
+ * Convenience message
+ * @since 1.485
+ **/
+ public String get(String key, String defaultValue) {
+ String v = get(key);
+ if (v == null)
+ v = defaultValue;
+ return v;
+ }
+
+ @Override
+ public String put(String key, String value) {
+ if (value == null)
+ throw new IllegalArgumentException("Null value not allowed as an environment variable: " + key);
+ return super.put(key, value);
+ }
+
+ /**
+ * Add a key/value but only if the value is not-null. Otherwise no-op.
+ * @since 1.556
+ */
+ public void putIfNotNull(String key, String value) {
+ if (value != null)
+ put(key, value);
+ }
+
+ /**
+ * Takes a string that looks like "a=b" and adds that to this map.
+ */
+ public void addLine(String line) {
+ int sep = line.indexOf('=');
+ if (sep > 0) {
+ put(line.substring(0, sep), line.substring(sep + 1));
+ }
+ }
+
+ /**
+ * Expands the variables in the given string by using environment variables represented in 'this'.
+ */
+ public String expand(String s) {
+ return Util.replaceMacro(s, this);
+ }
+
+ /**
+ * Creates a magic cookie that can be used as the model environment variable
+ * when we later kill the processes.
+ */
+ public static EnvVars createCookie() {
+ return new EnvVars("HUDSON_COOKIE", UUID.randomUUID().toString());
+ }
+
+ // /**
+ // * Obtains the environment variables of a remote peer.
+ // *
+ // * @param channel
+ // * Can be null, in which case the map indicating "N/A" will be returned.
+ // * @return
+ // * A fresh copy that can be owned and modified by the caller.
+ // */
+ // public static EnvVars getRemote(VirtualChannel channel) throws IOException, InterruptedException {
+ // if(channel==null)
+ // return new EnvVars("N/A","N/A");
+ // return channel.call(new GetEnvVars());
+ // }
+
+ // private static final class GetEnvVars extends MasterToSlaveCallable {
+ // public EnvVars call() {
+ // return new EnvVars(EnvVars.masterEnvVars);
+ // }
+ // private static final long serialVersionUID = 1L;
+ // }
+
+ /**
+ * Environmental variables that we've inherited.
+ *
+ *
+ * Despite what the name might imply, this is the environment variable
+ * of the current JVM process. And therefore, it is Jenkins master's environment
+ * variables only when you access this from the master.
+ *
+ *
+ * If you access this field from slaves, then this is the environment
+ * variable of the slave agent.
+ */
+ // public static final Map masterEnvVars = initMaster();
+
+ // private static EnvVars initMaster() {
+ // EnvVars vars = new EnvVars(System.getenv());
+ // vars.platform = Platform.current();
+ // if(Main.isUnitTest || Main.isDevelopmentMode)
+ // // if unit test is launched with maven debug switch,
+ // // we need to prevent forked Maven processes from seeing it, or else
+ // // they'll hang
+ // vars.remove("MAVEN_OPTS");
+ // return vars;
+ // }
+}
diff --git a/src/main/java/org/ow2/proactive/process_tree_killer/Platform.java b/src/main/java/org/ow2/proactive/process_tree_killer/Platform.java
new file mode 100644
index 0000000..2a090b6
--- /dev/null
+++ b/src/main/java/org/ow2/proactive/process_tree_killer/Platform.java
@@ -0,0 +1,79 @@
+/*
+ * ProActive Parallel Suite(TM):
+ * The Open Source library for parallel and distributed
+ * Workflows & Scheduling, Orchestration, Cloud Automation
+ * and Big Data Analysis on Enterprise Grids & Clouds.
+ *
+ * Copyright (c) 2007 - 2017 ActiveEon
+ * Contact: contact@activeeon.com
+ *
+ * This library is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation: version 3 of
+ * the License.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * If needed, contact us to obtain a release under GPL Version 2 or 3
+ * or a different license than the AGPL.
+ */
+package org.ow2.proactive.process_tree_killer;
+
+import java.io.File;
+import java.util.Locale;
+
+
+/**
+ * Strategy object that absorbs the platform differences.
+ *
+ *
+ * Do not switch/case on this enum, or do a comparison, as we may add new constants.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+public enum Platform {
+ WINDOWS(';'),
+ UNIX(':');
+
+ /**
+ * The character that separates paths in environment variables like PATH and CLASSPATH.
+ * On Windows ';' and on Unix ':'.
+ *
+ * @see File#pathSeparator
+ */
+ public final char pathSeparator;
+
+ private Platform(char pathSeparator) {
+ this.pathSeparator = pathSeparator;
+ }
+
+ public static Platform current() {
+ if (File.pathSeparatorChar == ':')
+ return UNIX;
+ return WINDOWS;
+ }
+
+ public static boolean isDarwin() {
+ // according to http://developer.apple.com/technotes/tn2002/tn2110.html
+ return System.getProperty("os.name").toLowerCase(Locale.ENGLISH).startsWith("mac");
+ }
+
+ /**
+ * Returns true if we run on Mac OS X >= 10.6
+ */
+ public static boolean isSnowLeopardOrLater() {
+ try {
+ return isDarwin() &&
+ new VersionNumber(System.getProperty("os.version")).compareTo(new VersionNumber("10.6")) >= 0;
+ } catch (IllegalArgumentException e) {
+ // failed to parse the version
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/org/ow2/proactive/process_tree_killer/ProcessKiller.java b/src/main/java/org/ow2/proactive/process_tree_killer/ProcessKiller.java
new file mode 100644
index 0000000..18a2204
--- /dev/null
+++ b/src/main/java/org/ow2/proactive/process_tree_killer/ProcessKiller.java
@@ -0,0 +1,77 @@
+/*
+ * ProActive Parallel Suite(TM):
+ * The Open Source library for parallel and distributed
+ * Workflows & Scheduling, Orchestration, Cloud Automation
+ * and Big Data Analysis on Enterprise Grids & Clouds.
+ *
+ * Copyright (c) 2007 - 2017 ActiveEon
+ * Contact: contact@activeeon.com
+ *
+ * This library is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation: version 3 of
+ * the License.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * If needed, contact us to obtain a release under GPL Version 2 or 3
+ * or a different license than the AGPL.
+ */
+package org.ow2.proactive.process_tree_killer;
+
+import java.io.IOException;
+import java.io.Serializable;
+
+
+/**
+ * Extension point that defines more elaborate way of killing processes, such as
+ * sudo or pfexec, for {@link ProcessTree}.
+ *
+ *
Lifecycle
+ *
+ * Each implementation of {@link ProcessKiller} is instantiated once on the master.
+ * Whenever a process needs to be killed, those implementations are serialized and sent over
+ * to the appropriate slave, then the {@link #kill(ProcessTree.OSProcess)} method is invoked
+ * to attempt to kill the process.
+ *
+ *
+ * One of the consequences of this design is that the implementation should be stateless
+ * and concurrent-safe. That is, the {@link #kill(ProcessTree.OSProcess)} method can be invoked by multiple threads
+ * concurrently on the single instance.
+ *
+ *
+ * Another consequence of this design is that if your {@link ProcessKiller} requires configuration,
+ * it needs to be serializable, and configuration needs to be updated atomically, as another
+ * thread may be calling into {@link #kill(ProcessTree.OSProcess)} just when you are updating your configuration.
+ *
+ * @author jpederzolli
+ * @author Kohsuke Kawaguchi
+ * @since 1.362
+ */
+public abstract class ProcessKiller implements Serializable {
+
+ /**
+ * Attempts to kill the given process.
+ *
+ * @param process process to be killed. Always a {@linkplain ProcessTree.Local local process}.
+ * @return
+ * true if the killing was successful, and Hudson won't try to use other {@link ProcessKiller}
+ * implementations to kill the process. false if the killing failed or is unattempted, and Hudson will continue
+ * to use the rest of the {@link ProcessKiller} implementations to try to kill the process.
+ * @throws IOException
+ * The caller will log this exception and otherwise treat as if the method returned false, and moves on
+ * to the next killer.
+ * @throws InterruptedException
+ * if the callee performs a time consuming operation and if the thread is canceled, do not catch
+ * {@link InterruptedException} and just let it thrown from the method.
+ */
+ public abstract boolean kill(ProcessTree.OSProcess process) throws IOException, InterruptedException;
+
+ private static final long serialVersionUID = 1L;
+}
diff --git a/src/main/java/org/ow2/proactive/process_tree_killer/ProcessTree.java b/src/main/java/org/ow2/proactive/process_tree_killer/ProcessTree.java
new file mode 100644
index 0000000..a24822f
--- /dev/null
+++ b/src/main/java/org/ow2/proactive/process_tree_killer/ProcessTree.java
@@ -0,0 +1,1182 @@
+/*
+ * ProActive Parallel Suite(TM):
+ * The Open Source library for parallel and distributed
+ * Workflows & Scheduling, Orchestration, Cloud Automation
+ * and Big Data Analysis on Enterprise Grids & Clouds.
+ *
+ * Copyright (c) 2007 - 2017 ActiveEon
+ * Contact: contact@activeeon.com
+ *
+ * This library is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation: version 3 of
+ * the License.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * If needed, contact us to obtain a release under GPL Version 2 or 3
+ * or a different license than the AGPL.
+ */
+package org.ow2.proactive.process_tree_killer;
+
+import com.sun.jna.Memory;
+import com.sun.jna.Native;
+import com.sun.jna.ptr.IntByReference;
+import org.jvnet.winp.WinProcess;
+import org.jvnet.winp.WinpException;
+
+import java.io.*;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.file.Files;
+import java.rmi.Remote;
+import java.util.*;
+import java.util.Map.Entry;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static com.sun.jna.Pointer.NULL;
+import static java.util.logging.Level.*;
+import static org.ow2.proactive.process_tree_killer.jna.GNUCLibrary.LIBC;
+
+
+/**
+ * Represents a snapshot of the process tree of the current system.
+ *
+ *
+ * A {@link ProcessTree} is really conceptually a map from process ID to a {@link OSProcess} object.
+ * When Hudson runs on platforms that support process introspection, this allows you to introspect
+ * and do some useful things on processes. On other platforms, the implementation falls back to
+ * "do nothing" behavior.
+ *
+ *
+ * {@link ProcessTree} is remotable.
+ *
+ * @author Kohsuke Kawaguchi
+ * @since 1.315
+ */
+@SuppressWarnings("all")
+public abstract class ProcessTree
+ implements Iterable, ProcessTreeRemoting.IProcessTree, Serializable {
+ /**
+ * To be filled in the constructor of the derived type.
+ */
+ protected final Map processes = new HashMap();
+
+ // instantiation only allowed for subtypes in this class
+ private ProcessTree() {
+ }
+
+ /**
+ * Gets the process given a specific ID, or null if no such process exists.
+ */
+ public final OSProcess get(int pid) {
+ return processes.get(pid);
+ }
+
+ /**
+ * Lists all the processes in the system.
+ */
+ public final Iterator iterator() {
+ return processes.values().iterator();
+ }
+
+ /**
+ * Try to convert {@link Process} into this process object
+ * or null if it fails (for example, maybe the snapshot is taken after
+ * this process has already finished.)
+ */
+ public abstract OSProcess get(Process proc);
+
+ /**
+ * Kills all the processes that have matching environment variables.
+ *
+ *
+ * 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 all. This is suitable for locating daemon processes
+ * that cannot be tracked by the regular ancestor/descendant relationship.
+ */
+ public abstract void killAll(Map modelEnvVars) throws InterruptedException;
+
+ /**
+ * Convenience method that does {@link #killAll(Map)} and {@link OSProcess#killRecursively()}.
+ * This is necessary to reliably kill the process and its descendants, as some OS
+ * may not implement {@link #killAll(Map)}.
+ *
+ * Either of the parameter can be null.
+ */
+ public void killAll(Process proc, Map modelEnvVars) throws InterruptedException {
+ LOGGER.fine("killAll: process=" + proc + " and envs=" + modelEnvVars);
+ OSProcess p = get(proc);
+ if (p != null)
+ p.killRecursively();
+ if (modelEnvVars != null)
+ killAll(modelEnvVars);
+ }
+
+ /**
+ * Represents a process.
+ */
+ public abstract class OSProcess implements ProcessTreeRemoting.IOSProcess, Serializable {
+ final int pid;
+
+ // instantiation only allowed for subtypes in this class
+ private OSProcess(int pid) {
+ this.pid = pid;
+ }
+
+ public final int getPid() {
+ return pid;
+ }
+
+ /**
+ * 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 OSProcess getParent();
+
+ /* package */ final ProcessTree getTree() {
+ return ProcessTree.this;
+ }
+
+ /**
+ * Immediate child processes.
+ */
+ public final List getChildren() {
+ List r = new ArrayList();
+ for (OSProcess p : ProcessTree.this)
+ if (p.getParent() == this)
+ r.add(p);
+ return r;
+ }
+
+ /**
+ * Kills this process.
+ */
+ public abstract void kill() throws InterruptedException;
+
+ /**
+ * Kills this process and all the descendants.
+ *
+ * Note that the notion of "descendants" is somewhat vague,
+ * in the presence of such things like daemons. On platforms
+ * where the recursive operation is not supported, this just kills
+ * the current process.
+ */
+ public abstract void killRecursively() throws InterruptedException;
+
+ /**
+ * Gets the command-line arguments of this process.
+ *
+ *
+ * On Windows, where the OS models command-line arguments as a single string, this method
+ * computes the approximated tokenization.
+ */
+ public abstract List getArguments();
+
+ /**
+ * 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 getEnvironmentVariables();
+
+ /**
+ * 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.)
+ */
+ public final boolean hasMatchingEnvVars(Map modelEnvVar) {
+ if (modelEnvVar.isEmpty())
+ // sanity check so that we don't start rampage.
+ return false;
+
+ SortedMap envs = getEnvironmentVariables();
+ for (Entry e : modelEnvVar.entrySet()) {
+ String v = envs.get(e.getKey());
+ if (v == null || !v.equals(e.getValue()))
+ return false; // no match
+ }
+
+ return true;
+ }
+
+ // /**
+ // * Executes a chunk of code at the same machine where this process resides.
+ // */
+ // public T act(ProcessCallable callable) throws IOException, InterruptedException {
+ // return callable.invoke(this, FilePath.localChannel);
+ // }
+
+ Object writeReplace() {
+ return new SerializedProcess(pid);
+ }
+ }
+
+ /**
+ * Serialized form of {@link OSProcess} is the PID and {@link ProcessTree}
+ */
+ private final class SerializedProcess implements Serializable {
+ private final int pid;
+
+ private static final long serialVersionUID = 1L;
+
+ private SerializedProcess(int pid) {
+ this.pid = pid;
+ }
+
+ Object readResolve() {
+ return get(pid);
+ }
+ }
+
+ // /**
+ // * Code that gets executed on the machine where the {@link OSProcess} is local.
+ // * Used to act on {@link OSProcess}.
+ // *
+ // * @see OSProcess#act(ProcessCallable)
+ // */
+ // public interface ProcessCallable extends Serializable {
+ // /**
+ // * Performs the computational task on the node where the data is located.
+ // *
+ // * @param process
+ // * {@link OSProcess} that represents the local process.
+ // * @param channel
+ // * The "back pointer" of the {@link Channel} that represents the communication
+ // * with the node from where the code was sent.
+ // */
+ // T invoke(OSProcess process, VirtualChannel channel) throws IOException;
+ // }
+
+ /**
+ * Gets the {@link ProcessTree} of 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 ProcessTree get() {
+ if (!enabled)
+ return DEFAULT;
+
+ try {
+ if (File.pathSeparatorChar == ';')
+ return new Windows();
+
+ String os = fixNull(System.getProperty("os.name"));
+ if (os.equals("Linux"))
+ return new Linux();
+ if (os.equals("SunOS"))
+ return new Solaris();
+ if (os.equals("Mac OS X"))
+ return new Darwin();
+ } catch (LinkageError e) {
+ LOGGER.log(Level.WARNING, "Failed to load winp. Reverting to the default", e);
+ enabled = false;
+ }
+
+ return DEFAULT;
+ }
+
+ private static String fixNull(String s) {
+ if (s == null)
+ return "";
+ else
+ return s;
+ }
+
+ //
+ //
+ // implementation follows
+ //-------------------------------------------
+ //
+
+ /**
+ * Empty process list as a default value if the platform doesn't support it.
+ */
+ /* package */ static final ProcessTree DEFAULT = new Local() {
+ public OSProcess get(final Process proc) {
+ return new OSProcess(-1) {
+ public OSProcess getParent() {
+ return null;
+ }
+
+ public void killRecursively() {
+ // fall back to a single process killer
+ proc.destroy();
+ }
+
+ public void kill() throws InterruptedException {
+ proc.destroy();
+ }
+
+ public List getArguments() {
+ return Collections.emptyList();
+ }
+
+ public EnvVars getEnvironmentVariables() {
+ return new EnvVars();
+ }
+ };
+ }
+
+ public void killAll(Map modelEnvVars) {
+ // no-op
+ }
+ };
+
+ private static final class Windows extends Local {
+ Windows() {
+ for (final WinProcess p : WinProcess.all()) {
+ int pid = p.getPid();
+ if (pid == 0 || pid == 4)
+ continue; // skip the System Idle and System processes
+ super.processes.put(pid, new OSProcess(pid) {
+ private EnvVars env;
+
+ private List args;
+
+ public OSProcess getParent() {
+ // windows process doesn't have parent/child relationship
+ return null;
+ }
+
+ public void killRecursively() throws InterruptedException {
+ LOGGER.finer("Killing recursively " + getPid());
+ p.killRecursively();
+ }
+
+ public void kill() throws InterruptedException {
+ LOGGER.finer("Killing " + getPid());
+ p.kill();
+ }
+
+ @Override
+ public synchronized List getArguments() {
+ if (args == null)
+ args = Arrays.asList(QuotedStringTokenizer.tokenize(p.getCommandLine()));
+ return args;
+ }
+
+ @Override
+ public synchronized EnvVars getEnvironmentVariables() {
+ if (env != null)
+ return env;
+ env = new EnvVars();
+
+ try {
+ env.putAll(p.getEnvironmentVariables());
+ } catch (WinpException e) {
+ LOGGER.log(FINE, "Failed to get environment variable ", e);
+ }
+ return env;
+ }
+ });
+
+ }
+ }
+
+ @Override
+ public OSProcess get(Process proc) {
+ return get(new WinProcess(proc).getPid());
+ }
+
+ public void killAll(Map modelEnvVars) throws InterruptedException {
+ for (OSProcess p : this) {
+ if (p.getPid() < 10)
+ continue; // ignore system processes like "idle process"
+
+ LOGGER.finest("Considering to kill " + p.getPid());
+
+ boolean matched;
+ try {
+ matched = p.hasMatchingEnvVars(modelEnvVars);
+ } catch (WinpException e) {
+ // likely a missing privilege
+ LOGGER.log(FINEST, " Failed to check environment variable match", e);
+ continue;
+ }
+
+ if (matched)
+ p.killRecursively();
+ else
+ LOGGER.finest("Environment variable didn't match");
+
+ }
+ }
+
+ static {
+ WinProcess.enableDebugPrivilege();
+ }
+ }
+
+ static abstract class Unix extends Local {
+
+ @Override
+ public OSProcess get(Process proc) {
+ try {
+ return get((Integer) UnixReflection.pid(proc));
+ } catch (IllegalAccessError e) { // impossible
+ IllegalAccessError x = new IllegalAccessError();
+ x.initCause(e);
+ throw x;
+ }
+ }
+
+ public void killAll(Map modelEnvVars) throws InterruptedException {
+ for (OSProcess p : this)
+ if (p.hasMatchingEnvVars(modelEnvVars))
+ p.killRecursively();
+ }
+ }
+
+ /**
+ * {@link ProcessTree} based on /proc.
+ */
+ static abstract class ProcfsUnix extends Unix {
+ ProcfsUnix() {
+ 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 OSProcess createProcess(int pid) throws IOException;
+ }
+
+ /**
+ * A process.
+ */
+ public abstract class UnixProcess extends OSProcess {
+ protected UnixProcess(int pid) {
+ super(pid);
+ }
+
+ protected final File getFile(String relativePath) {
+ return new File(new File("/proc/" + getPid()), relativePath);
+ }
+
+ /**
+ * Tries to kill this process.
+ */
+ public void kill() throws InterruptedException {
+ try {
+ int pid = getPid();
+ LOGGER.fine("Killing pid=" + pid);
+ UnixReflection.destroy(pid);
+ } 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() throws InterruptedException {
+ LOGGER.fine("Recursively killing pid=" + getPid());
+ for (OSProcess p : getChildren())
+ p.killRecursively();
+ kill();
+ }
+
+ /**
+ * Obtains the argument list of this process.
+ *
+ * @return
+ * empty list if failed (for example because the process is already dead,
+ * or the permission was denied.)
+ */
+ public abstract List getArguments();
+ }
+
+ /**
+ * Reflection used in the Unix support.
+ */
+ private static final class UnixReflection {
+ /**
+ * Field to access the PID of the process.
+ * Required for Java 8 and older JVMs.
+ */
+ private static final Field JAVA8_PID_FIELD;
+
+ /**
+ * Field to access the PID of the process.
+ * Required for Java 9 and above until this is replaced by multi-release JAR.
+ */
+ private static final Method JAVA9_PID_METHOD;
+
+ /**
+ * Method to destroy a process, given pid.
+ *
+ * Looking at the JavaSE source code, this is using SIGTERM (15)
+ */
+ private static final Method JAVA8_DESTROY_PROCESS;
+ private static final Method JAVA_9_PROCESSHANDLE_OF;
+ private static final Method JAVA_9_PROCESSHANDLE_DESTROY;
+
+ static {
+ try {
+ if (isPostJava8()) {
+ Class> clazz = Process.class;
+ JAVA9_PID_METHOD = clazz.getMethod("pid");
+ JAVA8_PID_FIELD = null;
+ Class> processHandleClazz = Class.forName("java.lang.ProcessHandle");
+ JAVA_9_PROCESSHANDLE_OF = processHandleClazz.getMethod("of", long.class);
+ JAVA_9_PROCESSHANDLE_DESTROY = processHandleClazz.getMethod("destroy");
+ JAVA8_DESTROY_PROCESS = null;
+ } else {
+ Class> clazz = Class.forName("java.lang.UNIXProcess");
+ JAVA8_PID_FIELD = clazz.getDeclaredField("pid");
+ JAVA8_PID_FIELD.setAccessible(true);
+ JAVA9_PID_METHOD = null;
+
+ JAVA8_DESTROY_PROCESS = clazz.getDeclaredMethod("destroyProcess", int.class, boolean.class);
+ JAVA8_DESTROY_PROCESS.setAccessible(true);
+ JAVA_9_PROCESSHANDLE_OF = null;
+ JAVA_9_PROCESSHANDLE_DESTROY = null;
+ }
+ } catch (ClassNotFoundException | NoSuchFieldException | NoSuchMethodException e) {
+ LinkageError x = new LinkageError("Cannot initialize reflection for Unix Processes", e);
+ throw x;
+ }
+ }
+
+ public static void destroy(int pid) throws IllegalAccessException,
+ InvocationTargetException {
+ if (JAVA8_DESTROY_PROCESS != null) {
+ JAVA8_DESTROY_PROCESS.invoke(null, pid, false);
+ } else {
+ final Optional handle = (Optional)JAVA_9_PROCESSHANDLE_OF.invoke(null, pid);
+ if (handle.isPresent()) {
+ JAVA_9_PROCESSHANDLE_DESTROY.invoke(handle.get());
+ }
+ }
+ }
+
+ public static int pid(Process proc) {
+ try {
+ if (JAVA8_PID_FIELD != null) {
+ return JAVA8_PID_FIELD.getInt(proc);
+ } else {
+ long pid = (long) JAVA9_PID_METHOD.invoke(proc);
+ if (pid > Integer.MAX_VALUE) {
+ throw new IllegalAccessError("PID is out of bounds: " + pid);
+ }
+ return (int) pid;
+ }
+ } catch (IllegalAccessException | InvocationTargetException e) { // impossible
+ IllegalAccessError x = new IllegalAccessError();
+ x.initCause(e);
+ throw x;
+ }
+ }
+
+ private static String getJavaVersionFromSystemProperty(){
+ return System.getProperty("java.version");
+ }
+
+ private static boolean isPostJava8(){
+ return !getJavaVersionFromSystemProperty().startsWith("1.");
+ }
+
+ }
+
+ static class Linux extends ProcfsUnix {
+ protected LinuxProcess createProcess(int pid) throws IOException {
+ return new LinuxProcess(pid);
+ }
+
+ class LinuxProcess extends UnixProcess {
+ private int ppid = -1;
+
+ private EnvVars envVars;
+
+ private List arguments;
+
+ LinuxProcess(int pid) throws IOException {
+ super(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 OSProcess getParent() {
+ return get(ppid);
+ }
+
+ public synchronized List getArguments() {
+ if (arguments != null)
+ return arguments;
+ arguments = new ArrayList();
+ try {
+ byte[] cmdline = readFileToByteArray(getFile("cmdline"));
+ int pos = 0;
+ for (int i = 0; i < cmdline.length; i++) {
+ byte b = cmdline[i];
+ if (b == 0) {
+ arguments.add(new String(cmdline, 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.
+ }
+ arguments = Collections.unmodifiableList(arguments);
+ return arguments;
+ }
+
+ public synchronized EnvVars getEnvironmentVariables() {
+ if (envVars != null)
+ return envVars;
+ envVars = new EnvVars();
+ try {
+ byte[] environ = 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;
+ }
+ }
+
+ public byte[] readFileToByteArray(File file) throws IOException {
+ return Files.readAllBytes(file.toPath());
+ }
+ }
+
+ /**
+ * 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.
+ */
+ static class Solaris extends ProcfsUnix {
+ protected OSProcess createProcess(final int pid) throws IOException {
+ return new SolarisProcess(pid);
+ }
+
+ private class SolarisProcess extends UnixProcess {
+ private final int ppid;
+
+ /**
+ * Address of the environment vector. Even on 64bit Solaris this is still 32bit pointer.
+ */
+ private final int envp;
+
+ /**
+ * Similarly, address of the arguments vector.
+ */
+ private final int argp;
+
+ private final int argc;
+
+ private EnvVars envVars;
+
+ private List arguments;
+
+ private SolarisProcess(int pid) throws IOException {
+ super(pid);
+
+ RandomAccessFile psinfo = new RandomAccessFile(getFile("psinfo"), "r");
+ try {
+ // see http://cvs.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/common/sys/procfs.h
+ //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.
+
+ // see http://cvs.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/cmd/ptools/pargs/pargs.c
+ // for how to read this information
+
+ psinfo.seek(8);
+ if (adjust(psinfo.readInt()) != pid)
+ throw new IOException("psinfo PID mismatch"); // sanity check
+ ppid = adjust(psinfo.readInt());
+
+ psinfo.seek(188); // now jump to pr_argc
+ argc = adjust(psinfo.readInt());
+ argp = adjust(psinfo.readInt());
+ envp = adjust(psinfo.readInt());
+ } finally {
+ psinfo.close();
+ }
+ if (ppid == -1)
+ throw new IOException("Failed to parse PPID from /proc/" + pid + "/status");
+
+ }
+
+ public OSProcess getParent() {
+ return get(ppid);
+ }
+
+ public synchronized List getArguments() {
+ if (arguments != null)
+ return arguments;
+
+ arguments = new ArrayList(argc);
+
+ try {
+ RandomAccessFile as = new RandomAccessFile(getFile("as"), "r");
+ if (LOGGER.isLoggable(FINER))
+ LOGGER.finer("Reading " + getFile("as"));
+ try {
+ for (int n = 0; n < argc; n++) {
+ // read a pointer to one entry
+ as.seek(to64(argp + n * 4));
+ int p = adjust(as.readInt());
+
+ arguments.add(readLine(as, p, "argv[" + n + "]"));
+ }
+ } finally {
+ 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.
+ }
+
+ arguments = Collections.unmodifiableList(arguments);
+ return arguments;
+ }
+
+ public synchronized EnvVars getEnvironmentVariables() {
+ if (envVars != null)
+ return envVars;
+ envVars = new EnvVars();
+
+ try {
+ RandomAccessFile as = new RandomAccessFile(getFile("as"), "r");
+ if (LOGGER.isLoggable(FINER))
+ LOGGER.finer("Reading " + getFile("as"));
+ try {
+ for (int n = 0;; n++) {
+ // read a pointer to one entry
+ as.seek(to64(envp + n * 4));
+ int p = adjust(as.readInt());
+ if (p == 0)
+ break; // completed the walk
+
+ // now read the null-terminated string
+ envVars.addLine(readLine(as, p, "env[" + n + "]"));
+ }
+ } finally {
+ 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;
+ }
+
+ private String readLine(RandomAccessFile as, int p, String prefix) throws IOException {
+ if (LOGGER.isLoggable(FINEST))
+ LOGGER.finest("Reading " + prefix + " at " + p);
+
+ as.seek(to64(p));
+ ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ int ch, i = 0;
+ while ((ch = as.read()) > 0) {
+ if ((++i) % 100 == 0 && LOGGER.isLoggable(FINEST))
+ LOGGER.finest(prefix + " is so far " + buf.toString());
+
+ buf.write(ch);
+ }
+ String line = buf.toString();
+ if (LOGGER.isLoggable(FINEST))
+ LOGGER.finest(prefix + " was " + line);
+ return line;
+ }
+ }
+
+ /**
+ * int to long conversion with zero-padding.
+ */
+ private static long to64(int i) {
+ return i & 0xFFFFFFFFL;
+ }
+
+ /**
+ * {@link DataInputStream} reads a value in big-endian, so
+ * convert it to the correct value on little-endian systems.
+ */
+ private static int adjust(int i) {
+ if (IS_LITTLE_ENDIAN)
+ return (i << 24) | ((i << 8) & 0x00FF0000) | ((i >> 8) & 0x0000FF00) | (i >>> 24);
+ else
+ return i;
+ }
+
+ }
+
+ /**
+ * Implementation for Mac OS X based on sysctl(3).
+ */
+ private static class Darwin extends Unix {
+ Darwin() {
+ String arch = System.getProperty("sun.arch.data.model");
+ if ("64".equals(arch)) {
+ sizeOf_kinfo_proc = sizeOf_kinfo_proc_64;
+ kinfo_proc_pid_offset = kinfo_proc_pid_offset_64;
+ kinfo_proc_ppid_offset = kinfo_proc_ppid_offset_64;
+ } else {
+ sizeOf_kinfo_proc = sizeOf_kinfo_proc_32;
+ kinfo_proc_pid_offset = kinfo_proc_pid_offset_32;
+ kinfo_proc_ppid_offset = kinfo_proc_ppid_offset_32;
+ }
+ try {
+ IntByReference underscore = new IntByReference(sizeOfInt);
+ IntByReference size = new IntByReference(sizeOfInt);
+ Memory m;
+ int nRetry = 0;
+ while (true) {
+ // find out how much memory we need to do this
+ if (LIBC.sysctl(MIB_PROC_ALL, 3, NULL, size, NULL, underscore) != 0)
+ throw new IOException("Failed to obtain memory requirement: " +
+ LIBC.strerror(Native.getLastError()));
+
+ // now try the real call
+ m = new Memory(size.getValue());
+ if (LIBC.sysctl(MIB_PROC_ALL, 3, m, size, NULL, underscore) != 0) {
+ if (Native.getLastError() == ENOMEM && nRetry++ < 16)
+ continue; // retry
+ throw new IOException("Failed to call kern.proc.all: " + LIBC.strerror(Native.getLastError()));
+ }
+ break;
+ }
+
+ int count = size.getValue() / sizeOf_kinfo_proc;
+ LOGGER.fine("Found " + count + " processes");
+
+ for (int base = 0; base < size.getValue(); base += sizeOf_kinfo_proc) {
+ int pid = m.getInt(base + kinfo_proc_pid_offset);
+ int ppid = m.getInt(base + kinfo_proc_ppid_offset);
+ // int effective_uid = m.getInt(base+304);
+ // byte[] comm = new byte[16];
+ // m.read(base+163,comm,0,16);
+
+ super.processes.put(pid, new DarwinProcess(pid, ppid));
+ }
+ } catch (IOException e) {
+ LOGGER.log(Level.WARNING, "Failed to obtain process list", e);
+ }
+ }
+
+ private class DarwinProcess extends UnixProcess {
+ private final int ppid;
+
+ private EnvVars envVars;
+
+ private List arguments;
+
+ DarwinProcess(int pid, int ppid) {
+ super(pid);
+ this.ppid = ppid;
+ }
+
+ public OSProcess getParent() {
+ return get(ppid);
+ }
+
+ public synchronized EnvVars getEnvironmentVariables() {
+ if (envVars != null)
+ return envVars;
+ parse();
+ return envVars;
+ }
+
+ public List getArguments() {
+ if (arguments != null)
+ return arguments;
+ parse();
+ return arguments;
+ }
+
+ private void parse() {
+ try {
+ // allocate them first, so that the parse error wil result in empty data
+ // and avoid retry.
+ arguments = new ArrayList();
+ envVars = new EnvVars();
+
+ IntByReference underscore = new IntByReference();
+
+ IntByReference argmaxRef = new IntByReference(0);
+ IntByReference size = new IntByReference(sizeOfInt);
+
+ // for some reason, I was never able to get sysctlbyname work.
+ // if(LIBC.sysctlbyname("kern.argmax", argmaxRef.getPointer(), size, NULL, _)!=0)
+ if (LIBC.sysctl(new int[] { CTL_KERN, KERN_ARGMAX }, 2, argmaxRef.getPointer(), size, NULL, underscore) != 0)
+ throw new IOException("Failed to get kernl.argmax: " + LIBC.strerror(Native.getLastError()));
+
+ int argmax = argmaxRef.getValue();
+
+ class StringArrayMemory extends Memory {
+ private long offset = 0;
+
+ StringArrayMemory(long l) {
+ super(l);
+ }
+
+ int readInt() {
+ int r = getInt(offset);
+ offset += sizeOfInt;
+ return r;
+ }
+
+ byte peek() {
+ return getByte(offset);
+ }
+
+ String readString() {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ byte ch;
+ while ((ch = getByte(offset++)) != '\0')
+ baos.write(ch);
+ return baos.toString();
+ }
+
+ void skip0() {
+ // skip padding '\0's
+ while (getByte(offset) == '\0')
+ offset++;
+ }
+ }
+ StringArrayMemory m = new StringArrayMemory(argmax);
+ size.setValue(argmax);
+ if (LIBC.sysctl(new int[] { CTL_KERN, KERN_PROCARGS2, pid }, 3, m, size, NULL, underscore) != 0)
+ throw new IOException("Failed to obtain ken.procargs2: " +
+ LIBC.strerror(Native.getLastError()));
+
+ /*
+ * Make a sysctl() call to get the raw argument space of the
+ * process. The layout is documented in start.s, which is part
+ * of the Csu project. In summary, it looks like:
+ *
+ * /---------------\ 0x00000000
+ * : :
+ * : :
+ * |---------------|
+ * | argc |
+ * |---------------|
+ * | arg[0] |
+ * |---------------|
+ * : :
+ * : :
+ * |---------------|
+ * | arg[argc - 1] |
+ * |---------------|
+ * | 0 |
+ * |---------------|
+ * | env[0] |
+ * |---------------|
+ * : :
+ * : :
+ * |---------------|
+ * | env[n] |
+ * |---------------|
+ * | 0 |
+ * |---------------| <-- Beginning of data returned by sysctl()
+ * | exec_path | is here.
+ * |:::::::::::::::|
+ * | |
+ * | String area. |
+ * | |
+ * |---------------| <-- Top of stack.
+ * : :
+ * : :
+ * \---------------/ 0xffffffff
+ */
+
+ // I find the Darwin source code of the 'ps' command helpful in understanding how it does this:
+ // see http://www.opensource.apple.com/source/adv_cmds/adv_cmds-147/ps/print.c
+ int argc = m.readInt();
+ String args0 = m.readString(); // exec path
+ m.skip0();
+ try {
+ for (int i = 0; i < argc; i++) {
+ arguments.add(m.readString());
+ }
+ } catch (IndexOutOfBoundsException e) {
+ throw new IllegalStateException("Failed to parse arguments: pid=" + pid + ", arg0=" + args0 +
+ ", arguments=" + arguments + ", nargs=" + argc +
+ ". Please run 'ps e " + pid +
+ "' and report this to https://issues.jenkins-ci.org/browse/JENKINS-9634",
+ e);
+ }
+
+ // read env vars that follow
+ while (m.peek() != 0)
+ envVars.addLine(m.readString());
+ } catch (IOException e) {
+ // this happens with insufficient permissions, so just ignore the problem.
+ }
+ }
+ }
+
+ // local constants
+ private final int sizeOf_kinfo_proc;
+
+ private static final int sizeOf_kinfo_proc_32 = 492; // on 32bit Mac OS X.
+
+ private static final int sizeOf_kinfo_proc_64 = 648; // on 64bit Mac OS X.
+
+ private final int kinfo_proc_pid_offset;
+
+ private static final int kinfo_proc_pid_offset_32 = 24;
+
+ private static final int kinfo_proc_pid_offset_64 = 40;
+
+ private final int kinfo_proc_ppid_offset;
+
+ private static final int kinfo_proc_ppid_offset_32 = 416;
+
+ private static final int kinfo_proc_ppid_offset_64 = 560;
+
+ private static final int sizeOfInt = Native.getNativeSize(int.class);
+
+ private static final int CTL_KERN = 1;
+
+ private static final int KERN_PROC = 14;
+
+ private static final int KERN_PROC_ALL = 0;
+
+ private static final int ENOMEM = 12;
+
+ private static int[] MIB_PROC_ALL = { CTL_KERN, KERN_PROC, KERN_PROC_ALL };
+
+ private static final int KERN_ARGMAX = 8;
+
+ private static final int KERN_PROCARGS2 = 49;
+ }
+
+ /**
+ * Represents a local process tree, where this JVM and the process tree run on the same system.
+ * (The opposite of {@link Remote}.)
+ */
+ public static abstract class Local extends ProcessTree {
+ Local() {
+ }
+ }
+
+ /*
+ * 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(ProcessTree.class.getName());
+
+ /**
+ * Flag to control this feature.
+ *
+ *
+ * This feature involves some native code, so we are allowing the user to disable this
+ * in case there's a fatal problem.
+ *
+ *
+ * This property supports two names for a compatibility reason.
+ */
+ public static boolean enabled = !Boolean.getBoolean(ProcessTree.class.getName() + ".disable");
+}
diff --git a/src/main/java/org/ow2/proactive/process_tree_killer/ProcessTreeRemoting.java b/src/main/java/org/ow2/proactive/process_tree_killer/ProcessTreeRemoting.java
new file mode 100644
index 0000000..20dfdd7
--- /dev/null
+++ b/src/main/java/org/ow2/proactive/process_tree_killer/ProcessTreeRemoting.java
@@ -0,0 +1,58 @@
+/*
+ * ProActive Parallel Suite(TM):
+ * The Open Source library for parallel and distributed
+ * Workflows & Scheduling, Orchestration, Cloud Automation
+ * and Big Data Analysis on Enterprise Grids & Clouds.
+ *
+ * Copyright (c) 2007 - 2017 ActiveEon
+ * Contact: contact@activeeon.com
+ *
+ * This library is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation: version 3 of
+ * the License.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * If needed, contact us to obtain a release under GPL Version 2 or 3
+ * or a different license than the AGPL.
+ */
+package org.ow2.proactive.process_tree_killer;
+
+import java.lang.reflect.Proxy;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * Remoting interfaces of {@link ProcessTree}.
+ *
+ * These classes need to be public due to the way {@link Proxy} works.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+public class ProcessTreeRemoting {
+ public interface IProcessTree {
+ void killAll(Map modelEnvVars) throws InterruptedException;
+ }
+
+ public interface IOSProcess {
+ int getPid();
+
+ IOSProcess getParent();
+
+ void kill() throws InterruptedException;
+
+ void killRecursively() throws InterruptedException;
+
+ List getArguments();
+
+ EnvVars getEnvironmentVariables();
+ }
+}
diff --git a/src/main/java/org/ow2/proactive/process_tree_killer/QuotedStringTokenizer.java b/src/main/java/org/ow2/proactive/process_tree_killer/QuotedStringTokenizer.java
new file mode 100644
index 0000000..5af17ad
--- /dev/null
+++ b/src/main/java/org/ow2/proactive/process_tree_killer/QuotedStringTokenizer.java
@@ -0,0 +1,473 @@
+/*
+ * ProActive Parallel Suite(TM):
+ * The Open Source library for parallel and distributed
+ * Workflows & Scheduling, Orchestration, Cloud Automation
+ * and Big Data Analysis on Enterprise Grids & Clouds.
+ *
+ * Copyright (c) 2007 - 2017 ActiveEon
+ * Contact: contact@activeeon.com
+ *
+ * This library is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation: version 3 of
+ * the License.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * If needed, contact us to obtain a release under GPL Version 2 or 3
+ * or a different license than the AGPL.
+ */
+package org.ow2.proactive.process_tree_killer;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.StringTokenizer;
+
+
+/* ------------------------------------------------------------ */
+
+/** StringTokenizer with Quoting support.
+ *
+ * This class is a copy of the java.util.StringTokenizer API and
+ * the behaviour is the same, except that single and doulbe quoted
+ * string values are recognized.
+ * Delimiters within quotes are not considered delimiters.
+ * Quotes can be escaped with '\'.
+ *
+ * @see StringTokenizer
+ * @author Greg Wilkins (gregw)
+ */
+public class QuotedStringTokenizer extends StringTokenizer {
+ private final static String __delim = " \t\n\r";
+
+ private String _string;
+
+ private String _delim = __delim;
+
+ private boolean _returnQuotes = false;
+
+ private boolean _returnDelimiters = false;
+
+ private StringBuilder _token;
+
+ private boolean _hasToken = false;
+
+ private int _i = 0;
+
+ private int _lastStart = 0;
+
+ private boolean _double = true;
+
+ private boolean _single = true;
+
+ public static String[] tokenize(String str) {
+ return new QuotedStringTokenizer(str).toArray();
+ }
+
+ public static String[] tokenize(String str, String delimiters) {
+ return new QuotedStringTokenizer(str, delimiters).toArray();
+ }
+
+ /* ------------------------------------------------------------ */
+ /**
+ *
+ * @param str
+ * String to tokenize.
+ * @param delim
+ * List of delimiter characters as string. Can be null, to default to ' \t\n\r'
+ * @param returnDelimiters
+ * If true, {@link #nextToken()} will include the delimiters, not just tokenized
+ * tokens.
+ * @param returnQuotes
+ * If true, {@link #nextToken()} will include the quotation characters when they are present.
+ */
+ public QuotedStringTokenizer(String str, String delim, boolean returnDelimiters, boolean returnQuotes) {
+ super("");
+ _string = str;
+ if (delim != null)
+ _delim = delim;
+ _returnDelimiters = returnDelimiters;
+ _returnQuotes = returnQuotes;
+
+ if (_delim.indexOf('\'') >= 0 || _delim.indexOf('"') >= 0)
+ throw new Error("Can't use quotes as delimiters: " + _delim);
+
+ _token = new StringBuilder(_string.length() > 1024 ? 512 : _string.length() / 2);
+ }
+
+ /* ------------------------------------------------------------ */
+ public QuotedStringTokenizer(String str, String delim, boolean returnDelimiters) {
+ this(str, delim, returnDelimiters, false);
+ }
+
+ /* ------------------------------------------------------------ */
+ public QuotedStringTokenizer(String str, String delim) {
+ this(str, delim, false, false);
+ }
+
+ /* ------------------------------------------------------------ */
+ public QuotedStringTokenizer(String str) {
+ this(str, null, false, false);
+ }
+
+ public String[] toArray() {
+ List r = new ArrayList();
+ while (hasMoreTokens())
+ r.add(nextToken());
+ return r.toArray(new String[r.size()]);
+ }
+
+ /* ------------------------------------------------------------ */
+ @Override
+ public boolean hasMoreTokens() {
+ // Already found a token
+ if (_hasToken)
+ return true;
+
+ _lastStart = _i;
+
+ int state = 0;
+ boolean escape = false;
+ while (_i < _string.length()) {
+ char c = _string.charAt(_i++);
+
+ switch (state) {
+ case 0: // Start
+ if (_delim.indexOf(c) >= 0) {
+ if (_returnDelimiters) {
+ _token.append(c);
+ return _hasToken = true;
+ }
+ } else if (c == '\'' && _single) {
+ if (_returnQuotes)
+ _token.append(c);
+ state = 2;
+ } else if (c == '\"' && _double) {
+ if (_returnQuotes)
+ _token.append(c);
+ state = 3;
+ } else {
+ _token.append(c);
+ _hasToken = true;
+ state = 1;
+ }
+ continue;
+
+ case 1: // Token
+ _hasToken = true;
+ if (escape) {
+ escape = false;
+ if (ESCAPABLE_CHARS.indexOf(c) < 0)
+ _token.append('\\');
+ _token.append(c);
+ } else if (_delim.indexOf(c) >= 0) {
+ if (_returnDelimiters)
+ _i--;
+ return _hasToken;
+ } else if (c == '\'' && _single) {
+ if (_returnQuotes)
+ _token.append(c);
+ state = 2;
+ } else if (c == '\"' && _double) {
+ if (_returnQuotes)
+ _token.append(c);
+ state = 3;
+ } else if (c == '\\') {
+ escape = true;
+ } else
+ _token.append(c);
+ continue;
+
+ case 2: // Single Quote
+ _hasToken = true;
+ if (escape) {
+ escape = false;
+ if (ESCAPABLE_CHARS.indexOf(c) < 0)
+ _token.append('\\');
+ _token.append(c);
+ } else if (c == '\'') {
+ if (_returnQuotes)
+ _token.append(c);
+ state = 1;
+ } else if (c == '\\') {
+ if (_returnQuotes)
+ _token.append(c);
+ escape = true;
+ } else
+ _token.append(c);
+ continue;
+
+ case 3: // Double Quote
+ _hasToken = true;
+ if (escape) {
+ escape = false;
+ if (ESCAPABLE_CHARS.indexOf(c) < 0)
+ _token.append('\\');
+ _token.append(c);
+ } else if (c == '\"') {
+ if (_returnQuotes)
+ _token.append(c);
+ state = 1;
+ } else if (c == '\\') {
+ if (_returnQuotes)
+ _token.append(c);
+ escape = true;
+ } else
+ _token.append(c);
+ continue;
+ }
+ }
+
+ return _hasToken;
+ }
+
+ /* ------------------------------------------------------------ */
+ @Override
+ public String nextToken() throws NoSuchElementException {
+ if (!hasMoreTokens() || _token == null)
+ throw new NoSuchElementException();
+ String t = _token.toString();
+ _token.setLength(0);
+ _hasToken = false;
+ return t;
+ }
+
+ /* ------------------------------------------------------------ */
+ @Override
+ public String nextToken(String delim) throws NoSuchElementException {
+ _delim = delim;
+ _i = _lastStart;
+ _token.setLength(0);
+ _hasToken = false;
+ return nextToken();
+ }
+
+ /* ------------------------------------------------------------ */
+ @Override
+ public boolean hasMoreElements() {
+ return hasMoreTokens();
+ }
+
+ /* ------------------------------------------------------------ */
+ @Override
+ public Object nextElement() throws NoSuchElementException {
+ return nextToken();
+ }
+
+ /* ------------------------------------------------------------ */
+ /** Not implemented.
+ */
+ @Override
+ public int countTokens() {
+ return -1;
+ }
+
+ /* ------------------------------------------------------------ */
+ /** Quote a string.
+ * The string is quoted only if quoting is required due to
+ * embeded delimiters, quote characters or the
+ * empty string.
+ * @param s The string to quote.
+ * @return quoted string
+ */
+ public static String quote(String s, String delim) {
+ if (s == null)
+ return null;
+ if (s.length() == 0)
+ return "\"\"";
+
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if (c == '\\' || c == '"' || c == '\'' || Character.isWhitespace(c) || delim.indexOf(c) >= 0) {
+ StringBuffer b = new StringBuffer(s.length() + 8);
+ quote(b, s);
+ return b.toString();
+ }
+ }
+
+ return s;
+ }
+
+ /* ------------------------------------------------------------ */
+ /** Quote a string.
+ * The string is quoted only if quoting is required due to
+ * embeded delimiters, quote characters or the
+ * empty string.
+ * @param s The string to quote.
+ * @return quoted string
+ */
+ public static String quote(String s) {
+ if (s == null)
+ return null;
+ if (s.length() == 0)
+ return "\"\"";
+
+ StringBuffer b = new StringBuffer(s.length() + 8);
+ quote(b, s);
+ return b.toString();
+
+ }
+
+ /* ------------------------------------------------------------ */
+ /** Quote a string into a StringBuffer.
+ * The characters ", \, \n, \r, \t, \f and \b are escaped
+ * @param buf The StringBuffer
+ * @param s The String to quote.
+ */
+ public static void quote(StringBuffer buf, String s) {
+ synchronized (buf) {
+ buf.append('"');
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ switch (c) {
+ case '"':
+ buf.append("\\\"");
+ continue;
+ case '\\':
+ buf.append("\\\\");
+ continue;
+ case '\n':
+ buf.append("\\n");
+ continue;
+ case '\r':
+ buf.append("\\r");
+ continue;
+ case '\t':
+ buf.append("\\t");
+ continue;
+ case '\f':
+ buf.append("\\f");
+ continue;
+ case '\b':
+ buf.append("\\b");
+ continue;
+
+ default:
+ buf.append(c);
+ continue;
+ }
+ }
+ buf.append('"');
+ }
+ }
+
+ /* ------------------------------------------------------------ */
+ /** Unquote a string.
+ * @param s The string to unquote.
+ * @return quoted string
+ */
+ public static String unquote(String s) {
+ if (s == null)
+ return null;
+ if (s.length() < 2)
+ return s;
+
+ char first = s.charAt(0);
+ char last = s.charAt(s.length() - 1);
+ if (first != last || (first != '"' && first != '\''))
+ return s;
+
+ StringBuilder b = new StringBuilder(s.length() - 2);
+ boolean escape = false;
+ for (int i = 1; i < s.length() - 1; i++) {
+ char c = s.charAt(i);
+
+ if (escape) {
+ escape = false;
+ switch (c) {
+ case 'n':
+ b.append('\n');
+ break;
+ case 'r':
+ b.append('\r');
+ break;
+ case 't':
+ b.append('\t');
+ break;
+ case 'f':
+ b.append('\f');
+ break;
+ case 'b':
+ b.append('\b');
+ break;
+ case 'u':
+ b.append((char) ((convertHexDigit((byte) s.charAt(i++)) << 24) +
+ (convertHexDigit((byte) s.charAt(i++)) << 16) +
+ (convertHexDigit((byte) s.charAt(i++)) << 8) +
+ (convertHexDigit((byte) s.charAt(i++)))));
+ break;
+ default:
+ b.append(c);
+ }
+ } else if (c == '\\') {
+ escape = true;
+ continue;
+ } else
+ b.append(c);
+ }
+
+ return b.toString();
+ }
+
+ /* ------------------------------------------------------------ */
+ /**
+ * @return handle double quotes if true
+ */
+ public boolean getDouble() {
+ return _double;
+ }
+
+ /* ------------------------------------------------------------ */
+ /**
+ * @param d handle double quotes if true
+ */
+ public void setDouble(boolean d) {
+ _double = d;
+ }
+
+ /* ------------------------------------------------------------ */
+ /**
+ * @return handle single quotes if true
+ */
+ public boolean getSingle() {
+ return _single;
+ }
+
+ /* ------------------------------------------------------------ */
+ /**
+ * @param single handle single quotes if true
+ */
+ public void setSingle(boolean single) {
+ _single = single;
+ }
+
+ /**
+ * @param b An ASCII encoded character 0-9 a-f A-F
+ * @return The byte value of the character 0-16.
+ */
+ public static byte convertHexDigit(byte b) {
+ if ((b >= '0') && (b <= '9'))
+ return (byte) (b - '0');
+ if ((b >= 'a') && (b <= 'f'))
+ return (byte) (b - 'a' + 10);
+ if ((b >= 'A') && (b <= 'F'))
+ return (byte) (b - 'A' + 10);
+ return 0;
+ }
+
+ /**
+ * Characters that can be escaped with \.
+ *
+ * Others, like, say, \W will be left alone instead of becoming just W.
+ * This is important to keep Hudson behave on Windows, which uses '\' as
+ * the directory separator.
+ */
+ private static final String ESCAPABLE_CHARS = "\\\"' ";
+}
diff --git a/src/main/java/org/ow2/proactive/process_tree_killer/Util.java b/src/main/java/org/ow2/proactive/process_tree_killer/Util.java
new file mode 100644
index 0000000..dd15d58
--- /dev/null
+++ b/src/main/java/org/ow2/proactive/process_tree_killer/Util.java
@@ -0,0 +1,107 @@
+/*
+ * ProActive Parallel Suite(TM):
+ * The Open Source library for parallel and distributed
+ * Workflows & Scheduling, Orchestration, Cloud Automation
+ * and Big Data Analysis on Enterprise Grids & Clouds.
+ *
+ * Copyright (c) 2007 - 2017 ActiveEon
+ * Contact: contact@activeeon.com
+ *
+ * This library is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation: version 3 of
+ * the License.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * If needed, contact us to obtain a release under GPL Version 2 or 3
+ * or a different license than the AGPL.
+ */
+package org.ow2.proactive.process_tree_killer;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+public class Util {
+
+ /**
+ * Pattern for capturing variables. Either $xyz, ${xyz} or ${a.b} but not $a.b, while ignoring "$$"
+ */
+ private static final Pattern VARIABLE = Pattern.compile("\\$([A-Za-z0-9_]+|\\{[A-Za-z0-9_.]+\\}|\\$)");
+
+ /**
+ * Concatenate multiple strings by inserting a separator.
+ */
+ public static String join(Collection> strings, String separator) {
+ StringBuilder buf = new StringBuilder();
+ boolean first = true;
+ for (Object s : strings) {
+ if (first)
+ first = false;
+ else
+ buf.append(separator);
+ buf.append(s);
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Replaces the occurrence of '$key' by properties.get('key').
+ *
+ *
+ * Unlike shell, undefined variables are left as-is (this behavior is the same as Ant.)
+ *
+ */
+
+ public static String replaceMacro(String s, Map properties) {
+ return replaceMacro(s, new VariableResolver.ByMap(properties));
+ }
+
+ /**
+ * Replaces the occurrence of '$key' by resolver.get('key').
+ *
+ *
+ * Unlike shell, undefined variables are left as-is (this behavior is the same as Ant.)
+ */
+ public static String replaceMacro(String s, VariableResolver resolver) {
+ if (s == null) {
+ return null;
+ }
+
+ int idx = 0;
+ while (true) {
+ Matcher m = VARIABLE.matcher(s);
+ if (!m.find(idx))
+ return s;
+
+ String key = m.group().substring(1);
+
+ // escape the dollar sign or get the key to resolve
+ String value;
+ if (key.charAt(0) == '$') {
+ value = "$";
+ } else {
+ if (key.charAt(0) == '{')
+ key = key.substring(1, key.length() - 1);
+ value = resolver.resolve(key);
+ }
+
+ if (value == null)
+ idx = m.end(); // skip this
+ else {
+ s = s.substring(0, m.start()) + value + s.substring(m.end());
+ idx = m.start() + value.length();
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/ow2/proactive/process_tree_killer/VariableResolver.java b/src/main/java/org/ow2/proactive/process_tree_killer/VariableResolver.java
new file mode 100644
index 0000000..67601f6
--- /dev/null
+++ b/src/main/java/org/ow2/proactive/process_tree_killer/VariableResolver.java
@@ -0,0 +1,106 @@
+/*
+ * ProActive Parallel Suite(TM):
+ * The Open Source library for parallel and distributed
+ * Workflows & Scheduling, Orchestration, Cloud Automation
+ * and Big Data Analysis on Enterprise Grids & Clouds.
+ *
+ * Copyright (c) 2007 - 2017 ActiveEon
+ * Contact: contact@activeeon.com
+ *
+ * This library is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation: version 3 of
+ * the License.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * If needed, contact us to obtain a release under GPL Version 2 or 3
+ * or a different license than the AGPL.
+ */
+package org.ow2.proactive.process_tree_killer;
+
+import java.util.Collection;
+import java.util.Map;
+
+
+/**
+ * Resolves variables to its value, while encapsulating
+ * how that resolution happens.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+@SuppressWarnings("all")
+public interface VariableResolver {
+ /**
+ * Receives a variable name and obtains the value associated with the name.
+ *
+ *
+ * This can be implemented simply on top of a {@link Map} (see {@link ByMap}), or
+ * this can be used like an expression evaluator.
+ *
+ * @param name
+ * Name of the variable to be resolved.
+ * Never null, never empty. The name shouldn't include the syntactic
+ * marker of an expression. IOW, it should be "foo" but not "${foo}".
+ * A part of the goal of this design is to abstract away the expression
+ * marker syntax.
+ * @return
+ * Object referenced by the name.
+ * Null if not found.
+ */
+ V resolve(String name);
+
+ /**
+ * Empty resolver that always returns null.
+ */
+ VariableResolver NONE = new VariableResolver() {
+ public Object resolve(String name) {
+ return null;
+ }
+ };
+
+ /**
+ * {@link VariableResolver} backed by a {@link Map}.
+ */
+ final class ByMap implements VariableResolver {
+ private final Map data;
+
+ public ByMap(Map data) {
+ this.data = data;
+ }
+
+ public V resolve(String name) {
+ return data.get(name);
+ }
+ }
+
+ /**
+ * Union of multiple {@link VariableResolver}.
+ */
+ final class Union implements VariableResolver {
+ private final VariableResolver extends V>[] resolvers;
+
+ public Union(VariableResolver extends V>... resolvers) {
+ this.resolvers = resolvers.clone();
+ }
+
+ public Union(Collection extends VariableResolver extends V>> resolvers) {
+ this.resolvers = resolvers.toArray(new VariableResolver[resolvers.size()]);
+ }
+
+ public V resolve(String name) {
+ for (VariableResolver extends V> r : resolvers) {
+ V v = r.resolve(name);
+ if (v != null)
+ return v;
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/org/ow2/proactive/process_tree_killer/VersionNumber.java b/src/main/java/org/ow2/proactive/process_tree_killer/VersionNumber.java
new file mode 100644
index 0000000..ca6f523
--- /dev/null
+++ b/src/main/java/org/ow2/proactive/process_tree_killer/VersionNumber.java
@@ -0,0 +1,512 @@
+/*
+ * ProActive Parallel Suite(TM):
+ * The Open Source library for parallel and distributed
+ * Workflows & Scheduling, Orchestration, Cloud Automation
+ * and Big Data Analysis on Enterprise Grids & Clouds.
+ *
+ * Copyright (c) 2007 - 2017 ActiveEon
+ * Contact: contact@activeeon.com
+ *
+ * This library is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation: version 3 of
+ * the License.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * If needed, contact us to obtain a release under GPL Version 2 or 3
+ * or a different license than the AGPL.
+ */
+package org.ow2.proactive.process_tree_killer;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Locale;
+import java.util.Properties;
+import java.util.Stack;
+
+
+/**
+ * Immutable representation of a version number based on the Mercury version numbering scheme.
+ *
+ * {@link VersionNumber}s are {@link Comparable}.
+ *
+ * Special tokens
+ *
+ * We allow a component to be not just a number, but also "ea", "ea1", "ea2".
+ * "ea" is treated as "ea0", and eaN < M for any M > 0.
+ *
+ *
+ * '*' is also allowed as a component, and '*' > M for any M > 0.
+ *
+ *
+ * 'SNAPSHOT' is also allowed as a component, and "N.SNAPSHOT" is interpreted as "N-1.*"
+ *
+ *
+ * 2.0.* > 2.0.1 > 2.0.1-SNAPSHOT > 2.0.0.99 > 2.0.0 > 2.0.ea > 2.0
+ *
+ *
+ * This class is re-implemented in 1.415. The class was originally introduced in 1.139
+ *
+ * @since 1.139
+ * @author Stephen Connolly (stephenc@apache.org)
+ * @author Kenney Westerhof (kenney@apache.org)
+ * @author Hervé Boutemy (hboutemy@apache.org)
+ */
+@SuppressWarnings("all")
+public class VersionNumber implements Comparable {
+ private String value;
+
+ private String canonical;
+
+ private ListItem items;
+
+ private interface Item {
+ public static final int INTEGER_ITEM = 0;
+
+ public static final int STRING_ITEM = 1;
+
+ public static final int LIST_ITEM = 2;
+
+ public static final int WILDCARD_ITEM = 3;
+
+ public int compareTo(Item item);
+
+ public int getType();
+
+ public boolean isNull();
+ }
+
+ /**
+ * Represents a wild-card item in the version item list.
+ */
+ private static class WildCardItem implements Item {
+
+ public int compareTo(Item item) {
+ if (item == null) // 1.* ( > 1.99) > 1
+ return 1;
+ switch (item.getType()) {
+ case INTEGER_ITEM:
+ case LIST_ITEM:
+ case STRING_ITEM:
+ return 1;
+ case WILDCARD_ITEM:
+ return 0;
+ default:
+ return 1;
+ }
+ }
+
+ public int getType() {
+ return WILDCARD_ITEM;
+ }
+
+ public boolean isNull() {
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "*";
+ }
+ }
+
+ /**
+ * Represents a numeric item in the version item list.
+ */
+ private static class IntegerItem implements Item {
+ private static final BigInteger BigInteger_ZERO = new BigInteger("0");
+
+ private final BigInteger value;
+
+ public static final IntegerItem ZERO = new IntegerItem();
+
+ private IntegerItem() {
+ this.value = BigInteger_ZERO;
+ }
+
+ public IntegerItem(String str) {
+ this.value = new BigInteger(str);
+ }
+
+ public int getType() {
+ return INTEGER_ITEM;
+ }
+
+ public boolean isNull() {
+ return BigInteger_ZERO.equals(value);
+ }
+
+ public int compareTo(Item item) {
+ if (item == null) {
+ return BigInteger_ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1
+ }
+
+ switch (item.getType()) {
+ case INTEGER_ITEM:
+ return value.compareTo(((IntegerItem) item).value);
+
+ case STRING_ITEM:
+ return 1; // 1.1 > 1-sp
+
+ case LIST_ITEM:
+ return 1; // 1.1 > 1-1
+
+ case WILDCARD_ITEM:
+ return 0;
+
+ default:
+ throw new RuntimeException("invalid item: " + item.getClass());
+ }
+ }
+
+ public String toString() {
+ return value.toString();
+ }
+ }
+
+ /**
+ * Represents a string in the version item list, usually a qualifier.
+ */
+ private static class StringItem implements Item {
+ private final static String[] QUALIFIERS = { "snapshot", "alpha", "beta", "milestone", "rc", "", "sp" };
+
+ private final static List _QUALIFIERS = Arrays.asList(QUALIFIERS);
+
+ private final static Properties ALIASES = new Properties();
+
+ static {
+ ALIASES.put("ga", "");
+ ALIASES.put("final", "");
+ ALIASES.put("cr", "rc");
+ ALIASES.put("ea", "rc");
+ }
+
+ /**
+ * A comparable for the empty-string qualifier. This one is used to determine if a given qualifier makes the
+ * version older than one without a qualifier, or more recent.
+ */
+ private static String RELEASE_VERSION_INDEX = String.valueOf(_QUALIFIERS.indexOf(""));
+
+ private String value;
+
+ public StringItem(String value, boolean followedByDigit) {
+ if (followedByDigit && value.length() == 1) {
+ // a1 = alpha-1, b1 = beta-1, m1 = milestone-1
+ switch (value.charAt(0)) {
+ case 'a':
+ value = "alpha";
+ break;
+ case 'b':
+ value = "beta";
+ break;
+ case 'm':
+ value = "milestone";
+ break;
+ }
+ }
+ this.value = ALIASES.getProperty(value, value);
+ }
+
+ public int getType() {
+ return STRING_ITEM;
+ }
+
+ public boolean isNull() {
+ return (comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX) == 0);
+ }
+
+ /**
+ * Returns a comparable for a qualifier.
+ *
+ * This method both takes into account the ordering of known qualifiers as well as lexical ordering for unknown
+ * qualifiers.
+ *
+ * just returning an Integer with the index here is faster, but requires a lot of if/then/else to check for -1
+ * or QUALIFIERS.size and then resort to lexical ordering. Most comparisons are decided by the first character,
+ * so this is still fast. If more characters are needed then it requires a lexical sort anyway.
+ *
+ * @param qualifier
+ * @return
+ */
+ public static String comparableQualifier(String qualifier) {
+ int i = _QUALIFIERS.indexOf(qualifier);
+
+ return i == -1 ? _QUALIFIERS.size() + "-" + qualifier : String.valueOf(i);
+ }
+
+ public int compareTo(Item item) {
+ if (item == null) {
+ // 1-rc < 1, 1-ga > 1
+ return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX);
+ }
+ switch (item.getType()) {
+ case INTEGER_ITEM:
+ return -1; // 1.any < 1.1 ?
+
+ case STRING_ITEM:
+ return comparableQualifier(value).compareTo(comparableQualifier(((StringItem) item).value));
+
+ case LIST_ITEM:
+ return -1; // 1.any < 1-1
+
+ case WILDCARD_ITEM:
+ return -1;
+
+ default:
+ throw new RuntimeException("invalid item: " + item.getClass());
+ }
+ }
+
+ public String toString() {
+ return value;
+ }
+ }
+
+ /**
+ * Represents a version list item. This class is used both for the global item list and for sub-lists (which start
+ * with '-(number)' in the version specification).
+ */
+ private static class ListItem extends ArrayList- implements Item {
+ public int getType() {
+ return LIST_ITEM;
+ }
+
+ public boolean isNull() {
+ return (size() == 0);
+ }
+
+ void normalize() {
+ for (ListIterator iterator = listIterator(size()); iterator.hasPrevious();) {
+ Item item = (Item) iterator.previous();
+ if (item.isNull()) {
+ iterator.remove(); // remove null trailing items: 0, "", empty list
+ } else {
+ break;
+ }
+ }
+ }
+
+ public int compareTo(Item item) {
+ if (item == null) {
+ if (size() == 0) {
+ return 0; // 1-0 = 1- (normalize) = 1
+ }
+ Item first = (Item) get(0);
+ return first.compareTo(null);
+ }
+
+ switch (item.getType()) {
+ case INTEGER_ITEM:
+ return -1; // 1-1 < 1.0.x
+
+ case STRING_ITEM:
+ return 1; // 1-1 > 1-sp
+
+ case LIST_ITEM:
+ Iterator left = iterator();
+ Iterator right = ((ListItem) item).iterator();
+
+ while (left.hasNext() || right.hasNext()) {
+ Item l = left.hasNext() ? (Item) left.next() : null;
+ Item r = right.hasNext() ? (Item) right.next() : null;
+
+ // if this is shorter, then invert the compare and mul with -1
+ int result = l == null ? -1 * r.compareTo(l) : l.compareTo(r);
+
+ if (result != 0) {
+ return result;
+ }
+ }
+
+ return 0;
+
+ case WILDCARD_ITEM:
+ return -1;
+
+ default:
+ throw new RuntimeException("invalid item: " + item.getClass());
+ }
+ }
+
+ public String toString() {
+ StringBuilder buffer = new StringBuilder("(");
+ for (Iterator
- iter = iterator(); iter.hasNext();) {
+ buffer.append(iter.next());
+ if (iter.hasNext()) {
+ buffer.append(',');
+ }
+ }
+ buffer.append(')');
+ return buffer.toString();
+ }
+ }
+
+ public VersionNumber(String version) {
+ parseVersion(version);
+ }
+
+ private void parseVersion(String version) {
+ this.value = version;
+
+ items = new ListItem();
+
+ version = version.toLowerCase(Locale.ENGLISH);
+
+ ListItem list = items;
+
+ Stack
- stack = new Stack
- ();
+ stack.push(list);
+
+ boolean isDigit = false;
+
+ int startIndex = 0;
+
+ for (int i = 0; i < version.length(); i++) {
+ char c = version.charAt(i);
+
+ if (c == '.') {
+ if (i == startIndex) {
+ list.add(IntegerItem.ZERO);
+ } else {
+ list.add(parseItem(isDigit, version.substring(startIndex, i)));
+ }
+ startIndex = i + 1;
+ } else if (c == '-') {
+ if (i == startIndex) {
+ list.add(IntegerItem.ZERO);
+ } else {
+ list.add(parseItem(isDigit, version.substring(startIndex, i)));
+ }
+ startIndex = i + 1;
+
+ if (isDigit) {
+ list.normalize(); // 1.0-* = 1-*
+
+ if ((i + 1 < version.length()) && Character.isDigit(version.charAt(i + 1))) {
+ // new ListItem only if previous were digits and new char is a digit,
+ // ie need to differentiate only 1.1 from 1-1
+ list.add(list = new ListItem());
+
+ stack.push(list);
+ }
+ }
+ } else if (c == '*') {
+ list.add(new WildCardItem());
+ startIndex = i + 1;
+ } else if (Character.isDigit(c)) {
+ if (!isDigit && i > startIndex) {
+ list.add(new StringItem(version.substring(startIndex, i), true));
+ startIndex = i;
+ }
+
+ isDigit = true;
+ } else if (Character.isWhitespace(c)) {
+ if (i > startIndex) {
+ if (isDigit) {
+ list.add(parseItem(true, version.substring(startIndex, i)));
+ } else {
+ list.add(new StringItem(version.substring(startIndex, i), true));
+ }
+ startIndex = i;
+ }
+
+ isDigit = false;
+ } else {
+ if (isDigit && i > startIndex) {
+ list.add(parseItem(true, version.substring(startIndex, i)));
+ startIndex = i;
+ }
+
+ isDigit = false;
+ }
+ }
+
+ if (version.length() > startIndex) {
+ list.add(parseItem(isDigit, version.substring(startIndex)));
+ }
+
+ while (!stack.isEmpty()) {
+ list = (ListItem) stack.pop();
+ list.normalize();
+ }
+
+ canonical = items.toString();
+ }
+
+ private static Item parseItem(boolean isDigit, String buf) {
+ return isDigit ? (Item) new IntegerItem(buf) : (Item) new StringItem(buf, false);
+ }
+
+ public int compareTo(VersionNumber o) {
+ return items.compareTo(o.items);
+ }
+
+ public String toString() {
+ return value;
+ }
+
+ public boolean equals(Object o) {
+ return (o instanceof VersionNumber) && canonical.equals(((VersionNumber) o).canonical);
+ }
+
+ public int hashCode() {
+ return canonical.hashCode();
+ }
+
+ public boolean isOlderThan(VersionNumber rhs) {
+ return compareTo(rhs) < 0;
+ }
+
+ public boolean isNewerThan(VersionNumber rhs) {
+ return compareTo(rhs) > 0;
+ }
+
+ public int digit(int idx) {
+ Iterator i = items.iterator();
+ Item item = (Item) i.next();
+ while (idx > 0 && i.hasNext()) {
+ if (item instanceof IntegerItem) {
+ idx--;
+ }
+ i.next();
+ }
+ return ((IntegerItem) item).value.intValue();
+ }
+
+ public static final Comparator DESCENDING = new Comparator() {
+ public int compare(VersionNumber o1, VersionNumber o2) {
+ return o2.compareTo(o1);
+ }
+ };
+}
diff --git a/src/main/java/org/ow2/proactive/process_tree_killer/jna/GNUCLibrary.java b/src/main/java/org/ow2/proactive/process_tree_killer/jna/GNUCLibrary.java
new file mode 100644
index 0000000..e9cff8e
--- /dev/null
+++ b/src/main/java/org/ow2/proactive/process_tree_killer/jna/GNUCLibrary.java
@@ -0,0 +1,129 @@
+/*
+ * ProActive Parallel Suite(TM):
+ * The Open Source library for parallel and distributed
+ * Workflows & Scheduling, Orchestration, Cloud Automation
+ * and Big Data Analysis on Enterprise Grids & Clouds.
+ *
+ * Copyright (c) 2007 - 2017 ActiveEon
+ * Contact: contact@activeeon.com
+ *
+ * This library is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation: version 3 of
+ * the License.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * If needed, contact us to obtain a release under GPL Version 2 or 3
+ * or a different license than the AGPL.
+ */
+package org.ow2.proactive.process_tree_killer.jna;
+
+import com.sun.jna.Library;
+import com.sun.jna.Memory;
+import com.sun.jna.Native;
+import com.sun.jna.NativeLong;
+import com.sun.jna.Pointer;
+import com.sun.jna.StringArray;
+import com.sun.jna.ptr.IntByReference;
+
+
+/**
+ * GNU C library.
+ *
+ *
+ * Not available on all platforms (such as Linux/PPC, IBM mainframe, etc.), so the caller should recover gracefully
+ * in case of {@link LinkageError}. See HUDSON-4820.
+ * @author Kohsuke Kawaguchi
+ */
+public interface GNUCLibrary extends Library {
+ int fork();
+
+ int kill(int pid, int signum);
+
+ int setsid();
+
+ int umask(int mask);
+
+ int getpid();
+
+ int geteuid();
+
+ int getegid();
+
+ int getppid();
+
+ int chdir(String dir);
+
+ int getdtablesize();
+
+ int execv(String path, StringArray args);
+
+ int execvp(String file, StringArray args);
+
+ int setenv(String name, String value, int replace);
+
+ int unsetenv(String name);
+
+ void perror(String msg);
+
+ String strerror(int errno);
+
+ int fcntl(int fd, int command);
+
+ int fcntl(int fd, int command, int flags);
+
+ // obtained from Linux. Needs to be checked if these values are portable.
+ int F_GETFD = 1;
+
+ int F_SETFD = 2;
+
+ int FD_CLOEXEC = 1;
+
+ int chown(String fileName, int uid, int gid);
+
+ int chmod(String fileName, int i);
+
+ int dup(int old);
+
+ int dup2(int old, int _new);
+
+ int close(int fd);
+
+ // see http://www.gnu.org/s/libc/manual/html_node/Renaming-Files.html
+ int rename(String oldname, String newname);
+
+ // this is listed in http://developer.apple.com/DOCUMENTATION/Darwin/Reference/ManPages/man3/sysctlbyname.3.html
+ // but not in http://www.gnu.org/software/libc/manual/html_node/System-Parameters.html#index-sysctl-3493
+ // perhaps it is only supported on BSD?
+ int sysctlbyname(String name, Pointer oldp, IntByReference oldlenp, Pointer newp, IntByReference newlen);
+
+ int sysctl(int[] mib, int nameLen, Pointer oldp, IntByReference oldlenp, Pointer newp, IntByReference newlen);
+
+ int sysctlnametomib(String name, Pointer mibp, IntByReference size);
+
+ /**
+ * Creates a symlink.
+ *
+ * See http://linux.die.net/man/3/symlink
+ */
+ int symlink(String oldname, String newname);
+
+ /**
+ * Read a symlink. The name will be copied into the specified memory, and returns the number of
+ * bytes copied. The string is not null-terminated.
+ *
+ * @return
+ * if the return value equals size, the caller needs to retry with a bigger buffer.
+ * If -1, error.
+ */
+ int readlink(String filename, Memory buffer, NativeLong size);
+
+ GNUCLibrary LIBC = (GNUCLibrary) Native.loadLibrary("c", GNUCLibrary.class);
+}
diff --git a/src/test/java/cloud/localstack/DDBEventMappingTest.java b/src/test/java/cloud/localstack/DDBEventMappingTest.java
new file mode 100644
index 0000000..5200b91
--- /dev/null
+++ b/src/test/java/cloud/localstack/DDBEventMappingTest.java
@@ -0,0 +1,39 @@
+package cloud.localstack;
+
+import cloud.localstack.lambda.DDBEventParser;
+import com.amazonaws.services.lambda.runtime.events.DynamodbEvent;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.*;
+
+import static cloud.localstack.LambdaExecutor.readFile;
+import static cloud.localstack.LambdaExecutor.get;
+
+public class DDBEventMappingTest {
+
+ static String fileName = "src/test/resources/DDBEventLambda.json";
+
+ @Test
+ public void testParseDDBEvent() throws Exception {
+ String fileContent = readFile(fileName);
+
+ ObjectMapper reader = new ObjectMapper();
+ @SuppressWarnings("deprecation")
+ Map map = reader.reader(Map.class).readValue(fileContent);
+
+ List