Skip to content

Commit 41982ca

Browse files
authored
Fix wkhtmltox hangs (#68)
1 parent 4dbeb5c commit 41982ca

6 files changed

Lines changed: 104 additions & 65 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
### Fixed
1212
- Github template tests fail in proxy environment ([#56](https://github.com/opendevstack/ods-document-generation-svc/issues/56))
1313
- Fix TIR and DTR documents are not properly indexed ([#55](https://github.com/opendevstack/ods-document-generation-svc/pull/55))
14+
- Fix wkhtmltox hangs ([#66](https://github.com/opendevstack/ods-document-generation-svc/pull/66))
1415

1516
### Changed
1617
- Updated maxRequestSize value from 100m to 200m

docker/Dockerfile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ MAINTAINER martin.etmajer@boehringer-ingelheim.com
44

55
WORKDIR /app
66
COPY app.jar ./app.jar
7+
COPY entrypoint.sh ./entrypoint.sh
78

89
# Install wkhtmltopdf
910
RUN yum update -y && \
1011
yum install -y libX11 libXext libXrender libjpeg xz xorg-x11-fonts-Type1 git-core && \
1112
curl -kLO http://mirror.centos.org/centos/8/AppStream/aarch64/os/Packages/xorg-x11-fonts-75dpi-7.5-19.el8.noarch.rpm && \
1213
rpm -Uvh xorg-x11-fonts-75dpi-7.5-19.el8.noarch.rpm && \
1314
curl -kLO https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/0.12.5/wkhtmltox-0.12.5-1.centos8.x86_64.rpm && \
14-
rpm -Uvh wkhtmltox-0.12.5-1.centos8.x86_64.rpm
15+
rpm -Uvh wkhtmltox-0.12.5-1.centos8.x86_64.rpm && chmod +x entrypoint.sh
1516

1617
# See https://docs.openshift.com/container-platform/3.9/creating_images/guidelines.html
1718
RUN chgrp -R 0 /app && \
@@ -20,5 +21,8 @@ RUN chgrp -R 0 /app && \
2021
USER 1001
2122

2223
EXPOSE 8080
24+
ENV JAVA_MEM_XMX="512m" \
25+
JAVA_MEM_XMS="128m" \
26+
JAVA_OPTS="-XX:+UseCompressedOops -XX:+UseG1GC -XX:MaxGCPauseMillis=1000"
2327

24-
CMD ["java", "-Xmx512m", "-jar", "app.jar"]
28+
ENTRYPOINT /app/entrypoint.sh

docker/entrypoint.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/bin/bash -e
2+
java $JAVA_OPTS -Xms$JAVA_MEM_XMS -Xmx$JAVA_MEM_XMX -jar app.jar

src/main/groovy/app/App.groovy

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package app
22

33
import com.typesafe.config.ConfigFactory
44
import groovy.util.logging.Slf4j
5+
import org.apache.commons.io.IOUtils
56
import org.apache.groovy.json.internal.LazyMap
67
import org.jooby.Jooby
78
import org.jooby.MediaType
@@ -33,9 +34,17 @@ class App extends Jooby {
3334
}
3435

3536
def pdf = new DocGen().generate(body.metadata.type, body.metadata.version, body.data)
36-
rsp.send([
37-
data: pdf.encodeBase64().toString()
38-
])
37+
try{
38+
pdf.withInputStream { is ->
39+
byte[] pdfBytes = IOUtils.toByteArray(is)
40+
rsp.send([
41+
data: Base64.getEncoder().encodeToString(pdfBytes)
42+
])
43+
}
44+
}finally{
45+
pdf.delete()
46+
}
47+
3948
})
4049
.consumes(MediaType.json)
4150
.produces(MediaType.json)

src/main/groovy/app/DocGen.groovy

Lines changed: 56 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,17 @@ import com.typesafe.config.Config
1010
import com.typesafe.config.ConfigFactory
1111

1212
import groovy.json.JsonOutput
13+
import org.apache.commons.io.output.TeeOutputStream
1314
import org.apache.pdfbox.pdmodel.PDDocument
1415
import org.apache.pdfbox.pdmodel.interactive.action.PDActionGoTo
1516
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination
1617
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink
1718

19+
import java.nio.channels.SeekableByteChannel
1820
import java.nio.file.Files
1921
import java.nio.file.Path
2022
import java.nio.file.Paths
23+
import java.nio.file.StandardOpenOption
2124
import java.time.Duration
2225

2326
import org.apache.commons.io.FileUtils
@@ -64,8 +67,8 @@ class DocGen implements Jooby.Module {
6467
}
6568

6669
// Generate a PDF document for a combination of template type, version and data
67-
def byte[] generate(String type, String version, Object data) {
68-
def result = []
70+
File generate(String type, String version, Object data) {
71+
File resultFile = null
6972
def tmpDir = null
7073

7174
try {
@@ -88,7 +91,7 @@ class DocGen implements Jooby.Module {
8891
}
8992

9093
// Convert the exected templates into a PDF document
91-
result = Util.convertHtmlToPDF(partials.document, partials.header, partials.footer, data)
94+
resultFile = Util.convertHtmlToPDF(partials.document, partials.header, partials.footer, data)
9295
} catch (Throwable e) {
9396
throw e
9497
} finally {
@@ -97,7 +100,7 @@ class DocGen implements Jooby.Module {
97100
}
98101
}
99102

100-
return result
103+
return resultFile
101104
}
102105

103106
// Read partial templates for a template type and version from the basePath directory
@@ -143,80 +146,81 @@ class DocGen implements Jooby.Module {
143146
}
144147

145148
// Convert a HTML document, with an optional header and footer, into a PDF
146-
static private def byte[] convertHtmlToPDF(Path documentHtmlFile, Path headerHtmlFile = null, Path footerHtmlFile = null, Object data) {
147-
def documentPDFFile = null
149+
static private File convertHtmlToPDF(Path documentHtmlFile, Path headerHtmlFile = null, Path footerHtmlFile = null, Object data) {
150+
def documentPDFFilePath = Files.createTempFile("document", ".pdf")
148151

149-
try {
150-
documentPDFFile = Files.createTempFile("document", ".pdf")
152+
def cmd = ["wkhtmltopdf", "--encoding", "UTF-8", "--no-outline", "--print-media-type"]
153+
cmd << "--enable-local-file-access"
154+
cmd.addAll(["-T", "40", "-R", "25", "-B", "25", "-L", "25"])
151155

152-
def cmd = ["wkhtmltopdf", "--encoding", "UTF-8", "--no-outline", "--print-media-type"]
153-
cmd << "--enable-local-file-access"
154-
cmd.addAll(["-T", "40", "-R", "25", "-B", "25", "-L", "25"])
155-
156-
if (data?.metadata?.header) {
157-
if (data.metadata.header.size() > 1) {
158-
cmd.addAll(["--header-center", """${data.metadata.header[0]}
156+
if (data?.metadata?.header) {
157+
if (data.metadata.header.size() > 1) {
158+
cmd.addAll(["--header-center", """${data.metadata.header[0]}
159159
${data.metadata.header[1]}"""])
160-
} else {
161-
cmd.addAll(["--header-center", data.metadata.header[0]])
162-
}
163-
164-
cmd.addAll(["--header-font-size", "10", "--header-spacing", "10"])
160+
} else {
161+
cmd.addAll(["--header-center", data.metadata.header[0]])
165162
}
166163

167-
cmd.addAll(["--footer-center", "'Page [page] of [topage]'", "--footer-font-size", "10"])
164+
cmd.addAll(["--header-font-size", "10", "--header-spacing", "10"])
165+
}
168166

169-
if (data?.metadata?.orientation) {
170-
cmd.addAll(["--orientation", data.metadata.orientation])
171-
}
167+
cmd.addAll(["--footer-center", "'Page [page] of [topage]'", "--footer-font-size", "10"])
172168

173-
cmd << documentHtmlFile.toFile().absolutePath
174-
cmd << documentPDFFile.toFile().absolutePath
169+
if (data?.metadata?.orientation) {
170+
cmd.addAll(["--orientation", data.metadata.orientation])
171+
}
172+
173+
cmd << documentHtmlFile.toFile().absolutePath
174+
cmd << documentPDFFilePath.toFile().absolutePath
175175

176-
println "[INFO]: executing cmd: ${cmd}"
177-
def result = Util.shell(cmd)
176+
println "[INFO]: executing cmd: ${cmd}"
177+
178+
def result = Util.shell(cmd)
179+
try{
178180
if (result.rc != 0) {
181+
String stderr = result.stderr.text
179182
println "[ERROR]: ${cmd} has exited with code ${result.rc}"
180-
println "[ERROR]: ${result.stderr}"
183+
println "[ERROR]: ${stderr}"
181184
throw new IllegalStateException(
182-
"PDF Creation of ${documentHtmlFile} failed!\r:${result.stderr}\r:Error code:${result.rc}")
185+
"PDF Creation of ${documentHtmlFile} failed!\r:${stderr}\r:Error code:${result.rc}")
183186
}
184-
185-
fixDestinations(documentPDFFile.toFile())
186-
187-
return Files.readAllBytes(documentPDFFile)
188-
} catch (Throwable e) {
189-
throw e
190-
} finally {
191-
if (documentPDFFile) {
192-
Files.delete(documentPDFFile)
187+
}finally{
188+
if(result!=null){
189+
result.stderr.close()
193190
}
194191
}
192+
193+
194+
File documentPDFFile = documentPDFFilePath.toFile()
195+
fixDestinations(documentPDFFile)
196+
197+
return documentPDFFile
195198
}
196199

197200
// Execute a command in the shell
198201
static private def Map shell(List<String> cmd) {
202+
199203
def proc = cmd.execute()
200-
// for some VERY complex docs - the process implementation hangs on wait() ...
201-
// switching to the below - seem to work but gets a weird NPE...
202-
// java.lang.NullPointerException: Cannot invoke method call() on null object
203-
// at app.DocGen$Util.shell(DocGen.groovy:193)
204-
ByteArrayOutputStream bosOut = new ByteArrayOutputStream()
205-
ByteArrayOutputStream bosErr = new ByteArrayOutputStream()
206-
try
204+
Path tempFilePath = Files.createTempFile("shell", ".bin")
205+
File tempFile = tempFilePath.toFile()
206+
FileOutputStream tempFileOutputStream = new FileOutputStream(tempFile)
207+
def errOutputStream = new TeeOutputStream(tempFileOutputStream, System.err)
208+
209+
try
207210
{
208-
proc.waitForProcessOutput(bosOut, bosErr)
209-
} catch (NullPointerException wtfEx) {
210-
//
211+
proc.waitForProcessOutput(System.out, errOutputStream)
212+
}finally{
213+
tempFileOutputStream.close()
211214
}
212215

213216
return [
214217
rc: proc.exitValue(),
215-
stderr: bosErr.toString(),
216-
stdout: bosOut.toString()
218+
stderr: Files.newInputStream(tempFilePath, StandardOpenOption.DELETE_ON_CLOSE)
217219
]
218220
}
219221

222+
223+
220224
/**
221225
* Fixes malformed PDF documents which use page numbers in local destinations, referencing the same document.
222226
* Page numbers should be used only for references to external documents.
@@ -232,6 +236,7 @@ ${data.metadata.header[1]}"""])
232236
def doc = PDDocument.load(file)
233237
fixDestinations(doc)
234238
doc.save(file)
239+
doc.close()
235240
}
236241

237242
/**

src/test/groovy/app/DocGenSpec.groovy

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ class DocGenSpec extends SpecHelper {
5959
def result = DocGen.Util.convertHtmlToPDF(documentHtmlFile, headerHtmlFile, footerHtmlFile, data)
6060

6161
then:
62-
assertThat(new String(result), startsWith("%PDF-1.4\n"))
62+
def firstLine
63+
result.withReader { firstLine = it.readLine()}
64+
assertThat(firstLine, startsWith("%PDF-1.4"))
6365

6466
cleanup:
6567
Files.delete(documentHtmlFile)
@@ -84,11 +86,18 @@ class DocGenSpec extends SpecHelper {
8486
)
8587

8688
when:
87-
def result = new DocGen().generate("InstallationReport", version, data)
89+
def resultFile = new DocGen().generate("InstallationReport", version, data)
8890

8991
then:
90-
assertThat(new String(result), startsWith("%PDF-1.4\n"))
91-
checkResult(result)
92+
def firstLine
93+
resultFile.withReader { firstLine = it.readLine()}
94+
assertThat(firstLine, startsWith("%PDF-1.4"))
95+
def is = new FileInputStream(resultFile);
96+
checkResult(is)
97+
98+
cleanup:
99+
if(is!=null)is.close()
100+
if(resultFile!=null)resultFile.delete()
92101
}
93102

94103
def "generateFromXunit"() {
@@ -120,16 +129,25 @@ class DocGenSpec extends SpecHelper {
120129

121130
when:
122131
println ("generating doc")
123-
def result = new DocGen().generate("DTR", version, data)
132+
def resultFile = new DocGen().generate("DTR", version, data)
124133

125134
then:
126135
println ("asserting generated file")
127-
assertThat(new String(result), startsWith("%PDF-1.4\n"))
128-
checkResult(result)
136+
def firstLine
137+
resultFile.withReader { firstLine = it.readLine()}
138+
assertThat(firstLine, startsWith("%PDF-1.4"))
139+
140+
def is = new FileInputStream(resultFile);
141+
checkResult(is)
142+
143+
144+
cleanup:
145+
if(is!=null)is.close()
146+
if(resultFile!=null)resultFile.delete()
129147
}
130148

131-
private void checkResult(byte[] result) {
132-
def resultDoc = PDDocument.load(result)
149+
private void checkResult(InputStream inputStream) {
150+
def resultDoc = PDDocument.load(inputStream)
133151
resultDoc.withCloseable { PDDocument doc ->
134152
doc.pages?.each { page ->
135153
page.getAnnotations { it.subtype == PDAnnotationLink.SUB_TYPE }

0 commit comments

Comments
 (0)