exit-handler.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. const os = require('os')
  2. const path = require('path')
  3. const writeFileAtomic = require('write-file-atomic')
  4. const mkdirp = require('mkdirp-infer-owner')
  5. const fs = require('graceful-fs')
  6. const errorMessage = require('./error-message.js')
  7. const replaceInfo = require('./replace-info.js')
  8. let exitHandlerCalled = false
  9. let logFileName
  10. let npm // set by the cli
  11. let wroteLogFile = false
  12. const getLogFile = () => {
  13. // we call this multiple times, so we need to treat it as a singleton because
  14. // the date is part of the name
  15. if (!logFileName)
  16. logFileName = path.resolve(npm.config.get('cache'), '_logs', (new Date()).toISOString().replace(/[.:]/g, '_') + '-debug.log')
  17. return logFileName
  18. }
  19. process.on('exit', code => {
  20. // process.emit is synchronous, so the timeEnd handler will run before the
  21. // unfinished timer check below
  22. process.emit('timeEnd', 'npm')
  23. npm.log.disableProgress()
  24. for (const [name, timers] of npm.timers)
  25. npm.log.verbose('unfinished npm timer', name, timers)
  26. if (npm.config.loaded && npm.config.get('timing')) {
  27. try {
  28. const file = path.resolve(npm.config.get('cache'), '_timing.json')
  29. const dir = path.dirname(npm.config.get('cache'))
  30. mkdirp.sync(dir)
  31. fs.appendFileSync(file, JSON.stringify({
  32. command: process.argv.slice(2),
  33. logfile: getLogFile(),
  34. version: npm.version,
  35. ...npm.timings,
  36. }) + '\n')
  37. const st = fs.lstatSync(path.dirname(npm.config.get('cache')))
  38. fs.chownSync(dir, st.uid, st.gid)
  39. fs.chownSync(file, st.uid, st.gid)
  40. } catch (ex) {
  41. // ignore
  42. }
  43. }
  44. if (!code)
  45. npm.log.info('ok')
  46. else
  47. npm.log.verbose('code', code)
  48. if (!exitHandlerCalled) {
  49. process.exitCode = code || 1
  50. npm.log.error('', 'Exit handler never called!')
  51. console.error('')
  52. npm.log.error('', 'This is an error with npm itself. Please report this error at:')
  53. npm.log.error('', ' <https://github.com/npm/cli/issues>')
  54. // TODO this doesn't have an npm.config.loaded guard
  55. writeLogFile()
  56. }
  57. // In timing mode we always write the log file
  58. if (npm.config.loaded && npm.config.get('timing') && !wroteLogFile)
  59. writeLogFile()
  60. if (wroteLogFile) {
  61. // just a line break
  62. if (npm.log.levels[npm.log.level] <= npm.log.levels.error)
  63. console.error('')
  64. npm.log.error(
  65. '',
  66. [
  67. 'A complete log of this run can be found in:',
  68. ' ' + getLogFile(),
  69. ].join('\n')
  70. )
  71. }
  72. // these are needed for the tests to have a clean slate in each test case
  73. exitHandlerCalled = false
  74. wroteLogFile = false
  75. })
  76. const exitHandler = (err) => {
  77. npm.log.disableProgress()
  78. if (!npm.config.loaded) {
  79. err = err || new Error('Exit prior to config file resolving.')
  80. console.error(err.stack || err.message)
  81. }
  82. // only show the notification if it finished.
  83. if (typeof npm.updateNotification === 'string') {
  84. const { level } = npm.log
  85. npm.log.level = 'notice'
  86. npm.log.notice('', npm.updateNotification)
  87. npm.log.level = level
  88. }
  89. exitHandlerCalled = true
  90. let exitCode
  91. let noLog
  92. if (err) {
  93. exitCode = 1
  94. // if we got a command that just shells out to something else, then it
  95. // will presumably print its own errors and exit with a proper status
  96. // code if there's a problem. If we got an error with a code=0, then...
  97. // something else went wrong along the way, so maybe an npm problem?
  98. const isShellout = npm.shelloutCommands.includes(npm.command)
  99. const quietShellout = isShellout && typeof err.code === 'number' && err.code
  100. if (quietShellout) {
  101. exitCode = err.code
  102. noLog = true
  103. } else if (typeof err === 'string') {
  104. noLog = true
  105. npm.log.error('', err)
  106. } else if (!(err instanceof Error)) {
  107. noLog = true
  108. npm.log.error('weird error', err)
  109. } else {
  110. if (!err.code) {
  111. const matchErrorCode = err.message.match(/^(?:Error: )?(E[A-Z]+)/)
  112. err.code = matchErrorCode && matchErrorCode[1]
  113. }
  114. for (const k of ['type', 'stack', 'statusCode', 'pkgid']) {
  115. const v = err[k]
  116. if (v)
  117. npm.log.verbose(k, replaceInfo(v))
  118. }
  119. npm.log.verbose('cwd', process.cwd())
  120. const args = replaceInfo(process.argv)
  121. npm.log.verbose('', os.type() + ' ' + os.release())
  122. npm.log.verbose('argv', args.map(JSON.stringify).join(' '))
  123. npm.log.verbose('node', process.version)
  124. npm.log.verbose('npm ', 'v' + npm.version)
  125. for (const k of ['code', 'syscall', 'file', 'path', 'dest', 'errno']) {
  126. const v = err[k]
  127. if (v)
  128. npm.log.error(k, v)
  129. }
  130. const msg = errorMessage(err, npm)
  131. for (const errline of [...msg.summary, ...msg.detail])
  132. npm.log.error(...errline)
  133. if (npm.config.loaded && npm.config.get('json')) {
  134. const error = {
  135. error: {
  136. code: err.code,
  137. summary: messageText(msg.summary),
  138. detail: messageText(msg.detail),
  139. },
  140. }
  141. console.error(JSON.stringify(error, null, 2))
  142. }
  143. if (typeof err.errno === 'number')
  144. exitCode = err.errno
  145. else if (typeof err.code === 'number')
  146. exitCode = err.code
  147. }
  148. }
  149. npm.log.verbose('exit', exitCode || 0)
  150. if (npm.log.level === 'silent')
  151. noLog = true
  152. // noLog is true if there was an error, including if config wasn't loaded, so
  153. // this doesn't need a config.loaded guard
  154. if (exitCode && !noLog)
  155. writeLogFile()
  156. // explicitly call process.exit now so we don't hang on things like the
  157. // update notifier, also flush stdout beforehand because process.exit doesn't
  158. // wait for that to happen.
  159. process.stdout.write('', () => process.exit(exitCode))
  160. }
  161. const messageText = msg => msg.map(line => line.slice(1).join(' ')).join('\n')
  162. const writeLogFile = () => {
  163. try {
  164. let logOutput = ''
  165. npm.log.record.forEach(m => {
  166. const p = [m.id, m.level]
  167. if (m.prefix)
  168. p.push(m.prefix)
  169. const pref = p.join(' ')
  170. m.message.trim().split(/\r?\n/)
  171. .map(line => (pref + ' ' + line).trim())
  172. .forEach(line => {
  173. logOutput += line + os.EOL
  174. })
  175. })
  176. const file = getLogFile()
  177. const dir = path.dirname(file)
  178. mkdirp.sync(dir)
  179. writeFileAtomic.sync(file, logOutput)
  180. const st = fs.lstatSync(path.dirname(npm.config.get('cache')))
  181. fs.chownSync(dir, st.uid, st.gid)
  182. fs.chownSync(file, st.uid, st.gid)
  183. // truncate once it's been written.
  184. npm.log.record.length = 0
  185. wroteLogFile = true
  186. } catch (ex) {
  187. }
  188. }
  189. module.exports = exitHandler
  190. module.exports.setNpm = (n) => {
  191. npm = n
  192. }