const Path = require('node:path') const RequestParser = require('./RequestParser') const CompileManager = require('./CompileManager') const Settings = require('@overleaf/settings') const Metrics = require('./Metrics') const ProjectPersistenceManager = require('./ProjectPersistenceManager') const logger = require('@overleaf/logger') const Errors = require('./Errors') const { notifyCLSICacheAboutBuild } = require('./CLSICacheHandler') let lastSuccessfulCompileTimestamp = 0 function timeSinceLastSuccessfulCompile() { return Date.now() - lastSuccessfulCompileTimestamp } function compile(req, res, next) { const timer = new Metrics.Timer('compile-request') RequestParser.parse(req.body, function (error, request) { if (error) { return next(error) } timer.opts = request.metricsOpts request.project_id = req.params.project_id if (req.params.user_id != null) { request.user_id = req.params.user_id } ProjectPersistenceManager.markProjectAsJustAccessed( request.project_id, function (error) { if (error) { return next(error) } const stats = {} const timings = {} CompileManager.doCompileWithLock( request, stats, timings, (error, result) => { let { buildId, outputFiles } = result || {} let code, status if (outputFiles == null) { outputFiles = [] } if (error instanceof Errors.AlreadyCompilingError) { code = 423 // Http 423 Locked status = 'compile-in-progress' } else if (error instanceof Errors.FilesOutOfSyncError) { code = 409 // Http 409 Conflict status = 'retry' logger.warn( { projectId: request.project_id, userId: request.user_id, }, 'files out of sync, please retry' ) } else if ( error?.code === 'EPIPE' || error instanceof Errors.TooManyCompileRequestsError ) { // docker returns EPIPE when shutting down code = 503 // send 503 Unavailable response status = 'unavailable' } else if (error?.terminated) { status = 'terminated' } else if (error?.validate) { status = `validation-${error.validate}` } else if (error?.timedout) { status = 'timedout' logger.debug( { err: error, projectId: request.project_id }, 'timeout running compile' ) } else if (error) { status = 'error' code = 500 logger.error( { err: error, projectId: request.project_id }, 'error running compile' ) } else { if ( outputFiles.some( file => file.path === 'output.pdf' && file.size > 0 ) ) { status = 'success' lastSuccessfulCompileTimestamp = Date.now() } else if (request.stopOnFirstError) { status = 'stopped-on-first-error' } else { status = 'failure' logger.warn( { projectId: request.project_id, outputFiles }, 'project failed to compile successfully, no output.pdf generated' ) } // log an error if any core files are found if (outputFiles.some(file => file.path === 'core')) { logger.error( { projectId: request.project_id, req, outputFiles }, 'core file found in output' ) } } if (error) { outputFiles = error.outputFiles || [] buildId = error.buildId } if ( status === 'success' && request.editorId && request.populateClsiCache ) { notifyCLSICacheAboutBuild({ projectId: request.project_id, userId: request.user_id, buildId: outputFiles[0].build, editorId: request.editorId, outputFiles, compileGroup: request.compileGroup, options: { compiler: request.compiler, draft: request.draft, imageName: request.imageName ? Path.basename(request.imageName) : undefined, rootResourcePath: request.rootResourcePath, stopOnFirstError: request.stopOnFirstError, }, }) } timer.done() res.status(code || 200).send({ compile: { status, error: error?.message || error, stats, timings, buildId, outputUrlPrefix: Settings.apis.clsi.outputUrlPrefix, outputFiles: outputFiles.map(file => ({ url: `${Settings.apis.clsi.url}/project/${request.project_id}` + (request.user_id != null ? `/user/${request.user_id}` : '') + `/build/${file.build}/output/${file.path}`, ...file, })), }, }) } ) } ) }) } function stopCompile(req, res, next) { const { project_id: projectId, user_id: userId } = req.params CompileManager.stopCompile(projectId, userId, function (error) { if (error) { return next(error) } res.sendStatus(204) }) } function clearCache(req, res, next) { ProjectPersistenceManager.clearProject( req.params.project_id, req.params.user_id, function (error) { if (error) { return next(error) } // No content res.sendStatus(204) } ) } function syncFromCode(req, res, next) { const { file, editorId, buildId, compileFromClsiCache } = req.query const line = parseInt(req.query.line, 10) const column = parseInt(req.query.column, 10) const { imageName } = req.query const projectId = req.params.project_id const userId = req.params.user_id CompileManager.syncFromCode( projectId, userId, file, line, column, { imageName, editorId, buildId, compileFromClsiCache }, function (error, pdfPositions) { if (error) { return next(error) } res.json({ pdf: pdfPositions, }) } ) } function syncFromPdf(req, res, next) { const page = parseInt(req.query.page, 10) const h = parseFloat(req.query.h) const v = parseFloat(req.query.v) const { imageName, editorId, buildId, compileFromClsiCache } = req.query const projectId = req.params.project_id const userId = req.params.user_id CompileManager.syncFromPdf( projectId, userId, page, h, v, { imageName, editorId, buildId, compileFromClsiCache }, function (error, codePositions) { if (error) { return next(error) } res.json({ code: codePositions, }) } ) } function wordcount(req, res, next) { const file = req.query.file || 'main.tex' const projectId = req.params.project_id const userId = req.params.user_id const { image } = req.query logger.debug({ image, file, projectId }, 'word count request') CompileManager.wordcount( projectId, userId, file, image, function (error, result) { if (error) { return next(error) } res.json({ texcount: result, }) } ) } function status(req, res, next) { res.send('OK') } module.exports = { compile, stopCompile, clearCache, syncFromCode, syncFromPdf, wordcount, status, timeSinceLastSuccessfulCompile, }