ClsiManager.js 26 KB

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