process.waitFor()永远不会返回

118
Process process = Runtime.getRuntime().exec("tasklist");
BufferedReader reader = 
    new BufferedReader(new InputStreamReader(process.getInputStream()));
process.waitFor();

请注意,在JAVA 8中,有一个waitFor重载函数,可以让您指定超时时间。这可能是更好的选择,以避免waitFor永远不返回的情况。 - Ikaso
在我的情况下,在读取输出流之前,我添加了waitFor(),这导致了死锁的情况。if(!process.waitFor(15, TimeUnit.MINUTES)) { process.destroy(); } else { process.getOutputStream().close(); BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream())); - SRJ
12个回答

170

waitFor()无法返回的原因有很多。

但通常归结为执行的命令没有退出。

这又可以有很多原因。

其中一个常见的原因是该进程生成了一些输出,而您没有从适当的流中读取。这意味着一旦缓冲区已满,进程就会被阻塞,并等待您的进程继续读取。您的进程反过来又等待另一个进程完成(但它不会完成,因为它在等待您的进程...)。这是一种经典的死锁情况。

您需要不断地从进程的输入流中读取内容,以确保其不会被阻塞。

有一篇很好的文章详细解释了Runtime.exec()存在的所有陷阱,并展示了绕过它们的方法,名为"When Runtime.exec() won't"(是的,这篇文章发表于2000年,但内容仍然适用!)


13
这个回答是正确的,但是它缺少一个代码示例来排除问题。请查看Peter Lawrey的答案,获取有用的代码以找出为什么waitFor()没有返回。 - ForguesR
如果该进程不应该退出?如果你运行 ls 它会退出,但如果你启动 httpd 呢? - d-b
@d-b:我不确定你的问题是什么。是的,这也是waitFor()不返回的另一个原因。 - Joachim Sauer
问题是:你如何处理这种情况?你希望httpd或其他服务保持开启状态。 - d-b
1
@d-b:在这种情况下,只需不调用waitFor()即可。 - Joachim Sauer

99

看起来您在等待输出完成之前没有读取它。只有当输出不填满缓冲区时,这种情况才可以接受。但如果输出填满了缓冲区,它将等待您读取输出,这是一个进退两难的局面。

也许您有一些错误未被读取。这会导致应用程序停止并使waitFor永远等待。一个简单的解决方法是将错误重定向到常规输出。

ProcessBuilder pb = new ProcessBuilder("tasklist");
pb.redirectErrorStream(true);
Process process = pb.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null)
    System.out.println("tasklist: " + line);
process.waitFor();

5
提示信息:ProcessBuilder是一个真正的构建器,您可以直接编写ProcessBuilder pb = new ProcessBuilder("tasklist").redirectErrorStream(true)。 - Jean-François Savard
3
我更愿意使用pb.redirectError(new File("/dev/null")); - Toochka
@Toochka 只是提供信息,redirectError 仅在 Java 1.7 及以上版本可用。 - ZhekaKozlov
3
我相信这应该是被接受的答案,我用它替换了我的代码,立刻就起作用了。 - Gerben Rampaart
1
为什么你已经在读取 err/out 直到收到 EOF,还需要使用 waitFor - Sybuser
显示剩余2条评论

45

来自Java文档:

java.lang

类 Process

因为一些原生平台只为标准输入和输出流提供有限的缓冲区大小,不能及时地写入子进程的输入流或读取子进程的输出流可能会导致子进程阻塞,甚至死锁。

如果未清除从进程到输出流的输入流(它是子进程的输出流)的缓冲区,则可能导致子进程阻塞。

试试这个:

Process process = Runtime.getRuntime().exec("tasklist");
BufferedReader reader =
new BufferedReader(new InputStreamReader(process.getInputStream()));
while ((reader.readLine()) != null) {}
process.waitFor();

21
两点注意事项:(1)使用ProcessBuilder + redirectErrorStream(true),那么就是安全的。否则,(2)你需要一个线程从Process.getInputStream()读取数据,另一个线程从Process.getErrorStream()读取数据。我刚刚花了约四个小时才弄明白这一点(!),也就是说,“艰难模式”。 - kevinarpe
1
您可以使用Apache Commons Exec功能同时消耗stdout和stderr流:DefaultExecutor executor = new DefaultExecutor(); PumpStreamHandler pumpStreamHandler = new PumpStreamHandler(stdoutOS,stderrOS); executor.setStreamHandler(pumpStreamHandler); executor.execute(cmdLine);,其中stoutOS和stderrOS是我创建的 BufferedOutputStream,用于将输出写入相应的文件。 - Matthew Wise
在我的情况下,我正在从Spring调用一个批处理文件,该文件内部打开一个编辑器。即使应用了process.close()的代码,我的代码仍然挂起。但是当我像上面建议的那样打开输入流并立即关闭时,问题就解决了。因此,在我的情况下,Spring正在等待流关闭信号。尽管我正在使用Java 8自动可关闭功能。 - shaILU
@kevinarpe 你可以使用 InputStream.available() 方法在单个线程中消耗两个流而不会阻塞。只要该方法返回一个正数,就保证读取操作不会被阻塞。 - Gili

