ClsiManager.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921
  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 } = 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. const projectStateHash = ClsiStateManager.computeHash(project, options)
  624. DocumentUpdaterHandler.getProjectDocsIfMatch(
  625. projectId,
  626. projectStateHash,
  627. (err, docs) => {
  628. if (err != null) {
  629. return callback(
  630. OError.tag(err, 'Failed to get project documents', {
  631. projectId,
  632. projectStateHash,
  633. })
  634. )
  635. }
  636. callback(null, projectStateHash, docs)
  637. }
  638. )
  639. },
  640. getOutputFileStream(projectId, userId, buildId, outputFilePath, callback) {
  641. const url = `${Settings.apis.clsi.url}/project/${projectId}/user/${userId}/build/${buildId}/output/${outputFilePath}`
  642. ClsiCookieManager.getCookieJar(projectId, userId, (err, jar) => {
  643. if (err != null) {
  644. return callback(
  645. OError.tag(err, 'Failed to get cookie jar', {
  646. projectId,
  647. userId,
  648. buildId,
  649. outputFilePath,
  650. })
  651. )
  652. }
  653. const options = { url, method: 'GET', timeout: 60 * 1000, jar }
  654. const readStream = request(options)
  655. callback(null, readStream)
  656. })
  657. },
  658. _buildRequestFromDocupdater(
  659. projectId,
  660. options,
  661. project,
  662. projectStateHash,
  663. docUpdaterDocs,
  664. callback
  665. ) {
  666. const docPath = ProjectEntityHandler.getAllDocPathsFromProject(project)
  667. const docs = {}
  668. for (const doc of docUpdaterDocs || []) {
  669. const path = docPath[doc._id]
  670. docs[path] = doc
  671. }
  672. // send new docs but not files as those are already on the clsi
  673. options = _.clone(options)
  674. options.syncType = 'incremental'
  675. options.syncState = projectStateHash
  676. // create stub doc entries for any possible root docs, if not
  677. // present in the docupdater. This allows finaliseRequest to
  678. // identify the root doc.
  679. const possibleRootDocIds = [options.rootDoc_id, project.rootDoc_id]
  680. for (const rootDocId of possibleRootDocIds) {
  681. if (rootDocId != null && rootDocId in docPath) {
  682. const path = docPath[rootDocId]
  683. if (docs[path] == null) {
  684. docs[path] = { _id: rootDocId, path }
  685. }
  686. }
  687. }
  688. ClsiManager._finaliseRequest(
  689. projectId,
  690. options,
  691. project,
  692. docs,
  693. [],
  694. callback
  695. )
  696. },
  697. _buildRequestFromMongo(
  698. projectId,
  699. options,
  700. project,
  701. projectStateHash,
  702. callback
  703. ) {
  704. ClsiManager._getContentFromMongo(projectId, (err, docs, files) => {
  705. if (err != null) {
  706. return callback(
  707. OError.tag(err, 'failed to get project contents from Mongo', {
  708. projectId,
  709. })
  710. )
  711. }
  712. options = {
  713. ...options,
  714. syncType: 'full',
  715. syncState: projectStateHash,
  716. }
  717. ClsiManager._finaliseRequest(
  718. projectId,
  719. options,
  720. project,
  721. docs,
  722. files,
  723. callback
  724. )
  725. })
  726. },
  727. _getContentFromMongo(projectId, callback) {
  728. DocumentUpdaterHandler.flushProjectToMongo(projectId, err => {
  729. if (err != null) {
  730. return callback(
  731. OError.tag(err, 'failed to flush project to Mongo', { projectId })
  732. )
  733. }
  734. ProjectEntityHandler.getAllDocs(projectId, (err, docs) => {
  735. if (err != null) {
  736. return callback(
  737. OError.tag(err, 'failed to get project docs', { projectId })
  738. )
  739. }
  740. ProjectEntityHandler.getAllFiles(projectId, (err, files) => {
  741. if (err != null) {
  742. return callback(
  743. OError.tag(err, 'failed to get project files', { projectId })
  744. )
  745. }
  746. if (files == null) {
  747. files = {}
  748. }
  749. callback(null, docs || {}, files || {})
  750. })
  751. })
  752. })
  753. },
  754. _finaliseRequest(projectId, options, project, docs, files, callback) {
  755. const resources = []
  756. let flags
  757. let rootResourcePath = null
  758. let rootResourcePathOverride = null
  759. let hasMainFile = false
  760. let numberOfDocsInProject = 0
  761. for (let path in docs) {
  762. const doc = docs[path]
  763. path = path.replace(/^\//, '') // Remove leading /
  764. numberOfDocsInProject++
  765. if (doc.lines != null) {
  766. // add doc to resources unless it is just a stub entry
  767. resources.push({
  768. path,
  769. content: doc.lines.join('\n'),
  770. })
  771. }
  772. if (
  773. project.rootDoc_id != null &&
  774. doc._id.toString() === project.rootDoc_id.toString()
  775. ) {
  776. rootResourcePath = path
  777. }
  778. if (
  779. options.rootDoc_id != null &&
  780. doc._id.toString() === options.rootDoc_id.toString()
  781. ) {
  782. rootResourcePathOverride = path
  783. }
  784. if (path === 'main.tex') {
  785. hasMainFile = true
  786. }
  787. }
  788. if (rootResourcePathOverride != null) {
  789. rootResourcePath = rootResourcePathOverride
  790. }
  791. if (rootResourcePath == null) {
  792. if (hasMainFile) {
  793. rootResourcePath = 'main.tex'
  794. } else if (numberOfDocsInProject === 1) {
  795. // only one file, must be the main document
  796. for (const path in docs) {
  797. // Remove leading /
  798. rootResourcePath = path.replace(/^\//, '')
  799. }
  800. } else {
  801. return callback(new OError('no main file specified', { projectId }))
  802. }
  803. }
  804. for (let path in files) {
  805. const file = files[path]
  806. path = path.replace(/^\//, '') // Remove leading /
  807. resources.push({
  808. path,
  809. url: `${Settings.apis.filestore.url}/project/${project._id}/file/${file._id}`,
  810. modified: file.created != null ? file.created.getTime() : undefined,
  811. })
  812. }
  813. if (options.fileLineErrors) {
  814. flags = ['-file-line-error']
  815. }
  816. callback(null, {
  817. compile: {
  818. options: {
  819. compiler: project.compiler,
  820. timeout: options.timeout,
  821. imageName: project.imageName,
  822. draft: !!options.draft,
  823. check: options.check,
  824. syncType: options.syncType,
  825. syncState: options.syncState,
  826. compileGroup: options.compileGroup,
  827. enablePdfCaching:
  828. (Settings.enablePdfCaching && options.enablePdfCaching) || false,
  829. flags: flags,
  830. },
  831. rootResourcePath,
  832. resources,
  833. },
  834. })
  835. },
  836. wordCount(projectId, userId, file, options, clsiserverid, callback) {
  837. ClsiManager._buildRequest(projectId, options, (err, req) => {
  838. if (err != null) {
  839. return callback(
  840. OError.tag(err, 'Failed to build CLSI request', {
  841. projectId,
  842. options,
  843. })
  844. )
  845. }
  846. const filename = file || req.compile.rootResourcePath
  847. const wordCountUrl = ClsiManager._getCompilerUrl(
  848. options.compileGroup,
  849. projectId,
  850. userId,
  851. 'wordcount'
  852. )
  853. const opts = {
  854. url: wordCountUrl,
  855. qs: {
  856. file: filename,
  857. image: req.compile.options.imageName,
  858. },
  859. json: true,
  860. method: 'GET',
  861. }
  862. ClsiManager._makeRequestWithClsiServerId(
  863. projectId,
  864. userId,
  865. opts,
  866. clsiserverid,
  867. (err, response, body) => {
  868. if (err != null) {
  869. return callback(
  870. OError.tag(err, 'CLSI request failed', { projectId })
  871. )
  872. }
  873. if (response.statusCode >= 200 && response.statusCode < 300) {
  874. callback(null, body)
  875. } else {
  876. callback(
  877. new OError(
  878. `CLSI returned non-success code: ${response.statusCode}`,
  879. {
  880. projectId,
  881. clsiResponse: body,
  882. statusCode: response.statusCode,
  883. }
  884. )
  885. )
  886. }
  887. }
  888. )
  889. })
  890. },
  891. }
  892. module.exports = ClsiManager