ClsiManager.js 22 KB

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