如何适当关闭Java中java.lang.Process的标准流?

4
这个问题涉及到java.lang.Process以及其如何处理标准输入、标准输出和标准错误流。
我们的项目中有一个类,它是org.apache.commons.io.IOUtils的扩展。在这个类中,我们新增了一个方法来安静地关闭Process-Object的std流。这样做是否合适?
/**
 * Method closes all underlying streams from the given Process object.
 * If Exit-Code is not equal to 0 then Process will be destroyed after 
 * closing the streams.
 *
 * It is guaranteed that everything possible is done to release resources
 * even when Throwables are thrown in between.
 *
 * In case of occurances of multiple Throwables then the first occured
 * Throwable will be thrown as Error, RuntimeException or (masked) IOException.
 *
 * The method is null-safe.
 */
public static void close(@Nullable Process process) throws IOException {
    if(process == null) {
      return;
    }

    Throwable t = null;

    try {
      close(process.getOutputStream());
    }
    catch(Throwable e) {
      t = e;
    }

    try{
      close(process.getInputStream());
    }
    catch(Throwable e) {
      t = (t == null) ? e : t;
    }

    try{
      close(process.getErrorStream());
    }
    catch (Throwable e) {
      t = (t == null) ? e : t;
    }

    try{
      try {
        if(process.waitFor() != 0){
          process.destroy();
        }
      }
      catch(InterruptedException e) {
        t = (t == null) ? e : t;
        process.destroy();
      }
    }
    catch (Throwable e) {
      t = (t == null) ? e : t;
    }

    if(t != null) {
      if(t instanceof Error) {
        throw (Error) t;
      }

      if(t instanceof RuntimeException) {
        throw (RuntimeException) t;
      }

      throw t instanceof IOException ? (IOException) t : new IOException(t);
    }
}

public static void closeQuietly(@Nullable Logger log, @Nullable Process process) {
  try {
    close(process);
  }
  catch (Exception e) {
    //log if Logger provided, otherwise discard
    logError(log, "Fehler beim Schließen des Process-Objekts (inkl. underlying streams)!", e);
  }
}

public static void close(@Nullable Closeable closeable) throws IOException {
  if(closeable != null) {
    closeable.close();
  }
}

这些方法基本上是在finally块中使用的。

我想知道的是,如果我使用这个实现是否安全?考虑到这样的事情:在其生命周期内,进程对象是否始终返回相同的stdin、stdout和stderr流? 或者说我可能错过了之前通过process的getInputStream()getOutputStream()getErrorStream()方法返回的关闭流操作?

StackOverflow.com上有一个相关问题:java:关闭子进程std流?

编辑

正如我和其他人在这里指出的:

  • 必须完全使用InputStreams。否则,子进程可能无法终止,因为其输出流中存在未完成的数据。
  • 必须关闭所有三个std流。不管是否使用过。
  • 当子进程正常终止时,一切都应该没问题。否则,必须强制终止。
  • 当子进程返回退出代码时,我们不需要destroy()它。它已经终止了。(即使不一定是通过Exit Code 0正常终止,但它已经终止。)
  • 我们需要监视waitFor(),并在超时后中断,以便进程有机会正常终止,但在其挂起时将其杀死。

未回答的部分:

  • 考虑并行消耗InputStreams的利弊。或者必须按特定顺序使用?

只是提醒一下,你的代码中存在一些严重的反模式。1. 几乎每个语句都有try/catch。2. 每次都捕获所有“Throwable”。3. 嵌套的try语句可以合并为一个,并使用两个catch语句。 - Bjarke Freund-Hansen
如何在不将其放入单独的try-catch块中时继续工作并关闭其他流? - Fabian Barney
由于Throwable可能在嵌套的catch块中发生,因此必须使用嵌套的try-catch块。对于捕获“Throwable”:什么更合适?我想捕获所有值得继续工作以尽力关闭资源的异常。当先前捕获时,我会在最后抛出一个Throwable。我在这里能做得更好吗? - Fabian Barney
请注意,这里是一个实用类,用于在其他地方的finally块中关闭资源。它的工作不是处理异常。它抛出第一个出现的异常。调用者必须处理它。但它的意图是尽一切可能释放由调用者提供的资源。 - Fabian Barney
3个回答

2

试图简化您的代码:

public static void close(@Nullable Process process) throws IOException
{
    if(process == null) { return; }

    try
    {
        close(process.getOutputStream());
        close(process.getInputStream());
        close(process.getErrorStream());

        if(process.waitFor() != 0)
        {
            process.destroy();
        }
    }
    catch(InterruptedException e)
    {
        process.destroy();
    }
    catch (RuntimeException e)
    {
        throw (e instanceof IOException) ? e : new IOException(e);
    }
}

通过捕获Throwable,我认为您希望捕获所有未检查的异常。这些异常要么是RuntimeException的派生类,要么是Error。然而,Error不应该被捕获,因此我已将Throwable替换为RuntimeException

(捕获所有RuntimeException仍然不是一个好主意。)


你的代码在关闭输出流之前发生异常时没有尝试关闭输入流。捕获Throwable的意图是为了在关闭资源时给予最高可能的保证。如果发生Throwable,第一个出现的Throwable将在最后抛出。这样做是因为如果出现问题,这很可能是你想要的Throwable。为什么在这种情况下不捕获Throwable?我应该捕获“Exception”吗?为什么? - Fabian Barney
IOException不能是RuntimeException。所以最后一个catch块的代码可以简化。但你的实现没有尽其所能关闭流。当这个异常在catch-interruptedException块中发生时,它可能会抛出其他异常。无论如何都要给一个+1,至少有人考虑到了这个问题。 - Fabian Barney

2

