JNA in Windows: auto terminate child processes using Windows Jobs

2

I need to launch a child process in my java application in Windows, and eventually my java app can be killed/terminated via task manager. So I need to "link" this child process with the parent process, to both be terminated if the parent process terminate.

In windows API we have the CreateJobObject and also the:

Based on the selenium classes Kernel32 and WindowsProcessGroup and created this class:

import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
import java.util.stream.Collectors;

import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinDef;
import com.sun.jna.platform.win32.WinNT;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.win32.W32APIOptions;

public final class ProcessChildAttached {

    public static abstract class Structure extends com.sun.jna.Structure {

        public Structure() {
            super();
        }

        public Structure(Pointer p) {
            super(p);
        }

        private List<String> fields;

        protected Class<? extends Structure> getFieldsClass() {
            Class<? extends Structure> ret = this.getClass();
            if (ByReference.class.isAssignableFrom(ret) && com.sun.jna.Structure.class.isAssignableFrom(ret.getSuperclass())) {
                ret = (Class<? extends Structure>) ret.getSuperclass();
            }
            return ret;
        }

        @Override
        protected List<String> getFieldOrder() {
            if (fields == null) {
                fields = Arrays.stream(getFieldsClass().getDeclaredFields()).map(df -> df.getName()).collect(Collectors.toList());
            }
            return fields;
        }

    }

    static interface Kernel32 extends com.sun.jna.platform.win32.Kernel32 {

        Kernel32 INSTANCE = Native.loadLibrary("kernel32", Kernel32.class, W32APIOptions.UNICODE_OPTIONS);

        WinNT.HANDLE CreateJobObject(WinBase.SECURITY_ATTRIBUTES attrs, String name);

        boolean SetInformationJobObject(HANDLE hJob, int JobObjectInfoClass, Pointer lpJobObjectInfo, int cbJobObjectInfoLength);

        boolean AssignProcessToJobObject(HANDLE hJob, HANDLE hProcess);

        boolean TerminateJobObject(HANDLE hJob, long uExitCode);

        int ResumeThread(HANDLE hThread);

        // 0x00000800
        int JOB_OBJECT_LIMIT_BREAKAWAY_OK = 2048;

        // 0x00002000
        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;

        // 0x00000004
        int CREATE_SUSPENDED = 4;

        // 0x01000000
        int CREATE_BREAKAWAY_FROM_JOB = 16777216;

        static 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;
        }

        static class IO_COUNTERS extends Structure {

            public ULONGLONG ReadOperationCount;
            public ULONGLONG WriteOperationCount;
            public ULONGLONG OtherOperationCount;
            public ULONGLONG ReadTransferCount;
            public ULONGLONG WriteTransferCount;
            public ULONGLONG OtherTransferCount;

        }

        static class JOBJECT_EXTENDED_LIMIT_INFORMATION extends Structure {

            public JOBJECT_EXTENDED_LIMIT_INFORMATION() {
            }

            public JOBJECT_EXTENDED_LIMIT_INFORMATION(Pointer memory) {
                super(memory);
            }

            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;

            public static class ByReference extends JOBJECT_EXTENDED_LIMIT_INFORMATION implements Structure.ByReference {

                public ByReference() {
                }

                public ByReference(Pointer memory) {
                    super(memory);
                }
            }
        }

        static class JOBOBJECT_BASIC_UI_RESTRICTIONS extends Structure {
            public JOBOBJECT_BASIC_UI_RESTRICTIONS() {
            }

            public JOBOBJECT_BASIC_UI_RESTRICTIONS(Pointer memory) {
                super(memory);
            }

            public int UIRestrictionsClass;

            public static class ByReference extends JOBOBJECT_BASIC_UI_RESTRICTIONS implements Structure.ByReference {
                public ByReference() {
                }

