New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chore(refactor): refactor exit handler and tests #3482
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,3 @@ | ||
const log = require('npmlog') | ||
const os = require('os') | ||
const path = require('path') | ||
const writeFileAtomic = require('write-file-atomic') | ||
|
@@ -13,8 +12,6 @@ let logFileName | |
let npm // set by the cli | ||
let wroteLogFile = false | ||
|
||
const timings = {} | ||
|
||
const getLogFile = () => { | ||
// we call this multiple times, so we need to treat it as a singleton because | ||
// the date is part of the name | ||
|
@@ -24,20 +21,13 @@ const getLogFile = () => { | |
return logFileName | ||
} | ||
|
||
process.on('timing', (name, value) => { | ||
if (timings[name]) | ||
timings[name] += value | ||
else | ||
timings[name] = value | ||
}) | ||
|
||
process.on('exit', code => { | ||
// process.emit is synchronous, so the timeEnd handler will run before the | ||
// unfinished timer check below | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👏 thank you for calling this out, it's a super nice reminder! |
||
process.emit('timeEnd', 'npm') | ||
log.disableProgress() | ||
npm.log.disableProgress() | ||
for (const [name, timers] of npm.timers) | ||
log.verbose('unfinished npm timer', name, timers) | ||
npm.log.verbose('unfinished npm timer', name, timers) | ||
|
||
if (npm.config.loaded && npm.config.get('timing')) { | ||
try { | ||
|
@@ -49,7 +39,7 @@ process.on('exit', code => { | |
command: process.argv.slice(2), | ||
logfile: getLogFile(), | ||
version: npm.version, | ||
...timings, | ||
...npm.timings, | ||
}) + '\n') | ||
|
||
const st = fs.lstatSync(path.dirname(npm.config.get('cache'))) | ||
|
@@ -61,14 +51,14 @@ process.on('exit', code => { | |
} | ||
|
||
if (!code) | ||
log.info('ok') | ||
npm.log.info('ok') | ||
else { | ||
log.verbose('code', code) | ||
npm.log.verbose('code', code) | ||
if (!exitHandlerCalled) { | ||
log.error('', 'Exit handler never called!') | ||
npm.log.error('', 'Exit handler never called!') | ||
console.error('') | ||
log.error('', 'This is an error with npm itself. Please report this error at:') | ||
log.error('', ' <https://github.com/npm/cli/issues>') | ||
npm.log.error('', 'This is an error with npm itself. Please report this error at:') | ||
npm.log.error('', ' <https://github.com/npm/cli/issues>') | ||
// TODO this doesn't have an npm.config.loaded guard | ||
writeLogFile() | ||
} | ||
|
@@ -78,10 +68,10 @@ process.on('exit', code => { | |
writeLogFile() | ||
if (wroteLogFile) { | ||
// just a line break | ||
if (log.levels[log.level] <= log.levels.error) | ||
if (npm.log.levels[npm.log.level] <= npm.log.levels.error) | ||
console.error('') | ||
|
||
log.error( | ||
npm.log.error( | ||
'', | ||
[ | ||
'A complete log of this run can be found in:', | ||
|
@@ -93,121 +83,114 @@ process.on('exit', code => { | |
// these are needed for the tests to have a clean slate in each test case | ||
exitHandlerCalled = false | ||
wroteLogFile = false | ||
|
||
// actually exit. | ||
process.exit(code) | ||
}) | ||
|
||
const exit = (code, noLog) => { | ||
log.verbose('exit', code || 0) | ||
if (log.level === 'silent') | ||
noLog = true | ||
|
||
// noLog is true if there was an error, including if config wasn't loaded, so | ||
// this doesn't need a config.loaded guard | ||
if (code && !noLog) | ||
writeLogFile() | ||
|
||
// 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 | ||
process.stdout.write('', () => { | ||
process.exit(code) | ||
}) | ||
} | ||
|
||
const exitHandler = (err) => { | ||
log.disableProgress() | ||
npm.log.disableProgress() | ||
if (!npm.config.loaded) { | ||
// logging won't work unless we pretend that it's ready | ||
err = err || new Error('Exit prior to config file resolving.') | ||
console.error(err.stack || err.message) | ||
} | ||
|
||
if (exitHandlerCalled) | ||
err = err || new Error('Exit handler 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. | ||
// only show the notification if it finished. | ||
if (typeof npm.updateNotification === 'string') { | ||
const { level } = log | ||
log.level = log.levels.notice | ||
log.notice('', npm.updateNotification) | ||
log.level = level | ||
const { level } = npm.log | ||
npm.log.level = 'notice' | ||
npm.log.notice('', npm.updateNotification) | ||
npm.log.level = level | ||
} | ||
|
||
exitHandlerCalled = true | ||
if (!err) | ||
return exit() | ||
|
||
// 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 err.code === 'number' && err.code | ||
if (quietShellout) | ||
return exit(err.code, true) | ||
else if (typeof err === 'string') { | ||
log.error('', err) | ||
return exit(1, true) | ||
} else if (!(err instanceof Error)) { | ||
log.error('weird error', err) | ||
return exit(1, true) | ||
} | ||
|
||
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)) | ||
let exitCode | ||
let noLog | ||
|
||
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.shelloutCommands.includes(npm.command) | ||
const quietShellout = isShellout && typeof err.code === 'number' && err.code | ||
if (quietShellout) { | ||
exitCode = err.code | ||
noLog = true | ||
} else if (typeof err === 'string') { | ||
noLog = true | ||
npm.log.error('', err) | ||
} else if (!(err instanceof Error)) { | ||
noLog = true | ||
npm.log.error('weird error', err) | ||
} 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) | ||
npm.log.verbose(k, replaceInfo(v)) | ||
} | ||
|
||
npm.log.verbose('cwd', process.cwd()) | ||
|
||
const args = replaceInfo(process.argv) | ||
npm.log.verbose('', os.type() + ' ' + os.release()) | ||
npm.log.verbose('argv', args.map(JSON.stringify).join(' ')) | ||
npm.log.verbose('node', process.version) | ||
npm.log.verbose('npm ', 'v' + npm.version) | ||
|
||
for (const k of ['code', 'syscall', 'file', 'path', 'dest', 'errno']) { | ||
const v = err[k] | ||
if (v) | ||
npm.log.error(k, v) | ||
} | ||
|
||
const msg = errorMessage(err, npm) | ||
for (const errline of [...msg.summary, ...msg.detail]) | ||
npm.log.error(...errline) | ||
|
||
if (npm.config.loaded && npm.config.get('json')) { | ||
const error = { | ||
error: { | ||
code: err.code, | ||
summary: messageText(msg.summary), | ||
detail: messageText(msg.detail), | ||
}, | ||
} | ||
console.error(JSON.stringify(error, null, 2)) | ||
} | ||
|
||
if (typeof err.errno === 'number') | ||
exitCode = err.errno | ||
else if (typeof err.code === 'number') | ||
exitCode = err.code | ||
} | ||
} | ||
npm.log.verbose('exit', exitCode || 0) | ||
|
||
log.verbose('cwd', process.cwd()) | ||
|
||
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 = err[k] | ||
if (v) | ||
log.error(k, v) | ||
} | ||
if (npm.log.level === 'silent') | ||
noLog = true | ||
|
||
const msg = errorMessage(err, npm) | ||
for (const errline of [...msg.summary, ...msg.detail]) | ||
log.error(...errline) | ||
|
||
if (npm.config.loaded && npm.config.get('json')) { | ||
const error = { | ||
error: { | ||
code: err.code, | ||
summary: messageText(msg.summary), | ||
detail: messageText(msg.detail), | ||
}, | ||
} | ||
console.error(JSON.stringify(error, null, 2)) | ||
} | ||
// noLog is true if there was an error, including if config wasn't loaded, so | ||
// this doesn't need a config.loaded guard | ||
if (exitCode && !noLog) | ||
writeLogFile() | ||
|
||
exit(typeof err.errno === 'number' ? err.errno : typeof err.code === 'number' ? err.code : 1) | ||
// explicitly call process.exit now so we don't hang on things like the | ||
// update notifier, also flush stdout beforehand because process.exit doesn't | ||
// wait for that to happen. | ||
process.stdout.write('', () => process.exit(exitCode)) | ||
} | ||
|
||
const messageText = msg => msg.map(line => line.slice(1).join(' ')).join('\n') | ||
|
||
const writeLogFile = () => { | ||
if (wroteLogFile) | ||
return | ||
|
||
try { | ||
let logOutput = '' | ||
log.record.forEach(m => { | ||
npm.log.record.forEach(m => { | ||
const p = [m.id, m.level] | ||
if (m.prefix) | ||
p.push(m.prefix) | ||
|
@@ -230,7 +213,7 @@ const writeLogFile = () => { | |
fs.chownSync(file, st.uid, st.gid) | ||
|
||
// truncate once it's been written. | ||
log.record.length = 0 | ||
npm.log.record.length = 0 | ||
wroteLogFile = true | ||
} catch (ex) { | ||
|
||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a great intermediate step to avoid the singleton issue 🎉