正如您所链接到的问题所述,最好读取并丢弃输出和错误流。如果您使用的是apache commons io,可以尝试以下方法:

new Thread(new Runnable() {public void run() {IOUtils.copy(process.getInputStream(), new NullOutputStream());}}).start();
new Thread(new Runnable() {public void run() {IOUtils.copy(process.getErrorStream(), new NullOutputStream());}}).start();

您想要在单独的线程中读取并丢弃stdout和stderr,以避免出现诸如进程阻塞等问题,当它写入足够的信息到stderr或stdout以填充缓冲区时。
如果您担心有太多线程,请参见此问题
我认为您不需要担心在将stdout、stdin复制到NullOutputStream时捕获IOExceptions,因为如果从进程stdout/stdin读取时出现IOException,则可能是由于进程本身已死亡,而写入NullOutputStream永远不会抛出异常。
您不需要检查waitFor()的返回状态。
您想等待进程完成吗?如果是这样,您可以执行以下操作:
while(true) {
     try
     {
         process.waitFor();
         break;
     } catch(InterruptedException e) {
         //ignore, spurious interrupted exceptions can occur
     }

}

从您提供的链接来看,当进程完成时,您确实需要关闭流,但是销毁操作会为您执行此操作。

因此,最终的方法变为:

public void close(Process process) {

    if(process == null) return;

    new Thread(new Runnable() {public void run() {IOUtils.copy(process.getInputStream(), new NullOutputStream());}}).start();
    new Thread(new Runnable() {public void run() {IOUtils.copy(process.getErrorStream(), new NullOutputStream());}}).start();
    while(true) {
        try
        {
            process.waitFor();
            //this will close stdin, stdout and stderr for the process
            process.destroy();
            break;
        } catch(InterruptedException e) {
            //ignore, spurious interrupted exceptions can occur
        }

   }
}

非常感谢您的回答。我已经快速阅读了它,并稍后会更详细地研究一下。首先,我必须说:您之前并不知道 Process-Object 发生了什么。在大多数情况下,我实际上是将结果写入 Outputstream,然后通过 InputStream 获取结果等。您说当我没有使用流时关闭它们是不必要的。好吧,忽略我们无法在这里知道的事实。这个链接说即使您没有使用它们,也必须关闭它们。他错了吗?有任何来源吗? - Fabian Barney
谢谢提供链接,我不知道需要关闭流。我进行了一些测试,确实需要关闭这些流。但是,destroy()方法会为您关闭它们(如果您查看destroy的源代码,至少对于UnixProcess,它会关闭它们,在Windows上也很可能如此)。我已经更新了我的答案。 - sbridges
我对这里的两件事感到有点“不舒服”,但也许你可以让我感觉更舒适。 :-)1.) 强烈相信 destroy() 会适当处理流。 2.) 创建非引用的“盲目”线程来消耗输入流,然后可能会挂起。另一个问题:当 waitFor() 正常返回时,是不是只调用 destroy() 来关闭流?当由于超时而中断时,waitFor() 抛出异常并且 destroy() 不执行,但在这种情况下,为了强制终止子进程,它是非常必要的! - Fabian Barney
更精确地说,如果子进程无法正常终止并且必须被强制终止,会发生什么?您的代码中没有处理这种情况,还是我漏掉了什么? - Fabian Barney
2
"虚假的中断异常可能会发生"?不,它们不会。你为什么这么认为? - Peter Štibraný
显示剩余3条评论

1

只是想让你知道我们代码库中目前有什么:

public static void close(@Nullable Process process) throws IOException {
  if (process == null) {
    return;
  }

  Throwable t = null;

  try {
    flushQuietly(process.getOutputStream());
  }
  catch (Throwable e) {
    t = mostImportantThrowable(t, e);
  }

  try {
    close(process.getOutputStream());
  }
  catch (Throwable e) {
    t = mostImportantThrowable(t, e);
  }

  try {
    skipAllQuietly(null, TIMEOUT, process.getInputStream());
  }
  catch (Throwable e) {
    t = mostImportantThrowable(t, e);
  }

  try {
    close(process.getInputStream());
  }
  catch (Throwable e) {
    t = mostImportantThrowable(t, e);
  }

  try {
    skipAllQuietly(null, TIMEOUT, process.getErrorStream());
  }
  catch (Throwable e) {
    t = mostImportantThrowable(t, e);
  }

  try {
    close(process.getErrorStream());
  }
  catch (Throwable e) {
    t = mostImportantThrowable(t, e);
  }

  try {
    try {
      Thread monitor = ThreadMonitor.start(TIMEOUT);
      process.waitFor();
      ThreadMonitor.stop(monitor);
    }
    catch (InterruptedException e) {
      t = mostImportantThrowable(t, e);
      process.destroy();
    }
  }
  catch (Throwable e) {
    t = mostImportantThrowable(t, e);
  }

  if (t != null) {
    if (t instanceof Error) {
      throw (Error) t;
    }

    if (t instanceof RuntimeException) {
      throw (RuntimeException) t;
    }

    throw t instanceof IOException ? (IOException) t : new IOException(t);
  }
}

skipAllQuietly(...) 消耗完整的 InputStreams。它在内部使用类似于 org.apache.commons.io.ThreadMonitor 的实现来中断消耗,如果给定的超时时间已经超过。

mostImportantThrowable(...) 决定应该返回什么 Throwable。错误优先于一切。首次发生的比后来发生的优先级高。这里没有什么非常重要的东西,因为这些 Throwable 很可能会被丢弃。我们想要继续工作,而且我们只能抛出一个异常,所以我们必须在最后决定抛出什么异常。

close(...) 是关闭资源的空指针安全实现,但在出现问题时会抛出异常。


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