                public ByReference(Pointer memory) {
                    super(memory);
                }
            }
        }

    }

    private Kernel32 kernel32 = Kernel32.INSTANCE;

    private String cmd;
    private String workingDirectory;
    private HANDLE hJob;
    private WinBase.PROCESS_INFORMATION.ByReference pi;

    public ProcessChildAttached(String cmd, String workingDirectory) {
        this.cmd = cmd;
        this.workingDirectory = workingDirectory;
    }

    public void start() {
        WinBase.STARTUPINFO si = new WinBase.STARTUPINFO();
        si.clear();

        pi = new WinBase.PROCESS_INFORMATION.ByReference();
        pi.clear();

        Kernel32.JOBJECT_EXTENDED_LIMIT_INFORMATION jeli = new Kernel32.JOBJECT_EXTENDED_LIMIT_INFORMATION.ByReference();
        jeli.clear();

        Kernel32.JOBOBJECT_BASIC_UI_RESTRICTIONS uli = new Kernel32.JOBOBJECT_BASIC_UI_RESTRICTIONS.ByReference();
        uli.clear();

        // Call SetHandleInformation. Take a look in SocketLock.cs

        hJob = kernel32.CreateJobObject(null, null);
        if (hJob.getPointer() == null) {
            throw new RuntimeException("Cannot create job object: " + kernel32.GetLastError());
        }

        // Hopefully, Windows will kill the job automatically if this process dies
        // But beware! Process Explorer can break this by keeping open a handle to all jobs!
        // http://forum.sysinternals.com/forum_posts.asp?TID=4094
        jeli.BasicLimitInformation.LimitFlags = Kernel32.JOB_OBJECT_LIMIT_BREAKAWAY_OK | Kernel32.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;

        if (!kernel32.SetInformationJobObject(hJob, Kernel32.JobObjectExtendedLimitInformation, jeli.getPointer(), jeli.size())) {
            throw new RuntimeException("Unable to set extended limit information on the job object: " + kernel32.GetLastError());
        }

        // crete job in sandbox with own global atom table
        uli.UIRestrictionsClass = Kernel32.JOB_OBJECT_UILIMIT_GLOBALATOMS;

        if (!kernel32.SetInformationJobObject(hJob, Kernel32.JobObjectBasicUIRestrictions, uli.getPointer(), uli.size())) {
            throw new RuntimeException("Unable to set ui limit information on the job object: " + kernel32.GetLastError());
        }

        WinDef.DWORD creationFlags = new WinDef.DWORD(Kernel32.CREATE_SUSPENDED | // Suspend so we can add to job
                Kernel32.CREATE_BREAKAWAY_FROM_JOB | // Allow ourselves to breakaway from Vista's PCA if necessary
                Kernel32.CREATE_NEW_PROCESS_GROUP);

        // Start the child process
        boolean result = kernel32.CreateProcess(null, // No module name (use command line).
                cmd, // Command line.
                null, // Process handle not inheritable.
                null, // Thread handle not inheritable.
                false, // Set handle inheritance to FALSE.
                creationFlags, // Set creation flags
                null, // Use parent's environment block.
                workingDirectory, // Use provided working directory, parent's directory if null.
                si, // Pointer to STARTUPINFO structure.
                pi); // Pointer to PROCESS_INFORMATION structure.
        if (!result) {
            throw new RuntimeException("Failed to create the process: " + kernel32.GetLastError());
        }

        if (!kernel32.AssignProcessToJobObject(hJob, pi.hProcess)) {
            throw new RuntimeException("Cannot assign process to job: " + kernel32.GetLastError());
        }

        if (kernel32.ResumeThread(pi.hThread) <= 0) {
            throw new RuntimeException("Cannot resume thread: " + kernel32.GetLastError());
        }

        kernel32.CloseHandle(pi.hThread);
        // Kernel32.CloseHandle(pi.hProcess);
    }

    public boolean isRunning() {
        return hJob != null;
    }

    public void destroy() {
        if (!isRunning()) {
            return;
        }

        kernel32.CloseHandle(pi.hProcess);
        pi = null;

        // This seems a trifle brutal. Oh well. Brutal it is.
        kernel32.TerminateJobObject(hJob, 666);
        kernel32.CloseHandle(hJob);
        hJob = null;

    }

    public int waitFor() {
        if (isRunning()) {
            kernel32.WaitForSingleObject(pi.hProcess, Kernel32.INFINITE);
            IntByReference exitCode = new IntByReference();
            if (kernel32.GetExitCodeProcess(pi.hProcess, exitCode)) {
                return exitCode.getValue();
            }
            destroy();
        }
        return -1;
    }

}

