diff --git a/README.md b/README.md
index 6d37244..8576765 100644
--- a/README.md
+++ b/README.md
@@ -170,6 +170,11 @@ This property controls how long the processing thread(s) remains after the last
order to avoid the overhead of starting up another processing thread, if processes are frequently run it may be desirable
for the processing thread to remain (linger) for some amount of time (default 2500ms).
+##### ``com.zaxxer.nuprocess.enableShutdownHook``
+On Windows, this enables creating processes with job objects, which ensures they die when the parent process dies, in all
+circumstances. On Linux and macOS, this creates a Java shutdown hook that quits created, running processes. This has the
+same limitations as shutdown hooks, which may not run in certain circumstances like termination. The default value is "true".
+
#### Related Projects
Charles Duffy has developed a Clojure wrapper library [here](https://github.com/threatgrid/asynp).
Julien Viet has developed a Vert.x 3 library [here](https://github.com/vietj/vertx-childprocess).
diff --git a/pom.xml b/pom.xml
index 9f8d77b..f5b5013 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,7 +62,7 @@
net.java.dev.jna
jna
- 5.8.0
+ 5.10.0
junit
diff --git a/src/main/java/com/zaxxer/nuprocess/windows/NuKernel32.java b/src/main/java/com/zaxxer/nuprocess/windows/NuKernel32.java
index a2b67a6..6745759 100644
--- a/src/main/java/com/zaxxer/nuprocess/windows/NuKernel32.java
+++ b/src/main/java/com/zaxxer/nuprocess/windows/NuKernel32.java
@@ -85,6 +85,14 @@ public static native int ReadFile(HANDLE hFile, ByteBuffer lpBuffer, int nNumber
public static native int WriteFile(HANDLE hFile, ByteBuffer lpBuffer, int nNumberOfBytesToWrite, IntByReference lpNumberOfBytesWritten,
NuKernel32.OVERLAPPED lpOverlapped);
+ public static native HANDLE CreateJobObject(SECURITY_ATTRIBUTES attrs, String name);
+
+ public static native boolean SetInformationJobObject(HANDLE hJob, int JobObjectInfoClass, Pointer lpJobObjectInfo, int cbJobObjectInfoLength); // {return false;};
+
+ public static native boolean AssignProcessToJobObject(HANDLE hJob, HANDLE hProcess); // {return false;};
+
+ public static native boolean TerminateJobObject(HANDLE hJob, long uExitCode); // {return false;};
+
/**
* The OVERLAPPED structure contains information used in
* asynchronous (or overlapped) input and output (I/O).
diff --git a/src/main/java/com/zaxxer/nuprocess/windows/NuWinNT.java b/src/main/java/com/zaxxer/nuprocess/windows/NuWinNT.java
index 3ab8a45..28b10e0 100644
--- a/src/main/java/com/zaxxer/nuprocess/windows/NuWinNT.java
+++ b/src/main/java/com/zaxxer/nuprocess/windows/NuWinNT.java
@@ -36,6 +36,7 @@ public interface NuWinNT
int CREATE_SUSPENDED = 0x00000004;
int CREATE_UNICODE_ENVIRONMENT = 0x00000400;
int CREATE_NO_WINDOW = 0x08000000;
+ int CREATE_NEW_PROCESS_GROUP = 0x00000200;
int ERROR_SUCCESS = 0;
int ERROR_BROKEN_PIPE = 109;
@@ -58,6 +59,17 @@ public interface NuWinNT
int STARTF_USESTDHANDLES = 0x100;
+ int JOB_OBJECT_LIMIT_BREAKAWAY_OK = 2048;
+ int JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 8192;
+ // see SetInformationJobObject at msdn
+ int JobObjectExtendedLimitInformation = 9;
+ // see SetInformationJobObject at msdn
+ int JobObjectBasicUIRestrictions = 4;
+ // 0x00000020
+ int JOB_OBJECT_UILIMIT_GLOBALATOMS = 0x00000020;
+ // 0x01000000
+ int CREATE_BREAKAWAY_FROM_JOB = 16777216;
+
HANDLE INVALID_HANDLE_VALUE = new HANDLE(HANDLE.INVALID);
class HANDLE extends PointerType
@@ -160,14 +172,58 @@ public ULONG_PTR getValue()
}
}
+ class ULONGLONG extends IntegerType implements Comparable
+ {
+
+ /**
+ * The Constant SIZE.
+ */
+ public static final int SIZE = Native.LONG_SIZE * 2;
+
+ /**
+ * Instantiates a new ULONGLONG.
+ */
+ public ULONGLONG()
+ {
+ this(0);
+ }
+
+ /**
+ * Instantiates a new ULONGLONG.
+ *
+ * @param value the value
+ */
+ public ULONGLONG(long value)
+ {
+ super(SIZE, value, true);
+ }
+
+ @Override public int compareTo(ULONGLONG other)
+ {
+ return compare(this, other);
+ }
+ }
+
+ class SIZE_T extends ULONG_PTR
+ {
+ public SIZE_T()
+ {
+ this(0);
+ }
+
+ public SIZE_T(long value)
+ {
+ super(value);
+ }
+ }
+
class SECURITY_ATTRIBUTES extends Structure
{
public DWORD dwLength;
public Pointer lpSecurityDescriptor;
public boolean bInheritHandle;
- @Override
- protected List getFieldOrder()
+ @Override protected List getFieldOrder()
{
return Arrays.asList("dwLength", "lpSecurityDescriptor", "bInheritHandle");
}
@@ -215,10 +271,299 @@ class PROCESS_INFORMATION extends Structure
public DWORD dwProcessId;
public DWORD dwThreadId;
- @Override
- protected List getFieldOrder()
+ @Override protected List getFieldOrder()
{
return Arrays.asList("hProcess", "hThread", "dwProcessId", "dwThreadId");
}
}
+
+ class LARGE_INTEGER extends Structure implements Comparable
+ {
+ public static class ByReference extends LARGE_INTEGER implements Structure.ByReference
+ {
+ }
+
+ public static class LowHigh extends Structure
+ {
+ public DWORD LowPart;
+ public DWORD HighPart;
+
+ public LowHigh()
+ {
+ super();
+ }
+
+ public LowHigh(long value)
+ {
+ this(new DWORD(value & 0xFFFFFFFFL), new DWORD((value >> 32) & 0xFFFFFFFFL));
+ }
+
+ public LowHigh(DWORD low, DWORD high)
+ {
+ LowPart = low;
+ HighPart = high;
+ }
+
+ public long longValue()
+ {
+ long loValue = LowPart.longValue();
+ long hiValue = HighPart.longValue();
+ return ((hiValue << 32) & 0xFFFFFFFF00000000L) | (loValue & 0xFFFFFFFFL);
+ }
+
+ @Override public String toString()
+ {
+ if ((LowPart == null) || (HighPart == null)) {
+ return "null";
+ }
+ else {
+ return Long.toString(longValue());
+ }
+ }
+
+ @Override protected List getFieldOrder()
+ {
+ return Arrays.asList("LowPart", "HighPart");
+ }
+ }
+
+ public static class UNION extends Union
+ {
+ public LowHigh lh;
+ public long value;
+
+ public UNION()
+ {
+ super();
+ }
+
+ public UNION(long value)
+ {
+ this.value = value;
+ this.lh = new LowHigh(value);
+ }
+
+ public long longValue()
+ {
+ return value;
+ }
+
+ @Override public String toString()
+ {
+ return Long.toString(longValue());
+ }
+ }
+
+ public UNION u;
+
+ public LARGE_INTEGER()
+ {
+ super();
+ }
+
+ public LARGE_INTEGER(long value)
+ {
+ this.u = new UNION(value);
+ }
+
+ /**
+ * Low DWORD.
+ *
+ * @return Low DWORD value
+ */
+ public DWORD getLow()
+ {
+ return u.lh.LowPart;
+ }
+
+ /**
+ * High DWORD.
+ *
+ * @return High DWORD value
+ */
+ public DWORD getHigh()
+ {
+ return u.lh.HighPart;
+ }
+
+ /**
+ * 64-bit value.
+ *
+ * @return The 64-bit value.
+ */
+ public long getValue()
+ {
+ return u.value;
+ }
+
+ @Override public int compareTo(LARGE_INTEGER other)
+ {
+ return compare(this, other);
+ }
+
+ @Override public String toString()
+ {
+ return (u == null) ? "null" : Long.toString(getValue());
+ }
+
+ /**
+ * Compares 2 LARGE_INTEGER values - - Note: a {@code null}
+ * value is considered greater than any non-{@code null} one
+ * (i.e., {@code null} values are "pushed" to the end
+ * of a sorted array / list of values)
+ *
+ * @param v1 The 1st value
+ * @param v2 The 2nd value
+ * @return 0 if values are equal (including if both are {@code null},
+ * negative if 1st value less than 2nd one, positive otherwise. Note:
+ * the comparison uses the {@link #getValue()}.
+ * @see IntegerType#compare(long, long)
+ */
+ public static int compare(LARGE_INTEGER v1, LARGE_INTEGER v2)
+ {
+ if (v1 == v2) {
+ return 0;
+ }
+ else if (v1 == null) {
+ return 1; // v2 cannot be null or v1 == v2 would hold
+ }
+ else if (v2 == null) {
+ return (-1);
+ }
+ else {
+ return IntegerType.compare(v1.getValue(), v2.getValue());
+ }
+ }
+
+ /**
+ * Compares a LARGE_INTEGER value with a {@code long} one. Note: if
+ * the LARGE_INTEGER value is {@code null} then it is consider greater
+ * than any {@code long} value.
+ *
+ * @param v1 The {@link LARGE_INTEGER} value
+ * @param v2 The {@code long} value
+ * @return 0 if values are equal, negative if 1st value less than 2nd one,
+ * positive otherwise. Note: the comparison uses the {@link #getValue()}.
+ * @see IntegerType#compare(long, long)
+ */
+ public static int compare(LARGE_INTEGER v1, long v2)
+ {
+ if (v1 == null) {
+ return 1;
+ }
+ else {
+ return IntegerType.compare(v1.getValue(), v2);
+ }
+ }
+
+ @Override protected List getFieldOrder()
+ {
+ return Arrays.asList("u");
+ }
+ }
+
+ class JOBJECT_BASIC_LIMIT_INFORMATION extends Structure
+ {
+ public LARGE_INTEGER PerProcessUserTimeLimit;
+ public LARGE_INTEGER PerJobUserTimeLimit;
+ public int LimitFlags;
+ public SIZE_T MinimumWorkingSetSize;
+ public SIZE_T MaximumWorkingSetSize;
+ public int ActiveProcessLimit;
+ public ULONG_PTR Affinity;
+ public int PriorityClass;
+ public int SchedulingClass;
+
+ @Override protected List getFieldOrder()
+ {
+ return Arrays.asList("PerProcessUserTimeLimit", "PerJobUserTimeLimit", "LimitFlags", "MinimumWorkingSetSize", "MaximumWorkingSetSize",
+ "ActiveProcessLimit", "Affinity", "PriorityClass", "SchedulingClass");
+ }
+ }
+
+ class IO_COUNTERS extends Structure
+ {
+
+ public ULONGLONG ReadOperationCount;
+ public ULONGLONG WriteOperationCount;
+ public ULONGLONG OtherOperationCount;
+ public ULONGLONG ReadTransferCount;
+ public ULONGLONG WriteTransferCount;
+ public ULONGLONG OtherTransferCount;
+
+ @Override protected List getFieldOrder()
+ {
+ return Arrays.asList("ReadOperationCount", "WriteOperationCount", "OtherOperationCount", "ReadTransferCount", "WriteTransferCount",
+ "OtherTransferCount");
+ }
+ }
+
+ class JOBJECT_EXTENDED_LIMIT_INFORMATION extends Structure
+ {
+
+ public JOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
+ public IO_COUNTERS IoInfo;
+ public SIZE_T ProcessMemoryLimit;
+ public SIZE_T JobMemoryLimit;
+ public SIZE_T PeakProcessMemoryUsed;
+ public SIZE_T PeakJobMemoryUsed;
+
+ @Override protected List getFieldOrder()
+ {
+ return Arrays.asList("BasicLimitInformation", "IoInfo", "ProcessMemoryLimit", "JobMemoryLimit", "PeakProcessMemoryUsed", "PeakJobMemoryUsed");
+ }
+
+ public JOBJECT_EXTENDED_LIMIT_INFORMATION()
+ {
+ }
+
+ public JOBJECT_EXTENDED_LIMIT_INFORMATION(Pointer memory)
+ {
+ super(memory);
+ }
+
+ public static class ByReference extends JOBJECT_EXTENDED_LIMIT_INFORMATION implements Structure.ByReference
+ {
+
+ public ByReference()
+ {
+ }
+
+ public ByReference(Pointer memory)
+ {
+ super(memory);
+ }
+ }
+ }
+
+ class JOBOBJECT_BASIC_UI_RESTRICTIONS extends Structure
+ {
+ public int UIRestrictionsClass;
+
+ public JOBOBJECT_BASIC_UI_RESTRICTIONS()
+ {
+ }
+
+ public JOBOBJECT_BASIC_UI_RESTRICTIONS(Pointer memory)
+ {
+ super(memory);
+ }
+
+ public static class ByReference extends JOBOBJECT_BASIC_UI_RESTRICTIONS implements Structure.ByReference
+ {
+ public ByReference()
+ {
+ }
+
+ public ByReference(Pointer memory)
+ {
+ super(memory);
+ }
+ }
+
+ @Override protected List getFieldOrder()
+ {
+ return Arrays.asList("UIRestrictionsClass");
+ }
+ }
}
diff --git a/src/main/java/com/zaxxer/nuprocess/windows/WindowsProcess.java b/src/main/java/com/zaxxer/nuprocess/windows/WindowsProcess.java
index 1f0ea72..b279602 100644
--- a/src/main/java/com/zaxxer/nuprocess/windows/WindowsProcess.java
+++ b/src/main/java/com/zaxxer/nuprocess/windows/WindowsProcess.java
@@ -21,8 +21,6 @@
import com.sun.jna.WString;
import com.zaxxer.nuprocess.NuProcess;
import com.zaxxer.nuprocess.NuProcessHandler;
-import com.zaxxer.nuprocess.windows.NuKernel32.OVERLAPPED;
-import com.zaxxer.nuprocess.windows.NuWinNT.*;
import java.nio.ByteBuffer;
import java.nio.file.Path;
@@ -37,6 +35,8 @@
import java.util.logging.Logger;
import static com.zaxxer.nuprocess.internal.Constants.NUMBER_OF_THREADS;
+import static com.zaxxer.nuprocess.windows.NuKernel32.*;
+import static com.zaxxer.nuprocess.windows.NuWinNT.*;
/**
* @author Brett Wooldridge
@@ -44,6 +44,7 @@
public final class WindowsProcess implements NuProcess
{
private static final boolean IS_SOFTEXIT_DETECTION;
+ private static final boolean CHILD_TERMINATES_WITH_PARENT;
// See https://github.com/JetBrains/jdk8u_jdk/blob/master/src/windows/native/java/lang/ProcessImpl_md.c#L36-L41
private static final int BUFFER_SIZE = 4096 + 24;
@@ -82,6 +83,8 @@ public final class WindowsProcess implements NuProcess
private volatile boolean outClosed;
private volatile boolean errClosed;
+ private HANDLE hJob;
+
private PROCESS_INFORMATION processInfo;
static {
@@ -95,19 +98,7 @@ public final class WindowsProcess implements NuProcess
processors[i] = new ProcessCompletions();
}
- if (Boolean.parseBoolean(System.getProperty("com.zaxxer.nuprocess.enableShutdownHook", "true"))) {
- Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
- @Override
- public void run()
- {
- for (int i = 0; i < processors.length; i++) {
- if (processors[i] != null) {
- processors[i].shutdown();
- }
- }
- }
- }));
- }
+ CHILD_TERMINATES_WITH_PARENT = Boolean.parseBoolean(System.getProperty("com.zaxxer.nuprocess.enableShutdownHook", "true"));
}
WindowsProcess(NuProcessHandler processListener)
@@ -197,6 +188,11 @@ public boolean hasPendingWrites()
public void destroy(boolean force)
{
NuKernel32.TerminateProcess(processInfo.hProcess, Integer.MAX_VALUE);
+
+ if (CHILD_TERMINATES_WITH_PARENT && hJob != null) {
+ NuKernel32.TerminateJobObject(hJob, 0);
+ NuKernel32.CloseHandle(hJob);
+ }
}
public int getPID(){
@@ -226,6 +222,8 @@ public void setProcessHandler(NuProcessHandler processHandler)
NuProcess start(List commands, String[] environment, Path cwd)
{
+ createJob();
+
callPreStart();
try {
@@ -235,6 +233,8 @@ NuProcess start(List commands, String[] environment, Path cwd)
callStart();
+ attachJob();
+
NuKernel32.ResumeThread(processInfo.hThread);
}
catch (Throwable e) {
@@ -250,6 +250,53 @@ NuProcess start(List commands, String[] environment, Path cwd)
return this;
}
+ private void attachJob()
+ {
+ if (!CHILD_TERMINATES_WITH_PARENT) {
+ return;
+ }
+
+ if (!AssignProcessToJobObject(hJob, getPidHandle())) {
+ throw new RuntimeException("Failed to attach to job " + Native.getLastError());
+ }
+ }
+
+ private void createJob()
+ {
+ if (!CHILD_TERMINATES_WITH_PARENT) {
+ return;
+ }
+
+ hJob = NuKernel32.CreateJobObject(null, null);
+ if (hJob.getPointer() == null) {
+ throw new RuntimeException("Unable to create job object: " + Native.getLastError());
+ }
+
+ JOBJECT_EXTENDED_LIMIT_INFORMATION.ByReference jobExtendedLimitInfo = new JOBJECT_EXTENDED_LIMIT_INFORMATION.ByReference();
+ jobExtendedLimitInfo.clear();
+
+ JOBOBJECT_BASIC_UI_RESTRICTIONS.ByReference jobUiRestrictions = new JOBOBJECT_BASIC_UI_RESTRICTIONS.ByReference();
+ jobUiRestrictions.clear();
+
+ // http://forum.sysinternals.com/forum_posts.asp?TID=4094
+ jobExtendedLimitInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_BREAKAWAY_OK | JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
+
+ jobExtendedLimitInfo.write();
+
+ if (!SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, jobExtendedLimitInfo.getPointer(), jobExtendedLimitInfo.size())) {
+ throw new RuntimeException("Unable to set extended limit information on the job object: " + Native.getLastError());
+ }
+
+ // crete job in sandbox with own global atom table
+ jobUiRestrictions.UIRestrictionsClass = JOB_OBJECT_UILIMIT_GLOBALATOMS;
+
+ jobUiRestrictions.write();
+
+ if (!SetInformationJobObject(hJob, JobObjectBasicUIRestrictions, jobUiRestrictions.getPointer(), jobUiRestrictions.size())) {
+ throw new RuntimeException("Unable to set ui limit information on the job object: " + Native.getLastError());
+ }
+ }
+
void run(List commands, String[] environment, Path cwd)
{
callPreStart();
@@ -297,7 +344,12 @@ private void prepareProcess(List commands, String[] environment, Path cw
processInfo = new PROCESS_INFORMATION();
- DWORD dwCreationFlags = new DWORD(NuWinNT.CREATE_NO_WINDOW | NuWinNT.CREATE_UNICODE_ENVIRONMENT | NuWinNT.CREATE_SUSPENDED);
+ int creationFlagValue = CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT | CREATE_SUSPENDED;
+ if (CHILD_TERMINATES_WITH_PARENT) {
+ creationFlagValue |= CREATE_BREAKAWAY_FROM_JOB | CREATE_NEW_PROCESS_GROUP;
+ }
+
+ DWORD dwCreationFlags = new DWORD(creationFlagValue);
char[] cwdChars = (cwd != null) ? Native.toCharArray(cwd.toAbsolutePath().toString()) : null;
if (!NuKernel32.CreateProcessW(null, getCommandLine(commands), null /*lpProcessAttributes*/, null /*lpThreadAttributes*/, true /*bInheritHandles*/,
dwCreationFlags, env, cwdChars, startupInfo, processInfo)) {