| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- import Path from 'node:path'
- import { promisify } from 'node:util'
- import Settings from '@overleaf/settings'
- import logger from '@overleaf/logger'
- import CommandRunner from './CommandRunner.js'
- import LatexMetrics from './LatexMetrics.js'
- import fs from 'node:fs'
- const { addLatexMkMetrics } = LatexMetrics
- const ProcessTable = {} // table of currently running jobs (pids or docker container names)
- const TIME_V_METRICS = [
- ['cpu-percent', /Percent of CPU this job got: (\d+)/m],
- ['cpu-time', /User time.*: (\d+.\d+)/m],
- ['sys-time', /System time.*: (\d+.\d+)/m],
- ]
- const COMPILER_FLAGS = {
- latex: '-pdfdvi',
- lualatex: '-lualatex',
- pdflatex: '-pdf',
- xelatex: '-xelatex',
- }
- function runLatex(projectId, options, callback) {
- const {
- directory,
- mainFile,
- image,
- environment,
- flags,
- compileGroup,
- stopOnFirstError,
- stats,
- timings,
- } = options
- const compiler = options.compiler || 'pdflatex'
- const timeout = options.timeout || 60000 // milliseconds
- logger.debug(
- {
- directory,
- compiler,
- timeout,
- mainFile,
- environment,
- flags,
- compileGroup,
- stopOnFirstError,
- },
- 'starting compile'
- )
- let command
- try {
- command = _buildLatexCommand(mainFile, {
- compiler,
- stopOnFirstError,
- flags,
- })
- } catch (err) {
- return callback(err)
- }
- const id = `${projectId}` // record running project under this id
- ProcessTable[id] = CommandRunner.run(
- projectId,
- command,
- directory,
- image,
- timeout,
- environment,
- compileGroup,
- null,
- function (error, output) {
- delete ProcessTable[id]
- if (error) {
- return callback(error)
- }
- if (stats.latexmk) {
- try {
- addLatexMkMetrics(output, stats)
- } catch (err) {
- logger.error({ err, projectId }, 'error adding latexmk metrics')
- }
- }
- // number of latex runs and whether there were errors
- const runs =
- output?.stdout?.match(/^Run number \d+ of .*latex/gm)?.length || // TeXLive 2022 and later
- output?.stderr?.match(/^Run number \d+ of .*latex/gm)?.length || // TeXLive 2021 and earlier
- 0
- const failed = output?.stdout?.match(/^Latexmk: Errors/m) != null ? 1 : 0
- // counters from latexmk output
- stats['latexmk-errors'] = failed
- stats['latex-runs'] = runs
- stats['latex-runs-with-errors'] = failed ? runs : 0
- stats[`latex-runs-${runs}`] = 1
- stats[`latex-runs-with-errors-${runs}`] = failed ? 1 : 0
- // timing information from /usr/bin/time
- const stderr = (output && output.stderr) || ''
- if (stderr.includes('Command being timed:')) {
- // Add metrics for runs with `$ time -v ...`
- for (const [timing, matcher] of TIME_V_METRICS) {
- const match = stderr.match(matcher)
- if (match) {
- timings[timing] = parseFloat(match[1])
- }
- }
- }
- // record output files
- _writeLogOutput(projectId, directory, output, () => {
- callback(error, output)
- })
- }
- )
- }
- function _writeLogOutput(projectId, directory, output, callback) {
- if (!output) {
- return callback()
- }
- // internal method for writing non-empty log files
- function _writeFile(file, content, cb) {
- if (content && content.length > 0) {
- fs.unlink(file, () => {
- fs.writeFile(file, content, { flag: 'wx' }, err => {
- if (err) {
- // don't fail on error
- logger.error({ err, projectId, file }, 'error writing log file')
- }
- cb()
- })
- })
- } else {
- cb()
- }
- }
- // write stdout and stderr, ignoring errors
- _writeFile(Path.join(directory, 'output.stdout'), output.stdout, () => {
- _writeFile(Path.join(directory, 'output.stderr'), output.stderr, () => {
- callback()
- })
- })
- }
- function isRunning(projectId) {
- const id = `${projectId}`
- return ProcessTable[id] != null
- }
- function killLatex(projectId, callback) {
- const id = `${projectId}`
- logger.debug({ id }, 'killing running compile')
- if (!isRunning(projectId)) {
- logger.warn({ id }, 'no such project to kill')
- callback(null)
- } else {
- CommandRunner.kill(ProcessTable[id], callback)
- }
- }
- function _buildLatexCommand(mainFile, opts = {}) {
- const command = []
- if (Settings.clsi?.strace) {
- command.push('strace', '-o', 'strace', '-ff')
- }
- if (Settings.clsi?.latexmkCommandPrefix) {
- command.push(...Settings.clsi.latexmkCommandPrefix)
- }
- // Basic command and flags
- command.push(
- 'latexmk',
- '-cd',
- '-jobname=output',
- '-auxdir=$COMPILE_DIR',
- '-outdir=$COMPILE_DIR',
- '-synctex=1',
- '-interaction=batchmode',
- '-time'
- )
- // Stop on first error option
- if (opts.stopOnFirstError) {
- command.push('-halt-on-error')
- } else {
- // Run all passes despite errors
- command.push('-f')
- }
- // Extra flags
- if (opts.flags) {
- command.push(...opts.flags)
- }
- // TeX Engine selection
- const compilerFlag = COMPILER_FLAGS[opts.compiler]
- if (compilerFlag) {
- command.push(compilerFlag)
- } else {
- throw new Error(`unknown compiler: ${opts.compiler}`)
- }
- // We want to run latexmk on the tex file which we will automatically
- // generate from the Rtex/Rmd/md file.
- mainFile = mainFile.replace(/\.(Rtex|md|Rmd|Rnw)$/, '.tex')
- command.push(Path.join('$COMPILE_DIR', mainFile))
- return command
- }
- export default {
- isRunning,
- runLatex,
- killLatex,
- promises: {
- runLatex: promisify(runLatex),
- killLatex: promisify(killLatex),
- },
- }
|