ClsiManager.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950
  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.debug(
  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 outputUrlPrefix = compile.outputUrlPrefix
  250. const validationProblems = undefined
  251. callback(
  252. null,
  253. status,
  254. outputFiles,
  255. clsiServerId,
  256. validationProblems,
  257. stats,
  258. timings,
  259. outputUrlPrefix
  260. )
  261. }
  262. )
  263. }
  264. )
  265. },
  266. _makeRequestWithClsiServerId(
  267. projectId,
  268. userId,
  269. compileGroup,
  270. opts,
  271. clsiserverid,
  272. callback
  273. ) {
  274. if (clsiserverid) {
  275. // ignore cookies and newBackend, go straight to the clsi node
  276. opts.qs = Object.assign({ compileGroup, clsiserverid }, opts.qs)
  277. request(opts, (err, response, body) => {
  278. if (err) {
  279. return callback(
  280. OError.tag(err, 'error making request to CLSI', { projectId })
  281. )
  282. }
  283. callback(null, response, body)
  284. })
  285. } else {
  286. ClsiManager._makeRequest(projectId, userId, compileGroup, opts, callback)
  287. }
  288. },
  289. _makeRequest(projectId, userId, compileGroup, opts, callback) {
  290. async.series(
  291. {
  292. currentBackend(cb) {
  293. const startTime = new Date()
  294. ClsiCookieManager.getCookieJar(
  295. projectId,
  296. userId,
  297. compileGroup,
  298. (err, jar, clsiServerId) => {
  299. if (err != null) {
  300. return callback(
  301. OError.tag(err, 'error getting cookie jar for CLSI request', {
  302. projectId,
  303. })
  304. )
  305. }
  306. opts.jar = jar
  307. const timer = new Metrics.Timer('compile.currentBackend')
  308. request(opts, (err, response, body) => {
  309. if (err != null) {
  310. return callback(
  311. OError.tag(err, 'error making request to CLSI', {
  312. projectId,
  313. })
  314. )
  315. }
  316. timer.done()
  317. Metrics.inc(
  318. `compile.currentBackend.response.${response.statusCode}`
  319. )
  320. ClsiCookieManager.setServerId(
  321. projectId,
  322. userId,
  323. compileGroup,
  324. response,
  325. clsiServerId,
  326. (err, newClsiServerId) => {
  327. if (err != null) {
  328. callback(
  329. OError.tag(err, 'error setting server id', {
  330. projectId,
  331. })
  332. )
  333. } else {
  334. // return as soon as the standard compile has returned
  335. callback(
  336. null,
  337. response,
  338. body,
  339. newClsiServerId || clsiServerId
  340. )
  341. }
  342. cb(err, {
  343. response,
  344. body,
  345. finishTime: new Date() - startTime,
  346. })
  347. }
  348. )
  349. })
  350. }
  351. )
  352. },
  353. newBackend(cb) {
  354. const startTime = new Date()
  355. ClsiManager._makeNewBackendRequest(
  356. projectId,
  357. userId,
  358. compileGroup,
  359. opts,
  360. (err, response, body) => {
  361. if (err != null) {
  362. logger.warn({ err }, 'Error making request to new CLSI backend')
  363. }
  364. if (response != null) {
  365. Metrics.inc(
  366. `compile.newBackend.response.${response.statusCode}`
  367. )
  368. }
  369. cb(err, {
  370. response,
  371. body,
  372. finishTime: new Date() - startTime,
  373. })
  374. }
  375. )
  376. },
  377. },
  378. (err, results) => {
  379. if (err != null) {
  380. // This was handled higher up
  381. return
  382. }
  383. if (results.newBackend != null && results.newBackend.response != null) {
  384. const currentStatusCode = results.currentBackend.response.statusCode
  385. const newStatusCode = results.newBackend.response.statusCode
  386. const statusCodeSame = newStatusCode === currentStatusCode
  387. const currentCompileTime = results.currentBackend.finishTime
  388. const newBackendCompileTime = results.newBackend.finishTime || 0
  389. const timeDifference = newBackendCompileTime - currentCompileTime
  390. logger.debug(
  391. {
  392. statusCodeSame,
  393. timeDifference,
  394. currentCompileTime,
  395. newBackendCompileTime,
  396. projectId,
  397. },
  398. 'both clsi requests returned'
  399. )
  400. }
  401. }
  402. )
  403. },
  404. _makeNewBackendRequest(projectId, userId, compileGroup, baseOpts, callback) {
  405. if (Settings.apis.clsi_new == null || Settings.apis.clsi_new.url == null) {
  406. return callback()
  407. }
  408. const opts = {
  409. ...baseOpts,
  410. url: baseOpts.url.replace(
  411. Settings.apis.clsi.url,
  412. Settings.apis.clsi_new.url
  413. ),
  414. }
  415. NewBackendCloudClsiCookieManager.getCookieJar(
  416. projectId,
  417. userId,
  418. compileGroup,
  419. (err, jar, clsiServerId) => {
  420. if (err != null) {
  421. return callback(
  422. OError.tag(err, 'error getting cookie jar for CLSI request', {
  423. projectId,
  424. })
  425. )
  426. }
  427. opts.jar = jar
  428. const timer = new Metrics.Timer('compile.newBackend')
  429. request(opts, (err, response, body) => {
  430. timer.done()
  431. if (err != null) {
  432. return callback(
  433. OError.tag(err, 'error making request to new CLSI', {
  434. projectId,
  435. opts,
  436. })
  437. )
  438. }
  439. NewBackendCloudClsiCookieManager.setServerId(
  440. projectId,
  441. userId,
  442. compileGroup,
  443. response,
  444. clsiServerId,
  445. err => {
  446. if (err != null) {
  447. return callback(
  448. OError.tag(err, 'error setting server id on new backend', {
  449. projectId,
  450. })
  451. )
  452. }
  453. callback(null, response, body)
  454. }
  455. )
  456. })
  457. }
  458. )
  459. },
  460. _getCompilerUrl(compileGroup, projectId, userId, action) {
  461. const u = new URL(`/project/${projectId}`, Settings.apis.clsi.url)
  462. if (userId != null) {
  463. u.pathname += `/user/${userId}`
  464. }
  465. if (action != null) {
  466. u.pathname += `/${action}`
  467. }
  468. u.search = new URLSearchParams({ compileGroup }).toString()
  469. return u.href
  470. },
  471. _postToClsi(projectId, userId, req, compileGroup, callback) {
  472. const compileUrl = this._getCompilerUrl(
  473. compileGroup,
  474. projectId,
  475. userId,
  476. 'compile'
  477. )
  478. const opts = {
  479. url: compileUrl,
  480. json: req,
  481. method: 'POST',
  482. }
  483. ClsiManager._makeRequest(
  484. projectId,
  485. userId,
  486. compileGroup,
  487. opts,
  488. (err, response, body, clsiServerId) => {
  489. if (err != null) {
  490. return callback(
  491. new OError(
  492. 'failed to make request to CLSI',
  493. {
  494. projectId,
  495. userId,
  496. compileOptions: req.compile.options,
  497. rootResourcePath: req.compile.rootResourcePath,
  498. },
  499. err
  500. )
  501. )
  502. }
  503. if (response.statusCode >= 200 && response.statusCode < 300) {
  504. callback(null, body, clsiServerId)
  505. } else if (response.statusCode === 413) {
  506. callback(null, { compile: { status: 'project-too-large' } })
  507. } else if (response.statusCode === 409) {
  508. callback(null, { compile: { status: 'conflict' } })
  509. } else if (response.statusCode === 423) {
  510. callback(null, { compile: { status: 'compile-in-progress' } })
  511. } else if (response.statusCode === 503) {
  512. callback(null, { compile: { status: 'unavailable' } })
  513. } else {
  514. callback(
  515. new OError(
  516. `CLSI returned non-success code: ${response.statusCode}`,
  517. {
  518. projectId,
  519. userId,
  520. compileOptions: req.compile.options,
  521. rootResourcePath: req.compile.rootResourcePath,
  522. clsiResponse: body,
  523. statusCode: response.statusCode,
  524. }
  525. )
  526. )
  527. }
  528. }
  529. )
  530. },
  531. _parseOutputFiles(projectId, rawOutputFiles = []) {
  532. const outputFiles = []
  533. for (const file of rawOutputFiles) {
  534. outputFiles.push({
  535. path: file.path, // the clsi is now sending this to web
  536. url: new URL(file.url).pathname, // the location of the file on the clsi, excluding the host part
  537. type: file.type,
  538. build: file.build,
  539. contentId: file.contentId,
  540. ranges: file.ranges,
  541. size: file.size,
  542. })
  543. }
  544. return outputFiles
  545. },
  546. _buildRequest(projectId, options, callback) {
  547. if (options == null) {
  548. options = {}
  549. }
  550. ProjectGetter.getProject(
  551. projectId,
  552. { compiler: 1, rootDoc_id: 1, imageName: 1, rootFolder: 1 },
  553. (err, project) => {
  554. if (err != null) {
  555. return callback(
  556. OError.tag(err, 'failed to get project', { projectId })
  557. )
  558. }
  559. if (project == null) {
  560. return callback(
  561. new Errors.NotFoundError(`project does not exist: ${projectId}`)
  562. )
  563. }
  564. if (!VALID_COMPILERS.includes(project.compiler)) {
  565. project.compiler = 'pdflatex'
  566. }
  567. if (options.incrementalCompilesEnabled || options.syncType != null) {
  568. // new way, either incremental or full
  569. const timer = new Metrics.Timer('editor.compile-getdocs-redis')
  570. ClsiManager.getContentFromDocUpdaterIfMatch(
  571. projectId,
  572. project,
  573. options,
  574. (err, projectStateHash, docUpdaterDocs) => {
  575. timer.done()
  576. if (err != null) {
  577. logger.error({ err, projectId }, 'error checking project state')
  578. // note: we don't bail out when there's an error getting
  579. // incremental files from the docupdater, we just fall back
  580. // to a normal compile below
  581. }
  582. // see if we can send an incremental update to the CLSI
  583. if (
  584. docUpdaterDocs != null &&
  585. options.syncType !== 'full' &&
  586. err == null
  587. ) {
  588. Metrics.inc('compile-from-redis')
  589. ClsiManager._buildRequestFromDocupdater(
  590. projectId,
  591. options,
  592. project,
  593. projectStateHash,
  594. docUpdaterDocs,
  595. callback
  596. )
  597. } else {
  598. Metrics.inc('compile-from-mongo')
  599. ClsiManager._buildRequestFromMongo(
  600. projectId,
  601. options,
  602. project,
  603. projectStateHash,
  604. callback
  605. )
  606. }
  607. }
  608. )
  609. } else {
  610. // old way, always from mongo
  611. const timer = new Metrics.Timer('editor.compile-getdocs-mongo')
  612. ClsiManager._getContentFromMongo(projectId, (err, docs, files) => {
  613. timer.done()
  614. if (err != null) {
  615. return callback(
  616. OError.tag(err, 'failed to get contents from Mongo', {
  617. projectId,
  618. })
  619. )
  620. }
  621. ClsiManager._finaliseRequest(
  622. projectId,
  623. options,
  624. project,
  625. docs,
  626. files,
  627. callback
  628. )
  629. })
  630. }
  631. }
  632. )
  633. },
  634. getContentFromDocUpdaterIfMatch(projectId, project, options, callback) {
  635. const projectStateHash = ClsiStateManager.computeHash(project, options)
  636. DocumentUpdaterHandler.getProjectDocsIfMatch(
  637. projectId,
  638. projectStateHash,
  639. (err, docs) => {
  640. if (err != null) {
  641. return callback(
  642. OError.tag(err, 'Failed to get project documents', {
  643. projectId,
  644. projectStateHash,
  645. })
  646. )
  647. }
  648. callback(null, projectStateHash, docs)
  649. }
  650. )
  651. },
  652. getOutputFileStream(
  653. projectId,
  654. userId,
  655. compileGroup,
  656. clsiServerId,
  657. buildId,
  658. outputFilePath,
  659. callback
  660. ) {
  661. const url = `${Settings.apis.clsi.url}/project/${projectId}/user/${userId}/build/${buildId}/output/${outputFilePath}`
  662. // TODO(das7pad): remove one week after landing frontend changes.
  663. ClsiCookieManager.getCookieJar(projectId, userId, '', (err, jar) => {
  664. if (err != null) {
  665. return callback(
  666. OError.tag(err, 'Failed to get cookie jar', {
  667. projectId,
  668. userId,
  669. buildId,
  670. outputFilePath,
  671. })
  672. )
  673. }
  674. const options = { url, method: 'GET', timeout: 60 * 1000, jar }
  675. if (clsiServerId) {
  676. options.qs = { compileGroup, clsiserverid: clsiServerId }
  677. delete options.jar
  678. }
  679. const readStream = request(options)
  680. callback(null, readStream)
  681. })
  682. },
  683. _buildRequestFromDocupdater(
  684. projectId,
  685. options,
  686. project,
  687. projectStateHash,
  688. docUpdaterDocs,
  689. callback
  690. ) {
  691. const docPath = ProjectEntityHandler.getAllDocPathsFromProject(project)
  692. const docs = {}
  693. for (const doc of docUpdaterDocs || []) {
  694. const path = docPath[doc._id]
  695. docs[path] = doc
  696. }
  697. // send new docs but not files as those are already on the clsi
  698. options = _.clone(options)
  699. options.syncType = 'incremental'
  700. options.syncState = projectStateHash
  701. // create stub doc entries for any possible root docs, if not
  702. // present in the docupdater. This allows finaliseRequest to
  703. // identify the root doc.
  704. const possibleRootDocIds = [options.rootDoc_id, project.rootDoc_id]
  705. for (const rootDocId of possibleRootDocIds) {
  706. if (rootDocId != null && rootDocId in docPath) {
  707. const path = docPath[rootDocId]
  708. if (docs[path] == null) {
  709. docs[path] = { _id: rootDocId, path }
  710. }
  711. }
  712. }
  713. ClsiManager._finaliseRequest(
  714. projectId,
  715. options,
  716. project,
  717. docs,
  718. [],
  719. callback
  720. )
  721. },
  722. _buildRequestFromMongo(
  723. projectId,
  724. options,
  725. project,
  726. projectStateHash,
  727. callback
  728. ) {
  729. ClsiManager._getContentFromMongo(projectId, (err, docs, files) => {
  730. if (err != null) {
  731. return callback(
  732. OError.tag(err, 'failed to get project contents from Mongo', {
  733. projectId,
  734. })
  735. )
  736. }
  737. options = {
  738. ...options,
  739. syncType: 'full',
  740. syncState: projectStateHash,
  741. }
  742. ClsiManager._finaliseRequest(
  743. projectId,
  744. options,
  745. project,
  746. docs,
  747. files,
  748. callback
  749. )
  750. })
  751. },
  752. _getContentFromMongo(projectId, callback) {
  753. DocumentUpdaterHandler.flushProjectToMongo(projectId, err => {
  754. if (err != null) {
  755. return callback(
  756. OError.tag(err, 'failed to flush project to Mongo', { projectId })
  757. )
  758. }
  759. ProjectEntityHandler.getAllDocs(projectId, (err, docs) => {
  760. if (err != null) {
  761. return callback(
  762. OError.tag(err, 'failed to get project docs', { projectId })
  763. )
  764. }
  765. ProjectEntityHandler.getAllFiles(projectId, (err, files) => {
  766. if (err != null) {
  767. return callback(
  768. OError.tag(err, 'failed to get project files', { projectId })
  769. )
  770. }
  771. if (files == null) {
  772. files = {}
  773. }
  774. callback(null, docs || {}, files || {})
  775. })
  776. })
  777. })
  778. },
  779. _finaliseRequest(projectId, options, project, docs, files, callback) {
  780. const resources = []
  781. let flags
  782. let rootResourcePath = null
  783. let rootResourcePathOverride = null
  784. let hasMainFile = false
  785. let numberOfDocsInProject = 0
  786. for (let path in docs) {
  787. const doc = docs[path]
  788. path = path.replace(/^\//, '') // Remove leading /
  789. numberOfDocsInProject++
  790. if (doc.lines != null) {
  791. // add doc to resources unless it is just a stub entry
  792. resources.push({
  793. path,
  794. content: doc.lines.join('\n'),
  795. })
  796. }
  797. if (
  798. project.rootDoc_id != null &&
  799. doc._id.toString() === project.rootDoc_id.toString()
  800. ) {
  801. rootResourcePath = path
  802. }
  803. if (
  804. options.rootDoc_id != null &&
  805. doc._id.toString() === options.rootDoc_id.toString()
  806. ) {
  807. rootResourcePathOverride = path
  808. }
  809. if (path === 'main.tex') {
  810. hasMainFile = true
  811. }
  812. }
  813. if (rootResourcePathOverride != null) {
  814. rootResourcePath = rootResourcePathOverride
  815. }
  816. if (rootResourcePath == null) {
  817. if (hasMainFile) {
  818. rootResourcePath = 'main.tex'
  819. } else if (numberOfDocsInProject === 1) {
  820. // only one file, must be the main document
  821. for (const path in docs) {
  822. // Remove leading /
  823. rootResourcePath = path.replace(/^\//, '')
  824. }
  825. } else {
  826. return callback(new OError('no main file specified', { projectId }))
  827. }
  828. }
  829. for (let path in files) {
  830. const file = files[path]
  831. path = path.replace(/^\//, '') // Remove leading /
  832. resources.push({
  833. path,
  834. url: `${Settings.apis.filestore.url}/project/${project._id}/file/${file._id}`,
  835. modified: file.created != null ? file.created.getTime() : undefined,
  836. })
  837. }
  838. if (options.fileLineErrors) {
  839. flags = ['-file-line-error']
  840. }
  841. callback(null, {
  842. compile: {
  843. options: {
  844. compiler: project.compiler,
  845. timeout: options.timeout,
  846. imageName: project.imageName,
  847. draft: Boolean(options.draft),
  848. stopOnFirstError: Boolean(options.stopOnFirstError),
  849. check: options.check,
  850. syncType: options.syncType,
  851. syncState: options.syncState,
  852. compileGroup: options.compileGroup,
  853. enablePdfCaching:
  854. (Settings.enablePdfCaching && options.enablePdfCaching) || false,
  855. flags,
  856. metricsMethod: options.compileGroup,
  857. },
  858. rootResourcePath,
  859. resources,
  860. },
  861. })
  862. },
  863. wordCount(projectId, userId, file, options, clsiserverid, callback) {
  864. const { compileGroup } = options
  865. ClsiManager._buildRequest(projectId, options, (err, req) => {
  866. if (err != null) {
  867. return callback(
  868. OError.tag(err, 'Failed to build CLSI request', {
  869. projectId,
  870. options,
  871. })
  872. )
  873. }
  874. const filename = file || req.compile.rootResourcePath
  875. const wordCountUrl = ClsiManager._getCompilerUrl(
  876. compileGroup,
  877. projectId,
  878. userId,
  879. 'wordcount'
  880. )
  881. const opts = {
  882. url: wordCountUrl,
  883. qs: {
  884. file: filename,
  885. image: req.compile.options.imageName,
  886. },
  887. json: true,
  888. method: 'GET',
  889. }
  890. ClsiManager._makeRequestWithClsiServerId(
  891. projectId,
  892. userId,
  893. compileGroup,
  894. opts,
  895. clsiserverid,
  896. (err, response, body) => {
  897. if (err != null) {
  898. return callback(
  899. OError.tag(err, 'CLSI request failed', { projectId })
  900. )
  901. }
  902. if (response.statusCode >= 200 && response.statusCode < 300) {
  903. callback(null, body)
  904. } else {
  905. callback(
  906. new OError(
  907. `CLSI returned non-success code: ${response.statusCode}`,
  908. {
  909. projectId,
  910. clsiResponse: body,
  911. statusCode: response.statusCode,
  912. }
  913. )
  914. )
  915. }
  916. }
  917. )
  918. })
  919. },
  920. }
  921. module.exports = ClsiManager