2019-08-12 07:33:01 -04:00
|
|
|
const async = require('async')
|
2019-05-29 05:21:06 -04:00
|
|
|
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')
|
2019-08-13 08:36:10 -04:00
|
|
|
const OError = require('@overleaf/o-error')
|
|
|
|
|
2019-05-29 05:21:06 -04:00
|
|
|
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')
|
2020-10-30 04:10:50 -04:00
|
|
|
const Metrics = require('@overleaf/metrics')
|
2019-05-29 05:21:06 -04:00
|
|
|
const Errors = require('../Errors/Errors')
|
|
|
|
|
2019-08-13 08:36:10 -04:00
|
|
|
const VALID_COMPILERS = ['pdflatex', 'latex', 'xelatex', 'lualatex']
|
|
|
|
|
2019-08-12 07:33:01 -04:00
|
|
|
const ClsiManager = {
|
|
|
|
sendRequest(projectId, userId, options, callback) {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (options == null) {
|
|
|
|
options = {}
|
|
|
|
}
|
2019-08-13 08:36:10 -04:00
|
|
|
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
|
|
|
|
)
|
2020-06-12 04:27:19 -04:00
|
|
|
} else if (status === 'unavailable') {
|
|
|
|
return ClsiManager.sendRequestOnce(
|
|
|
|
projectId,
|
|
|
|
userId,
|
|
|
|
{ ...options, syncType: 'full', forceNewClsiServer: true },
|
|
|
|
callback
|
|
|
|
)
|
2019-08-13 08:36:10 -04:00
|
|
|
}
|
|
|
|
callback(null, status, ...result)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
|
2019-08-12 07:33:01 -04:00
|
|
|
sendRequestOnce(projectId, userId, options, callback) {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (options == null) {
|
|
|
|
options = {}
|
|
|
|
}
|
2019-08-13 08:36:10 -04:00
|
|
|
ClsiManager._buildRequest(projectId, options, (err, req) => {
|
|
|
|
if (err != null) {
|
|
|
|
if (err.message === 'no main file specified') {
|
2019-05-29 05:21:06 -04:00
|
|
|
return callback(null, 'validation-problems', null, null, {
|
2019-08-13 08:36:10 -04:00
|
|
|
mainFile: err.message
|
2019-05-29 05:21:06 -04:00
|
|
|
})
|
|
|
|
} else {
|
2019-08-13 08:36:10 -04:00
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'Could not build request to CLSI', {
|
|
|
|
projectId,
|
|
|
|
options
|
|
|
|
})
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
}
|
2019-08-13 08:36:10 -04:00
|
|
|
ClsiManager._sendBuiltRequest(
|
|
|
|
projectId,
|
|
|
|
userId,
|
|
|
|
req,
|
|
|
|
options,
|
|
|
|
(err, status, ...result) => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'CLSI compile failed', { projectId, userId })
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
|
|
|
}
|
|
|
|
callback(null, status, ...result)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
// for public API requests where there is no project id
|
2019-08-12 07:33:01 -04:00
|
|
|
sendExternalRequest(submissionId, clsiRequest, options, callback) {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (options == null) {
|
|
|
|
options = {}
|
|
|
|
}
|
2019-08-12 07:33:01 -04:00
|
|
|
ClsiManager._sendBuiltRequest(
|
|
|
|
submissionId,
|
2019-05-29 05:21:06 -04:00
|
|
|
null,
|
2019-08-12 07:33:01 -04:00
|
|
|
clsiRequest,
|
2019-05-29 05:21:06 -04:00
|
|
|
options,
|
2019-08-13 08:36:10 -04:00
|
|
|
(err, status, ...result) => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'CLSI compile failed', {
|
|
|
|
submissionId,
|
|
|
|
clsiRequest,
|
|
|
|
options
|
|
|
|
})
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-08-13 08:36:10 -04:00
|
|
|
callback(null, status, ...result)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
2019-08-12 07:33:01 -04:00
|
|
|
stopCompile(projectId, userId, options, callback) {
|
2019-08-13 08:36:10 -04:00
|
|
|
if (options == null) {
|
|
|
|
options = {}
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
const compilerUrl = this._getCompilerUrl(
|
2019-08-13 08:36:10 -04:00
|
|
|
options.compileGroup,
|
2019-08-12 07:33:01 -04:00
|
|
|
projectId,
|
|
|
|
userId,
|
2019-05-29 05:21:06 -04:00
|
|
|
'compile/stop'
|
|
|
|
)
|
|
|
|
const opts = {
|
|
|
|
url: compilerUrl,
|
|
|
|
method: 'POST'
|
|
|
|
}
|
2019-08-12 07:33:01 -04:00
|
|
|
ClsiManager._makeRequest(projectId, opts, callback)
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
|
2021-01-21 07:20:55 -05:00
|
|
|
deleteAuxFiles(projectId, userId, options, clsiserverid, callback) {
|
2019-08-13 08:36:10 -04:00
|
|
|
if (options == null) {
|
|
|
|
options = {}
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
const compilerUrl = this._getCompilerUrl(
|
2019-08-13 08:36:10 -04:00
|
|
|
options.compileGroup,
|
2019-08-12 07:33:01 -04:00
|
|
|
projectId,
|
|
|
|
userId
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
|
|
|
const opts = {
|
|
|
|
url: compilerUrl,
|
|
|
|
method: 'DELETE'
|
|
|
|
}
|
2021-01-21 07:20:55 -05:00
|
|
|
ClsiManager._makeRequestWithClsiServerId(
|
|
|
|
projectId,
|
|
|
|
opts,
|
|
|
|
clsiserverid,
|
|
|
|
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 })
|
2021-01-07 08:54:37 -05:00
|
|
|
)
|
2021-01-21 07:20:55 -05:00
|
|
|
}
|
|
|
|
if (docUpdaterErr != null) {
|
|
|
|
return callback(
|
|
|
|
OError.tag(
|
|
|
|
docUpdaterErr,
|
|
|
|
'Failed to clear project state in doc updater',
|
|
|
|
{ projectId }
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
callback()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
|
2019-08-12 07:33:01 -04:00
|
|
|
_sendBuiltRequest(projectId, userId, req, options, callback) {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (options == null) {
|
|
|
|
options = {}
|
|
|
|
}
|
2020-06-12 04:27:19 -04:00
|
|
|
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
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
2019-08-12 07:33:01 -04:00
|
|
|
ClsiFormatChecker.checkRecoursesForProblems(
|
2019-05-29 05:21:06 -04:00
|
|
|
req.compile != null ? req.compile.resources : undefined,
|
2019-08-13 08:36:10 -04:00
|
|
|
(err, validationProblems) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (err != null) {
|
2019-08-13 08:36:10 -04:00
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(
|
|
|
|
err,
|
|
|
|
'could not check resources for potential problems before sending to clsi'
|
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
|
|
|
}
|
|
|
|
if (validationProblems != null) {
|
|
|
|
logger.log(
|
2019-08-12 07:33:01 -04:00
|
|
|
{ projectId, validationProblems },
|
2019-05-29 05:21:06 -04:00
|
|
|
'problems with users latex before compile was attempted'
|
|
|
|
)
|
|
|
|
return callback(
|
|
|
|
null,
|
|
|
|
'validation-problems',
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
validationProblems
|
|
|
|
)
|
|
|
|
}
|
2019-08-12 07:33:01 -04:00
|
|
|
ClsiManager._postToClsi(
|
|
|
|
projectId,
|
|
|
|
userId,
|
2019-05-29 05:21:06 -04:00
|
|
|
req,
|
|
|
|
options.compileGroup,
|
2019-08-13 08:36:10 -04:00
|
|
|
(err, response) => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'error sending request to clsi', {
|
|
|
|
projectId,
|
|
|
|
userId
|
|
|
|
})
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
|
|
|
}
|
2019-08-13 08:36:10 -04:00
|
|
|
ClsiCookieManager._getServerId(projectId, (err, clsiServerId) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (err != null) {
|
2019-08-13 08:36:10 -04:00
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'error getting server id', { projectId })
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
const outputFiles = ClsiManager._parseOutputFiles(
|
2019-08-12 07:33:01 -04:00
|
|
|
projectId,
|
|
|
|
response && response.compile && response.compile.outputFiles
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
2019-08-12 07:33:01 -04:00
|
|
|
callback(
|
2019-05-29 05:21:06 -04:00
|
|
|
null,
|
2019-08-12 07:33:01 -04:00
|
|
|
response && response.compile && response.compile.status,
|
2019-05-29 05:21:06 -04:00
|
|
|
outputFiles,
|
|
|
|
clsiServerId
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
2021-01-21 07:20:55 -05:00
|
|
|
_makeRequestWithClsiServerId(projectId, opts, clsiserverid, callback) {
|
|
|
|
if (clsiserverid) {
|
|
|
|
// ignore cookies and newBackend, go straight to the clsi node
|
|
|
|
opts.qs = Object.assign({ clsiserverid }, opts.qs)
|
|
|
|
request(opts, (err, response, body) => {
|
|
|
|
if (err) {
|
|
|
|
return callback(
|
|
|
|
OError.tag(err, 'error making request to CLSI', { projectId })
|
|
|
|
)
|
|
|
|
}
|
|
|
|
callback(null, response, body)
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
ClsiManager._makeRequest(projectId, opts, callback)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-08-12 07:33:01 -04:00
|
|
|
_makeRequest(projectId, opts, callback) {
|
|
|
|
async.series(
|
2019-05-29 05:21:06 -04:00
|
|
|
{
|
|
|
|
currentBackend(cb) {
|
|
|
|
const startTime = new Date()
|
2019-08-13 08:36:10 -04:00
|
|
|
ClsiCookieManager.getCookieJar(projectId, (err, jar) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (err != null) {
|
2019-08-13 08:36:10 -04:00
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'error getting cookie jar for CLSI request', {
|
|
|
|
projectId
|
|
|
|
})
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
opts.jar = jar
|
|
|
|
const timer = new Metrics.Timer('compile.currentBackend')
|
2019-08-13 08:36:10 -04:00
|
|
|
request(opts, (err, response, body) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (err != null) {
|
2019-08-13 08:36:10 -04:00
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'error making request to CLSI', { projectId })
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
|
|
|
}
|
2019-08-13 08:36:10 -04:00
|
|
|
timer.done()
|
|
|
|
Metrics.inc(
|
|
|
|
`compile.currentBackend.response.${response.statusCode}`
|
|
|
|
)
|
|
|
|
ClsiCookieManager.setServerId(projectId, response, err => {
|
2019-08-12 07:33:01 -04:00
|
|
|
if (err != null) {
|
2019-08-13 08:36:10 -04:00
|
|
|
callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'error setting server id', { projectId })
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
|
|
|
} else {
|
|
|
|
// return as soon as the standard compile has returned
|
|
|
|
callback(null, response, body)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-08-12 07:33:01 -04:00
|
|
|
cb(err, {
|
|
|
|
response,
|
|
|
|
body,
|
|
|
|
finishTime: new Date() - startTime
|
|
|
|
})
|
|
|
|
})
|
2019-05-29 05:21:06 -04:00
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
newBackend(cb) {
|
|
|
|
const startTime = new Date()
|
2019-08-13 08:36:10 -04:00
|
|
|
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
|
|
|
|
})
|
|
|
|
}
|
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
},
|
2019-08-13 08:36:10 -04:00
|
|
|
(err, results) => {
|
2019-08-12 07:33:01 -04:00
|
|
|
if (err != null) {
|
2019-08-13 08:36:10 -04:00
|
|
|
// This was handled higher up
|
2019-08-12 07:33:01 -04:00
|
|
|
return
|
|
|
|
}
|
2019-08-13 08:36:10 -04:00
|
|
|
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'
|
|
|
|
)
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
2019-08-12 07:33:01 -04:00
|
|
|
_makeNewBackendRequest(projectId, baseOpts, callback) {
|
2019-08-13 08:36:10 -04:00
|
|
|
if (Settings.apis.clsi_new == null || Settings.apis.clsi_new.url == null) {
|
2019-05-29 05:21:06 -04:00
|
|
|
return callback()
|
|
|
|
}
|
2019-08-13 08:36:10 -04:00
|
|
|
const opts = {
|
|
|
|
...baseOpts,
|
|
|
|
url: baseOpts.url.replace(
|
|
|
|
Settings.apis.clsi.url,
|
|
|
|
Settings.apis.clsi_new.url
|
|
|
|
)
|
|
|
|
}
|
|
|
|
NewBackendCloudClsiCookieManager.getCookieJar(projectId, (err, jar) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (err != null) {
|
2019-08-13 08:36:10 -04:00
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'error getting cookie jar for CLSI request', {
|
|
|
|
projectId
|
|
|
|
})
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
opts.jar = jar
|
|
|
|
const timer = new Metrics.Timer('compile.newBackend')
|
2019-08-13 08:36:10 -04:00
|
|
|
request(opts, (err, response, body) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
timer.done()
|
|
|
|
if (err != null) {
|
2019-08-13 08:36:10 -04:00
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'error making request to new CLSI', {
|
|
|
|
projectId,
|
|
|
|
opts
|
|
|
|
})
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
|
|
|
}
|
2019-08-12 07:33:01 -04:00
|
|
|
NewBackendCloudClsiCookieManager.setServerId(
|
|
|
|
projectId,
|
2019-05-29 05:21:06 -04:00
|
|
|
response,
|
2019-08-13 08:36:10 -04:00
|
|
|
err => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (err != null) {
|
2019-08-13 08:36:10 -04:00
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'error setting server id on new backend', {
|
|
|
|
projectId
|
|
|
|
})
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
|
|
|
}
|
2019-08-13 08:36:10 -04:00
|
|
|
callback(null, response, body)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2019-08-12 07:33:01 -04:00
|
|
|
_getCompilerUrl(compileGroup, projectId, userId, action) {
|
2019-05-29 05:21:06 -04:00
|
|
|
const host = Settings.apis.clsi.url
|
2019-08-12 07:33:01 -04:00
|
|
|
let path = `/project/${projectId}`
|
|
|
|
if (userId != null) {
|
|
|
|
path += `/user/${userId}`
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
if (action != null) {
|
|
|
|
path += `/${action}`
|
|
|
|
}
|
|
|
|
return `${host}${path}`
|
|
|
|
},
|
|
|
|
|
2019-08-12 07:33:01 -04:00
|
|
|
_postToClsi(projectId, userId, req, compileGroup, callback) {
|
2019-05-29 05:21:06 -04:00
|
|
|
const compileUrl = this._getCompilerUrl(
|
|
|
|
compileGroup,
|
2019-08-12 07:33:01 -04:00
|
|
|
projectId,
|
|
|
|
userId,
|
2019-05-29 05:21:06 -04:00
|
|
|
'compile'
|
|
|
|
)
|
|
|
|
const opts = {
|
|
|
|
url: compileUrl,
|
|
|
|
json: req,
|
|
|
|
method: 'POST'
|
|
|
|
}
|
2019-08-13 08:36:10 -04:00
|
|
|
ClsiManager._makeRequest(projectId, opts, (err, response, body) => {
|
|
|
|
if (err != null) {
|
2019-09-09 07:52:12 -04:00
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
new OError('failed to make request to CLSI', {
|
|
|
|
projectId,
|
|
|
|
userId,
|
|
|
|
compileOptions: req.compile.options,
|
|
|
|
rootResourcePath: req.compile.rootResourcePath
|
2019-09-09 07:52:12 -04:00
|
|
|
})
|
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
2019-08-12 07:33:01 -04:00
|
|
|
callback(null, body)
|
2019-05-29 05:21:06 -04:00
|
|
|
} else if (response.statusCode === 413) {
|
2019-08-12 07:33:01 -04:00
|
|
|
callback(null, { compile: { status: 'project-too-large' } })
|
2019-05-29 05:21:06 -04:00
|
|
|
} else if (response.statusCode === 409) {
|
2019-08-12 07:33:01 -04:00
|
|
|
callback(null, { compile: { status: 'conflict' } })
|
2019-05-29 05:21:06 -04:00
|
|
|
} else if (response.statusCode === 423) {
|
2019-08-12 07:33:01 -04:00
|
|
|
callback(null, { compile: { status: 'compile-in-progress' } })
|
2020-06-12 04:27:19 -04:00
|
|
|
} else if (response.statusCode === 503) {
|
|
|
|
callback(null, { compile: { status: 'unavailable' } })
|
2019-05-29 05:21:06 -04:00
|
|
|
} else {
|
2019-08-13 08:36:10 -04:00
|
|
|
callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
new OError(`CLSI returned non-success code: ${response.statusCode}`, {
|
|
|
|
projectId,
|
|
|
|
userId,
|
|
|
|
compileOptions: req.compile.options,
|
|
|
|
rootResourcePath: req.compile.rootResourcePath,
|
|
|
|
clsiResponse: body,
|
|
|
|
statusCode: response.statusCode
|
2019-08-13 08:36:10 -04:00
|
|
|
})
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2019-08-13 08:36:10 -04:00
|
|
|
_parseOutputFiles(projectId, rawOutputFiles = []) {
|
2019-05-29 05:21:06 -04:00
|
|
|
const outputFiles = []
|
2019-08-12 07:33:01 -04:00
|
|
|
for (const file of rawOutputFiles) {
|
2019-05-29 05:21:06 -04:00
|
|
|
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
|
|
|
|
},
|
|
|
|
|
2019-08-12 07:33:01 -04:00
|
|
|
_buildRequest(projectId, options, callback) {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (options == null) {
|
|
|
|
options = {}
|
|
|
|
}
|
2019-08-12 07:33:01 -04:00
|
|
|
ProjectGetter.getProject(
|
|
|
|
projectId,
|
2020-02-11 08:19:47 -05:00
|
|
|
{ compiler: 1, rootDoc_id: 1, imageName: 1, rootFolder: 1 },
|
2019-08-13 08:36:10 -04:00
|
|
|
(err, project) => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'failed to get project', { projectId })
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
if (project == null) {
|
|
|
|
return callback(
|
2019-08-12 07:33:01 -04:00
|
|
|
new Errors.NotFoundError(`project does not exist: ${projectId}`)
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
|
|
|
}
|
2019-08-13 08:36:10 -04:00
|
|
|
if (!VALID_COMPILERS.includes(project.compiler)) {
|
2019-05-29 05:21:06 -04:00
|
|
|
project.compiler = 'pdflatex'
|
|
|
|
}
|
|
|
|
|
2020-02-11 08:19:47 -05:00
|
|
|
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) => {
|
2020-02-11 08:19:47 -05:00
|
|
|
timer.done()
|
|
|
|
if (err != null) {
|
2020-02-11 08:19:47 -05:00
|
|
|
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
|
2020-02-11 08:19:47 -05:00
|
|
|
)
|
|
|
|
}
|
2020-02-11 08:19:47 -05:00
|
|
|
}
|
|
|
|
)
|
|
|
|
} 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(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'failed to get contents from Mongo', {
|
|
|
|
projectId
|
|
|
|
})
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
2020-02-11 08:19:47 -05:00
|
|
|
}
|
|
|
|
ClsiManager._finaliseRequest(
|
|
|
|
projectId,
|
|
|
|
options,
|
|
|
|
project,
|
|
|
|
docs,
|
|
|
|
files,
|
|
|
|
callback
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
2019-08-12 07:33:01 -04:00
|
|
|
getContentFromDocUpdaterIfMatch(projectId, project, options, callback) {
|
2019-08-13 08:36:10 -04:00
|
|
|
ClsiStateManager.computeHash(project, options, (err, projectStateHash) => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'Failed to compute project state hash', { projectId })
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-08-12 07:33:01 -04:00
|
|
|
DocumentUpdaterHandler.getProjectDocsIfMatch(
|
|
|
|
projectId,
|
2019-05-29 05:21:06 -04:00
|
|
|
projectStateHash,
|
2019-08-13 08:36:10 -04:00
|
|
|
(err, docs) => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'Failed to get project documents', {
|
|
|
|
projectId,
|
|
|
|
projectStateHash
|
|
|
|
})
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-08-12 07:33:01 -04:00
|
|
|
callback(null, projectStateHash, docs)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
)
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2019-08-12 07:33:01 -04:00
|
|
|
getOutputFileStream(projectId, userId, buildId, outputFilePath, callback) {
|
2020-12-15 05:23:54 -05:00
|
|
|
const url = `${Settings.apis.clsi.url}/project/${projectId}/user/${userId}/build/${buildId}/output/${outputFilePath}`
|
2019-08-13 08:36:10 -04:00
|
|
|
ClsiCookieManager.getCookieJar(projectId, (err, jar) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (err != null) {
|
2019-08-13 08:36:10 -04:00
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'Failed to get cookie jar', {
|
|
|
|
projectId,
|
|
|
|
userId,
|
|
|
|
buildId,
|
|
|
|
outputFilePath
|
|
|
|
})
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
const options = { url, method: 'GET', timeout: 60 * 1000, jar }
|
|
|
|
const readStream = request(options)
|
2019-08-12 07:33:01 -04:00
|
|
|
callback(null, readStream)
|
2019-05-29 05:21:06 -04:00
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
_buildRequestFromDocupdater(
|
2019-08-12 07:33:01 -04:00
|
|
|
projectId,
|
2019-05-29 05:21:06 -04:00
|
|
|
options,
|
|
|
|
project,
|
|
|
|
projectStateHash,
|
|
|
|
docUpdaterDocs,
|
|
|
|
callback
|
|
|
|
) {
|
2019-08-13 08:36:10 -04:00
|
|
|
ProjectEntityHandler.getAllDocPathsFromProject(project, (err, docPath) => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'Failed to get doc paths', { projectId })
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
const docs = {}
|
2019-08-12 07:33:01 -04:00
|
|
|
for (let doc of docUpdaterDocs || []) {
|
2019-08-13 08:36:10 -04:00
|
|
|
const path = docPath[doc._id]
|
2019-05-29 05:21:06 -04:00
|
|
|
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]
|
2019-08-12 07:33:01 -04:00
|
|
|
for (const rootDocId of possibleRootDocIds) {
|
|
|
|
if (rootDocId != null && rootDocId in docPath) {
|
2019-08-13 08:36:10 -04:00
|
|
|
const path = docPath[rootDocId]
|
2019-05-29 05:21:06 -04:00
|
|
|
if (docs[path] == null) {
|
2019-08-12 07:33:01 -04:00
|
|
|
docs[path] = { _id: rootDocId, path }
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-08-12 07:33:01 -04:00
|
|
|
ClsiManager._finaliseRequest(
|
|
|
|
projectId,
|
2019-05-29 05:21:06 -04:00
|
|
|
options,
|
|
|
|
project,
|
|
|
|
docs,
|
|
|
|
[],
|
|
|
|
callback
|
|
|
|
)
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
_buildRequestFromMongo(
|
2019-08-12 07:33:01 -04:00
|
|
|
projectId,
|
2019-05-29 05:21:06 -04:00
|
|
|
options,
|
|
|
|
project,
|
|
|
|
projectStateHash,
|
|
|
|
callback
|
|
|
|
) {
|
2019-08-13 08:36:10 -04:00
|
|
|
ClsiManager._getContentFromMongo(projectId, (err, docs, files) => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'failed to get project contents from Mongo', {
|
|
|
|
projectId
|
|
|
|
})
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
|
|
|
}
|
|
|
|
options = {
|
|
|
|
...options,
|
|
|
|
syncType: 'full',
|
|
|
|
syncState: projectStateHash
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-08-12 07:33:01 -04:00
|
|
|
ClsiManager._finaliseRequest(
|
|
|
|
projectId,
|
2019-05-29 05:21:06 -04:00
|
|
|
options,
|
|
|
|
project,
|
|
|
|
docs,
|
|
|
|
files,
|
|
|
|
callback
|
|
|
|
)
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2019-08-12 07:33:01 -04:00
|
|
|
_getContentFromMongo(projectId, callback) {
|
2019-08-13 08:36:10 -04:00
|
|
|
DocumentUpdaterHandler.flushProjectToMongo(projectId, err => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'failed to flush project to Mongo', { projectId })
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-08-13 08:36:10 -04:00
|
|
|
ProjectEntityHandler.getAllDocs(projectId, (err, docs) => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'failed to get project docs', { projectId })
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-08-13 08:36:10 -04:00
|
|
|
ProjectEntityHandler.getAllFiles(projectId, (err, files) => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'failed to get project files', { projectId })
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
if (files == null) {
|
|
|
|
files = {}
|
|
|
|
}
|
2019-08-13 08:36:10 -04:00
|
|
|
callback(null, docs || {}, files || {})
|
2019-05-29 05:21:06 -04:00
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2019-08-12 07:33:01 -04:00
|
|
|
_finaliseRequest(projectId, options, project, docs, files, callback) {
|
2019-05-29 05:21:06 -04:00
|
|
|
const resources = []
|
|
|
|
let rootResourcePath = null
|
|
|
|
let rootResourcePathOverride = null
|
|
|
|
let hasMainFile = false
|
|
|
|
let numberOfDocsInProject = 0
|
|
|
|
|
2019-08-13 08:36:10 -04:00
|
|
|
for (let path in docs) {
|
|
|
|
const doc = docs[path]
|
2019-05-29 05:21:06 -04:00
|
|
|
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
|
2019-08-13 08:36:10 -04:00
|
|
|
for (const path in docs) {
|
|
|
|
// Remove leading /
|
2019-05-29 05:21:06 -04:00
|
|
|
rootResourcePath = path.replace(/^\//, '')
|
2019-08-13 08:36:10 -04:00
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
} else {
|
2020-08-11 05:28:29 -04:00
|
|
|
return callback(new OError('no main file specified', { projectId }))
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-13 08:36:10 -04:00
|
|
|
for (let path in files) {
|
2019-05-29 05:21:06 -04:00
|
|
|
const file = files[path]
|
|
|
|
path = path.replace(/^\//, '') // Remove leading /
|
|
|
|
resources.push({
|
|
|
|
path,
|
2020-12-15 05:23:54 -05:00
|
|
|
url: `${Settings.apis.filestore.url}/project/${project._id}/file/${file._id}`,
|
2019-05-29 05:21:06 -04:00
|
|
|
modified: file.created != null ? file.created.getTime() : undefined
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-08-12 07:33:01 -04:00
|
|
|
callback(null, {
|
2019-05-29 05:21:06 -04:00
|
|
|
compile: {
|
|
|
|
options: {
|
|
|
|
compiler: project.compiler,
|
|
|
|
timeout: options.timeout,
|
|
|
|
imageName: project.imageName,
|
|
|
|
draft: !!options.draft,
|
|
|
|
check: options.check,
|
|
|
|
syncType: options.syncType,
|
2020-06-19 04:22:09 -04:00
|
|
|
syncState: options.syncState,
|
|
|
|
compileGroup: options.compileGroup
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
rootResourcePath,
|
|
|
|
resources
|
|
|
|
}
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2021-01-21 07:20:55 -05:00
|
|
|
wordCount(projectId, userId, file, options, clsiserverid, callback) {
|
2019-08-13 08:36:10 -04:00
|
|
|
ClsiManager._buildRequest(projectId, options, (err, req) => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(
|
2020-08-11 05:28:29 -04:00
|
|
|
OError.tag(err, 'Failed to build CLSI request', {
|
|
|
|
projectId,
|
|
|
|
options
|
|
|
|
})
|
2019-08-13 08:36:10 -04:00
|
|
|
)
|
2019-08-12 07:33:01 -04:00
|
|
|
}
|
|
|
|
const filename = file || req.compile.rootResourcePath
|
|
|
|
const wordCountUrl = ClsiManager._getCompilerUrl(
|
2019-08-13 08:36:10 -04:00
|
|
|
options.compileGroup,
|
2019-08-12 07:33:01 -04:00
|
|
|
projectId,
|
|
|
|
userId,
|
2019-05-29 05:21:06 -04:00
|
|
|
'wordcount'
|
|
|
|
)
|
|
|
|
const opts = {
|
2019-08-12 07:33:01 -04:00
|
|
|
url: wordCountUrl,
|
2019-05-29 05:21:06 -04:00
|
|
|
qs: {
|
|
|
|
file: filename,
|
|
|
|
image: req.compile.options.imageName
|
|
|
|
},
|
|
|
|
method: 'GET'
|
|
|
|
}
|
2021-01-21 07:20:55 -05:00
|
|
|
ClsiManager._makeRequestWithClsiServerId(
|
|
|
|
projectId,
|
|
|
|
opts,
|
|
|
|
clsiserverid,
|
|
|
|
(err, response, body) => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(
|
|
|
|
OError.tag(err, 'CLSI request failed', { projectId })
|
2020-08-11 05:28:29 -04:00
|
|
|
)
|
2021-01-21 07:20:55 -05:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2021-01-21 07:20:55 -05:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-12 07:33:01 -04:00
|
|
|
module.exports = ClsiManager
|