let npm // set by the cli let cbCalled = false const log = require('npmlog') let itWorked = false const path = require('path') const writeFileAtomic = require('write-file-atomic') const mkdirp = require('mkdirp-infer-owner') const fs = require('graceful-fs') let wroteLogFile = false let exitCode = 0 const errorMessage = require('./error-message.js') const replaceInfo = require('./replace-info.js') let logFileName const getLogFile = () => { // we call this multiple times, so we need to treat it as a singleton because // the date is part of the name if (!logFileName) logFileName = path.resolve(npm.config.get('cache'), '_logs', (new Date()).toISOString().replace(/[.:]/g, '_') + '-debug.log') return logFileName } const timings = {} process.on('timing', (name, value) => { if (timings[name]) timings[name] += value else timings[name] = value }) process.on('exit', code => { process.emit('timeEnd', 'npm') log.disableProgress() if (npm.config && npm.config.loaded && npm.config.get('timing')) { try { const file = path.resolve(npm.config.get('cache'), '_timing.json') const dir = path.dirname(npm.config.get('cache')) mkdirp.sync(dir) fs.appendFileSync(file, JSON.stringify({ command: process.argv.slice(2), logfile: getLogFile(), version: npm.version, ...timings, }) + '\n') const st = fs.lstatSync(path.dirname(npm.config.get('cache'))) fs.chownSync(dir, st.uid, st.gid) fs.chownSync(file, st.uid, st.gid) } catch (ex) { // ignore } } if (code) itWorked = false if (itWorked) log.info('ok') else { if (!cbCalled) { log.error('', 'cb() never called!') console.error('') log.error('', 'This is an error with npm itself. Please report this error at:') log.error('', ' ') writeLogFile() } if (code) log.verbose('code', code) } if (npm.config && npm.config.loaded && npm.config.get('timing') && !wroteLogFile) writeLogFile() if (wroteLogFile) { // just a line break if (log.levels[log.level] <= log.levels.error) console.error('') log.error( '', [ 'A complete log of this run can be found in:', ' ' + getLogFile(), ].join('\n') ) wroteLogFile = false } // actually exit. if (exitCode === 0 && !itWorked) exitCode = 1 if (exitCode !== 0) process.exit(exitCode) }) const exit = (code, noLog) => { exitCode = exitCode || process.exitCode || code log.verbose('exit', code) if (log.level === 'silent') noLog = true const reallyExit = () => { itWorked = !code // Exit directly -- nothing in the CLI should still be running in the // background at this point, and this makes sure anything left dangling // for whatever reason gets thrown away, instead of leaving the CLI open // // Commands that expect long-running actions should just delay `cb()` process.stdout.write('', () => { process.exit(code) }) } if (code && !noLog) writeLogFile() reallyExit() } const errorHandler = (er) => { log.disableProgress() if (!npm.config || !npm.config.loaded) { // logging won't work unless we pretend that it's ready er = er || new Error('Exit prior to config file resolving.') console.error(er.stack || er.message) } if (cbCalled) er = er || new Error('Callback called more than once.') // only show the notification if it finished before the other stuff we // were doing. no need to hang on `npm -v` or something. if (typeof npm.updateNotification === 'string') { const { level } = log log.level = log.levels.notice log.notice('', npm.updateNotification) log.level = level } cbCalled = true if (!er) return exit(0) // 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.shelloutCommands.includes(npm.command) const quietShellout = isShellout && typeof er.code === 'number' && er.code if (quietShellout) return exit(er.code, true) else if (typeof er === 'string') { log.error('', er) return exit(1, true) } else if (!(er instanceof Error)) { log.error('weird error', er) return exit(1, true) } if (!er.code) { const matchErrorCode = er.message.match(/^(?:Error: )?(E[A-Z]+)/) er.code = matchErrorCode && matchErrorCode[1] } for (const k of ['type', 'stack', 'statusCode', 'pkgid']) { const v = er[k] if (v) log.verbose(k, replaceInfo(v)) } log.verbose('cwd', process.cwd()) const os = require('os') const args = replaceInfo(process.argv) log.verbose('', os.type() + ' ' + os.release()) log.verbose('argv', args.map(JSON.stringify).join(' ')) log.verbose('node', process.version) log.verbose('npm ', 'v' + npm.version) for (const k of ['code', 'syscall', 'file', 'path', 'dest', 'errno']) { const v = er[k] if (v) log.error(k, v) } const msg = errorMessage(er, npm) for (const errline of [...msg.summary, ...msg.detail]) log.error(...errline) if (npm.config && npm.config.get('json')) { const error = { error: { code: er.code, summary: messageText(msg.summary), detail: messageText(msg.detail), }, } console.error(JSON.stringify(error, null, 2)) } exit(typeof er.errno === 'number' ? er.errno : typeof er.code === 'number' ? er.code : 1) } const messageText = msg => msg.map(line => line.slice(1).join(' ')).join('\n') const writeLogFile = () => { if (wroteLogFile) return const os = require('os') try { let logOutput = '' log.record.forEach(m => { const p = [m.id, m.level] if (m.prefix) p.push(m.prefix) const pref = p.join(' ') m.message.trim().split(/\r?\n/) .map(line => (pref + ' ' + line).trim()) .forEach(line => { logOutput += line + os.EOL }) }) const file = getLogFile() const dir = path.dirname(file) mkdirp.sync(dir) writeFileAtomic.sync(file, logOutput) const st = fs.lstatSync(path.dirname(npm.config.get('cache'))) fs.chownSync(dir, st.uid, st.gid) fs.chownSync(file, st.uid, st.gid) // truncate once it's been written. log.record.length = 0 wroteLogFile = true } catch (ex) { } } module.exports = errorHandler module.exports.exit = exit module.exports.setNpm = (n) => { npm = n }