To test it, I just created this demo:

public class Demo {

    public static void main(String[] args) {
        ProcessChildAttached proc = new ProcessChildAttached("my-child-process.exe", ".");

        proc.start();

        System.out.println("Now kill this java.exe and child process will die too");

        try (Scanner sc = new Scanner(System.in)) {
            System.out.println("And and destroy child process? [ y/N ]");
            String option = sc.nextLine();
            if (!"Y".equalsIgnoreCase(option)) {
                return;
            }
        }

        proc.destroy();
    }

}

The problem is that the JobObject is created in my app (inside java.exe process) but the limits are not there, and if I kill the parent process (java.exe) the child process keep running.

Take a look on the process (java.exe) properties:

Job wihtout limits

The expected is something like this (spotify process example):

Spotify Job limits are correct

Using the same code in Lazarus (Free Pascal), everything goes fine too:

uses 
   jwawinbase, JwaWinNT

var
  fProcessInfo: TProcessInformation;
  fJob: Cardinal;

procedure Start();
var
  si: TStartupInfo;
  limits : TJobObjectExtendedLimitInformation;
  ui: TJobObjectBasicUiRestrictions;
  createFlags: integer;
begin
  FillChar(fProcessInfo, SizeOf(fProcessInfo), 0);
  fJob := 0;

  fJob := CreateJobObject(nil, nil);
  if fJob = 0 then
    RaiseLastOSError;

  FillChar(Limits,SizeOf(Limits),0);
  with Limits,BasicLimitInformation do
    LimitFlags := JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE or JOB_OBJECT_LIMIT_BREAKAWAY_OK;

  if not SetInformationJobObject(fJob, JobObjectExtendedLimitInformation, @limits, SizeOf(limits)) then
    RaiseLastOSError;

  FillChar(ui, SizeOf(ui), 0);
  ui.UIRestrictionsClass := JOB_OBJECT_UILIMIT_GLOBALATOMS;

  if not SetInformationJobObject(fJob, JobObjectBasicUIRestrictions, @ui, SizeOf(ui)) then
    RaiseLastOSError;

  Fillchar(si, SizeOf(si), 0);
  si.cb := SizeOf(si);

  createFlags := CREATE_SUSPENDED or CREATE_BREAKAWAY_FROM_JOB or NORMAL_PRIORITY_CLASS or CREATE_NEW_CONSOLE or CREATE_NEW_PROCESS_GROUP;

  if not CreateProcess(nil, PChar('notepad.exe'), nil, nil, false, createFlags, nil, nil, si, fProcessInfo) then
    RaiseLastOSError;

  if not AssignProcessToJobObject(fJob, fProcessInfo.hProcess) then
    RaiseLastOSError;

  if ResumeThread(fProcessInfo.hThread) = $FFFFFFFF then
    RaiseLastOSError;
end;

Can someone help me to solve this in my java app?

winapi
subprocess
jobs
jna
asked on Stack Overflow May 23, 2018 by Beto Neto • edited May 24, 2018 by Beto Neto

1 Answer

1

Solved!

I need to write the fields to memory before pass the Structures to the methods call.

