0

I want to start a processes such that the JVM can die but the spawned processes continues to run even if it is writing to STDOUT.

I first tried using a ProcessBuilder with the output set to Files and passing in:

cmd /c myCmd.exe arg0 arg1

However even after closing all Input/Output streams, if I call Process#.waitFor, it does not return until myCmd.exe has finished. It seems it is still attached to the JVM in some way (even though the JVM can probably die at this point and not affect the child proc).

I then tried the start command, it seems that is not on the path (I couldn't find the bin in c:\windows) so I ran it under cmd the arguments (separated by space) passed to ProcessBuilder became:

cmd /c start /b myCmd.exe arg0 arg2 >log 2>&1

That results in:

  • Process#.waitFor returning before myCmd.exe finished.
  • ⚠ It seemed that I needed to use a different log file from the one passed to the ProcessBuilder
  • ✘ I then found the escaping become weird if the command run was echo and the argument was ^^^^\foo it would write to the log file ^\foo, I also noticed if I gave it "^^^^\foo" it would return the same thing ie "^^^^\foo".

So:

  1. Is calling cmd.exe /c start /b the correct thing to do?
  2. Am I doing something wrong with the escaping (which is really what I give to process builder), should I perhaps be doing something different because of cmd.exe calling start, perhaps I need to actually escape in some way? Perhaps I don't understand windows processes do they even have proper support for taking an array of arguments?
  3. Am I going about this the wrong way should I be trying to call a native library from C? If so what would it be I don't mind if I have to call a C program to get my process running in the background.
Luke
  • 724
  • 3
  • 15
  • you can call a non-java application from your java code, that 'll still run after the jvm is terminated – Stultuske May 24 '18 at 06:04
  • There's no escape from the JVM. Once you run Java code, all of it is managed by the JVM. – Kolt Penny May 24 '18 at 06:26
  • 2
    I don't understand why you are using cmd at all. This is common to see here. You use cmd when working interactively. But from a program you use the programmatic interface. CreateProcess. – David Heffernan May 24 '18 at 06:27
  • With `cmd /c`, the shell waits for the command to exit. Java has a handle for the cmd.exe process and can in turn wait on it. With `cmd /c start /b`, the shell does not wait for the command to exit, so waiting on the handle for cmd.exe in Java returns immediately. The `/b` option of CMD's internal `start` command (there is no start.exe) prevents `start` from using the `CREATE_NEW_CONSOLE` flag when it calls `CreateProcess`, or causes it to use `SEE_MASK_NO_CONSOLE` if it calls `ShellExecuteEx` (the fall-back if `CreateProcess` fails). – Eryk Sun May 24 '18 at 14:10
  • Regarding your question about escaping in CMD, `^^` becomes a single `^`. So the first CMD instance parses `^^^^` as `^^`. Then `start` executes the internal `echo` command (there is no echo.exe by default in Windows) via `cmd /k echo ^^\foo`. This becomes `^\foo` in the log. This last instance of CMD is left running (because of the implicit `/k` option) and attached to stdin, which will be a confusing mess if stdin is the interactive console input. Regarding double quotes, "^" is a literal character when quoted. – Eryk Sun May 24 '18 at 14:34
  • Regarding the overall command line, Windows processes *do not* get an "array of arguments". They're passed the command line string verbatim, and it's up to each process how it should parse its command line. If it's using `argv` from Microsoft's C/C++ `[w]main` entry point, or `CommandLineToArgvW`, then it follows [these rules](https://docs.microsoft.com/en-us/cpp/cpp/parsing-cpp-command-line-arguments). – Eryk Sun May 24 '18 at 14:40
  • I am going down the `CreateProcess` path although I am stuck on passing an environment see https://stackoverflow.com/questions/50520947/how-to-pass-a-environment-pointer-to-windows-createprocess-in-java-using-jna @DavidHeffernan – Luke May 25 '18 at 03:10
  • @Luke, why would you call `CreateProcess` directly in Java? That's an extreme step if you're simply looking to avoid the CMD shell. Surely `ProcessBuilder` ultimately calls `CreateProcess`, but wrapped behind a cross-platform interface. You haven't clearly defined what you want from a 'detached' process. Anything that has a handle to the process (e.g. from `OpenProcess`) can wait for it to exit. That isn't 'attached'. The concept of a "detached process" in Windows generally refers to running a console application without attaching to a console, via the `DETACHED_PROCESS` creation flag. – Eryk Sun May 25 '18 at 05:47
  • No I generally wouldn't, however it seems the JVM has some flaws with how its processes are created it is not easy to tell the JVM to just let the process run in the background and not hold onto resources related to the processes. Also it seems that the escaping doesn't really work, as soon as a `"` appears things get messy e.g. redirects `>log.txt` suddenly became arguments to the command – Luke May 25 '18 at 06:29

1 Answers1

0

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
Luke
  • 724
  • 3
  • 15
  • `CREATE_NEW_CONSOLE` allocates a new console host that creates a window. `DETACHED_PROCESS` prevents attaching to or allocating a console. It's an error to combine these two flags. `CREATE_NO_WINDOW` allocates a new console host that creates no window, but it's ignored when combined with either of the other two flags. – Eryk Sun Jun 16 '18 at 01:20
  • The most compatible configuration is to use `CREATE_NEW_CONSOLE` and tell the console to create the window hidden via `STARTUPINFO` with `STARTF_USESHOWWINDOW` in `dwFlags` and `wShowWindow` set to `SW_HIDE`. If you instead execute without a console (detached), setting unused standard handles to the NUL character device can help avoid problems with applications that fail if their standard handles aren't valid File handles. – Eryk Sun Jun 16 '18 at 01:21