从Java中运行外部程序,读取输出,允许中断。

16

我想从Java中启动一个进程,读取其输出并获取其返回代码。但在它执行时,我想能够取消它。我首先启动进程:

ProcessBuilder pb = new ProcessBuilder(args);
pb.redirectErrorStream(true);
Process proc = pb.start();

如果我调用proc.waitFor(),直到进程退出之前我就不能做任何事情。因此,我认为我需要像这样做:

while (true) {
  see if process has exited
  capture some output from the process
  decide if I want to cancel it, and if so, cancel it
  sleep for a while
}

这样做对吗?有人能给我一个在Java中实现这个的例子吗?

5个回答

11
这是我认为您想要做的一个例子:

以下是我认为您想要做的示例:

ProcessBuilder pb = new ProcessBuilder(args);
pb.redirectErrorStream(true);
Process proc = pb.start();

InputStream is = proc.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);

String line;
int exit = -1;

while ((line = br.readLine()) != null) {
    // Outputs your process execution
    System.out.println(line);
    try {
        exit = proc.exitValue();
        if (exit == 0)  {
            // Process finished
        }
    } catch (IllegalThreadStateException t) {
        // The process has not yet finished. 
        // Should we stop it?
        if (processMustStop())
            // processMustStop can return true 
            // after time out, for example.
            proc.destroy();
    }
}

你可以改进它 :-) 我现在没有真正的环境来测试它,但是你可以在这里找到更多信息。


2
如果一段时间没有输出会怎么样?这个程序不会在 br.readLine() 处一直挂起,直到有一行可读取的输出吗?我不确定我的进程会发送多少或少量的输出。 - JW.
2
Go语言中,你可以创建一个独立的线程来分析进程是否需要停止。它可以与while循环并行运行,并在进程挂起一段时间(或任何其他条件)后销毁该进程。 - Paulo Guedes
当进程完成后,您将从readLine()调用中获得null(已确认)。只有在进程未退出时才会挂起。 - Quintin Willison

8
我建议查看Apache Commons Exec,以避免重复造车轮。它有一些不错的功能,比如可以选择同步和异步执行,以及一个标准解决方案来生成一个看门狗进程,以防止执行超时。

我之前反对这个方案是因为我想要一个“纯净”的程序,只使用核心JDK。但你说的对,这种方式更好,解决了很多我在尝试创建自己的程序时没有意识到的问题。 - Gojira

5
像这样的辅助类可以解决问题:
public class ProcessWatcher implements Runnable {

    private Process p;
    private volatile boolean finished = false;

    public ProcessWatcher(Process p) {
        this.p = p;
        new Thread(this).start();
    }

    public boolean isFinished() {
        return finished;
    }

    public void run() {
        try {
            p.waitFor();
        } catch (Exception e) {}
        finished = true;
    }

}

您可以按照您描述的方式实现循环:
Process p = Runtime.getRuntime().exec("whatever command");
ProcessWatcher pw = new ProcessWatcher(p);
InputStream output = p.getInputStream();
while(!pw.isFinished()) {
    processOutput(output);
    if(shouldCancel()) p.destroy();
    Thread.sleep(500);
}

根据您想要销毁进程的条件,您可能希望在单独的线程中执行此操作。否则,您可能会在等待更多程序输出进行处理时被阻塞,从而无法真正选择销毁它。

编辑:麦克道尔在下面的评论中是100%正确的,因此我已将finished变量设为volatile。


1
“finished”变量应该至少声明为“volatile”。 - McDowell
这是我的担忧;如果有一段时间没有输出怎么办?如果我调用类似 BufferedReader.readLine() 这样的方法,我不希望它一直挂起直到有输出;我想要有取消的机会。 - JW.
你可以使用以下代码创建一个新的Runnable对象: public void run() { while (!pw.isFinished()) { if (shouldCancel()) p.destroy(); Thread.sleep(500); } }然后通过调用new Thread(myrunnable).start()来启动新线程。 - Marty Lamb
或者你的 processOutput() 方法可以检查 output.available(),只有在不会阻塞的情况下才读取。如果你选择这条路线,那么你就不能使用 readLine() - 你必须自己处理行尾。 - Marty Lamb
1
或者,如果取消是来自GUI用户请求的话,那么您可以从处理用户事件的线程(例如按钮按下)中调用process.destroy()。 - Marty Lamb

2
这个怎么样(看看 jcabi-heroku-maven-plugin 的工作方式):
/**
 * Wait for the process to stop, logging its output in parallel.
 * @param process The process to wait for
 * @return Stdout produced by the process
 * @throws InterruptedException If interrupted in between
 */
private String waitFor(final Process process) throws InterruptedException {
    final BufferedReader reader = new BufferedReader(
        new InputStreamReader(process.getInputStream())
    );
    final CountDownLatch done = new CountDownLatch(1);
    final StringBuffer stdout = new StringBuffer();
    new Thread(
        new VerboseRunnable(
            new Callable<Void>() {
                @Override
                public Void call() throws Exception {
                    while (true) {
                        final String line = reader.readLine();
                        if (line == null) {
                            break;
                        }
                        System.out.println(">> " + line);
                        stdout.append(line);
                    }
                    done.countDown();
                    return null;
                }
            },
            false
        )
    ).start();
    try {
        process.waitFor();
    } finally {
        done.await();
        IOUtils.closeQuietly(reader);
    }
    return stdout.toString();
}

提示:现在可以通过jcabi-log工件中的com.jcabi.log.VerboseProcess类来使用此实现。

0
什么会让你决定终止进程——异步事件(如用户输入)还是同步事件(例如,进程已完成你想要它做的事情)?我猜是前者——来自用户的输入使您决定取消子进程。
另外,您期望子进程产生多少输出?如果很多,那么如果您不及时从其输出流中读取数据,该子进程可能会被阻塞。
您的情况可能会有所不同,但似乎您可能需要至少两个不同的线程——一个用于决定是否取消进程,另一个处理子进程的输出。
在这里查看更多详细信息:http://java.sun.com/developer/JDCTechTips/2005/tt0727.html#2

我已经在一个线程中运行,当需要取消时,线程外的某个人将设置我的一个字段。 - JW.

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