| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838 |
- const async = require('async')
- const Settings = require('settings-sharelatex')
- const request = require('request')
- const ProjectGetter = require('../Project/ProjectGetter')
- const ProjectEntityHandler = require('../Project/ProjectEntityHandler')
- const logger = require('logger-sharelatex')
- const Url = require('url')
- const OError = require('@overleaf/o-error')
- const ClsiCookieManager = require('./ClsiCookieManager')(
- Settings.apis.clsi != null ? Settings.apis.clsi.backendGroupName : undefined
- )
- const NewBackendCloudClsiCookieManager = require('./ClsiCookieManager')(
- Settings.apis.clsi_new != null
- ? Settings.apis.clsi_new.backendGroupName
- : undefined
- )
- const ClsiStateManager = require('./ClsiStateManager')
- const _ = require('underscore')
- const ClsiFormatChecker = require('./ClsiFormatChecker')
- const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
- const Metrics = require('@overleaf/metrics')
- const Errors = require('../Errors/Errors')
- const VALID_COMPILERS = ['pdflatex', 'latex', 'xelatex', 'lualatex']
- const ClsiManager = {
- sendRequest(projectId, userId, options, callback) {
- if (options == null) {
- options = {}
- }
- ClsiManager.sendRequestOnce(
- projectId,
- userId,
- options,
- (err, status, ...result) => {
- if (err != null) {
- return callback(err)
- }
- if (status === 'conflict') {
- // Try again, with a full compile
- return ClsiManager.sendRequestOnce(
- projectId,
- userId,
- { ...options, syncType: 'full' },
- callback
- )
- } else if (status === 'unavailable') {
- return ClsiManager.sendRequestOnce(
- projectId,
- userId,
- { ...options, syncType: 'full', forceNewClsiServer: true },
- callback
- )
- }
- callback(null, status, ...result)
- }
- )
- },
- sendRequestOnce(projectId, userId, options, callback) {
- if (options == null) {
- options = {}
- }
- ClsiManager._buildRequest(projectId, options, (err, req) => {
- if (err != null) {
- if (err.message === 'no main file specified') {
- return callback(null, 'validation-problems', null, null, {
- mainFile: err.message
- })
- } else {
- return callback(
- OError.tag(err, 'Could not build request to CLSI', {
- projectId,
- options
- })
- )
- }
- }
- ClsiManager._sendBuiltRequest(
- projectId,
- userId,
- req,
- options,
- (err, status, ...result) => {
- if (err != null) {
- return callback(
- OError.tag(err, 'CLSI compile failed', { projectId, userId })
- )
- }
- callback(null, status, ...result)
- }
- )
- })
- },
- // for public API requests where there is no project id
- sendExternalRequest(submissionId, clsiRequest, options, callback) {
- if (options == null) {
- options = {}
- }
- ClsiManager._sendBuiltRequest(
- submissionId,
- null,
- clsiRequest,
- options,
- (err, status, ...result) => {
- if (err != null) {
- return callback(
- OError.tag(err, 'CLSI compile failed', {
- submissionId,
- clsiRequest,
- options
- })
- )
- }
- callback(null, status, ...result)
- }
- )
- },
- stopCompile(projectId, userId, options, callback) {
- if (options == null) {
- options = {}
- }
- const compilerUrl = this._getCompilerUrl(
- options.compileGroup,
- projectId,
- userId,
- 'compile/stop'
- )
- const opts = {
- url: compilerUrl,
- method: 'POST'
- }
- ClsiManager._makeRequest(projectId, opts, callback)
- },
- deleteAuxFiles(projectId, userId, options, callback) {
- if (options == null) {
- options = {}
- }
- const compilerUrl = this._getCompilerUrl(
- options.compileGroup,
- projectId,
- userId
- )
- const opts = {
- url: compilerUrl,
- method: 'DELETE'
- }
- ClsiManager._makeRequest(projectId, opts, clsiErr => {
- // always clear the project state from the docupdater, even if there
- // was a problem with the request to the clsi
- DocumentUpdaterHandler.clearProjectState(projectId, docUpdaterErr => {
- if (clsiErr != null) {
- return callback(
- OError.tag(clsiErr, 'Failed to delete aux files', { projectId })
- )
- }
- if (docUpdaterErr != null) {
- return callback(
- OError.tag(
- docUpdaterErr,
- 'Failed to clear project state in doc updater',
- { projectId }
- )
- )
- }
- callback()
- })
- })
- },
- _sendBuiltRequest(projectId, userId, req, options, callback) {
- if (options == null) {
- options = {}
- }
- if (options.forceNewClsiServer) {
- // Clear clsi cookie, then try again
- return ClsiCookieManager.clearServerId(projectId, err => {
- if (err) {
- return callback(err)
- }
- options.forceNewClsiServer = false // backend has now been reset
- return ClsiManager._sendBuiltRequest(
- projectId,
- userId,
- req,
- options,
- callback
- )
- })
- }
- ClsiFormatChecker.checkRecoursesForProblems(
- req.compile != null ? req.compile.resources : undefined,
- (err, validationProblems) => {
- if (err != null) {
- return callback(
- OError.tag(
- err,
- 'could not check resources for potential problems before sending to clsi'
- )
- )
- }
- if (validationProblems != null) {
- logger.log(
- { projectId, validationProblems },
- 'problems with users latex before compile was attempted'
- )
- return callback(
- null,
- 'validation-problems',
- null,
- null,
- validationProblems
- )
- }
- ClsiManager._postToClsi(
- projectId,
- userId,
- req,
- options.compileGroup,
- (err, response) => {
- if (err != null) {
- return callback(
- OError.tag(err, 'error sending request to clsi', {
- projectId,
- userId
- })
- )
- }
- ClsiCookieManager._getServerId(projectId, (err, clsiServerId) => {
- if (err != null) {
- return callback(
- OError.tag(err, 'error getting server id', { projectId })
- )
- }
- const outputFiles = ClsiManager._parseOutputFiles(
- projectId,
- response && response.compile && response.compile.outputFiles
- )
- callback(
- null,
- response && response.compile && response.compile.status,
- outputFiles,
- clsiServerId
- )
- })
- }
- )
- }
- )
- },
- _makeRequest(projectId, opts, callback) {
- async.series(
- {
- currentBackend(cb) {
- const startTime = new Date()
- ClsiCookieManager.getCookieJar(projectId, (err, jar) => {
- if (err != null) {
- return callback(
- OError.tag(err, 'error getting cookie jar for CLSI request', {
- projectId
- })
- )
- }
- opts.jar = jar
- const timer = new Metrics.Timer('compile.currentBackend')
- request(opts, (err, response, body) => {
- if (err != null) {
- return callback(
- OError.tag(err, 'error making request to CLSI', { projectId })
- )
- }
- timer.done()
- Metrics.inc(
- `compile.currentBackend.response.${response.statusCode}`
- )
- ClsiCookieManager.setServerId(projectId, response, err => {
- if (err != null) {
- callback(
- OError.tag(err, 'error setting server id', { projectId })
- )
- } else {
- // return as soon as the standard compile has returned
- callback(null, response, body)
- }
- cb(err, {
- response,
- body,
- finishTime: new Date() - startTime
- })
- })
- })
- })
- },
- newBackend(cb) {
- const startTime = new Date()
- ClsiManager._makeNewBackendRequest(
- projectId,
- opts,
- (err, response, body) => {
- if (err != null) {
- logger.warn({ err }, 'Error making request to new CLSI backend')
- }
- if (response != null) {
- Metrics.inc(
- `compile.newBackend.response.${response.statusCode}`
- )
- }
- cb(err, {
- response,
- body,
- finishTime: new Date() - startTime
- })
- }
- )
- }
- },
- (err, results) => {
- if (err != null) {
- // This was handled higher up
- return
- }
- if (results.newBackend != null && results.newBackend.response != null) {
- const currentStatusCode = results.currentBackend.response.statusCode
- const newStatusCode = results.newBackend.response.statusCode
- const statusCodeSame = newStatusCode === currentStatusCode
- const currentCompileTime = results.currentBackend.finishTime
- const newBackendCompileTime = results.newBackend.finishTime || 0
- const timeDifference = newBackendCompileTime - currentCompileTime
- logger.log(
- {
- statusCodeSame,
- timeDifference,
- currentCompileTime,
- newBackendCompileTime,
- projectId
- },
- 'both clsi requests returned'
- )
- }
- }
- )
- },
- _makeNewBackendRequest(projectId, baseOpts, callback) {
- if (Settings.apis.clsi_new == null || Settings.apis.clsi_new.url == null) {
- return callback()
- }
- const opts = {
- ...baseOpts,
- url: baseOpts.url.replace(
- Settings.apis.clsi.url,
- Settings.apis.clsi_new.url
- )
- }
- NewBackendCloudClsiCookieManager.getCookieJar(projectId, (err, jar) => {
- if (err != null) {
- return callback(
- OError.tag(err, 'error getting cookie jar for CLSI request', {
- projectId
- })
- )
- }
- opts.jar = jar
- const timer = new Metrics.Timer('compile.newBackend')
- request(opts, (err, response, body) => {
- timer.done()
- if (err != null) {
- return callback(
- OError.tag(err, 'error making request to new CLSI', {
- projectId,
- opts
- })
- )
- }
- NewBackendCloudClsiCookieManager.setServerId(
- projectId,
- response,
- err => {
- if (err != null) {
- return callback(
- OError.tag(err, 'error setting server id on new backend', {
- projectId
- })
- )
- }
- callback(null, response, body)
- }
- )
- })
- })
- },
- _getCompilerUrl(compileGroup, projectId, userId, action) {
- const host = Settings.apis.clsi.url
- let path = `/project/${projectId}`
- if (userId != null) {
- path += `/user/${userId}`
- }
- if (action != null) {
- path += `/${action}`
- }
- return `${host}${path}`
- },
- _postToClsi(projectId, userId, req, compileGroup, callback) {
- const compileUrl = this._getCompilerUrl(
- compileGroup,
- projectId,
- userId,
- 'compile'
- )
- const opts = {
- url: compileUrl,
- json: req,
- method: 'POST'
- }
- ClsiManager._makeRequest(projectId, opts, (err, response, body) => {
- if (err != null) {
- return callback(
- new OError('failed to make request to CLSI', {
- projectId,
- userId,
- compileOptions: req.compile.options,
- rootResourcePath: req.compile.rootResourcePath
- })
- )
- }
- if (response.statusCode >= 200 && response.statusCode < 300) {
- callback(null, body)
- } else if (response.statusCode === 413) {
- callback(null, { compile: { status: 'project-too-large' } })
- } else if (response.statusCode === 409) {
- callback(null, { compile: { status: 'conflict' } })
- } else if (response.statusCode === 423) {
- callback(null, { compile: { status: 'compile-in-progress' } })
- } else if (response.statusCode === 503) {
- callback(null, { compile: { status: 'unavailable' } })
- } else {
- callback(
- new OError(`CLSI returned non-success code: ${response.statusCode}`, {
- projectId,
- userId,
- compileOptions: req.compile.options,
- rootResourcePath: req.compile.rootResourcePath,
- clsiResponse: body,
- statusCode: response.statusCode
- })
- )
- }
- })
- },
- _parseOutputFiles(projectId, rawOutputFiles = []) {
- const outputFiles = []
- for (const file of rawOutputFiles) {
- outputFiles.push({
- path: file.path, // the clsi is now sending this to web
- url: Url.parse(file.url).path, // the location of the file on the clsi, excluding the host part
- type: file.type,
- build: file.build
- })
- }
- return outputFiles
- },
- _buildRequest(projectId, options, callback) {
- if (options == null) {
- options = {}
- }
- ProjectGetter.getProject(
- projectId,
- { compiler: 1, rootDoc_id: 1, imageName: 1, rootFolder: 1 },
- (err, project) => {
- if (err != null) {
- return callback(
- OError.tag(err, 'failed to get project', { projectId })
- )
- }
- if (project == null) {
- return callback(
- new Errors.NotFoundError(`project does not exist: ${projectId}`)
- )
- }
- if (!VALID_COMPILERS.includes(project.compiler)) {
- project.compiler = 'pdflatex'
- }
- if (options.incrementalCompilesEnabled || options.syncType != null) {
- // new way, either incremental or full
- const timer = new Metrics.Timer('editor.compile-getdocs-redis')
- ClsiManager.getContentFromDocUpdaterIfMatch(
- projectId,
- project,
- options,
- (err, projectStateHash, docUpdaterDocs) => {
- timer.done()
- if (err != null) {
- logger.error({ err, projectId }, 'error checking project state')
- // note: we don't bail out when there's an error getting
- // incremental files from the docupdater, we just fall back
- // to a normal compile below
- }
- // see if we can send an incremental update to the CLSI
- if (
- docUpdaterDocs != null &&
- options.syncType !== 'full' &&
- err == null
- ) {
- Metrics.inc('compile-from-redis')
- ClsiManager._buildRequestFromDocupdater(
- projectId,
- options,
- project,
- projectStateHash,
- docUpdaterDocs,
- callback
- )
- } else {
- Metrics.inc('compile-from-mongo')
- ClsiManager._buildRequestFromMongo(
- projectId,
- options,
- project,
- projectStateHash,
- callback
- )
- }
- }
- )
- } else {
- // old way, always from mongo
- const timer = new Metrics.Timer('editor.compile-getdocs-mongo')
- ClsiManager._getContentFromMongo(projectId, (err, docs, files) => {
- timer.done()
- if (err != null) {
- return callback(
- OError.tag(err, 'failed to get contents from Mongo', {
- projectId
- })
- )
- }
- ClsiManager._finaliseRequest(
- projectId,
- options,
- project,
- docs,
- files,
- callback
- )
- })
- }
- }
- )
- },
- getContentFromDocUpdaterIfMatch(projectId, project, options, callback) {
- ClsiStateManager.computeHash(project, options, (err, projectStateHash) => {
- if (err != null) {
- return callback(
- OError.tag(err, 'Failed to compute project state hash', { projectId })
- )
- }
- DocumentUpdaterHandler.getProjectDocsIfMatch(
- projectId,
- projectStateHash,
- (err, docs) => {
- if (err != null) {
- return callback(
- OError.tag(err, 'Failed to get project documents', {
- projectId,
- projectStateHash
- })
- )
- }
- callback(null, projectStateHash, docs)
- }
- )
- })
- },
- getOutputFileStream(projectId, userId, buildId, outputFilePath, callback) {
- const url = `${Settings.apis.clsi.url}/project/${projectId}/user/${userId}/build/${buildId}/output/${outputFilePath}`
- ClsiCookieManager.getCookieJar(projectId, (err, jar) => {
- if (err != null) {
- return callback(
- OError.tag(err, 'Failed to get cookie jar', {
- projectId,
- userId,
- buildId,
- outputFilePath
- })
- )
- }
- const options = { url, method: 'GET', timeout: 60 * 1000, jar }
- const readStream = request(options)
- callback(null, readStream)
- })
- },
- _buildRequestFromDocupdater(
- projectId,
- options,
- project,
- projectStateHash,
- docUpdaterDocs,
- callback
- ) {
- ProjectEntityHandler.getAllDocPathsFromProject(project, (err, docPath) => {
- if (err != null) {
- return callback(
- OError.tag(err, 'Failed to get doc paths', { projectId })
- )
- }
- const docs = {}
- for (let doc of docUpdaterDocs || []) {
- const path = docPath[doc._id]
- docs[path] = doc
- }
- // send new docs but not files as those are already on the clsi
- options = _.clone(options)
- options.syncType = 'incremental'
- options.syncState = projectStateHash
- // create stub doc entries for any possible root docs, if not
- // present in the docupdater. This allows finaliseRequest to
- // identify the root doc.
- const possibleRootDocIds = [options.rootDoc_id, project.rootDoc_id]
- for (const rootDocId of possibleRootDocIds) {
- if (rootDocId != null && rootDocId in docPath) {
- const path = docPath[rootDocId]
- if (docs[path] == null) {
- docs[path] = { _id: rootDocId, path }
- }
- }
- }
- ClsiManager._finaliseRequest(
- projectId,
- options,
- project,
- docs,
- [],
- callback
- )
- })
- },
- _buildRequestFromMongo(
- projectId,
- options,
- project,
- projectStateHash,
- callback
- ) {
- ClsiManager._getContentFromMongo(projectId, (err, docs, files) => {
- if (err != null) {
- return callback(
- OError.tag(err, 'failed to get project contents from Mongo', {
- projectId
- })
- )
- }
- options = {
- ...options,
- syncType: 'full',
- syncState: projectStateHash
- }
- ClsiManager._finaliseRequest(
- projectId,
- options,
- project,
- docs,
- files,
- callback
- )
- })
- },
- _getContentFromMongo(projectId, callback) {
- DocumentUpdaterHandler.flushProjectToMongo(projectId, err => {
- if (err != null) {
- return callback(
- OError.tag(err, 'failed to flush project to Mongo', { projectId })
- )
- }
- ProjectEntityHandler.getAllDocs(projectId, (err, docs) => {
- if (err != null) {
- return callback(
- OError.tag(err, 'failed to get project docs', { projectId })
- )
- }
- ProjectEntityHandler.getAllFiles(projectId, (err, files) => {
- if (err != null) {
- return callback(
- OError.tag(err, 'failed to get project files', { projectId })
- )
- }
- if (files == null) {
- files = {}
- }
- callback(null, docs || {}, files || {})
- })
- })
- })
- },
- _finaliseRequest(projectId, options, project, docs, files, callback) {
- const resources = []
- let rootResourcePath = null
- let rootResourcePathOverride = null
- let hasMainFile = false
- let numberOfDocsInProject = 0
- for (let path in docs) {
- const doc = docs[path]
- path = path.replace(/^\//, '') // Remove leading /
- numberOfDocsInProject++
- if (doc.lines != null) {
- // add doc to resources unless it is just a stub entry
- resources.push({
- path,
- content: doc.lines.join('\n')
- })
- }
- if (
- project.rootDoc_id != null &&
- doc._id.toString() === project.rootDoc_id.toString()
- ) {
- rootResourcePath = path
- }
- if (
- options.rootDoc_id != null &&
- doc._id.toString() === options.rootDoc_id.toString()
- ) {
- rootResourcePathOverride = path
- }
- if (path === 'main.tex') {
- hasMainFile = true
- }
- }
- if (rootResourcePathOverride != null) {
- rootResourcePath = rootResourcePathOverride
- }
- if (rootResourcePath == null) {
- if (hasMainFile) {
- rootResourcePath = 'main.tex'
- } else if (numberOfDocsInProject === 1) {
- // only one file, must be the main document
- for (const path in docs) {
- // Remove leading /
- rootResourcePath = path.replace(/^\//, '')
- }
- } else {
- return callback(new OError('no main file specified', { projectId }))
- }
- }
- for (let path in files) {
- const file = files[path]
- path = path.replace(/^\//, '') // Remove leading /
- resources.push({
- path,
- url: `${Settings.apis.filestore.url}/project/${project._id}/file/${file._id}`,
- modified: file.created != null ? file.created.getTime() : undefined
- })
- }
- callback(null, {
- compile: {
- options: {
- compiler: project.compiler,
- timeout: options.timeout,
- imageName: project.imageName,
- draft: !!options.draft,
- check: options.check,
- syncType: options.syncType,
- syncState: options.syncState,
- compileGroup: options.compileGroup
- },
- rootResourcePath,
- resources
- }
- })
- },
- wordCount(projectId, userId, file, options, callback) {
- ClsiManager._buildRequest(projectId, options, (err, req) => {
- if (err != null) {
- return callback(
- OError.tag(err, 'Failed to build CLSI request', {
- projectId,
- options
- })
- )
- }
- const filename = file || req.compile.rootResourcePath
- const wordCountUrl = ClsiManager._getCompilerUrl(
- options.compileGroup,
- projectId,
- userId,
- 'wordcount'
- )
- const opts = {
- url: wordCountUrl,
- qs: {
- file: filename,
- image: req.compile.options.imageName
- },
- method: 'GET'
- }
- ClsiManager._makeRequest(projectId, opts, (err, response, body) => {
- if (err != null) {
- return callback(OError.tag(err, 'CLSI request failed', { projectId }))
- }
- if (response.statusCode >= 200 && response.statusCode < 300) {
- callback(null, body)
- } else {
- callback(
- new OError(
- `CLSI returned non-success code: ${response.statusCode}`,
- {
- projectId,
- clsiResponse: body,
- statusCode: response.statusCode
- }
- )
- )
- }
- })
- })
- }
- }
- module.exports = ClsiManager
|