ClsiManager.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947
  1. const async = require('async')
  2. const Settings = require('@overleaf/settings')
  3. const request = require('request')
  4. const ProjectGetter = require('../Project/ProjectGetter')
  5. const ProjectEntityHandler = require('../Project/ProjectEntityHandler')
  6. const logger = require('@overleaf/logger')
  7. const { URL, URLSearchParams } = require('url')
  8. const OError = require('@overleaf/o-error')
  9. const ClsiCookieManager = require('./ClsiCookieManager')(
  10. Settings.apis.clsi != null ? Settings.apis.clsi.backendGroupName : undefined
  11. )
  12. const NewBackendCloudClsiCookieManager = require('./ClsiCookieManager')(
  13. Settings.apis.clsi_new != null
  14. ? Settings.apis.clsi_new.backendGroupName
  15. : undefined
  16. )
  17. const ClsiStateManager = require('./ClsiStateManager')
  18. const _ = require('underscore')
  19. const ClsiFormatChecker = require('./ClsiFormatChecker')
  20. const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
  21. const Metrics = require('@overleaf/metrics')
  22. const Errors = require('../Errors/Errors')
  23. const VALID_COMPILERS = ['pdflatex', 'latex', 'xelatex', 'lualatex']
  24. const ClsiManager = {
  25. sendRequest(projectId, userId, options, callback) {
  26. if (options == null) {
  27. options = {}
  28. }
  29. ClsiManager.sendRequestOnce(
  30. projectId,
  31. userId,
  32. options,
  33. (err, status, ...result) => {
  34. if (err != null) {
  35. return callback(err)
  36. }
  37. if (status === 'conflict') {
  38. // Try again, with a full compile
  39. return ClsiManager.sendRequestOnce(
  40. projectId,
  41. userId,
  42. { ...options, syncType: 'full' },
  43. callback
  44. )
  45. } else if (status === 'unavailable') {
  46. return ClsiManager.sendRequestOnce(
  47. projectId,
  48. userId,
  49. { ...options, syncType: 'full', forceNewClsiServer: true },
  50. callback
  51. )
  52. }
  53. callback(null, status, ...result)
  54. }
  55. )
  56. },
  57. sendRequestOnce(projectId, userId, options, callback) {
  58. if (options == null) {
  59. options = {}
  60. }
  61. ClsiManager._buildRequest(projectId, options, (err, req) => {
  62. if (err != null) {
  63. if (err.message === 'no main file specified') {
  64. return callback(null, 'validation-problems', null, null, {
  65. mainFile: err.message,
  66. })
  67. } else {
  68. return callback(
  69. OError.tag(err, 'Could not build request to CLSI', {
  70. projectId,
  71. options,
  72. })
  73. )
  74. }
  75. }
  76. ClsiManager._sendBuiltRequest(
  77. projectId,
  78. userId,
  79. req,
  80. options,
  81. (err, status, ...result) => {
  82. if (err != null) {
  83. return callback(
  84. OError.tag(err, 'CLSI compile failed', { projectId, userId })
  85. )
  86. }
  87. callback(null, status, ...result)
  88. }
  89. )
  90. })
  91. },
  92. // for public API requests where there is no project id
  93. sendExternalRequest(submissionId, clsiRequest, options, callback) {
  94. if (options == null) {
  95. options = {}
  96. }
  97. ClsiManager._sendBuiltRequest(
  98. submissionId,
  99. null,
  100. clsiRequest,
  101. options,
  102. (err, status, ...result) => {
  103. if (err != null) {
  104. return callback(
  105. OError.tag(err, 'CLSI compile failed', {
  106. submissionId,
  107. options,
  108. })
  109. )
  110. }
  111. callback(null, status, ...result)
  112. }
  113. )
  114. },
  115. stopCompile(projectId, userId, options, callback) {
  116. if (options == null) {
  117. options = {}
  118. }
  119. const { compileGroup } = options
  120. const compilerUrl = this._getCompilerUrl(
  121. compileGroup,
  122. projectId,
  123. userId,
  124. 'compile/stop'
  125. )
  126. const opts = {
  127. url: compilerUrl,
  128. method: 'POST',
  129. }
  130. ClsiManager._makeRequest(projectId, userId, compileGroup, opts, callback)
  131. },
  132. deleteAuxFiles(projectId, userId, options, clsiserverid, callback) {
  133. if (options == null) {
  134. options = {}
  135. }
  136. const { compileGroup } = options
  137. const compilerUrl = this._getCompilerUrl(compileGroup, projectId, userId)
  138. const opts = {
  139. url: compilerUrl,
  140. method: 'DELETE',
  141. }
  142. ClsiManager._makeRequestWithClsiServerId(
  143. projectId,
  144. userId,
  145. compileGroup,
  146. opts,
  147. clsiserverid,
  148. clsiErr => {
  149. // always clear the project state from the docupdater, even if there
  150. // was a problem with the request to the clsi
  151. DocumentUpdaterHandler.clearProjectState(projectId, docUpdaterErr => {
  152. ClsiCookieManager.clearServerId(projectId, userId, redisError => {
  153. if (clsiErr) {
  154. return callback(
  155. OError.tag(clsiErr, 'Failed to delete aux files', { projectId })
  156. )
  157. }
  158. if (docUpdaterErr) {
  159. return callback(
  160. OError.tag(
  161. docUpdaterErr,
  162. 'Failed to clear project state in doc updater',
  163. { projectId }
  164. )
  165. )
  166. }
  167. if (redisError) {
  168. // redis errors need wrapping as the instance may be shared
  169. return callback(
  170. OError(
  171. 'Failed to clear clsi persistence',
  172. { projectId },
  173. redisError
  174. )
  175. )
  176. }
  177. callback()
  178. })
  179. })
  180. }
  181. )
  182. },
  183. _sendBuiltRequest(projectId, userId, req, options, callback) {
  184. if (options == null) {
  185. options = {}
  186. }
  187. if (options.forceNewClsiServer) {
  188. // Clear clsi cookie, then try again
  189. return ClsiCookieManager.clearServerId(projectId, userId, err => {
  190. if (err) {
  191. return callback(err)
  192. }
  193. options.forceNewClsiServer = false // backend has now been reset
  194. return ClsiManager._sendBuiltRequest(
  195. projectId,
  196. userId,
  197. req,
  198. options,
  199. callback
  200. )
  201. })
  202. }
  203. ClsiFormatChecker.checkRecoursesForProblems(
  204. req.compile != null ? req.compile.resources : undefined,
  205. (err, validationProblems) => {
  206. if (err != null) {
  207. return callback(
  208. OError.tag(
  209. err,
  210. 'could not check resources for potential problems before sending to clsi'
  211. )
  212. )
  213. }
  214. if (validationProblems != null) {
  215. logger.log(
  216. { projectId, validationProblems },
  217. 'problems with users latex before compile was attempted'
  218. )
  219. return callback(
  220. null,
  221. 'validation-problems',
  222. null,
  223. null,
  224. validationProblems
  225. )
  226. }
  227. ClsiManager._postToClsi(
  228. projectId,
  229. userId,
  230. req,
  231. options.compileGroup,
  232. (err, response, clsiServerId) => {
  233. if (err != null) {
  234. return callback(
  235. OError.tag(err, 'error sending request to clsi', {
  236. projectId,
  237. userId,
  238. })
  239. )
  240. }
  241. const outputFiles = ClsiManager._parseOutputFiles(
  242. projectId,
  243. response && response.compile && response.compile.outputFiles
  244. )
  245. const compile = (response && response.compile) || {}
  246. const status = compile.status
  247. const stats = compile.stats
  248. const timings = compile.timings
  249. const validationProblems = undefined
  250. callback(
  251. null,
  252. status,
  253. outputFiles,
  254. clsiServerId,
  255. validationProblems,
  256. stats,
  257. timings
  258. )
  259. }
  260. )
  261. }
  262. )
  263. },
  264. _makeRequestWithClsiServerId(
  265. projectId,
  266. userId,
  267. compileGroup,
  268. opts,
  269. clsiserverid,
  270. callback
  271. ) {
  272. if (clsiserverid) {
  273. // ignore cookies and newBackend, go straight to the clsi node
  274. opts.qs = Object.assign({ compileGroup, clsiserverid }, opts.qs)
  275. request(opts, (err, response, body) => {
  276. if (err) {
  277. return callback(
  278. OError.tag(err, 'error making request to CLSI', { projectId })
  279. )
  280. }
  281. callback(null, response, body)
  282. })
  283. } else {
  284. ClsiManager._makeRequest(projectId, userId, compileGroup, opts, callback)
  285. }
  286. },
  287. _makeRequest(projectId, userId, compileGroup, opts, callback) {
  288. async.series(
  289. {
  290. currentBackend(cb) {
  291. const startTime = new Date()
  292. ClsiCookieManager.getCookieJar(
  293. projectId,
  294. userId,
  295. compileGroup,
  296. (err, jar, clsiServerId) => {
  297. if (err != null) {
  298. return callback(
  299. OError.tag(err, 'error getting cookie jar for CLSI request', {
  300. projectId,
  301. })
  302. )
  303. }
  304. opts.jar = jar
  305. const timer = new Metrics.Timer('compile.currentBackend')
  306. request(opts, (err, response, body) => {
  307. if (err != null) {
  308. return callback(
  309. OError.tag(err, 'error making request to CLSI', {
  310. projectId,
  311. })
  312. )
  313. }
  314. timer.done()
  315. Metrics.inc(
  316. `compile.currentBackend.response.${response.statusCode}`
  317. )
  318. ClsiCookieManager.setServerId(
  319. projectId,
  320. userId,
  321. compileGroup,
  322. response,
  323. clsiServerId,
  324. (err, newClsiServerId) => {
  325. if (err != null) {
  326. callback(
  327. OError.tag(err, 'error setting server id', {
  328. projectId,
  329. })
  330. )
  331. } else {
  332. // return as soon as the standard compile has returned
  333. callback(
  334. null,
  335. response,
  336. body,
  337. newClsiServerId || clsiServerId
  338. )
  339. }
  340. cb(err, {
  341. response,
  342. body,
  343. finishTime: new Date() - startTime,
  344. })
  345. }
  346. )
  347. })
  348. }
  349. )
  350. },
  351. newBackend(cb) {
  352. const startTime = new Date()
  353. ClsiManager._makeNewBackendRequest(
  354. projectId,
  355. userId,
  356. compileGroup,
  357. opts,
  358. (err, response, body) => {
  359. if (err != null) {
  360. logger.warn({ err }, 'Error making request to new CLSI backend')
  361. }
  362. if (response != null) {
  363. Metrics.inc(
  364. `compile.newBackend.response.${response.statusCode}`
  365. )
  366. }
  367. cb(err, {
  368. response,
  369. body,
  370. finishTime: new Date() - startTime,
  371. })
  372. }
  373. )
  374. },
  375. },
  376. (err, results) => {
  377. if (err != null) {
  378. // This was handled higher up
  379. return
  380. }
  381. if (results.newBackend != null && results.newBackend.response != null) {
  382. const currentStatusCode = results.currentBackend.response.statusCode
  383. const newStatusCode = results.newBackend.response.statusCode
  384. const statusCodeSame = newStatusCode === currentStatusCode
  385. const currentCompileTime = results.currentBackend.finishTime
  386. const newBackendCompileTime = results.newBackend.finishTime || 0
  387. const timeDifference = newBackendCompileTime - currentCompileTime
  388. logger.log(
  389. {
  390. statusCodeSame,
  391. timeDifference,
  392. currentCompileTime,
  393. newBackendCompileTime,
  394. projectId,
  395. },
  396. 'both clsi requests returned'
  397. )
  398. }
  399. }
  400. )
  401. },
  402. _makeNewBackendRequest(projectId, userId, compileGroup, baseOpts, callback) {
  403. if (Settings.apis.clsi_new == null || Settings.apis.clsi_new.url == null) {
  404. return callback()
  405. }
  406. const opts = {
  407. ...baseOpts,
  408. url: baseOpts.url.replace(
  409. Settings.apis.clsi.url,
  410. Settings.apis.clsi_new.url
  411. ),
  412. }
  413. NewBackendCloudClsiCookieManager.getCookieJar(
  414. projectId,
  415. userId,
  416. compileGroup,
  417. (err, jar, clsiServerId) => {
  418. if (err != null) {
  419. return callback(
  420. OError.tag(err, 'error getting cookie jar for CLSI request', {
  421. projectId,
  422. })
  423. )
  424. }
  425. opts.jar = jar
  426. const timer = new Metrics.Timer('compile.newBackend')
  427. request(opts, (err, response, body) => {
  428. timer.done()
  429. if (err != null) {
  430. return callback(
  431. OError.tag(err, 'error making request to new CLSI', {
  432. projectId,
  433. opts,
  434. })
  435. )
  436. }
  437. NewBackendCloudClsiCookieManager.setServerId(
  438. projectId,
  439. userId,
  440. compileGroup,
  441. response,
  442. clsiServerId,
  443. err => {
  444. if (err != null) {
  445. return callback(
  446. OError.tag(err, 'error setting server id on new backend', {
  447. projectId,
  448. })
  449. )
  450. }
  451. callback(null, response, body)
  452. }
  453. )
  454. })
  455. }
  456. )
  457. },
  458. _getCompilerUrl(compileGroup, projectId, userId, action) {
  459. const u = new URL(`/project/${projectId}`, Settings.apis.clsi.url)
  460. if (userId != null) {
  461. u.pathname += `/user/${userId}`
  462. }
  463. if (action != null) {
  464. u.pathname += `/${action}`
  465. }
  466. u.search = new URLSearchParams({ compileGroup }).toString()
  467. return u.href
  468. },
  469. _postToClsi(projectId, userId, req, compileGroup, callback) {
  470. const compileUrl = this._getCompilerUrl(
  471. compileGroup,
  472. projectId,
  473. userId,
  474. 'compile'
  475. )
  476. const opts = {
  477. url: compileUrl,
  478. json: req,
  479. method: 'POST',
  480. }
  481. ClsiManager._makeRequest(
  482. projectId,
  483. userId,
  484. compileGroup,
  485. opts,
  486. (err, response, body, clsiServerId) => {
  487. if (err != null) {
  488. return callback(
  489. new OError(
  490. 'failed to make request to CLSI',
  491. {
  492. projectId,
  493. userId,
  494. compileOptions: req.compile.options,
  495. rootResourcePath: req.compile.rootResourcePath,
  496. },
  497. err
  498. )
  499. )
  500. }
  501. if (response.statusCode >= 200 && response.statusCode < 300) {
  502. callback(null, body, clsiServerId)
  503. } else if (response.statusCode === 413) {
  504. callback(null, { compile: { status: 'project-too-large' } })
  505. } else if (response.statusCode === 409) {
  506. callback(null, { compile: { status: 'conflict' } })
  507. } else if (response.statusCode === 423) {
  508. callback(null, { compile: { status: 'compile-in-progress' } })
  509. } else if (response.statusCode === 503) {
  510. callback(null, { compile: { status: 'unavailable' } })
  511. } else {
  512. callback(
  513. new OError(
  514. `CLSI returned non-success code: ${response.statusCode}`,
  515. {
  516. projectId,
  517. userId,
  518. compileOptions: req.compile.options,
  519. rootResourcePath: req.compile.rootResourcePath,
  520. clsiResponse: body,
  521. statusCode: response.statusCode,
  522. }
  523. )
  524. )
  525. }
  526. }
  527. )
  528. },
  529. _parseOutputFiles(projectId, rawOutputFiles = []) {
  530. const outputFiles = []
  531. for (const file of rawOutputFiles) {
  532. outputFiles.push({
  533. path: file.path, // the clsi is now sending this to web
  534. url: new URL(file.url).pathname, // the location of the file on the clsi, excluding the host part
  535. type: file.type,
  536. build: file.build,
  537. contentId: file.contentId,
  538. ranges: file.ranges,
  539. size: file.size,
  540. })
  541. }
  542. return outputFiles
  543. },
  544. _buildRequest(projectId, options, callback) {
  545. if (options == null) {
  546. options = {}
  547. }
  548. ProjectGetter.getProject(
  549. projectId,
  550. { compiler: 1, rootDoc_id: 1, imageName: 1, rootFolder: 1 },
  551. (err, project) => {
  552. if (err != null) {
  553. return callback(
  554. OError.tag(err, 'failed to get project', { projectId })
  555. )
  556. }
  557. if (project == null) {
  558. return callback(
  559. new Errors.NotFoundError(`project does not exist: ${projectId}`)
  560. )
  561. }
  562. if (!VALID_COMPILERS.includes(project.compiler)) {
  563. project.compiler = 'pdflatex'
  564. }
  565. if (options.incrementalCompilesEnabled || options.syncType != null) {
  566. // new way, either incremental or full
  567. const timer = new Metrics.Timer('editor.compile-getdocs-redis')
  568. ClsiManager.getContentFromDocUpdaterIfMatch(
  569. projectId,
  570. project,
  571. options,
  572. (err, projectStateHash, docUpdaterDocs) => {
  573. timer.done()
  574. if (err != null) {
  575. logger.error({ err, projectId }, 'error checking project state')
  576. // note: we don't bail out when there's an error getting
  577. // incremental files from the docupdater, we just fall back
  578. // to a normal compile below
  579. }
  580. // see if we can send an incremental update to the CLSI
  581. if (
  582. docUpdaterDocs != null &&
  583. options.syncType !== 'full' &&
  584. err == null
  585. ) {
  586. Metrics.inc('compile-from-redis')
  587. ClsiManager._buildRequestFromDocupdater(
  588. projectId,
  589. options,
  590. project,
  591. projectStateHash,
  592. docUpdaterDocs,
  593. callback
  594. )
  595. } else {
  596. Metrics.inc('compile-from-mongo')
  597. ClsiManager._buildRequestFromMongo(
  598. projectId,
  599. options,
  600. project,
  601. projectStateHash,
  602. callback
  603. )
  604. }
  605. }
  606. )
  607. } else {
  608. // old way, always from mongo
  609. const timer = new Metrics.Timer('editor.compile-getdocs-mongo')
  610. ClsiManager._getContentFromMongo(projectId, (err, docs, files) => {
  611. timer.done()
  612. if (err != null) {
  613. return callback(
  614. OError.tag(err, 'failed to get contents from Mongo', {
  615. projectId,
  616. })
  617. )
  618. }
  619. ClsiManager._finaliseRequest(
  620. projectId,
  621. options,
  622. project,
  623. docs,
  624. files,
  625. callback
  626. )
  627. })
  628. }
  629. }
  630. )
  631. },
  632. getContentFromDocUpdaterIfMatch(projectId, project, options, callback) {
  633. const projectStateHash = ClsiStateManager.computeHash(project, options)
  634. DocumentUpdaterHandler.getProjectDocsIfMatch(
  635. projectId,
  636. projectStateHash,
  637. (err, docs) => {
  638. if (err != null) {
  639. return callback(
  640. OError.tag(err, 'Failed to get project documents', {
  641. projectId,
  642. projectStateHash,
  643. })
  644. )
  645. }
  646. callback(null, projectStateHash, docs)
  647. }
  648. )
  649. },
  650. getOutputFileStream(
  651. projectId,
  652. userId,
  653. compileGroup,
  654. clsiServerId,
  655. buildId,
  656. outputFilePath,
  657. callback
  658. ) {
  659. const url = `${Settings.apis.clsi.url}/project/${projectId}/user/${userId}/build/${buildId}/output/${outputFilePath}`
  660. // TODO(das7pad): remove one week after landing frontend changes.
  661. ClsiCookieManager.getCookieJar(projectId, userId, '', (err, jar) => {
  662. if (err != null) {
  663. return callback(
  664. OError.tag(err, 'Failed to get cookie jar', {
  665. projectId,
  666. userId,
  667. buildId,
  668. outputFilePath,
  669. })
  670. )
  671. }
  672. const options = { url, method: 'GET', timeout: 60 * 1000, jar }
  673. if (clsiServerId) {
  674. options.qs = { compileGroup, clsiserverid: clsiServerId }
  675. delete options.jar
  676. }
  677. const readStream = request(options)
  678. callback(null, readStream)
  679. })
  680. },
  681. _buildRequestFromDocupdater(
  682. projectId,
  683. options,
  684. project,
  685. projectStateHash,
  686. docUpdaterDocs,
  687. callback
  688. ) {
  689. const docPath = ProjectEntityHandler.getAllDocPathsFromProject(project)
  690. const docs = {}
  691. for (const doc of docUpdaterDocs || []) {
  692. const path = docPath[doc._id]
  693. docs[path] = doc
  694. }
  695. // send new docs but not files as those are already on the clsi
  696. options = _.clone(options)
  697. options.syncType = 'incremental'
  698. options.syncState = projectStateHash
  699. // create stub doc entries for any possible root docs, if not
  700. // present in the docupdater. This allows finaliseRequest to
  701. // identify the root doc.
  702. const possibleRootDocIds = [options.rootDoc_id, project.rootDoc_id]
  703. for (const rootDocId of possibleRootDocIds) {
  704. if (rootDocId != null && rootDocId in docPath) {
  705. const path = docPath[rootDocId]
  706. if (docs[path] == null) {
  707. docs[path] = { _id: rootDocId, path }
  708. }
  709. }
  710. }
  711. ClsiManager._finaliseRequest(
  712. projectId,
  713. options,
  714. project,
  715. docs,
  716. [],
  717. callback
  718. )
  719. },
  720. _buildRequestFromMongo(
  721. projectId,
  722. options,
  723. project,
  724. projectStateHash,
  725. callback
  726. ) {
  727. ClsiManager._getContentFromMongo(projectId, (err, docs, files) => {
  728. if (err != null) {
  729. return callback(
  730. OError.tag(err, 'failed to get project contents from Mongo', {
  731. projectId,
  732. })
  733. )
  734. }
  735. options = {
  736. ...options,
  737. syncType: 'full',
  738. syncState: projectStateHash,
  739. }
  740. ClsiManager._finaliseRequest(
  741. projectId,
  742. options,
  743. project,
  744. docs,
  745. files,
  746. callback
  747. )
  748. })
  749. },
  750. _getContentFromMongo(projectId, callback) {
  751. DocumentUpdaterHandler.flushProjectToMongo(projectId, err => {
  752. if (err != null) {
  753. return callback(
  754. OError.tag(err, 'failed to flush project to Mongo', { projectId })
  755. )
  756. }
  757. ProjectEntityHandler.getAllDocs(projectId, (err, docs) => {
  758. if (err != null) {
  759. return callback(
  760. OError.tag(err, 'failed to get project docs', { projectId })
  761. )
  762. }
  763. ProjectEntityHandler.getAllFiles(projectId, (err, files) => {
  764. if (err != null) {
  765. return callback(
  766. OError.tag(err, 'failed to get project files', { projectId })
  767. )
  768. }
  769. if (files == null) {
  770. files = {}
  771. }
  772. callback(null, docs || {}, files || {})
  773. })
  774. })
  775. })
  776. },
  777. _finaliseRequest(projectId, options, project, docs, files, callback) {
  778. const resources = []
  779. let flags
  780. let rootResourcePath = null
  781. let rootResourcePathOverride = null
  782. let hasMainFile = false
  783. let numberOfDocsInProject = 0
  784. for (let path in docs) {
  785. const doc = docs[path]
  786. path = path.replace(/^\//, '') // Remove leading /
  787. numberOfDocsInProject++
  788. if (doc.lines != null) {
  789. // add doc to resources unless it is just a stub entry
  790. resources.push({
  791. path,
  792. content: doc.lines.join('\n'),
  793. })
  794. }
  795. if (
  796. project.rootDoc_id != null &&
  797. doc._id.toString() === project.rootDoc_id.toString()
  798. ) {
  799. rootResourcePath = path
  800. }
  801. if (
  802. options.rootDoc_id != null &&
  803. doc._id.toString() === options.rootDoc_id.toString()
  804. ) {
  805. rootResourcePathOverride = path
  806. }
  807. if (path === 'main.tex') {
  808. hasMainFile = true
  809. }
  810. }
  811. if (rootResourcePathOverride != null) {
  812. rootResourcePath = rootResourcePathOverride
  813. }
  814. if (rootResourcePath == null) {
  815. if (hasMainFile) {
  816. rootResourcePath = 'main.tex'
  817. } else if (numberOfDocsInProject === 1) {
  818. // only one file, must be the main document
  819. for (const path in docs) {
  820. // Remove leading /
  821. rootResourcePath = path.replace(/^\//, '')
  822. }
  823. } else {
  824. return callback(new OError('no main file specified', { projectId }))
  825. }
  826. }
  827. for (let path in files) {
  828. const file = files[path]
  829. path = path.replace(/^\//, '') // Remove leading /
  830. resources.push({
  831. path,
  832. url: `${Settings.apis.filestore.url}/project/${project._id}/file/${file._id}`,
  833. modified: file.created != null ? file.created.getTime() : undefined,
  834. })
  835. }
  836. if (options.fileLineErrors) {
  837. flags = ['-file-line-error']
  838. }
  839. callback(null, {
  840. compile: {
  841. options: {
  842. compiler: project.compiler,
  843. timeout: options.timeout,
  844. imageName: project.imageName,
  845. draft: !!options.draft,
  846. check: options.check,
  847. syncType: options.syncType,
  848. syncState: options.syncState,
  849. compileGroup: options.compileGroup,
  850. enablePdfCaching:
  851. (Settings.enablePdfCaching && options.enablePdfCaching) || false,
  852. flags: flags,
  853. metricsMethod: options.compileGroup,
  854. },
  855. rootResourcePath,
  856. resources,
  857. },
  858. })
  859. },
  860. wordCount(projectId, userId, file, options, clsiserverid, callback) {
  861. const { compileGroup } = options
  862. ClsiManager._buildRequest(projectId, options, (err, req) => {
  863. if (err != null) {
  864. return callback(
  865. OError.tag(err, 'Failed to build CLSI request', {
  866. projectId,
  867. options,
  868. })
  869. )
  870. }
  871. const filename = file || req.compile.rootResourcePath
  872. const wordCountUrl = ClsiManager._getCompilerUrl(
  873. compileGroup,
  874. projectId,
  875. userId,
  876. 'wordcount'
  877. )
  878. const opts = {
  879. url: wordCountUrl,
  880. qs: {
  881. file: filename,
  882. image: req.compile.options.imageName,
  883. },
  884. json: true,
  885. method: 'GET',
  886. }
  887. ClsiManager._makeRequestWithClsiServerId(
  888. projectId,
  889. userId,
  890. compileGroup,
  891. opts,
  892. clsiserverid,
  893. (err, response, body) => {
  894. if (err != null) {
  895. return callback(
  896. OError.tag(err, 'CLSI request failed', { projectId })
  897. )
  898. }
  899. if (response.statusCode >= 200 && response.statusCode < 300) {
  900. callback(null, body)
  901. } else {
  902. callback(
  903. new OError(
  904. `CLSI returned non-success code: ${response.statusCode}`,
  905. {
  906. projectId,
  907. clsiResponse: body,
  908. statusCode: response.statusCode,
  909. }
  910. )
  911. )
  912. }
  913. }
  914. )
  915. })
  916. },
  917. }
  918. module.exports = ClsiManager