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