ClsiManager.js 26 KB

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