12

我想在之前的答案中补充一些内容,但由于我没有足够的声望来发表评论,所以我只能添加一个回答。这是针对在Java编程的安卓用户。

根据RollingBoy的帖子,这段代码对我几乎起作用:

Process process = Runtime.getRuntime().exec("tasklist");
BufferedReader reader =
new BufferedReader(new InputStreamReader(process.getInputStream()));
while ((reader.readLine()) != null) {}
process.waitFor();

在我的情况下,waitFor() 没有被释放是因为我执行了一条没有返回值的语句("ip adddr flush eth0")。解决这个问题的简单方法是确保你的语句总是会返回某些内容。对我来说,这意味着执行以下命令:"ip adddr flush eth0 && echo done"。你可以整天读取缓冲区,但如果没有任何返回值,你的线程将永远不会释放它的等待。

希望能对某人有所帮助!


2
当你没有足够的声望来评论时,不要绕过它并且仍然发表评论。将其作为自己的答案并从中获得声望! - anon
我认为挂起的不是 process.waitFor(),而是如果没有输出,则会挂起 reader.readLine()。我尝试使用 waitFor(long, TimeUnit) 设置超时时间,以防出现问题,并发现是读取操作挂起了。这使得设置了超时的版本需要另一个线程来执行读取操作... - osundblad

10

有几种可能性:

  1. 您没有消耗完进程的 stdout 输出。
  2. 您没有消耗完进程的 stderr 输出。
  3. 进程正在等待您的 输入 ,但您尚未提供它,或者您尚未关闭进程的 stdin
  4. 进程在一个死循环中旋转。

5

正如其他人所提到的,您需要消耗 stderrstdout

与其他答案相比,自 Java 1.7 以来,它更加容易。 您不再需要自己创建线程来读取 stderrstdout

只需使用ProcessBuilder并使用方法redirectOutputredirectErrorredirectErrorStream结合使用即可。

String directory = "/working/dir";
File out = new File(...); // File to write stdout to
File err = new File(...); // File to write stderr to
ProcessBuilder builder = new ProcessBuilder();
builder.directory(new File(directory));
builder.command(command);
builder.redirectOutput(out); // Redirect stdout to file
if(out == err) { 
  builder.redirectErrorStream(true); // Combine stderr into stdout
} else { 
  builder.redirectError(err); // Redirect stderr to file
}
Process process = builder.start();

3
你应该尝试在同一个 while 循环中消耗输出和错误信息。
    private void runCMD(String CMD) throws IOException, InterruptedException {
    System.out.println("Standard output: " + CMD);
    Process process = Runtime.getRuntime().exec(CMD);

    // Get input streams
    BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
    BufferedReader stdError = new BufferedReader(new InputStreamReader(process.getErrorStream()));
    String line = "";
    String newLineCharacter = System.getProperty("line.separator");

    boolean isOutReady = false;
    boolean isErrorReady = false;
    boolean isProcessAlive = false;

    boolean isErrorOut = true;
    boolean isErrorError = true;


    System.out.println("Read command ");
    while (process.isAlive()) {
        //Read the stdOut

        do {
            isOutReady = stdInput.ready();
            //System.out.println("OUT READY " + isOutReady);
            isErrorOut = true;
            isErrorError = true;

            if (isOutReady) {
                line = stdInput.readLine();
                isErrorOut = false;
                System.out.println("=====================================================================================" + line + newLineCharacter);
            }
            isErrorReady = stdError.ready();
            //System.out.println("ERROR READY " + isErrorReady);
            if (isErrorReady) {
                line = stdError.readLine();
                isErrorError = false;
                System.out.println("ERROR::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::" + line + newLineCharacter);

            }
            isProcessAlive = process.isAlive();
            //System.out.println("Process Alive " + isProcessAlive);
            if (!isProcessAlive) {
                System.out.println(":::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Process DIE " + line + newLineCharacter);
                line = null;
                isErrorError = false;
                process.waitFor(1000, TimeUnit.MILLISECONDS);
            }

        } while (line != null);

        //Nothing else to read, lets pause for a bit before trying again
        System.out.println("PROCESS WAIT FOR");
        process.waitFor(100, TimeUnit.MILLISECONDS);
    }
    System.out.println("Command finished");
}

