ClsiManager.js 24 KB

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