从Runtime.getRuntime()。exec()启动wkhtmltopdf:永远不会终止?

时间:2011-03-31 20:59:52

标签: java process runtime.exec wkhtmltopdf

我正在我的Java应用程序中启动wkhtmltopdf(Tomcat服务器的一部分,在Win7 64位的Eclipse Helios中以调试模式运行):我想等待它完成,然后再做更多的东西。

String cmd[] = {"wkhtmltopdf", htmlPathIn, pdfPathOut};
Process proc = Runtime.getRuntime().exec( cmd, null );

proc.waitFor();

但是waitFor()永远不会回来。我仍然可以在Windows任务管理器中看到该过程(我将命令行传递给exec():看起来很好)。它的工作原理。 wkhtmltopdf生成我期望的PDF,就在我期望的地方。我可以打开它,重命名它,无论如何,即使在进程仍在运行时(在我手动终止它之前)。

从命令行,一切都很好:

c:\wrk>wkhtmltopdf C:\Temp\foo.html c:\wrk\foo.pdf
Loading pages (1/6)
Counting pages (2/6)
Resolving links (4/6)
Loading headers and footers (5/6)
Printing pages (6/6)
Done

这个过程很好,生活还在继续。

那么runtime.exec()导致wkhtmltopdf永不终止的原因是什么?

我可以抓住proc.getInputStream()并查找“完成”,但那是......卑鄙的。我想要更通用的东西。

我使用和不使用工作目录调用exec()。我尝试过使用和不使用空的“env”数组。没有快乐。

为什么我的流程悬而未决,我该怎么做才能解决它?

PS:我已经尝试过其他几个命令行应用程序,它们都表现出相同的行为。

进一步的执行困境。

我正在努力阅读标准输出&错误,没有成功。从命令行,我知道应该有一些非常类似于我的命令行体验,但是当我读取proc.getInputStream()返回的输入流时,我立即得到一个EOL(-1,我正在使用{{1 }})。

我检查了JavaDoc for Process,找到了这个

  

父进程使用这些流向子进程提供输入并从子进程获取输出。由于某些本机平台仅为标准输入和输出流提供有限的缓冲区大小,因此无法及时写入输入流或读取子进程的输出流可能导致[b]子进程阻塞,甚至死锁[/ b]。

重点补充。所以我试过了。标准输出inputStream上的第一个'read()'被阻塞,直到我杀死进程...

使用WKHTMLTOPDF

使用通用命令行ap&没有params所以它应该“转储使用并终止”,它会删除相应的std :: out,然后终止。

有趣!

JVM版本问题?我正在使用1.6.0_23。最新的是... v24。我刚检查了更改日志,看不到任何有希望的东西,但我还是会尝试更新。


好。不要让输入流填充或阻止它们。校验。 inputStream.read()也可以阻止这种情况,但不是非常明亮。

一般情况下(包括我测试过的通用命令行应用程序)。

但在具体的中,它会失败。似乎wkhtmltopdf正在使用一些终端操作/光标内容来执行ASCII图形进度条。我相信这会导致inputStream立即返回EOF,而不是给我正确的值。

有什么想法吗?几乎不是一个交易破坏者,但它肯定会很好。

4 个答案:

答案 0 :(得分:10)

我和你有同样的问题,我解决了它。以下是我的发现:

由于某种原因,wkhtmltopdf的输出转到进程的STDERR而不是STDOUT。我已经通过从Java和perl调用wkhtmltopdf验证了这一点

因此,例如在java中,你必须这样做:

//ProcessBuilder is the recommended way of creating processes since Java 1.5 
//Runtime.getRuntime().exec() is deprecated. Do not use. 
ProcessBuilder pb = new ProcessBuilder("wkhtmltopdf.exe", htmlFilePath, pdfFilePath);
Process process = pb.start();

BufferedReader errStreamReader = new BufferedReader(new  InputStreamReader(process.getErrorStream())); 
//not "process.getInputStream()" 
String line = errStreamReader.readLine(); 
while(line != null) 
{ 
    System.out.println(line); //or whatever else
    line = reader.readLine(); 
}

另外,如果你从java生成一个进程,你必须从stdout和stderr流中读取(即使你什么也不做),否则流缓冲区将填满,进程将挂起并永不返回。

为了防范你的代码,以防wkhtmltopdf的开发人员决定写入stdout,你可以将子进程的stderr重定向到stdout并只读取一个这样的流:

ProcessBuilder pb = new ProcessBuilder("wkhtmltopdf.exe", htmlFilePath, pdfFilePath); 
pb.redirectErrorStream(true); 
Process process = pb.start(); 
BufferedReader inStreamReader = new BufferedReader(new  InputStreamReader(process.getInputStream())); 

实际上,我在所有需要从java生成外部进程的情况下执行此操作。这样我就不必阅读两个流。

如果您不希望主线程被阻塞,您还应该在不同的线程中读取生成进程的流,因为从流中读取是阻塞的。

希望这有帮助。

UPDATE :我在项目页面中raised this issue并且回答说这是设计的,因为wkhtmltopdf支持在STDOUT中提供实际的pdf输出。有关更多详细信息和Java代码,请参阅链接。

答案 1 :(得分:4)

进程有3个流:输入,输出和错误。您可以使用单独的进程同时读取输出和错误流。例如,请参阅this question and its accepted answeralso this one

答案 2 :(得分:2)

答案 3 :(得分:2)

    final Semaphore semaphore = new Semaphore(numOfThreads);
    final String whktmlExe = tmpwhktmlExePath;
    int doccount = 0;
    try{
        File fileObject = new File(inputDir);
        for(final File f : fileObject.listFiles()) {

            if(f.getAbsolutePath().endsWith(".html")) {
                doccount ++;
                if(doccount >500 ) {
                    LOG.info(" done with conversion of 1000 docs exiting ");
                    break;
                }
                System.out.println(" inside for before "+semaphore.availablePermits());
                semaphore.acquire();
                System.out.println(" inside for after "+semaphore.availablePermits() + " ---" +f.getName());
                new java.lang.Thread() {
                    public void run() {
                        try {
                            String F_ =  f.getName().replaceAll(".html", ".pdf") ;
                            ProcessBuilder pb = new ProcessBuilder(whktmlExe , f.getAbsolutePath(), outPutDir + F_ .replaceAll(" ", "_") );//"wkhtmltopdf.exe", htmlFilePath, pdfFilePath);
                            pb.redirectErrorStream(true);
                            Process process = pb.start();
                            BufferedReader errStreamReader = new BufferedReader(new  InputStreamReader(process.getInputStream()));  
                            String line = errStreamReader.readLine(); 
                            while(line != null) 
                            { 
                                System.err.println(line); //or whatever else
                                line = errStreamReader.readLine(); 
                            }

                            System.out.println("after completion for ");
                        } catch (Exception e) {
                            e.printStackTrace();
                        }finally {
                            System.out.println(" in finally releasing ");
                        semaphore.release();
                        }
                  }
                }.start();
            }
        }
    }catch (Exception ex) {
        LOG.error(" *** Error in pdf generation *** ", ex);
    }

    while (semaphore.availablePermits() < numOfThreads) {//till all threads finish 
        LOG.info( " Waiting for all threads to exit "+ semaphore.availablePermits() + " --- " +( numOfThreads - semaphore.availablePermits()));
        java.lang.Thread.sleep(10000);
    }