Files
Verso/services/clsi/app/js/LocalCommandRunner.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

128 lines
3.5 KiB
JavaScript

/* eslint-disable
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import { spawn } from 'node:child_process'
import { promisify } from 'node:util'
import Path from 'node:path'
import _ from 'lodash'
import logger from '@overleaf/logger'
let CommandRunner
logger.debug('using standard command runner')
// Track PIDs that have been intentionally killed so that the close handler
// can detect termination even when the child exits with a numeric code instead
// of being reported as killed by a signal (e.g. exit code 4 from latexmk).
const killedPids = new Set()
export default CommandRunner = {
run(
projectId,
command,
directory,
image,
timeout,
environment,
compileGroup,
cwd,
callback
) {
let key, value
callback = _.once(callback)
const spawnCwd = cwd ? Path.join(directory, cwd) : directory
command = Array.from(command).map(arg =>
arg.toString().replace('$COMPILE_DIR', directory)
)
logger.debug(
{ projectId, command, directory, cwd: spawnCwd },
'running command'
)
logger.warn('timeouts and sandboxing are not enabled with CommandRunner')
// merge environment settings
const env = {}
for (key in process.env) {
value = process.env[key]
env[key] = value
}
for (key in environment) {
value = environment[key]
env[key] = value
}
// run command as detached process so it has its own process group (which can be killed if needed)
const proc = spawn(command[0], command.slice(1), {
cwd: spawnCwd,
env,
stdio: ['pipe', 'pipe', 'ignore'],
detached: true,
})
let stdout = ''
proc.stdout.setEncoding('utf8').on('data', data => (stdout += data))
proc.on('error', function (err) {
killedPids.delete(proc.pid)
logger.err(
{ err, projectId, command, directory },
'error running command'
)
return callback(err)
})
proc.on('close', function (code, signal) {
let err
logger.debug({ code, signal, projectId }, 'command exited')
const wasKilled = killedPids.delete(proc.pid)
if (signal === 'SIGTERM' || wasKilled) {
err = new Error('terminated')
err.terminated = true
return callback(err)
} else if (code === 1) {
// exit status from chktex (and any compiler that exits 1 on failure)
err = new Error('exited')
err.code = code
err.stdout = stdout // preserve captured output for callers
return callback(err)
} else {
return callback(null, { stdout, exitCode: code })
}
})
return proc.pid
}, // return process id to allow job to be killed if necessary
kill(pid, callback) {
if (callback == null) {
callback = function () {}
}
try {
killedPids.add(pid)
process.kill(-pid) // kill all processes in group
} catch (err) {
killedPids.delete(pid)
return callback(err)
}
return callback()
},
canRunSyncTeXInOutputDir() {
return true
},
}
CommandRunner.promises = {
run: promisify(CommandRunner.run),
kill: promisify(CommandRunner.kill),
}