如何在完全分离的java下的Windows中启动命令

时间:2018-05-24 06:02:56

标签: java windows winapi cmd

我想启动一个进程,使得JVM可能会死,但即使它正在写入STDOUT,生成的进程也会继续运行。

我首先尝试使用ProcessBuilder并将输出设置为Files并传入:

cmd /c myCmd.exe arg0 arg1

然而,即使在关闭所有输入/输出流之后,如果我调用Process#.waitFor,则在myCmd.exe完成之前它不会返回。它似乎仍以某种方式连接到JVM(即使JVM可能在此时死亡并且不会影响子proc)。

然后我尝试了start命令,似乎不在路径上(我找不到c:\windows中的bin)所以我在cmd参数下运行它(传递给ProcessBuilder的空格分隔成了:

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

结果是:

  • Process#.waitFormyCmd.exe完成之前返回。
  • ⚠我似乎需要使用传递给ProcessBuilder的其他日志文件
  • ✘然后我发现如果命令运行echo并且参数为^^^^\foo它将写入日志文件^\foo,则转义变得奇怪,我也注意到我是否给了它"^^^^\foo"它会返回相同的内容,即"^^^^\foo"

所以:

  1. 正在调用cmd.exe /c start /b做正确的事吗?
  2. 我在逃避时遇到了什么问题(这实际上是我给流程构建者提供的),我可能会因为cmd.exe调用start而做一些不同的事情,或许我需要真正逃脱某种程度上来说?也许我不理解Windows进程他们甚至对获取一系列参数有适当的支持吗?
  3. 我是否应该尝试从C调用本机库?如果是这样的话,我不介意如果必须调用C程序让我的进程在后台运行。

1 个答案:

答案 0 :(得分:0)

我认为在后台进程中创建一个分离的解决方案,JVM没有引用它也支持以理智的方式传递任何参数的可能性,它是使用CreateProcess API。为此我用过:

  <dependency>
    <groupId>net.java.dev.jna</groupId>
    <artifactId>platform</artifactId>
    <version>3.5.0</version>
  </dependency>

(这是旧版本,但它恰好已经在使用中)。

让它运行的Jave代码是:

/**
 * 
 * @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;
}
  • ✓即使JVM停止,启动的进程似乎仍在继续运行。
  • ✓我不需要处理来自ProcessBuilder的STDOUT / STDERR,后来又需要重新定位实际运行的命令。
  • ✓如果你可以正确地逃避你的命令(这样做的代码不在答案中,因为它不是我的分享)你可以传递像"这样的东西,我无法知道如何使用ProcessBuilder cmd /c start /b command ProcessBuilder。似乎JVM正在进行一些转义,因此可能无法构造所需的字符串来获得正确的命令。
  • ✓我可以看到JVM保存到stdout / stderr的文件句柄在进程完成之前被释放。
  • ✓我可以创建13k任务,而JVM没有为JVM提供62MB内存的OOM(看起来JVM没有保留资源,就像有些人最终只会做cmd /c和{{1 }}。
  • ✓未创建中间cmd.exe