Files
Verso/services/clsi/app/js/QuartoRunner.js
T
claude 09f5329a07
Build and Deploy Verso / deploy (push) Successful in 10m37s
Fix compile error propagation: 'failure' instead of HTTP 500
LocalCommandRunner: attach captured stdout to the error object when
  exit code is 1, so callers can read Quarto's output even on failure.

QuartoRunner: stop propagating plain 'exited' errors from Quarto up
  to CompileManager. A Quarto exit-code-1 is a compile failure, not a
  server error — CLSI already detects failure by the absence of
  output.pdf and returns status='failure' (HTTP 200). Previously it
  fell through to the generic error handler (HTTP 500), which caused
  the frontend to show "Server Error" instead of the log panel.
  Only true process-level errors (terminated, timedout) are propagated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 13:22:58 +00:00

106 lines
3.1 KiB
JavaScript

import Path from 'node:path'
import { promisify } from 'node:util'
import logger from '@overleaf/logger'
import CommandRunner from './CommandRunner.js'
import fs from 'node:fs'
// Maps currently-running Quarto jobs: compileName → PID (or docker container id)
const ProcessTable = {}
function runQuarto(compileName, options, callback) {
const { directory, mainFile, image, environment, compileGroup } = options
const timeout = options.timeout || 60000
logger.debug(
{ directory, timeout, mainFile, compileGroup },
'starting quarto compile'
)
const command = _buildQuartoCommand(mainFile)
ProcessTable[compileName] = CommandRunner.run(
compileName,
command,
directory,
image,
timeout,
environment || {},
compileGroup,
null,
function (error, output) {
delete ProcessTable[compileName]
// Propagate real process-level errors (killed, timed out) but NOT
// ordinary non-zero exit codes from Quarto itself. A Quarto compile
// failure (exit code 1) is not a server error — the absence of
// output.pdf is sufficient for CompileController to return 'failure'.
if (error && (error.terminated || error.timedout)) {
return callback(error)
}
// On exit-code-1 errors LocalCommandRunner attaches stdout to the
// error object; merge it so _writeLogOutput can persist it.
const combined = output || (error ? { stdout: error.stdout || '' } : null)
_writeLogOutput(compileName, directory, combined, () =>
callback(null, combined)
)
}
)
}
function _buildQuartoCommand(mainFile) {
// Run through a POSIX shell so stderr is merged into stdout (2>&1).
// Quarto writes all progress and error messages to stderr; without this
// the log panel would be empty on failure.
// LocalCommandRunner replaces $COMPILE_DIR before the shell sees the
// string, so no shell variable expansion occurs for that token.
const quartoArgs = [
'quarto',
'render',
`$COMPILE_DIR/${mainFile}`,
'--to',
'typst',
'--output',
'output.pdf',
].join(' ')
return ['/bin/sh', '-c', `${quartoArgs} 2>&1`]
}
function _writeLogOutput(compileName, directory, output, callback) {
const content = (output && output.stdout) || ''
if (!content) return callback()
// Write to output.log so the PDF-preview log panel picks it up
const logFile = Path.join(directory, 'output.log')
fs.unlink(logFile, () => {
fs.writeFile(logFile, content, { flag: 'wx' }, err => {
if (err) {
logger.error({ err, compileName, logFile }, 'error writing quarto log')
}
callback()
})
})
}
function isRunning(compileName) {
return ProcessTable[compileName] != null
}
function killQuarto(compileName, callback) {
logger.debug({ compileName }, 'killing running quarto compile')
if (!isRunning(compileName)) {
logger.warn({ compileName }, 'no such compile to kill')
return callback(null)
}
CommandRunner.kill(ProcessTable[compileName], callback)
}
export default {
isRunning,
runQuarto,
killQuarto,
promises: {
runQuarto: promisify(runQuarto),
killQuarto: promisify(killQuarto),
},
}