exit-handler.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. const { log, output, META } = require('proc-log')
  2. const { errorMessage, getExitCodeFromError } = require('../utils/error-message.js')
  3. class ExitHandler {
  4. #npm = null
  5. #process = null
  6. #exited = false
  7. #exitErrorMessage = false
  8. #noNpmError = false
  9. get #hasNpm () {
  10. return !!this.#npm
  11. }
  12. get #loaded () {
  13. return !!this.#npm?.loaded
  14. }
  15. get #showExitErrorMessage () {
  16. if (!this.#loaded) {
  17. return false
  18. }
  19. if (!this.#exited) {
  20. return true
  21. }
  22. return this.#exitErrorMessage
  23. }
  24. get #notLoadedOrExited () {
  25. return !this.#loaded && !this.#exited
  26. }
  27. setNpm (npm) {
  28. this.#npm = npm
  29. }
  30. constructor ({ process }) {
  31. this.#process = process
  32. this.#process.on('exit', this.#handleProcessExitAndReset)
  33. }
  34. registerUncaughtHandlers () {
  35. this.#process.on('uncaughtException', this.#handleExit)
  36. this.#process.on('unhandledRejection', this.#handleExit)
  37. }
  38. exit (err) {
  39. this.#handleExit(err)
  40. }
  41. #handleProcessExitAndReset = (code) => {
  42. this.#handleProcessExit(code)
  43. // Reset all the state. This is only relevant for tests since in reality the process fully exits here.
  44. this.#process.off('exit', this.#handleProcessExitAndReset)
  45. this.#process.off('uncaughtException', this.#handleExit)
  46. this.#process.off('unhandledRejection', this.#handleExit)
  47. if (this.#loaded) {
  48. this.#npm.unload()
  49. }
  50. this.#npm = null
  51. this.#exited = false
  52. this.#exitErrorMessage = false
  53. }
  54. #handleProcessExit (code) {
  55. const numCode = Number(code) || 0
  56. // Always exit w/ a non-zero code if exit handler was not called
  57. const exitCode = this.#exited ? numCode : (numCode || 1)
  58. this.#process.exitCode = exitCode
  59. if (this.#notLoadedOrExited) {
  60. // Exit handler was not called and npm was not loaded so we have to log something
  61. this.#logConsoleError(new Error(`Process exited unexpectedly with code: ${exitCode}`))
  62. return
  63. }
  64. if (this.#logNoNpmError()) {
  65. return
  66. }
  67. const os = require('node:os')
  68. log.verbose('cwd', this.#process.cwd())
  69. log.verbose('os', `${os.type()} ${os.release()}`)
  70. log.verbose('node', this.#process.version)
  71. log.verbose('npm ', `v${this.#npm.version}`)
  72. // only show the notification if it finished
  73. if (typeof this.#npm.updateNotification === 'string') {
  74. log.notice('', this.#npm.updateNotification, { [META]: true, force: true })
  75. }
  76. if (!this.#exited) {
  77. log.error('', 'Exit handler never called!')
  78. log.error('', 'This is an error with npm itself. Please report this error at:')
  79. log.error('', ' <https://github.com/npm/cli/issues>')
  80. if (this.#npm.silent) {
  81. output.error('')
  82. }
  83. }
  84. log.verbose('exit', exitCode)
  85. if (exitCode) {
  86. log.verbose('code', exitCode)
  87. } else {
  88. log.info('ok')
  89. }
  90. if (this.#showExitErrorMessage) {
  91. log.error('', this.#npm.exitErrorMessage())
  92. }
  93. }
  94. #logConsoleError (err) {
  95. // Run our error message formatters on all errors even if we have no npm or an unloaded npm.
  96. // This will clean the error and possible return a formatted message about EACCESS or something.
  97. const { summary, detail } = errorMessage(err, this.#npm)
  98. const formatted = [...new Set([...summary, ...detail].flat().filter(Boolean))].join('\n')
  99. // If we didn't get anything from the formatted message then just display the full stack
  100. // eslint-disable-next-line no-console
  101. console.error(formatted === err.message ? err.stack : formatted)
  102. }
  103. #logNoNpmError (err) {
  104. if (this.#hasNpm) {
  105. return false
  106. }
  107. // Make sure we only log this error once
  108. if (!this.#noNpmError) {
  109. this.#noNpmError = true
  110. this.#logConsoleError(
  111. new Error(`Exit prior to setting npm in exit handler`, err ? { cause: err } : {})
  112. )
  113. }
  114. return true
  115. }
  116. #handleExit = (err) => {
  117. this.#exited = true
  118. // No npm at all
  119. if (this.#logNoNpmError(err)) {
  120. return this.#process.exit(this.#process.exitCode || getExitCodeFromError(err) || 1)
  121. }
  122. // npm was never loaded but we still might have a config loading error or something similar that we can run through the error message formatter to give the user a clue as to what happened.
  123. if (!this.#loaded) {
  124. this.#logConsoleError(new Error('Exit prior to config file resolving', { cause: err }))
  125. return this.#process.exit(this.#process.exitCode || getExitCodeFromError(err) || 1)
  126. }
  127. this.#exitErrorMessage = err?.suppressError === true ? false : !!err
  128. // Prefer the exit code of the error, then the current process exit code, then set it to 1 if we still have an error.
  129. // Otherwise, we call process.exit with undefined so that it can determine the final exit code
  130. const exitCode = err?.exitCode ?? this.#process.exitCode ?? (err ? 1 : undefined)
  131. // explicitly call process.exit now so we don't hang on things like the update notifier
  132. // also flush stdout/err beforehand because process.exit doesn't wait for that to happen.
  133. this.#process.stderr.write('', () => this.#process.stdout.write('', () => {
  134. this.#process.exit(exitCode)
  135. }))
  136. }
  137. }
  138. module.exports = ExitHandler