LocalCommandRunner.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. /* eslint-disable
  2. no-return-assign,
  3. no-unused-vars,
  4. */
  5. // TODO: This file was created by bulk-decaffeinate.
  6. // Fix any style issues and re-enable lint.
  7. /*
  8. * decaffeinate suggestions:
  9. * DS101: Remove unnecessary use of Array.from
  10. * DS102: Remove unnecessary code created because of implicit returns
  11. * DS207: Consider shorter variations of null checks
  12. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  13. */
  14. import { spawn } from 'node:child_process'
  15. import { promisify } from 'node:util'
  16. import Path from 'node:path'
  17. import _ from 'lodash'
  18. import logger from '@overleaf/logger'
  19. let CommandRunner
  20. logger.debug('using standard command runner')
  21. // Track PIDs that have been intentionally killed so that the close handler
  22. // can detect termination even when the child exits with a numeric code instead
  23. // of being reported as killed by a signal (e.g. exit code 4 from latexmk).
  24. const killedPids = new Set()
  25. export default CommandRunner = {
  26. run(
  27. projectId,
  28. command,
  29. directory,
  30. image,
  31. timeout,
  32. environment,
  33. compileGroup,
  34. cwd,
  35. callback
  36. ) {
  37. let key, value
  38. callback = _.once(callback)
  39. const spawnCwd = cwd ? Path.join(directory, cwd) : directory
  40. command = Array.from(command).map(arg =>
  41. arg.toString().replace('$COMPILE_DIR', directory)
  42. )
  43. logger.debug(
  44. { projectId, command, directory, cwd: spawnCwd },
  45. 'running command'
  46. )
  47. logger.warn('timeouts and sandboxing are not enabled with CommandRunner')
  48. // merge environment settings
  49. const env = {}
  50. for (key in process.env) {
  51. value = process.env[key]
  52. env[key] = value
  53. }
  54. for (key in environment) {
  55. value = environment[key]
  56. env[key] = value
  57. }
  58. // run command as detached process so it has its own process group (which can be killed if needed)
  59. const proc = spawn(command[0], command.slice(1), {
  60. cwd: spawnCwd,
  61. env,
  62. stdio: ['pipe', 'pipe', 'ignore'],
  63. detached: true,
  64. })
  65. let stdout = ''
  66. proc.stdout.setEncoding('utf8').on('data', data => (stdout += data))
  67. proc.on('error', function (err) {
  68. killedPids.delete(proc.pid)
  69. logger.err(
  70. { err, projectId, command, directory },
  71. 'error running command'
  72. )
  73. return callback(err)
  74. })
  75. proc.on('close', function (code, signal) {
  76. let err
  77. logger.debug({ code, signal, projectId }, 'command exited')
  78. const wasKilled = killedPids.delete(proc.pid)
  79. if (signal === 'SIGTERM' || wasKilled) {
  80. err = new Error('terminated')
  81. err.terminated = true
  82. return callback(err)
  83. } else if (code === 1) {
  84. // exit status from chktex
  85. err = new Error('exited')
  86. err.code = code
  87. return callback(err)
  88. } else {
  89. return callback(null, { stdout, exitCode: code })
  90. }
  91. })
  92. return proc.pid
  93. }, // return process id to allow job to be killed if necessary
  94. kill(pid, callback) {
  95. if (callback == null) {
  96. callback = function () {}
  97. }
  98. try {
  99. killedPids.add(pid)
  100. process.kill(-pid) // kill all processes in group
  101. } catch (err) {
  102. killedPids.delete(pid)
  103. return callback(err)
  104. }
  105. return callback()
  106. },
  107. canRunSyncTeXInOutputDir() {
  108. return true
  109. },
  110. }
  111. CommandRunner.promises = {
  112. run: promisify(CommandRunner.run),
  113. kill: promisify(CommandRunner.kill),
  114. }