ClsiManager.js 27 KB

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