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 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
|
const os = require('os')
const fs = require('fs')
const log = require('./log-shim.js')
const errorMessage = require('./error-message.js')
const replaceInfo = require('./replace-info.js')
const messageText = msg => msg.map(line => line.slice(1).join(' ')).join('\n')
const indent = (val) => Array.isArray(val) ? val.map(v => indent(v)) : ` ${val}`
let npm = null // set by the cli
let exitHandlerCalled = false
let showLogFileError = false
process.on('exit', code => {
log.disableProgress()
// process.emit is synchronous, so the timeEnd handler will run before the
// unfinished timer check below
process.emit('timeEnd', 'npm')
const hasLoadedNpm = npm?.config.loaded
// Unfinished timers can be read before config load
if (npm) {
for (const [name, timer] of npm.unfinishedTimers) {
log.verbose('unfinished npm timer', name, timer)
}
}
if (!code) {
log.info('ok')
} else {
log.verbose('code', code)
}
if (!exitHandlerCalled) {
process.exitCode = code || 1
log.error('', 'Exit handler never called!')
// eslint-disable-next-line no-console
console.error('')
log.error('', 'This is an error with npm itself. Please report this error at:')
log.error('', ' <https://github.com/npm/cli/issues>')
showLogFileError = true
}
// npm must be loaded to know where the log file was written
if (hasLoadedNpm) {
// write the timing file now, this might do nothing based on the configs set.
// we need to call it here in case it errors so we dont tell the user
// about a timing file that doesn't exist
npm.writeTimingFile()
const logsDir = npm.logsDir
const logFiles = npm.logFiles
const timingDir = npm.timingDir
const timingFile = npm.timingFile
const timing = npm.config.get('timing')
const logsMax = npm.config.get('logs-max')
// Determine whether to show log file message and why it is
// being shown since in timing mode we always show the log file message
const logMethod = showLogFileError ? 'error' : timing ? 'info' : null
if (logMethod) {
if (!npm.silent) {
// just a line break if not in silent mode
// eslint-disable-next-line no-console
console.error('')
}
const message = []
if (timingFile) {
message.push('Timing info written to:', indent(timingFile))
} else if (timing) {
message.push(
`The timing file was not written due to an error writing to the directory: ${timingDir}`
)
}
if (logFiles.length) {
message.push('A complete log of this run can be found in:', ...indent(logFiles))
} else if (logsMax <= 0) {
// user specified no log file
message.push(`Log files were not written due to the config logs-max=${logsMax}`)
} else {
// could be an error writing to the directory
message.push(
`Log files were not written due to an error writing to the directory: ${logsDir}`,
'You can rerun the command with `--loglevel=verbose` to see the logs in your terminal'
)
}
log[logMethod]('', message.join('\n'))
}
// This removes any listeners npm setup, mostly for tests to avoid max listener warnings
npm.unload()
}
// these are needed for the tests to have a clean slate in each test case
exitHandlerCalled = false
showLogFileError = false
})
const exitHandler = err => {
exitHandlerCalled = true
log.disableProgress()
const hasLoadedNpm = npm?.config.loaded
if (!npm) {
err = err || new Error('Exit prior to setting npm in exit handler')
// eslint-disable-next-line no-console
console.error(err.stack || err.message)
return process.exit(1)
}
if (!hasLoadedNpm) {
err = err || new Error('Exit prior to config file resolving.')
// eslint-disable-next-line no-console
console.error(err.stack || err.message)
}
// only show the notification if it finished.
if (typeof npm.updateNotification === 'string') {
const { level } = log
log.level = 'notice'
log.notice('', npm.updateNotification)
log.level = level
}
let exitCode = process.exitCode || 0
let noLogMessage = exitCode !== 0
let jsonError
if (err) {
exitCode = 1
// if we got a command that just shells out to something else, then it
// will presumably print its own errors and exit with a proper status
// code if there's a problem. If we got an error with a code=0, then...
// something else went wrong along the way, so maybe an npm problem?
const isShellout = npm.commandInstance && npm.commandInstance.constructor.isShellout
const quietShellout = isShellout && typeof err.code === 'number' && err.code
if (quietShellout) {
exitCode = err.code
noLogMessage = true
} else if (typeof err === 'string') {
// XXX: we should stop throwing strings
log.error('', err)
noLogMessage = true
} else if (!(err instanceof Error)) {
log.error('weird error', err)
noLogMessage = true
} else {
if (!err.code) {
const matchErrorCode = err.message.match(/^(?:Error: )?(E[A-Z]+)/)
err.code = matchErrorCode && matchErrorCode[1]
}
for (const k of ['type', 'stack', 'statusCode', 'pkgid']) {
const v = err[k]
if (v) {
log.verbose(k, replaceInfo(v))
}
}
log.verbose('cwd', process.cwd())
log.verbose('', os.type() + ' ' + os.release())
log.verbose('node', process.version)
log.verbose('npm ', 'v' + npm.version)
for (const k of ['code', 'syscall', 'file', 'path', 'dest', 'errno']) {
const v = err[k]
if (v) {
log.error(k, v)
}
}
const { summary, detail, files = [] } = errorMessage(err, npm)
for (let [file, content] of files) {
file = `${npm.logPath}${file}`
content = `'Log files:\n${npm.logFiles.join('\n')}\n\n${content.trim()}\n`
try {
fs.writeFileSync(file, content)
detail.push(['', `\n\nFor a full report see:\n${file}`])
} catch (err) {
log.warn('', `Could not write error message to ${file} due to ${err}`)
}
}
for (const errline of [...summary, ...detail]) {
log.error(...errline)
}
if (hasLoadedNpm && npm.config.get('json')) {
jsonError = {
error: {
code: err.code,
summary: messageText(summary),
detail: messageText(detail),
},
}
}
if (typeof err.errno === 'number') {
exitCode = err.errno
} else if (typeof err.code === 'number') {
exitCode = err.code
}
}
}
if (hasLoadedNpm) {
npm.flushOutput(jsonError)
}
log.verbose('exit', exitCode || 0)
showLogFileError = (hasLoadedNpm && npm.silent) || noLogMessage
? false
: !!exitCode
// 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.
let flushed = 0
const flush = [process.stderr, process.stdout]
const exit = () => ++flushed === flush.length && process.exit(exitCode)
flush.forEach((f) => f.write('', exit))
}
module.exports = exitHandler
module.exports.setNpm = n => (npm = n)
|