8

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

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
加载页面 (1/6)
计数页数 (2/6)
解析链接 (4/6)
加载页眉和页脚 (5/6)
打印页数 (6/6)
完毕

这个过程很好地退出了,生活还在继续。

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

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

我在有和没有工作目录的情况下都调用了 exec()。我尝试过使用和不使用空的“env”数组。没有喜悦。

为什么我的进程挂起,我能做些什么来解决它?

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

进一步的执行困境。

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

我检查了JavaDoc for Process,发现了这个

父进程使用这些流向子进程提供输入并从子进程获取输出。由于部分原生平台只为标准输入输出流提供有限的缓冲区大小,未能及时写入子进程的输入流或读取输出流可能会导致[b]子进程阻塞,甚至死锁[/b]。

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

使用 WKHTMLTOPDF

使用通用命令行 ap 并且没有参数,因此它应该“转储使用并终止”,它会吸出适当的 std::out,然后终止。

有趣的!

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


好的。不要让输入流填满,否则它们会阻塞。查看。 .close()也可以防止这种情况,但不是非常明亮。

这通常有效(包括我测试过的通用命令行应用程序)。

然而,具体而言,它会下降。看来 wkhtmltopdf 正在使用一些终端操作/光标的东西来做一个 ASCII 图形进度条。我相信这会导致 inputStream 立即返回 EOF 而不是给我正确的值。

有任何想法吗?几乎不会破坏交易,但它肯定会很高兴。

4

4 回答 4

11

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

出于某种原因,来自 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 的开发人员决定写入标准输出,您可以将子进程的标准错误重定向到标准输出并只读取一个流,如下所示:

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

实际上,在我必须从 java 生成外部进程的所有情况下,我都会这样做。这样我就不必阅读两个流。

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

希望这可以帮助。

更新:我在项目页面中提出了这个问题,并回答说这是设计使然,因为 wkhtmltopdf 支持在 STDOUT 中提供实际的 pdf 输出。有关更多详细信息和 java 代码,请参阅链接。

于 2012-01-16T19:08:39.987 回答
4

一个进程有 3 个流:输入、输出和错误。您可以使用单独的进程同时读取输出和错误流。请参阅此问题及其已接受的答案以及例如此问题。

于 2011-03-31T21:10:14.193 回答
2

您应该从不同线程中的流中读取。

于 2011-03-31T21:04:22.977 回答
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);
    }
于 2012-11-02T12:13:17.400 回答