| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795 |
- 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 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('metrics-sharelatex')
- const Errors = require('../Errors/Errors')
- const ClsiManager = {
- sendRequest(projectId, userId, options, callback) {
- if (options == null) {
- options = {}
- }
- ClsiManager.sendRequestOnce(projectId, userId, options, function(
- error,
- status,
- ...result
- ) {
- if (error != null) {
- return callback(error)
- }
- if (status === 'conflict') {
- options = _.clone(options)
- options.syncType = 'full' // force full compile
- ClsiManager.sendRequestOnce(projectId, userId, options, callback) // try again
- } else {
- callback(error, status, ...result)
- }
- })
- },
- sendRequestOnce(projectId, userId, options, callback) {
- if (options == null) {
- options = {}
- }
- ClsiManager._buildRequest(projectId, options, function(error, req) {
- if (error != null) {
- if (error.message === 'no main file specified') {
- return callback(null, 'validation-problems', null, null, {
- mainFile: error.message
- })
- } else {
- return callback(error)
- }
- }
- logger.log({ projectId }, 'sending compile to CLSI')
- ClsiManager._sendBuiltRequest(projectId, userId, req, options, function(
- error,
- status,
- ...result
- ) {
- if (error != null) {
- return callback(error)
- }
- callback(error, status, ...result)
- })
- })
- },
- // for public API requests where there is no project id
- sendExternalRequest(submissionId, clsiRequest, options, callback) {
- if (options == null) {
- options = {}
- }
- logger.log(
- { submissionId },
- 'sending external compile to CLSI',
- clsiRequest
- )
- ClsiManager._sendBuiltRequest(
- submissionId,
- null,
- clsiRequest,
- options,
- function(error, status, ...result) {
- if (error != null) {
- return callback(error)
- }
- callback(error, status, ...result)
- }
- )
- },
- stopCompile(projectId, userId, options, callback) {
- const compilerUrl = this._getCompilerUrl(
- options != null ? options.compileGroup : undefined,
- projectId,
- userId,
- 'compile/stop'
- )
- const opts = {
- url: compilerUrl,
- method: 'POST'
- }
- ClsiManager._makeRequest(projectId, opts, callback)
- },
- deleteAuxFiles(projectId, userId, options, callback) {
- const compilerUrl = this._getCompilerUrl(
- options != null ? options.compileGroup : undefined,
- projectId,
- userId
- )
- const opts = {
- url: compilerUrl,
- method: 'DELETE'
- }
- ClsiManager._makeRequest(projectId, opts, clsiError =>
- // always clear the project state from the docupdater, even if there
- // was a problem with the request to the clsi
- DocumentUpdaterHandler.clearProjectState(projectId, function(
- docUpdaterError
- ) {
- const error = clsiError || docUpdaterError
- if (error != null) {
- return callback(error)
- }
- callback()
- })
- )
- },
- _sendBuiltRequest(projectId, userId, req, options, callback) {
- if (options == null) {
- options = {}
- }
- ClsiFormatChecker.checkRecoursesForProblems(
- req.compile != null ? req.compile.resources : undefined,
- function(err, validationProblems) {
- if (err != null) {
- logger.warn(
- err,
- projectId,
- 'could not check resources for potential problems before sending to clsi'
- )
- return callback(err)
- }
- 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,
- function(error, response) {
- if (error != null) {
- logger.warn(
- { err: error, projectId },
- 'error sending request to clsi'
- )
- return callback(error)
- }
- if (response != null) {
- logger.log(
- {
- projectId,
- outputFilesLength:
- response.outputFiles && response.outputFiles.length,
- status: response.status,
- compile_status: response.compile && response.compile.status
- },
- 'received compile response from CLSI'
- )
- }
- ClsiCookieManager._getServerId(projectId, function(
- err,
- clsiServerId
- ) {
- if (err != null) {
- logger.warn({ err, projectId }, 'error getting server id')
- return callback(err)
- }
- 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, function(err, jar) {
- if (err != null) {
- logger.warn({ err }, 'error getting cookie jar for clsi request')
- return callback(err)
- }
- opts.jar = jar
- const timer = new Metrics.Timer('compile.currentBackend')
- request(opts, function(err, response, body) {
- timer.done()
- Metrics.inc(
- `compile.currentBackend.response.${
- response != null ? response.statusCode : undefined
- }`
- )
- if (err != null) {
- logger.warn(
- { err, projectId, url: opts != null ? opts.url : undefined },
- 'error making request to clsi'
- )
- return callback(err)
- }
- ClsiCookieManager.setServerId(projectId, response, function(err) {
- if (err != null) {
- logger.warn({ err, projectId }, 'error setting server id')
- }
- callback(err, response, body) // return as soon as the standard compile has returned
- cb(err, {
- response,
- body,
- finishTime: new Date() - startTime
- })
- })
- })
- })
- },
- newBackend(cb) {
- const startTime = new Date()
- ClsiManager._makeNewBackendRequest(projectId, opts, function(
- err,
- response,
- body
- ) {
- Metrics.inc(
- `compile.newBackend.response.${
- response != null ? response.statusCode : undefined
- }`
- )
- cb(err, {
- response,
- body,
- finishTime: new Date() - startTime
- })
- })
- }
- },
- function(err, results) {
- if (err != null) {
- logger.warn({ err }, 'Error making request to CLSI')
- return
- }
- const timeDifference =
- (results.newBackend != null
- ? results.newBackend.finishTime
- : undefined) -
- (results.currentBackend != null
- ? results.currentBackend.finishTime
- : undefined)
- const newStatusCode =
- results.newBackend &&
- results.newBackend.response &&
- results.newBackend.response.statusCode
- const currentStatusCode =
- results.currentBackend &&
- results.currentBackend.response &&
- results.currentBackend.response.statusCode
- const statusCodeSame = newStatusCode === currentStatusCode
- const currentCompileTime =
- results.currentBackend != null
- ? results.currentBackend.finishTime
- : undefined
- const newBackendCompileTime =
- results.newBackend != null ? results.newBackend.finishTime : undefined
- 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
- : undefined) == null
- ) {
- return callback()
- }
- const opts = _.clone(baseOpts)
- opts.url = opts.url.replace(
- Settings.apis.clsi.url,
- Settings.apis.clsi_new != null ? Settings.apis.clsi_new.url : undefined
- )
- NewBackendCloudClsiCookieManager.getCookieJar(projectId, function(
- err,
- jar
- ) {
- if (err != null) {
- logger.warn({ err }, 'error getting cookie jar for clsi request')
- return callback(err)
- }
- opts.jar = jar
- const timer = new Metrics.Timer('compile.newBackend')
- request(opts, function(err, response, body) {
- timer.done()
- if (err != null) {
- logger.warn(
- { err, projectId, url: opts != null ? opts.url : undefined },
- 'error making request to new clsi'
- )
- return callback(err)
- }
- NewBackendCloudClsiCookieManager.setServerId(
- projectId,
- response,
- function(err) {
- if (err != null) {
- logger.warn(
- { err, projectId },
- 'error setting server id new backend'
- )
- }
- callback(err, 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, function(error, response, body) {
- if (error != null) {
- return callback(error)
- }
- 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 {
- error = new Error(
- `CLSI returned non-success code: ${response.statusCode}`
- )
- logger.warn({ err: error, projectId }, 'CLSI returned failure code')
- callback(error, body)
- }
- })
- },
- _parseOutputFiles(projectId, rawOutputFiles) {
- if (rawOutputFiles == null) {
- 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
- },
- VALID_COMPILERS: ['pdflatex', 'latex', 'xelatex', 'lualatex'],
- _buildRequest(projectId, options, callback) {
- if (options == null) {
- options = {}
- }
- ProjectGetter.getProject(
- projectId,
- { compiler: 1, rootDoc_id: 1, imageName: 1, rootFolder: 1 },
- function(error, project) {
- let timer
- if (error != null) {
- return callback(error)
- }
- if (project == null) {
- return callback(
- new Errors.NotFoundError(`project does not exist: ${projectId}`)
- )
- }
- if (!ClsiManager.VALID_COMPILERS.includes(project.compiler)) {
- project.compiler = 'pdflatex'
- }
- if (options.incrementalCompilesEnabled || options.syncType != null) {
- // new way, either incremental or full
- timer = new Metrics.Timer('editor.compile-getdocs-redis')
- ClsiManager.getContentFromDocUpdaterIfMatch(
- projectId,
- project,
- options,
- function(error, projectStateHash, docUpdaterDocs) {
- timer.done()
- if (error != null) {
- logger.error(
- { err: error, 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
- } else {
- logger.log(
- {
- projectId,
- projectStateHash,
- docs: docUpdaterDocs != null
- },
- 'checked project state'
- )
- }
- // see if we can send an incremental update to the CLSI
- if (
- docUpdaterDocs != null &&
- options.syncType !== 'full' &&
- error == 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
- timer = new Metrics.Timer('editor.compile-getdocs-mongo')
- ClsiManager._getContentFromMongo(projectId, function(
- error,
- docs,
- files
- ) {
- timer.done()
- if (error != null) {
- return callback(error)
- }
- ClsiManager._finaliseRequest(
- projectId,
- options,
- project,
- docs,
- files,
- callback
- )
- })
- }
- }
- )
- },
- getContentFromDocUpdaterIfMatch(projectId, project, options, callback) {
- ClsiStateManager.computeHash(project, options, function(
- error,
- projectStateHash
- ) {
- if (error != null) {
- return callback(error)
- }
- DocumentUpdaterHandler.getProjectDocsIfMatch(
- projectId,
- projectStateHash,
- function(error, docs) {
- if (error != null) {
- return callback(error)
- }
- 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, function(err, jar) {
- if (err != null) {
- return callback(err)
- }
- 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, function(
- error,
- docPath
- ) {
- let path
- if (error != null) {
- return callback(error)
- }
- const docs = {}
- for (let doc of docUpdaterDocs || []) {
- 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) {
- 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, function(error, docs, files) {
- if (error != null) {
- return callback(error)
- }
- options = _.clone(options)
- options.syncType = 'full'
- options.syncState = projectStateHash
- ClsiManager._finaliseRequest(
- projectId,
- options,
- project,
- docs,
- files,
- callback
- )
- })
- },
- _getContentFromMongo(projectId, callback) {
- DocumentUpdaterHandler.flushProjectToMongo(projectId, function(error) {
- if (error != null) {
- return callback(error)
- }
- ProjectEntityHandler.getAllDocs(projectId, function(error, docs) {
- if (docs == null) {
- docs = {}
- }
- if (error != null) {
- return callback(error)
- }
- ProjectEntityHandler.getAllFiles(projectId, function(error, files) {
- if (files == null) {
- files = {}
- }
- if (error != null) {
- return callback(error)
- }
- callback(null, docs, files)
- })
- })
- })
- },
- _finaliseRequest(projectId, options, project, docs, files, callback) {
- let doc, path
- const resources = []
- let rootResourcePath = null
- let rootResourcePathOverride = null
- let hasMainFile = false
- let numberOfDocsInProject = 0
- for (path in docs) {
- 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) {
- logger.warn(
- { projectId },
- 'no root document found, setting to main.tex'
- )
- rootResourcePath = 'main.tex'
- } else if (numberOfDocsInProject === 1) {
- // only one file, must be the main document
- for (path in docs) {
- doc = docs[path]
- rootResourcePath = path.replace(/^\//, '')
- } // Remove leading /
- logger.warn(
- { projectId, rootResourcePath },
- 'no root document found, single document in project'
- )
- } else {
- return callback(new Error('no main file specified'))
- }
- }
- for (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
- },
- rootResourcePath,
- resources
- }
- })
- },
- wordCount(projectId, userId, file, options, callback) {
- ClsiManager._buildRequest(projectId, options, function(error, req) {
- if (error != null) {
- return callback(error)
- }
- const filename = file || req.compile.rootResourcePath
- const wordCountUrl = ClsiManager._getCompilerUrl(
- options != null ? options.compileGroup : undefined,
- projectId,
- userId,
- 'wordcount'
- )
- const opts = {
- url: wordCountUrl,
- qs: {
- file: filename,
- image: req.compile.options.imageName
- },
- method: 'GET'
- }
- ClsiManager._makeRequest(projectId, opts, function(
- error,
- response,
- body
- ) {
- if (error != null) {
- return callback(error)
- }
- if (response.statusCode >= 200 && response.statusCode < 300) {
- callback(null, body)
- } else {
- error = new Error(
- `CLSI returned non-success code: ${response.statusCode}`
- )
- logger.warn({ err: error, projectId }, 'CLSI returned failure code')
- callback(error, body)
- }
- })
- })
- }
- }
- module.exports = ClsiManager
|