3
出于同样的原因,您还可以使用inheritIO()将Java控制台映射到外部应用程序控制台,例如:
ProcessBuilder pb = new ProcessBuilder(appPath, arguments);

pb.directory(new File(appFile.getParent()));
pb.inheritIO();

Process process = pb.start();
int success = process.waitFor();

1

这是我使用的一种方法。 注意:此方法中可能有些代码不适用于您,请尝试忽略它。例如"logStandardOut(...)","git-bash"等。

private String exeShellCommand(String doCommand, String inDir, boolean ignoreErrors) {
logStandardOut("> %s", doCommand);

ProcessBuilder builder = new ProcessBuilder();
StringBuilder stdOut = new StringBuilder();
StringBuilder stdErr = new StringBuilder();

boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows");
if (isWindows) {
  String gitBashPathForWindows = "C:\\Program Files\\Git\\bin\\bash";
  builder.command(gitBashPathForWindows, "-c", doCommand);
} else {
  builder.command("bash", "-c", doCommand);
}

//Do we need to change dirs?
if (inDir != null) {
  builder.directory(new File(inDir));
}

//Execute it
Process process = null;
BufferedReader brStdOut;
BufferedReader brStdErr;
try {
  //Start the command line process
  process = builder.start();

  //This hangs on a large file
  // https://dev59.com/r2035IYBdhLWcg3wVud1
  //exitCode = process.waitFor();

  //This will have both StdIn and StdErr
  brStdOut = new BufferedReader(new InputStreamReader(process.getInputStream()));
  brStdErr = new BufferedReader(new InputStreamReader(process.getErrorStream()));

  //Get the process output
  String line = null;
  String newLineCharacter = System.getProperty("line.separator");

  while (process.isAlive()) {
    //Read the stdOut
    while ((line = brStdOut.readLine()) != null) {
      stdOut.append(line + newLineCharacter);
    }

    //Read the stdErr
    while ((line = brStdErr.readLine()) != null) {
      stdErr.append(line + newLineCharacter);
    }

    //Nothing else to read, lets pause for a bit before trying again
    process.waitFor(100, TimeUnit.MILLISECONDS);
  }

  //Read anything left, after the process exited
  while ((line = brStdOut.readLine()) != null) {
    stdOut.append(line + newLineCharacter);
  }

  //Read anything left, after the process exited
  while ((line = brStdErr.readLine()) != null) {
    stdErr.append(line + newLineCharacter);
  }

  //cleanup
  if (brStdOut != null) {
    brStdOut.close();
  }

  if (brStdErr != null) {
    brStdOut.close();
  }

  //Log non-zero exit values
  if (!ignoreErrors && process.exitValue() != 0) {
    String exMsg = String.format("%s%nprocess.exitValue=%s", stdErr, process.exitValue());
    throw new ExecuteCommandException(exMsg);
  }

} catch (ExecuteCommandException e) {
  throw e;
} catch (Exception e) {
  throw new ExecuteCommandException(stdErr.toString(), e);
} finally {
  //Log the results
  logStandardOut(stdOut.toString());
  logStandardError(stdErr.toString());
}

return stdOut.toString();

}


1

我认为我观察到了一个类似的问题:一些进程启动后,似乎成功运行但从未完成。函数waitFor()除非我在任务管理器中杀死进程,否则会永远等待。
然而,在命令行长度为127个字符或更短的情况下,一切都正常工作。如果长文件名是不可避免的,您可能希望使用环境变量,这可以使命令行字符串保持短。您可以生成一个批处理文件(使用FileWriter),其中在调用实际要运行的程序之前设置环境变量。
这样一个批处理文件的内容可能如下:

    set INPUTFILE="C:\Directory 0\Subdirectory 1\AnyFileName"
    set OUTPUTFILE="C:\Directory 2\Subdirectory 3\AnotherFileName"
    set MYPROG="C:\Directory 4\Subdirectory 5\ExecutableFileName.exe"
    %MYPROG% %INPUTFILE% %OUTPUTFILE%

最后一步是使用Runtime运行此批处理文件。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接