ClsiManager.js 25 KB

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