I think the solution to create a detached in background process which the JVM holds no references to which also supports the possibility to pass any arguments in a sane way is it to use the CreateProcess
API. For this I used:
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>platform</artifactId>
<version>3.5.0</version>
</dependency>
(It is an older version but it happened to be already in use).
The Jave code to get it working is:
/**
*
* @param command a pre escaped command line e.g. c:\perl.exe c:\my.pl arg
* @param env A non null environment.
* @param stdoutFile
* @param stderrFile
* @param workingDir
*/
public void execute(String command, Map<String, String> env,
File stdoutFile, File stderrFile, String workingDir) {
WinBase.SECURITY_ATTRIBUTES sa = new WinBase.SECURITY_ATTRIBUTES();
sa.bInheritHandle = true; // I think the child processes gets handles I make with
// with this sa.
sa.lpSecurityDescriptor = null; // Use default access token from current proc.
HANDLE stdout = makeFileHandle(sa, stdoutFile);
HANDLE stderr = null;
if(stderrFile != null &&
!stderrFile.getAbsolutePath().equals(stdoutFile.getAbsolutePath())) {
stderr = makeFileHandle(sa, stderrFile);
}
try {
WinBase.STARTUPINFO si = new WinBase.STARTUPINFO();
// Assume si.cb is set by the JVM.
si.dwFlags |= WinBase.STARTF_USESTDHANDLES;
si.hStdInput = null; // No stdin for the child.
si.hStdOutput = stdout;
si.hStdError = Optional.ofNullable(stderr).orElse(stdout);
DWORD dword = new DWORD();
dword.setValue(WinBase.CREATE_UNICODE_ENVIRONMENT | // Probably makes sense.
WinBase.CREATE_NO_WINDOW | // Well we don't want a window so this makes sense.
WinBase.DETACHED_PROCESS); // I think this would let the JVM die without stopping the child
// Newer versions of platform don't use a reference.
WinBase.PROCESS_INFORMATION.ByReference processInfoByRef = new WinBase.PROCESS_INFORMATION.ByReference();
boolean result = Kernel32.INSTANCE
.CreateProcess(null, // use next argument to get the task to run
command,
null, // Don't let the child inherit a handle to itself, because I saw someone else do it.
null, // Don't let the child inherit a handle to its own main thread, because I saw someone else do it.
true, // Must be true to pass any handle to the spawned thread including STDOUT and STDERR
dword,
asPointer(createEnvironmentBlock(env)), // I hope that the new processes copies this memory
workingDir,
si,
processInfoByRef);
// Put code in try block.
try {
// Did it start?
if(!result) {
throw new RuntimeException("Could not start command: " + command);
}
} finally {
// Apparently both parent and child need to close the handles.
Kernel32.INSTANCE.CloseHandle(processInfoByRef.hProcess);
Kernel32.INSTANCE.CloseHandle(processInfoByRef.hThread);
}
} finally {
// We need to close this
// https://stackoverflow.com/questions/6581103/do-i-have-to-close-inherited-handle-later-owned-by-child-process
Kernel32.INSTANCE.CloseHandle(stdout);
if(stderr != null) {
Kernel32.INSTANCE.CloseHandle(stderr);
}
}
}
private HANDLE makeFileHandle(WinBase.SECURITY_ATTRIBUTES sa, File file) {
return Kernel32.INSTANCE
.CreateFile(file.getAbsolutePath(),
Kernel32.FILE_APPEND_DATA, // IDK I saw this in an example.
Kernel32.FILE_SHARE_WRITE | Kernel32.FILE_SHARE_READ,
sa,
Kernel32.OPEN_ALWAYS,
Kernel32.FILE_ATTRIBUTE_NORMAL,
null);
}
public static byte[] createEnvironmentBlock(Map<String, String> env) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
// This charset seems to work.
Charset charset = StandardCharsets.UTF_16LE;
try {
for(Entry<String, String> entry : env.entrySet()) {
bos.write(entry.getKey().getBytes(charset));
bos.write("=".getBytes(charset));
bos.write(entry.getValue().getBytes(charset));
bos.write(0);
bos.write(0);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
bos.write(0);
bos.write(0);
return bos.toByteArray();
}
public static Pointer asPointer(byte[] data) {
Pointer pointer = new Memory(data.length);
pointer.write(0, data, 0, data.length);
return pointer;
}
- ✓ The process started seems to keep running even if the JVM is stopped.
- ✓ I didn't need to have to deal with STDOUT/STDERR from
ProcessBuilder
and later another one from needing to redirect the command actually run.
- ✓ If you can correctly escape your command (code to do that is not in the answer as it is not mine to share) you can pass things like
"
which I could not work out how to do when using ProcessBuilder
with cmd /c start /b command
. It seemed the JVM was doing some escaping making it perhaps impossible to construct the needed string to get the correct command.
- ✓ I could see the file handles held by the JVM to stdout/stderr are released before the process finishes.
- ✓ I could create 13k tasks without the JVM throwing an OOM with 62MB of memory given to the JVM (looks like the JVM is not holding resources like some people will end up with just doing
ProcessBuilder
and cmd /c
.
- ✓ an intermediate
cmd.exe
is not created