CompileManager.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. no-return-assign,
  5. no-undef,
  6. no-unused-vars,
  7. */
  8. // TODO: This file was created by bulk-decaffeinate.
  9. // Fix any style issues and re-enable lint.
  10. /*
  11. * decaffeinate suggestions:
  12. * DS101: Remove unnecessary use of Array.from
  13. * DS102: Remove unnecessary code created because of implicit returns
  14. * DS103: Rewrite code to no longer use __guard__
  15. * DS207: Consider shorter variations of null checks
  16. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  17. */
  18. let CompileManager
  19. const ResourceWriter = require('./ResourceWriter')
  20. const LatexRunner = require('./LatexRunner')
  21. const OutputFileFinder = require('./OutputFileFinder')
  22. const OutputCacheManager = require('./OutputCacheManager')
  23. const Settings = require('settings-sharelatex')
  24. const Path = require('path')
  25. const logger = require('logger-sharelatex')
  26. const Metrics = require('./Metrics')
  27. const child_process = require('child_process')
  28. const DraftModeManager = require('./DraftModeManager')
  29. const TikzManager = require('./TikzManager')
  30. const LockManager = require('./LockManager')
  31. const fs = require('fs')
  32. const fse = require('fs-extra')
  33. const os = require('os')
  34. const async = require('async')
  35. const Errors = require('./Errors')
  36. const CommandRunner = require('./CommandRunner')
  37. const getCompileName = function(project_id, user_id) {
  38. if (user_id != null) {
  39. return `${project_id}-${user_id}`
  40. } else {
  41. return project_id
  42. }
  43. }
  44. const getCompileDir = (project_id, user_id) =>
  45. Path.join(Settings.path.compilesDir, getCompileName(project_id, user_id))
  46. module.exports = CompileManager = {
  47. doCompileWithLock(request, callback) {
  48. if (callback == null) {
  49. callback = function(error, outputFiles) {}
  50. }
  51. const compileDir = getCompileDir(request.project_id, request.user_id)
  52. const lockFile = Path.join(compileDir, '.project-lock')
  53. // use a .project-lock file in the compile directory to prevent
  54. // simultaneous compiles
  55. return fse.ensureDir(compileDir, function(error) {
  56. if (error != null) {
  57. return callback(error)
  58. }
  59. return LockManager.runWithLock(
  60. lockFile,
  61. releaseLock => CompileManager.doCompile(request, releaseLock),
  62. callback
  63. )
  64. })
  65. },
  66. doCompile(request, callback) {
  67. if (callback == null) {
  68. callback = function(error, outputFiles) {}
  69. }
  70. const compileDir = getCompileDir(request.project_id, request.user_id)
  71. let timer = new Metrics.Timer('write-to-disk')
  72. logger.log(
  73. { project_id: request.project_id, user_id: request.user_id },
  74. 'syncing resources to disk'
  75. )
  76. return ResourceWriter.syncResourcesToDisk(request, compileDir, function(
  77. error,
  78. resourceList
  79. ) {
  80. // NOTE: resourceList is insecure, it should only be used to exclude files from the output list
  81. if (error != null && error instanceof Errors.FilesOutOfSyncError) {
  82. logger.warn(
  83. { project_id: request.project_id, user_id: request.user_id },
  84. 'files out of sync, please retry'
  85. )
  86. return callback(error)
  87. } else if (error != null) {
  88. logger.err(
  89. {
  90. err: error,
  91. project_id: request.project_id,
  92. user_id: request.user_id
  93. },
  94. 'error writing resources to disk'
  95. )
  96. return callback(error)
  97. }
  98. logger.log(
  99. {
  100. project_id: request.project_id,
  101. user_id: request.user_id,
  102. time_taken: Date.now() - timer.start
  103. },
  104. 'written files to disk'
  105. )
  106. timer.done()
  107. const injectDraftModeIfRequired = function(callback) {
  108. if (request.draft) {
  109. return DraftModeManager.injectDraftMode(
  110. Path.join(compileDir, request.rootResourcePath),
  111. callback
  112. )
  113. } else {
  114. return callback()
  115. }
  116. }
  117. const createTikzFileIfRequired = callback =>
  118. TikzManager.checkMainFile(
  119. compileDir,
  120. request.rootResourcePath,
  121. resourceList,
  122. function(error, needsMainFile) {
  123. if (error != null) {
  124. return callback(error)
  125. }
  126. if (needsMainFile) {
  127. return TikzManager.injectOutputFile(
  128. compileDir,
  129. request.rootResourcePath,
  130. callback
  131. )
  132. } else {
  133. return callback()
  134. }
  135. }
  136. )
  137. // set up environment variables for chktex
  138. const env = {}
  139. if (Settings.texliveOpenoutAny && Settings.texliveOpenoutAny !== '') {
  140. // override default texlive openout_any environment variable
  141. env.openout_any = Settings.texliveOpenoutAny
  142. }
  143. // only run chktex on LaTeX files (not knitr .Rtex files or any others)
  144. const isLaTeXFile =
  145. request.rootResourcePath != null
  146. ? request.rootResourcePath.match(/\.tex$/i)
  147. : undefined
  148. if (request.check != null && isLaTeXFile) {
  149. env.CHKTEX_OPTIONS = '-nall -e9 -e10 -w15 -w16'
  150. env.CHKTEX_ULIMIT_OPTIONS = '-t 5 -v 64000'
  151. if (request.check === 'error') {
  152. env.CHKTEX_EXIT_ON_ERROR = 1
  153. }
  154. if (request.check === 'validate') {
  155. env.CHKTEX_VALIDATE = 1
  156. }
  157. }
  158. // apply a series of file modifications/creations for draft mode and tikz
  159. return async.series(
  160. [injectDraftModeIfRequired, createTikzFileIfRequired],
  161. function(error) {
  162. if (error != null) {
  163. return callback(error)
  164. }
  165. timer = new Metrics.Timer('run-compile')
  166. // find the image tag to log it as a metric, e.g. 2015.1 (convert . to - for graphite)
  167. let tag =
  168. __guard__(
  169. __guard__(
  170. request.imageName != null
  171. ? request.imageName.match(/:(.*)/)
  172. : undefined,
  173. x1 => x1[1]
  174. ),
  175. x => x.replace(/\./g, '-')
  176. ) || 'default'
  177. if (!request.project_id.match(/^[0-9a-f]{24}$/)) {
  178. tag = 'other'
  179. } // exclude smoke test
  180. Metrics.inc('compiles')
  181. Metrics.inc(`compiles-with-image.${tag}`)
  182. const compileName = getCompileName(
  183. request.project_id,
  184. request.user_id
  185. )
  186. return LatexRunner.runLatex(
  187. compileName,
  188. {
  189. directory: compileDir,
  190. mainFile: request.rootResourcePath,
  191. compiler: request.compiler,
  192. timeout: request.timeout,
  193. image: request.imageName,
  194. flags: request.flags,
  195. environment: env,
  196. compileGroup: request.compileGroup
  197. },
  198. function(error, output, stats, timings) {
  199. // request was for validation only
  200. let metric_key, metric_value
  201. if (request.check === 'validate') {
  202. const result = (error != null
  203. ? error.code
  204. : undefined)
  205. ? 'fail'
  206. : 'pass'
  207. error = new Error('validation')
  208. error.validate = result
  209. }
  210. // request was for compile, and failed on validation
  211. if (
  212. request.check === 'error' &&
  213. (error != null ? error.message : undefined) === 'exited'
  214. ) {
  215. error = new Error('compilation')
  216. error.validate = 'fail'
  217. }
  218. // compile was killed by user, was a validation, or a compile which failed validation
  219. if (
  220. (error != null ? error.terminated : undefined) ||
  221. (error != null ? error.validate : undefined) ||
  222. (error != null ? error.timedout : undefined)
  223. ) {
  224. OutputFileFinder.findOutputFiles(
  225. resourceList,
  226. compileDir,
  227. function(err, outputFiles) {
  228. if (err != null) {
  229. return callback(err)
  230. }
  231. error.outputFiles = outputFiles // return output files so user can check logs
  232. return callback(error)
  233. }
  234. )
  235. return
  236. }
  237. // compile completed normally
  238. if (error != null) {
  239. return callback(error)
  240. }
  241. Metrics.inc('compiles-succeeded')
  242. const object = stats || {}
  243. for (metric_key in object) {
  244. metric_value = object[metric_key]
  245. Metrics.count(metric_key, metric_value)
  246. }
  247. const object1 = timings || {}
  248. for (metric_key in object1) {
  249. metric_value = object1[metric_key]
  250. Metrics.timing(metric_key, metric_value)
  251. }
  252. const loadavg =
  253. typeof os.loadavg === 'function' ? os.loadavg() : undefined
  254. if (loadavg != null) {
  255. Metrics.gauge('load-avg', loadavg[0])
  256. }
  257. const ts = timer.done()
  258. logger.log(
  259. {
  260. project_id: request.project_id,
  261. user_id: request.user_id,
  262. time_taken: ts,
  263. stats,
  264. timings,
  265. loadavg
  266. },
  267. 'done compile'
  268. )
  269. if ((stats != null ? stats['latex-runs'] : undefined) > 0) {
  270. Metrics.timing('run-compile-per-pass', ts / stats['latex-runs'])
  271. }
  272. if (
  273. (stats != null ? stats['latex-runs'] : undefined) > 0 &&
  274. (timings != null ? timings['cpu-time'] : undefined) > 0
  275. ) {
  276. Metrics.timing(
  277. 'run-compile-cpu-time-per-pass',
  278. timings['cpu-time'] / stats['latex-runs']
  279. )
  280. }
  281. return OutputFileFinder.findOutputFiles(
  282. resourceList,
  283. compileDir,
  284. function(error, outputFiles) {
  285. if (error != null) {
  286. return callback(error)
  287. }
  288. return OutputCacheManager.saveOutputFiles(
  289. outputFiles,
  290. compileDir,
  291. (error, newOutputFiles) => callback(null, newOutputFiles)
  292. )
  293. }
  294. )
  295. }
  296. )
  297. }
  298. )
  299. })
  300. },
  301. stopCompile(project_id, user_id, callback) {
  302. if (callback == null) {
  303. callback = function(error) {}
  304. }
  305. const compileName = getCompileName(project_id, user_id)
  306. return LatexRunner.killLatex(compileName, callback)
  307. },
  308. clearProject(project_id, user_id, _callback) {
  309. if (_callback == null) {
  310. _callback = function(error) {}
  311. }
  312. const callback = function(error) {
  313. _callback(error)
  314. return (_callback = function() {})
  315. }
  316. const compileDir = getCompileDir(project_id, user_id)
  317. return CompileManager._checkDirectory(compileDir, function(err, exists) {
  318. if (err != null) {
  319. return callback(err)
  320. }
  321. if (!exists) {
  322. return callback()
  323. } // skip removal if no directory present
  324. const proc = child_process.spawn('rm', ['-r', compileDir])
  325. proc.on('error', callback)
  326. let stderr = ''
  327. proc.stderr.setEncoding('utf8').on('data', chunk => (stderr += chunk))
  328. return proc.on('close', function(code) {
  329. if (code === 0) {
  330. return callback(null)
  331. } else {
  332. return callback(new Error(`rm -r ${compileDir} failed: ${stderr}`))
  333. }
  334. })
  335. })
  336. },
  337. _findAllDirs(callback) {
  338. if (callback == null) {
  339. callback = function(error, allDirs) {}
  340. }
  341. const root = Settings.path.compilesDir
  342. return fs.readdir(root, function(err, files) {
  343. if (err != null) {
  344. return callback(err)
  345. }
  346. const allDirs = Array.from(files).map(file => Path.join(root, file))
  347. return callback(null, allDirs)
  348. })
  349. },
  350. clearExpiredProjects(max_cache_age_ms, callback) {
  351. if (callback == null) {
  352. callback = function(error) {}
  353. }
  354. const now = Date.now()
  355. // action for each directory
  356. const expireIfNeeded = (checkDir, cb) =>
  357. fs.stat(checkDir, function(err, stats) {
  358. if (err != null) {
  359. return cb()
  360. } // ignore errors checking directory
  361. const age = now - stats.mtime
  362. const hasExpired = age > max_cache_age_ms
  363. if (hasExpired) {
  364. return fse.remove(checkDir, cb)
  365. } else {
  366. return cb()
  367. }
  368. })
  369. // iterate over all project directories
  370. return CompileManager._findAllDirs(function(error, allDirs) {
  371. if (error != null) {
  372. return callback()
  373. }
  374. return async.eachSeries(allDirs, expireIfNeeded, callback)
  375. })
  376. },
  377. _checkDirectory(compileDir, callback) {
  378. if (callback == null) {
  379. callback = function(error, exists) {}
  380. }
  381. return fs.lstat(compileDir, function(err, stats) {
  382. if ((err != null ? err.code : undefined) === 'ENOENT') {
  383. return callback(null, false) // directory does not exist
  384. } else if (err != null) {
  385. logger.err(
  386. { dir: compileDir, err },
  387. 'error on stat of project directory for removal'
  388. )
  389. return callback(err)
  390. } else if (!(stats != null ? stats.isDirectory() : undefined)) {
  391. logger.err(
  392. { dir: compileDir, stats },
  393. 'bad project directory for removal'
  394. )
  395. return callback(new Error('project directory is not directory'))
  396. } else {
  397. return callback(null, true)
  398. }
  399. })
  400. }, // directory exists
  401. syncFromCode(project_id, user_id, file_name, line, column, callback) {
  402. // If LaTeX was run in a virtual environment, the file path that synctex expects
  403. // might not match the file path on the host. The .synctex.gz file however, will be accessed
  404. // wherever it is on the host.
  405. if (callback == null) {
  406. callback = function(error, pdfPositions) {}
  407. }
  408. const compileName = getCompileName(project_id, user_id)
  409. const base_dir = Settings.path.synctexBaseDir(compileName)
  410. const file_path = base_dir + '/' + file_name
  411. const compileDir = getCompileDir(project_id, user_id)
  412. const synctex_path = `${base_dir}/output.pdf`
  413. const command = ['code', synctex_path, file_path, line, column]
  414. return fse.ensureDir(compileDir, function(error) {
  415. if (error != null) {
  416. logger.err(
  417. { error, project_id, user_id, file_name },
  418. 'error ensuring dir for sync from code'
  419. )
  420. return callback(error)
  421. }
  422. return CompileManager._runSynctex(project_id, user_id, command, function(
  423. error,
  424. stdout
  425. ) {
  426. if (error != null) {
  427. return callback(error)
  428. }
  429. logger.log(
  430. { project_id, user_id, file_name, line, column, command, stdout },
  431. 'synctex code output'
  432. )
  433. return callback(
  434. null,
  435. CompileManager._parseSynctexFromCodeOutput(stdout)
  436. )
  437. })
  438. })
  439. },
  440. syncFromPdf(project_id, user_id, page, h, v, callback) {
  441. if (callback == null) {
  442. callback = function(error, filePositions) {}
  443. }
  444. const compileName = getCompileName(project_id, user_id)
  445. const compileDir = getCompileDir(project_id, user_id)
  446. const base_dir = Settings.path.synctexBaseDir(compileName)
  447. const synctex_path = `${base_dir}/output.pdf`
  448. const command = ['pdf', synctex_path, page, h, v]
  449. return fse.ensureDir(compileDir, function(error) {
  450. if (error != null) {
  451. logger.err(
  452. { error, project_id, user_id, file_name },
  453. 'error ensuring dir for sync to code'
  454. )
  455. return callback(error)
  456. }
  457. return CompileManager._runSynctex(project_id, user_id, command, function(
  458. error,
  459. stdout
  460. ) {
  461. if (error != null) {
  462. return callback(error)
  463. }
  464. logger.log(
  465. { project_id, user_id, page, h, v, stdout },
  466. 'synctex pdf output'
  467. )
  468. return callback(
  469. null,
  470. CompileManager._parseSynctexFromPdfOutput(stdout, base_dir)
  471. )
  472. })
  473. })
  474. },
  475. _checkFileExists(path, callback) {
  476. if (callback == null) {
  477. callback = function(error) {}
  478. }
  479. const synctexDir = Path.dirname(path)
  480. const synctexFile = Path.join(synctexDir, 'output.synctex.gz')
  481. return fs.stat(synctexDir, function(error, stats) {
  482. if ((error != null ? error.code : undefined) === 'ENOENT') {
  483. return callback(
  484. new Errors.NotFoundError('called synctex with no output directory')
  485. )
  486. }
  487. if (error != null) {
  488. return callback(error)
  489. }
  490. return fs.stat(synctexFile, function(error, stats) {
  491. if ((error != null ? error.code : undefined) === 'ENOENT') {
  492. return callback(
  493. new Errors.NotFoundError('called synctex with no output file')
  494. )
  495. }
  496. if (error != null) {
  497. return callback(error)
  498. }
  499. if (!(stats != null ? stats.isFile() : undefined)) {
  500. return callback(new Error('not a file'))
  501. }
  502. return callback()
  503. })
  504. })
  505. },
  506. _runSynctex(project_id, user_id, command, callback) {
  507. if (callback == null) {
  508. callback = function(error, stdout) {}
  509. }
  510. const seconds = 1000
  511. command.unshift('/opt/synctex')
  512. const directory = getCompileDir(project_id, user_id)
  513. const timeout = 60 * 1000 // increased to allow for large projects
  514. const compileName = getCompileName(project_id, user_id)
  515. const compileGroup = 'synctex'
  516. return CommandRunner.run(
  517. compileName,
  518. command,
  519. directory,
  520. Settings.clsi != null ? Settings.clsi.docker.image : undefined,
  521. timeout,
  522. {},
  523. compileGroup,
  524. function(error, output) {
  525. if (error != null) {
  526. logger.err(
  527. { err: error, command, project_id, user_id },
  528. 'error running synctex'
  529. )
  530. return callback(error)
  531. }
  532. return callback(null, output.stdout)
  533. }
  534. )
  535. },
  536. _parseSynctexFromCodeOutput(output) {
  537. const results = []
  538. for (const line of Array.from(output.split('\n'))) {
  539. const [node, page, h, v, width, height] = Array.from(line.split('\t'))
  540. if (node === 'NODE') {
  541. results.push({
  542. page: parseInt(page, 10),
  543. h: parseFloat(h),
  544. v: parseFloat(v),
  545. height: parseFloat(height),
  546. width: parseFloat(width)
  547. })
  548. }
  549. }
  550. return results
  551. },
  552. _parseSynctexFromPdfOutput(output, base_dir) {
  553. const results = []
  554. for (let line of Array.from(output.split('\n'))) {
  555. let column, file_path, node
  556. ;[node, file_path, line, column] = Array.from(line.split('\t'))
  557. if (node === 'NODE') {
  558. const file = file_path.slice(base_dir.length + 1)
  559. results.push({
  560. file,
  561. line: parseInt(line, 10),
  562. column: parseInt(column, 10)
  563. })
  564. }
  565. }
  566. return results
  567. },
  568. wordcount(project_id, user_id, file_name, image, callback) {
  569. if (callback == null) {
  570. callback = function(error, pdfPositions) {}
  571. }
  572. logger.log({ project_id, user_id, file_name, image }, 'running wordcount')
  573. const file_path = `$COMPILE_DIR/${file_name}`
  574. const command = [
  575. 'texcount',
  576. '-nocol',
  577. '-inc',
  578. file_path,
  579. `-out=${file_path}.wc`
  580. ]
  581. const compileDir = getCompileDir(project_id, user_id)
  582. const timeout = 60 * 1000
  583. const compileName = getCompileName(project_id, user_id)
  584. const compileGroup = 'wordcount'
  585. return fse.ensureDir(compileDir, function(error) {
  586. if (error != null) {
  587. logger.err(
  588. { error, project_id, user_id, file_name },
  589. 'error ensuring dir for sync from code'
  590. )
  591. return callback(error)
  592. }
  593. return CommandRunner.run(
  594. compileName,
  595. command,
  596. compileDir,
  597. image,
  598. timeout,
  599. {},
  600. compileGroup,
  601. function(error) {
  602. if (error != null) {
  603. return callback(error)
  604. }
  605. return fs.readFile(
  606. compileDir + '/' + file_name + '.wc',
  607. 'utf-8',
  608. function(err, stdout) {
  609. if (err != null) {
  610. // call it node_err so sentry doesn't use random path error as unique id so it can't be ignored
  611. logger.err(
  612. { node_err: err, command, compileDir, project_id, user_id },
  613. 'error reading word count output'
  614. )
  615. return callback(err)
  616. }
  617. const results = CompileManager._parseWordcountFromOutput(stdout)
  618. logger.log(
  619. { project_id, user_id, wordcount: results },
  620. 'word count results'
  621. )
  622. return callback(null, results)
  623. }
  624. )
  625. }
  626. )
  627. })
  628. },
  629. _parseWordcountFromOutput(output) {
  630. const results = {
  631. encode: '',
  632. textWords: 0,
  633. headWords: 0,
  634. outside: 0,
  635. headers: 0,
  636. elements: 0,
  637. mathInline: 0,
  638. mathDisplay: 0,
  639. errors: 0,
  640. messages: ''
  641. }
  642. for (const line of Array.from(output.split('\n'))) {
  643. const [data, info] = Array.from(line.split(':'))
  644. if (data.indexOf('Encoding') > -1) {
  645. results.encode = info.trim()
  646. }
  647. if (data.indexOf('in text') > -1) {
  648. results.textWords = parseInt(info, 10)
  649. }
  650. if (data.indexOf('in head') > -1) {
  651. results.headWords = parseInt(info, 10)
  652. }
  653. if (data.indexOf('outside') > -1) {
  654. results.outside = parseInt(info, 10)
  655. }
  656. if (data.indexOf('of head') > -1) {
  657. results.headers = parseInt(info, 10)
  658. }
  659. if (data.indexOf('Number of floats/tables/figures') > -1) {
  660. results.elements = parseInt(info, 10)
  661. }
  662. if (data.indexOf('Number of math inlines') > -1) {
  663. results.mathInline = parseInt(info, 10)
  664. }
  665. if (data.indexOf('Number of math displayed') > -1) {
  666. results.mathDisplay = parseInt(info, 10)
  667. }
  668. if (data === '(errors') {
  669. // errors reported as (errors:123)
  670. results.errors = parseInt(info, 10)
  671. }
  672. if (line.indexOf('!!! ') > -1) {
  673. // errors logged as !!! message !!!
  674. results.messages += line + '\n'
  675. }
  676. }
  677. return results
  678. }
  679. }
  680. function __guard__(value, transform) {
  681. return typeof value !== 'undefined' && value !== null
  682. ? transform(value)
  683. : undefined
  684. }