This is the final code (search for comment // <<<< WRITE THE FIELDS TO NATIVE MEMORY):

import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
import java.util.stream.Collectors;

import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinDef;
import com.sun.jna.platform.win32.WinNT;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.win32.W32APIOptions;

public final class ProcessChildAttached {

    public static abstract class Structure extends com.sun.jna.Structure {

        public Structure() {
            super();
        }

        public Structure(Pointer p) {
            super(p);
        }

        private List<String> fields;

        protected Class<? extends Structure> getFieldsClass() {
            Class<? extends Structure> ret = this.getClass();
            if (ByReference.class.isAssignableFrom(ret) && com.sun.jna.Structure.class.isAssignableFrom(ret.getSuperclass())) {
                ret = (Class<? extends Structure>) ret.getSuperclass();
            }
            return ret;
        }

        @Override
        protected List<String> getFieldOrder() {
            if (fields == null) {
                fields = Arrays.stream(getFieldsClass().getDeclaredFields()).map(df -> df.getName()).collect(Collectors.toList());
            }
            return fields;
        }

    }

    static interface Kernel32 extends com.sun.jna.platform.win32.Kernel32 {

        Kernel32 INSTANCE = Native.loadLibrary("kernel32", Kernel32.class, W32APIOptions.UNICODE_OPTIONS);

        WinNT.HANDLE CreateJobObject(WinBase.SECURITY_ATTRIBUTES attrs, String name);

        boolean SetInformationJobObject(HANDLE hJob, int JobObjectInfoClass, Pointer lpJobObjectInfo, int cbJobObjectInfoLength);

        boolean AssignProcessToJobObject(HANDLE hJob, HANDLE hProcess);

        boolean TerminateJobObject(HANDLE hJob, long uExitCode);

        int ResumeThread(HANDLE hThread);

        // 0x00000800
        int JOB_OBJECT_LIMIT_BREAKAWAY_OK = 2048;

        // 0x00002000
        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;

        // 0x00000004
        int CREATE_SUSPENDED = 4;

        // 0x01000000
        int CREATE_BREAKAWAY_FROM_JOB = 16777216;

        static 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;
        }

        static class IO_COUNTERS extends Structure {

            public ULONGLONG ReadOperationCount;
            public ULONGLONG WriteOperationCount;
            public ULONGLONG OtherOperationCount;
            public ULONGLONG ReadTransferCount;
            public ULONGLONG WriteTransferCount;
            public ULONGLONG OtherTransferCount;

        }

        static class JOBJECT_EXTENDED_LIMIT_INFORMATION extends Structure {

            public JOBJECT_EXTENDED_LIMIT_INFORMATION() {
            }

            public JOBJECT_EXTENDED_LIMIT_INFORMATION(Pointer memory) {
                super(memory);
            }

            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;

            public static class ByReference extends JOBJECT_EXTENDED_LIMIT_INFORMATION implements Structure.ByReference {

                public ByReference() {
                }

                public ByReference(Pointer memory) {
                    super(memory);
                }
            }
        }

        static class JOBOBJECT_BASIC_UI_RESTRICTIONS extends Structure {
            public JOBOBJECT_BASIC_UI_RESTRICTIONS() {
            }

            public JOBOBJECT_BASIC_UI_RESTRICTIONS(Pointer memory) {
                super(memory);
            }

            public int UIRestrictionsClass;

            public static class ByReference extends JOBOBJECT_BASIC_UI_RESTRICTIONS implements Structure.ByReference {
                public ByReference() {
                }

                public ByReference(Pointer memory) {
                    super(memory);
                }
            }
        }

    }

    private Kernel32 kernel32 = Kernel32.INSTANCE;

    private String cmd;
    private String workingDirectory;
    private HANDLE hJob;
    private WinBase.PROCESS_INFORMATION.ByReference pi;

    public ProcessChildAttached(String cmd, String workingDirectory) {
        this.cmd = cmd;
        this.workingDirectory = workingDirectory;
    }

    public void start() {
        WinBase.STARTUPINFO si = new WinBase.STARTUPINFO();
        si.clear();

        pi = new WinBase.PROCESS_INFORMATION.ByReference();
        pi.clear();

        Kernel32.JOBJECT_EXTENDED_LIMIT_INFORMATION jeli = new Kernel32.JOBJECT_EXTENDED_LIMIT_INFORMATION.ByReference();
        jeli.clear();

        Kernel32.JOBOBJECT_BASIC_UI_RESTRICTIONS uli = new Kernel32.JOBOBJECT_BASIC_UI_RESTRICTIONS.ByReference();
        uli.clear();

        // Call SetHandleInformation. Take a look in SocketLock.cs

        hJob = kernel32.CreateJobObject(null, null);
        if (hJob.getPointer() == null) {
            throw new RuntimeException("Cannot create job object: " + kernel32.GetLastError());
        }

        // Hopefully, Windows will kill the job automatically if this process dies
        // But beware! Process Explorer can break this by keeping open a handle to all jobs!
        // http://forum.sysinternals.com/forum_posts.asp?TID=4094
        jeli.BasicLimitInformation.LimitFlags = Kernel32.JOB_OBJECT_LIMIT_BREAKAWAY_OK | Kernel32.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;

        jeli.write(); // <<<< WRITE THE FIELDS TO NATIVE MEMORY

        if (!kernel32.SetInformationJobObject(hJob, Kernel32.JobObjectExtendedLimitInformation, jeli.getPointer(), jeli.size())) {
            throw new RuntimeException("Unable to set extended limit information on the job object: " + kernel32.GetLastError());
        }

        // crete job in sandbox with own global atom table
        uli.UIRestrictionsClass = Kernel32.JOB_OBJECT_UILIMIT_GLOBALATOMS;

        uli.write(); // <<<< WRITE THE FIELDS TO NATIVE MEMORY

        if (!kernel32.SetInformationJobObject(hJob, Kernel32.JobObjectBasicUIRestrictions, uli.getPointer(), uli.size())) {
            throw new RuntimeException("Unable to set ui limit information on the job object: " + kernel32.GetLastError());
        }

        WinDef.DWORD creationFlags = new WinDef.DWORD(Kernel32.CREATE_SUSPENDED | // Suspend so we can add to job
                Kernel32.CREATE_BREAKAWAY_FROM_JOB | // Allow ourselves to breakaway from Vista's PCA if necessary
                Kernel32.CREATE_NEW_PROCESS_GROUP);

        // Start the child process
        boolean result = kernel32.CreateProcess(null, // No module name (use command line).
                cmd, // Command line.
                null, // Process handle not inheritable.
                null, // Thread handle not inheritable.
                false, // Set handle inheritance to FALSE.
                creationFlags, // Set creation flags
                null, // Use parent's environment block.
                workingDirectory, // Use provided working directory, parent's directory if null.
                si, // Pointer to STARTUPINFO structure.
                pi); // Pointer to PROCESS_INFORMATION structure.
        if (!result) {
            throw new RuntimeException("Failed to create the process: " + kernel32.GetLastError());
        }

        if (!kernel32.AssignProcessToJobObject(hJob, pi.hProcess)) {
            throw new RuntimeException("Cannot assign process to job: " + kernel32.GetLastError());
        }

        if (kernel32.ResumeThread(pi.hThread) <= 0) {
            throw new RuntimeException("Cannot resume thread: " + kernel32.GetLastError());
        }

        kernel32.CloseHandle(pi.hThread);
        // Kernel32.CloseHandle(pi.hProcess);
    }

    public boolean isRunning() {
        return hJob != null;
    }

    public void destroy() {
        if (!isRunning()) {
            return;
        }

        kernel32.CloseHandle(pi.hProcess);
        pi = null;

        // This seems a trifle brutal. Oh well. Brutal it is.
        kernel32.TerminateJobObject(hJob, 666);
        kernel32.CloseHandle(hJob);
        hJob = null;

    }

    public int waitFor() {
        if (isRunning()) {
            kernel32.WaitForSingleObject(pi.hProcess, Kernel32.INFINITE);
            IntByReference exitCode = new IntByReference();
            if (kernel32.GetExitCodeProcess(pi.hProcess, exitCode)) {
                return exitCode.getValue();
            }
            destroy();
        }
        return -1;
    }

}
answered on Stack Overflow May 24, 2018 by Beto Neto

User contributions licensed under CC BY-SA 3.0