diff --git a/services/clsi/app/js/QuartoRunner.js b/services/clsi/app/js/QuartoRunner.js index 4ba220594e..de9b74c9c2 100644 --- a/services/clsi/app/js/QuartoRunner.js +++ b/services/clsi/app/js/QuartoRunner.js @@ -82,9 +82,18 @@ function _buildQuartoCommand(mainFile, exportMode) { // deck compiles and is fully portable/offline. // - 'pdf-slides': render the deck, then print it to a faithful slide PDF // with decktape (headless Chromium), one slide per page. + // NB: Quarto's --metadata/-M flag uses COLON syntax (KEY:VALUE), not "=". + // Using "=" makes Quarto register a bogus key literally named + // "embed-resources=true" and silently leave the real option unset, so the + // deck stays non-self-contained (references a sibling "_files/" dir). + // - embed-resources: inline CSS/JS/images into the single .html + // - self-contained-math: also inline MathJax (not embedded by default), + // so equations render in a fully offline/portable file + // - chalkboard: must be off — it is incompatible with embed-resources and + // errors the render if left enabled const renderFlags = exportMode === 'html-standalone' - ? ' -M embed-resources=true -M chalkboard=false' + ? ' -M embed-resources:true -M self-contained-math:true -M chalkboard:false' : '' let tail = @@ -94,8 +103,11 @@ function _buildQuartoCommand(mainFile, exportMode) { if (exportMode === 'pdf-slides') { // After producing output.html, print it to output-slides.pdf. --no-sandbox // is required for Chromium running as a non-root user inside the container. + // --no-sandbox: Chromium can't sandbox as a non-root container user. + // --disable-dev-shm-usage: containers usually have a tiny /dev/shm, which + // makes Chromium crash mid-render; this routes its shared memory to /tmp. tail += - ` && decktape --chrome-arg=--no-sandbox "$(pwd)/output.html" output-slides.pdf 2>&1` + ` && decktape --chrome-arg=--no-sandbox --chrome-arg=--disable-dev-shm-usage "$(pwd)/output.html" output-slides.pdf 2>&1` } const cmd = diff --git a/services/web/app/src/Features/Compile/PresentationExportController.mjs b/services/web/app/src/Features/Compile/PresentationExportController.mjs index d13aee8b83..312872184f 100644 --- a/services/web/app/src/Features/Compile/PresentationExportController.mjs +++ b/services/web/app/src/Features/Compile/PresentationExportController.mjs @@ -27,6 +27,29 @@ const FORMATS = { }, } +// Best-effort fetch of the export compile's output.log so export failures can +// show the underlying error (e.g. decktape/Chromium output). Never throws. +async function _fetchLog(projectId, userId, clsiServerId, buildId) { + if (!buildId) return '' + try { + const compileAsUser = Settings.disablePerUserCompiles ? undefined : userId + const stream = await ClsiManager.promises.getOutputFileStream( + projectId, + compileAsUser, + clsiServerId, + buildId, + 'output.log' + ) + const chunks = [] + for await (const chunk of stream) chunks.push(chunk) + const text = Buffer.concat(chunks).toString('utf8') + // Keep it bounded — the tail holds the relevant error. + return text.length > 8000 ? text.slice(-8000) : text + } catch { + return '' + } +} + async function exportPresentation(req, res) { const projectId = req.params.Project_id const userId = SessionManager.getLoggedInUserId(req.session) @@ -41,12 +64,18 @@ async function exportPresentation(req, res) { }) if (!buildId || !outputFiles?.some(f => f.path === format.file)) { - return res - .status(400) - .send( - `Export failed: the project did not produce ${format.file} (compile status: ${status}). ` + - `This export is only available for RevealJS presentations.` - ) + // The expected artefact wasn't produced. Surface the compile log (which + // for the PDF path includes decktape/Chromium's own stderr) as plain + // text so the failure is diagnosable, instead of returning an HTML page + // the browser saves as "pdf.htm". + const log = await _fetchLog(projectId, userId, clsiServerId, buildId) + res.status(400).type('text/plain') + return res.send( + `Export failed: the project did not produce ${format.file} ` + + `(compile status: ${status}). This export is only available for ` + + `RevealJS presentations.\n\n` + + (log ? `--- compile log ---\n${log}` : '(no compile log available)') + ) } const compileAsUser = Settings.disablePerUserCompiles ? undefined : userId