CompileManager.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
  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. return OutputFileFinder.findOutputFiles(
  287. resourceList,
  288. compileDir,
  289. function (error, outputFiles) {
  290. if (error != null) {
  291. return callback(error)
  292. }
  293. return OutputCacheManager.saveOutputFiles(
  294. { request, stats, timings },
  295. outputFiles,
  296. compileDir,
  297. outputDir,
  298. (err, newOutputFiles) => {
  299. if (err) {
  300. const {
  301. project_id: projectId,
  302. user_id: userId
  303. } = request
  304. logger.err(
  305. { projectId, userId, err },
  306. 'failed to save output files'
  307. )
  308. }
  309. // Emit compile time.
  310. timings.compile = ts
  311. timings.compileE2E = timerE2E.done()
  312. if (stats['pdf-size']) {
  313. emitPdfStats(stats, timings)
  314. }
  315. callback(null, newOutputFiles, stats, timings)
  316. }
  317. )
  318. }
  319. )
  320. }
  321. )
  322. }
  323. )
  324. })
  325. },
  326. stopCompile(project_id, user_id, callback) {
  327. if (callback == null) {
  328. callback = function (error) {}
  329. }
  330. const compileName = getCompileName(project_id, user_id)
  331. return LatexRunner.killLatex(compileName, callback)
  332. },
  333. clearProject(project_id, user_id, _callback) {
  334. if (_callback == null) {
  335. _callback = function (error) {}
  336. }
  337. const callback = function (error) {
  338. _callback(error)
  339. return (_callback = function () {})
  340. }
  341. const compileDir = getCompileDir(project_id, user_id)
  342. const outputDir = getOutputDir(project_id, user_id)
  343. return CompileManager._checkDirectory(compileDir, function (err, exists) {
  344. if (err != null) {
  345. return callback(err)
  346. }
  347. if (!exists) {
  348. return callback()
  349. } // skip removal if no directory present
  350. const proc = child_process.spawn('rm', [
  351. '-r',
  352. '-f',
  353. '--',
  354. compileDir,
  355. outputDir
  356. ])
  357. proc.on('error', callback)
  358. let stderr = ''
  359. proc.stderr.setEncoding('utf8').on('data', (chunk) => (stderr += chunk))
  360. return proc.on('close', function (code) {
  361. if (code === 0) {
  362. return callback(null)
  363. } else {
  364. return callback(
  365. new Error(`rm -r ${compileDir} ${outputDir} failed: ${stderr}`)
  366. )
  367. }
  368. })
  369. })
  370. },
  371. _findAllDirs(callback) {
  372. if (callback == null) {
  373. callback = function (error, allDirs) {}
  374. }
  375. const root = Settings.path.compilesDir
  376. return fs.readdir(root, function (err, files) {
  377. if (err != null) {
  378. return callback(err)
  379. }
  380. const allDirs = Array.from(files).map((file) => Path.join(root, file))
  381. return callback(null, allDirs)
  382. })
  383. },
  384. clearExpiredProjects(max_cache_age_ms, callback) {
  385. if (callback == null) {
  386. callback = function (error) {}
  387. }
  388. const now = Date.now()
  389. // action for each directory
  390. const expireIfNeeded = (checkDir, cb) =>
  391. fs.stat(checkDir, function (err, stats) {
  392. if (err != null) {
  393. return cb()
  394. } // ignore errors checking directory
  395. const age = now - stats.mtime
  396. const hasExpired = age > max_cache_age_ms
  397. if (hasExpired) {
  398. return fse.remove(checkDir, cb)
  399. } else {
  400. return cb()
  401. }
  402. })
  403. // iterate over all project directories
  404. return CompileManager._findAllDirs(function (error, allDirs) {
  405. if (error != null) {
  406. return callback()
  407. }
  408. return async.eachSeries(allDirs, expireIfNeeded, callback)
  409. })
  410. },
  411. _checkDirectory(compileDir, callback) {
  412. if (callback == null) {
  413. callback = function (error, exists) {}
  414. }
  415. return fs.lstat(compileDir, function (err, stats) {
  416. if ((err != null ? err.code : undefined) === 'ENOENT') {
  417. return callback(null, false) // directory does not exist
  418. } else if (err != null) {
  419. logger.err(
  420. { dir: compileDir, err },
  421. 'error on stat of project directory for removal'
  422. )
  423. return callback(err)
  424. } else if (!(stats != null ? stats.isDirectory() : undefined)) {
  425. logger.err(
  426. { dir: compileDir, stats },
  427. 'bad project directory for removal'
  428. )
  429. return callback(new Error('project directory is not directory'))
  430. } else {
  431. return callback(null, true)
  432. }
  433. })
  434. }, // directory exists
  435. syncFromCode(
  436. project_id,
  437. user_id,
  438. file_name,
  439. line,
  440. column,
  441. imageName,
  442. callback
  443. ) {
  444. // If LaTeX was run in a virtual environment, the file path that synctex expects
  445. // might not match the file path on the host. The .synctex.gz file however, will be accessed
  446. // wherever it is on the host.
  447. if (callback == null) {
  448. callback = function (error, pdfPositions) {}
  449. }
  450. const compileName = getCompileName(project_id, user_id)
  451. const base_dir = Settings.path.synctexBaseDir(compileName)
  452. const file_path = base_dir + '/' + file_name
  453. const compileDir = getCompileDir(project_id, user_id)
  454. const synctex_path = `${base_dir}/output.pdf`
  455. const command = ['code', synctex_path, file_path, line, column]
  456. CompileManager._runSynctex(
  457. project_id,
  458. user_id,
  459. command,
  460. imageName,
  461. function (error, stdout) {
  462. if (error != null) {
  463. return callback(error)
  464. }
  465. logger.log(
  466. { project_id, user_id, file_name, line, column, command, stdout },
  467. 'synctex code output'
  468. )
  469. return callback(
  470. null,
  471. CompileManager._parseSynctexFromCodeOutput(stdout)
  472. )
  473. }
  474. )
  475. },
  476. syncFromPdf(project_id, user_id, page, h, v, imageName, callback) {
  477. if (callback == null) {
  478. callback = function (error, filePositions) {}
  479. }
  480. const compileName = getCompileName(project_id, user_id)
  481. const compileDir = getCompileDir(project_id, user_id)
  482. const base_dir = Settings.path.synctexBaseDir(compileName)
  483. const synctex_path = `${base_dir}/output.pdf`
  484. const command = ['pdf', synctex_path, page, h, v]
  485. CompileManager._runSynctex(
  486. project_id,
  487. user_id,
  488. command,
  489. imageName,
  490. function (error, stdout) {
  491. if (error != null) {
  492. return callback(error)
  493. }
  494. logger.log(
  495. { project_id, user_id, page, h, v, stdout },
  496. 'synctex pdf output'
  497. )
  498. return callback(
  499. null,
  500. CompileManager._parseSynctexFromPdfOutput(stdout, base_dir)
  501. )
  502. }
  503. )
  504. },
  505. _checkFileExists(dir, filename, callback) {
  506. if (callback == null) {
  507. callback = function (error) {}
  508. }
  509. const file = Path.join(dir, filename)
  510. return fs.stat(dir, function (error, stats) {
  511. if ((error != null ? error.code : undefined) === 'ENOENT') {
  512. return callback(new Errors.NotFoundError('no output directory'))
  513. }
  514. if (error != null) {
  515. return callback(error)
  516. }
  517. return fs.stat(file, function (error, stats) {
  518. if ((error != null ? error.code : undefined) === 'ENOENT') {
  519. return callback(new Errors.NotFoundError('no output file'))
  520. }
  521. if (error != null) {
  522. return callback(error)
  523. }
  524. if (!(stats != null ? stats.isFile() : undefined)) {
  525. return callback(new Error('not a file'))
  526. }
  527. return callback()
  528. })
  529. })
  530. },
  531. _runSynctex(project_id, user_id, command, imageName, callback) {
  532. if (callback == null) {
  533. callback = function (error, stdout) {}
  534. }
  535. const seconds = 1000
  536. command.unshift('/opt/synctex')
  537. const directory = getCompileDir(project_id, user_id)
  538. const timeout = 60 * 1000 // increased to allow for large projects
  539. const compileName = getCompileName(project_id, user_id)
  540. const compileGroup = 'synctex'
  541. CompileManager._checkFileExists(directory, 'output.synctex.gz', (error) => {
  542. if (error) {
  543. return callback(error)
  544. }
  545. return CommandRunner.run(
  546. compileName,
  547. command,
  548. directory,
  549. imageName ||
  550. (Settings.clsi && Settings.clsi.docker
  551. ? Settings.clsi.docker.image
  552. : undefined),
  553. timeout,
  554. {},
  555. compileGroup,
  556. function (error, output) {
  557. if (error != null) {
  558. logger.err(
  559. { err: error, command, project_id, user_id },
  560. 'error running synctex'
  561. )
  562. return callback(error)
  563. }
  564. return callback(null, output.stdout)
  565. }
  566. )
  567. })
  568. },
  569. _parseSynctexFromCodeOutput(output) {
  570. const results = []
  571. for (const line of Array.from(output.split('\n'))) {
  572. const [node, page, h, v, width, height] = Array.from(line.split('\t'))
  573. if (node === 'NODE') {
  574. results.push({
  575. page: parseInt(page, 10),
  576. h: parseFloat(h),
  577. v: parseFloat(v),
  578. height: parseFloat(height),
  579. width: parseFloat(width)
  580. })
  581. }
  582. }
  583. return results
  584. },
  585. _parseSynctexFromPdfOutput(output, base_dir) {
  586. const results = []
  587. for (let line of Array.from(output.split('\n'))) {
  588. let column, file_path, node
  589. ;[node, file_path, line, column] = Array.from(line.split('\t'))
  590. if (node === 'NODE') {
  591. const file = file_path.slice(base_dir.length + 1)
  592. results.push({
  593. file,
  594. line: parseInt(line, 10),
  595. column: parseInt(column, 10)
  596. })
  597. }
  598. }
  599. return results
  600. },
  601. wordcount(project_id, user_id, file_name, image, callback) {
  602. if (callback == null) {
  603. callback = function (error, pdfPositions) {}
  604. }
  605. logger.log({ project_id, user_id, file_name, image }, 'running wordcount')
  606. const file_path = `$COMPILE_DIR/${file_name}`
  607. const command = [
  608. 'texcount',
  609. '-nocol',
  610. '-inc',
  611. file_path,
  612. `-out=${file_path}.wc`
  613. ]
  614. const compileDir = getCompileDir(project_id, user_id)
  615. const timeout = 60 * 1000
  616. const compileName = getCompileName(project_id, user_id)
  617. const compileGroup = 'wordcount'
  618. return fse.ensureDir(compileDir, function (error) {
  619. if (error != null) {
  620. logger.err(
  621. { error, project_id, user_id, file_name },
  622. 'error ensuring dir for sync from code'
  623. )
  624. return callback(error)
  625. }
  626. return CommandRunner.run(
  627. compileName,
  628. command,
  629. compileDir,
  630. image,
  631. timeout,
  632. {},
  633. compileGroup,
  634. function (error) {
  635. if (error != null) {
  636. return callback(error)
  637. }
  638. return fs.readFile(
  639. compileDir + '/' + file_name + '.wc',
  640. 'utf-8',
  641. function (err, stdout) {
  642. if (err != null) {
  643. // call it node_err so sentry doesn't use random path error as unique id so it can't be ignored
  644. logger.err(
  645. { node_err: err, command, compileDir, project_id, user_id },
  646. 'error reading word count output'
  647. )
  648. return callback(err)
  649. }
  650. const results = CompileManager._parseWordcountFromOutput(stdout)
  651. logger.log(
  652. { project_id, user_id, wordcount: results },
  653. 'word count results'
  654. )
  655. return callback(null, results)
  656. }
  657. )
  658. }
  659. )
  660. })
  661. },
  662. _parseWordcountFromOutput(output) {
  663. const results = {
  664. encode: '',
  665. textWords: 0,
  666. headWords: 0,
  667. outside: 0,
  668. headers: 0,
  669. elements: 0,
  670. mathInline: 0,
  671. mathDisplay: 0,
  672. errors: 0,
  673. messages: ''
  674. }
  675. for (const line of Array.from(output.split('\n'))) {
  676. const [data, info] = Array.from(line.split(':'))
  677. if (data.indexOf('Encoding') > -1) {
  678. results.encode = info.trim()
  679. }
  680. if (data.indexOf('in text') > -1) {
  681. results.textWords = parseInt(info, 10)
  682. }
  683. if (data.indexOf('in head') > -1) {
  684. results.headWords = parseInt(info, 10)
  685. }
  686. if (data.indexOf('outside') > -1) {
  687. results.outside = parseInt(info, 10)
  688. }
  689. if (data.indexOf('of head') > -1) {
  690. results.headers = parseInt(info, 10)
  691. }
  692. if (data.indexOf('Number of floats/tables/figures') > -1) {
  693. results.elements = parseInt(info, 10)
  694. }
  695. if (data.indexOf('Number of math inlines') > -1) {
  696. results.mathInline = parseInt(info, 10)
  697. }
  698. if (data.indexOf('Number of math displayed') > -1) {
  699. results.mathDisplay = parseInt(info, 10)
  700. }
  701. if (data === '(errors') {
  702. // errors reported as (errors:123)
  703. results.errors = parseInt(info, 10)
  704. }
  705. if (line.indexOf('!!! ') > -1) {
  706. // errors logged as !!! message !!!
  707. results.messages += line + '\n'
  708. }
  709. }
  710. return results
  711. }
  712. }
  713. function __guard__(value, transform) {
  714. return typeof value !== 'undefined' && value !== null
  715. ? transform(value)
  716. : undefined
  717. }