• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1const os = require('os')
2const fs = require('fs')
3
4const log = require('./log-shim.js')
5const errorMessage = require('./error-message.js')
6const replaceInfo = require('./replace-info.js')
7
8let npm = null // set by the cli
9let exitHandlerCalled = false
10let showLogFileError = false
11
12process.on('exit', code => {
13  log.disableProgress()
14
15  // process.emit is synchronous, so the timeEnd handler will run before the
16  // unfinished timer check below
17  process.emit('timeEnd', 'npm')
18
19  const hasLoadedNpm = npm?.config.loaded
20
21  // Unfinished timers can be read before config load
22  if (npm) {
23    for (const [name, timer] of npm.unfinishedTimers) {
24      log.verbose('unfinished npm timer', name, timer)
25    }
26  }
27
28  if (!code) {
29    log.info('ok')
30  } else {
31    log.verbose('code', code)
32  }
33
34  if (!exitHandlerCalled) {
35    process.exitCode = code || 1
36    log.error('', 'Exit handler never called!')
37    // eslint-disable-next-line no-console
38    console.error('')
39    log.error('', 'This is an error with npm itself. Please report this error at:')
40    log.error('', '    <https://github.com/npm/cli/issues>')
41    showLogFileError = true
42  }
43
44  // npm must be loaded to know where the log file was written
45  if (hasLoadedNpm) {
46    // write the timing file now, this might do nothing based on the configs set.
47    // we need to call it here in case it errors so we dont tell the user
48    // about a timing file that doesn't exist
49    npm.writeTimingFile()
50
51    const logsDir = npm.logsDir
52    const logFiles = npm.logFiles
53
54    const timingDir = npm.timingDir
55    const timingFile = npm.timingFile
56
57    const timing = npm.config.get('timing')
58    const logsMax = npm.config.get('logs-max')
59
60    // Determine whether to show log file message and why it is
61    // being shown since in timing mode we always show the log file message
62    const logMethod = showLogFileError ? 'error' : timing ? 'info' : null
63
64    if (logMethod) {
65      if (!npm.silent) {
66        // just a line break if not in silent mode
67        // eslint-disable-next-line no-console
68        console.error('')
69      }
70
71      const message = []
72
73      if (timingFile) {
74        message.push(`Timing info written to: ${timingFile}`)
75      } else if (timing) {
76        message.push(
77          `The timing file was not written due to an error writing to the directory: ${timingDir}`
78        )
79      }
80
81      if (logFiles.length) {
82        message.push(`A complete log of this run can be found in: ${logFiles}`)
83      } else if (logsMax <= 0) {
84        // user specified no log file
85        message.push(`Log files were not written due to the config logs-max=${logsMax}`)
86      } else {
87        // could be an error writing to the directory
88        message.push(
89          `Log files were not written due to an error writing to the directory: ${logsDir}`,
90          'You can rerun the command with `--loglevel=verbose` to see the logs in your terminal'
91        )
92      }
93
94      log[logMethod]('', message.join('\n'))
95    }
96
97    // This removes any listeners npm setup, mostly for tests to avoid max listener warnings
98    npm.unload()
99  }
100
101  // these are needed for the tests to have a clean slate in each test case
102  exitHandlerCalled = false
103  showLogFileError = false
104})
105
106const exitHandler = err => {
107  exitHandlerCalled = true
108
109  log.disableProgress()
110
111  const hasLoadedNpm = npm?.config.loaded
112
113  if (!npm) {
114    err = err || new Error('Exit prior to setting npm in exit handler')
115    // eslint-disable-next-line no-console
116    console.error(err.stack || err.message)
117    return process.exit(1)
118  }
119
120  if (!hasLoadedNpm) {
121    err = err || new Error('Exit prior to config file resolving.')
122    // eslint-disable-next-line no-console
123    console.error(err.stack || err.message)
124  }
125
126  // only show the notification if it finished.
127  if (typeof npm.updateNotification === 'string') {
128    const { level } = log
129    log.level = 'notice'
130    log.notice('', npm.updateNotification)
131    log.level = level
132  }
133
134  let exitCode = process.exitCode || 0
135  let noLogMessage = exitCode !== 0
136  let jsonError
137
138  if (err) {
139    exitCode = 1
140    // if we got a command that just shells out to something else, then it
141    // will presumably print its own errors and exit with a proper status
142    // code if there's a problem.  If we got an error with a code=0, then...
143    // something else went wrong along the way, so maybe an npm problem?
144    const isShellout = npm.isShellout
145    const quietShellout = isShellout && typeof err.code === 'number' && err.code
146    if (quietShellout) {
147      exitCode = err.code
148      noLogMessage = true
149    } else if (typeof err === 'string') {
150      // XXX: we should stop throwing strings
151      log.error('', err)
152      noLogMessage = true
153    } else if (!(err instanceof Error)) {
154      log.error('weird error', err)
155      noLogMessage = true
156    } else {
157      if (!err.code) {
158        const matchErrorCode = err.message.match(/^(?:Error: )?(E[A-Z]+)/)
159        err.code = matchErrorCode && matchErrorCode[1]
160      }
161
162      for (const k of ['type', 'stack', 'statusCode', 'pkgid']) {
163        const v = err[k]
164        if (v) {
165          log.verbose(k, replaceInfo(v))
166        }
167      }
168
169      log.verbose('cwd', process.cwd())
170      log.verbose('', os.type() + ' ' + os.release())
171      log.verbose('node', process.version)
172      log.verbose('npm ', 'v' + npm.version)
173
174      for (const k of ['code', 'syscall', 'file', 'path', 'dest', 'errno']) {
175        const v = err[k]
176        if (v) {
177          log.error(k, v)
178        }
179      }
180
181      const { summary, detail, json, files = [] } = errorMessage(err, npm)
182      jsonError = json
183
184      for (let [file, content] of files) {
185        file = `${npm.logPath}${file}`
186        content = `'Log files:\n${npm.logFiles.join('\n')}\n\n${content.trim()}\n`
187        try {
188          fs.writeFileSync(file, content)
189          detail.push(['', `\n\nFor a full report see:\n${file}`])
190        } catch (logFileErr) {
191          log.warn('', `Could not write error message to ${file} due to ${logFileErr}`)
192        }
193      }
194
195      for (const errline of [...summary, ...detail]) {
196        log.error(...errline)
197      }
198
199      if (typeof err.errno === 'number') {
200        exitCode = err.errno
201      } else if (typeof err.code === 'number') {
202        exitCode = err.code
203      }
204    }
205  }
206
207  if (hasLoadedNpm) {
208    npm.flushOutput(jsonError)
209  }
210
211  log.verbose('exit', exitCode || 0)
212
213  showLogFileError = (hasLoadedNpm && npm.silent) || noLogMessage
214    ? false
215    : !!exitCode
216
217  // explicitly call process.exit now so we don't hang on things like the
218  // update notifier, also flush stdout/err beforehand because process.exit doesn't
219  // wait for that to happen.
220  let flushed = 0
221  const flush = [process.stderr, process.stdout]
222  const exit = () => ++flushed === flush.length && process.exit(exitCode)
223  flush.forEach((f) => f.write('', exit))
224}
225
226module.exports = exitHandler
227module.exports.setNpm = n => (npm = n)
228