CompileManager.js 23 KB

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