LatexRunner.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. no-return-assign,
  5. no-unused-vars,
  6. */
  7. // TODO: This file was created by bulk-decaffeinate.
  8. // Fix any style issues and re-enable lint.
  9. /*
  10. * decaffeinate suggestions:
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * DS103: Rewrite code to no longer use __guard__
  13. * DS207: Consider shorter variations of null checks
  14. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  15. */
  16. let LatexRunner
  17. const Path = require('path')
  18. const Settings = require('@overleaf/settings')
  19. const logger = require('logger-sharelatex')
  20. const Metrics = require('./Metrics')
  21. const CommandRunner = require('./CommandRunner')
  22. const fs = require('fs')
  23. const ProcessTable = {} // table of currently running jobs (pids or docker container names)
  24. const TIME_V_METRICS = Object.entries({
  25. 'cpu-percent': /Percent of CPU this job got: (\d+)/m,
  26. 'cpu-time': /User time.*: (\d+.\d+)/m,
  27. 'sys-time': /System time.*: (\d+.\d+)/m,
  28. })
  29. module.exports = LatexRunner = {
  30. runLatex(project_id, options, callback) {
  31. let command
  32. if (callback == null) {
  33. callback = function (error) {}
  34. }
  35. let {
  36. directory,
  37. mainFile,
  38. compiler,
  39. timeout,
  40. image,
  41. environment,
  42. flags,
  43. compileGroup,
  44. } = options
  45. if (!compiler) {
  46. compiler = 'pdflatex'
  47. }
  48. if (!timeout) {
  49. timeout = 60000
  50. } // milliseconds
  51. logger.log(
  52. {
  53. directory,
  54. compiler,
  55. timeout,
  56. mainFile,
  57. environment,
  58. flags,
  59. compileGroup,
  60. },
  61. 'starting compile'
  62. )
  63. // We want to run latexmk on the tex file which we will automatically
  64. // generate from the Rtex/Rmd/md file.
  65. mainFile = mainFile.replace(/\.(Rtex|md|Rmd)$/, '.tex')
  66. if (compiler === 'pdflatex') {
  67. command = LatexRunner._pdflatexCommand(mainFile, flags)
  68. } else if (compiler === 'latex') {
  69. command = LatexRunner._latexCommand(mainFile, flags)
  70. } else if (compiler === 'xelatex') {
  71. command = LatexRunner._xelatexCommand(mainFile, flags)
  72. } else if (compiler === 'lualatex') {
  73. command = LatexRunner._lualatexCommand(mainFile, flags)
  74. } else {
  75. return callback(new Error(`unknown compiler: ${compiler}`))
  76. }
  77. if (Settings.clsi != null ? Settings.clsi.strace : undefined) {
  78. command = ['strace', '-o', 'strace', '-ff'].concat(command)
  79. }
  80. const id = `${project_id}` // record running project under this id
  81. return (ProcessTable[id] = CommandRunner.run(
  82. project_id,
  83. command,
  84. directory,
  85. image,
  86. timeout,
  87. environment,
  88. compileGroup,
  89. function (error, output) {
  90. delete ProcessTable[id]
  91. if (error != null) {
  92. return callback(error)
  93. }
  94. const runs =
  95. __guard__(
  96. __guard__(output != null ? output.stderr : undefined, x1 =>
  97. x1.match(/^Run number \d+ of .*latex/gm)
  98. ),
  99. x => x.length
  100. ) || 0
  101. const failed =
  102. __guard__(output != null ? output.stdout : undefined, x2 =>
  103. x2.match(/^Latexmk: Errors/m)
  104. ) != null
  105. ? 1
  106. : 0
  107. // counters from latexmk output
  108. const stats = {}
  109. stats['latexmk-errors'] = failed
  110. stats['latex-runs'] = runs
  111. stats['latex-runs-with-errors'] = failed ? runs : 0
  112. stats[`latex-runs-${runs}`] = 1
  113. stats[`latex-runs-with-errors-${runs}`] = failed ? 1 : 0
  114. // timing information from /usr/bin/time
  115. const timings = {}
  116. const stderr = (output && output.stderr) || ''
  117. if (stderr.includes('Command being timed:')) {
  118. // Add metrics for runs with `$ time -v ...`
  119. for (const [timing, matcher] of TIME_V_METRICS) {
  120. const match = stderr.match(matcher)
  121. if (match) {
  122. timings[timing] = parseFloat(match[1])
  123. }
  124. }
  125. }
  126. // record output files
  127. LatexRunner.writeLogOutput(project_id, directory, output, () => {
  128. return callback(error, output, stats, timings)
  129. })
  130. }
  131. ))
  132. },
  133. writeLogOutput(project_id, directory, output, callback) {
  134. if (!output) {
  135. return callback()
  136. }
  137. // internal method for writing non-empty log files
  138. function _writeFile(file, content, cb) {
  139. if (content && content.length > 0) {
  140. fs.writeFile(file, content, err => {
  141. if (err) {
  142. logger.error({ project_id, file }, 'error writing log file') // don't fail on error
  143. }
  144. cb()
  145. })
  146. } else {
  147. cb()
  148. }
  149. }
  150. // write stdout and stderr, ignoring errors
  151. _writeFile(Path.join(directory, 'output.stdout'), output.stdout, () => {
  152. _writeFile(Path.join(directory, 'output.stderr'), output.stderr, () => {
  153. callback()
  154. })
  155. })
  156. },
  157. killLatex(project_id, callback) {
  158. if (callback == null) {
  159. callback = function (error) {}
  160. }
  161. const id = `${project_id}`
  162. logger.log({ id }, 'killing running compile')
  163. if (ProcessTable[id] == null) {
  164. logger.warn({ id }, 'no such project to kill')
  165. return callback(null)
  166. } else {
  167. return CommandRunner.kill(ProcessTable[id], callback)
  168. }
  169. },
  170. _latexmkBaseCommand(flags) {
  171. let args = [
  172. 'latexmk',
  173. '-cd',
  174. '-f',
  175. '-jobname=output',
  176. '-auxdir=$COMPILE_DIR',
  177. '-outdir=$COMPILE_DIR',
  178. '-synctex=1',
  179. '-interaction=batchmode',
  180. ]
  181. if (flags) {
  182. args = args.concat(flags)
  183. }
  184. return (
  185. __guard__(
  186. Settings != null ? Settings.clsi : undefined,
  187. x => x.latexmkCommandPrefix
  188. ) || []
  189. ).concat(args)
  190. },
  191. _pdflatexCommand(mainFile, flags) {
  192. return LatexRunner._latexmkBaseCommand(flags).concat([
  193. '-pdf',
  194. Path.join('$COMPILE_DIR', mainFile),
  195. ])
  196. },
  197. _latexCommand(mainFile, flags) {
  198. return LatexRunner._latexmkBaseCommand(flags).concat([
  199. '-pdfdvi',
  200. Path.join('$COMPILE_DIR', mainFile),
  201. ])
  202. },
  203. _xelatexCommand(mainFile, flags) {
  204. return LatexRunner._latexmkBaseCommand(flags).concat([
  205. '-xelatex',
  206. Path.join('$COMPILE_DIR', mainFile),
  207. ])
  208. },
  209. _lualatexCommand(mainFile, flags) {
  210. return LatexRunner._latexmkBaseCommand(flags).concat([
  211. '-lualatex',
  212. Path.join('$COMPILE_DIR', mainFile),
  213. ])
  214. },
  215. }
  216. function __guard__(value, transform) {
  217. return typeof value !== 'undefined' && value !== null
  218. ? transform(value)
  219. : undefined
  220. }