doctor.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. const cacache = require('cacache')
  2. const chalk = require('chalk')
  3. const fs = require('fs')
  4. const fetch = require('make-fetch-happen')
  5. const table = require('text-table')
  6. const which = require('which')
  7. const pacote = require('pacote')
  8. const { resolve } = require('path')
  9. const semver = require('semver')
  10. const { promisify } = require('util')
  11. const ansiTrim = require('./utils/ansi-trim.js')
  12. const isWindows = require('./utils/is-windows.js')
  13. const ping = require('./utils/ping.js')
  14. const { registry: { default: defaultRegistry } } = require('./utils/config/definitions.js')
  15. const lstat = promisify(fs.lstat)
  16. const readdir = promisify(fs.readdir)
  17. const access = promisify(fs.access)
  18. const { R_OK, W_OK, X_OK } = fs.constants
  19. const maskLabel = mask => {
  20. const label = []
  21. if (mask & R_OK)
  22. label.push('readable')
  23. if (mask & W_OK)
  24. label.push('writable')
  25. if (mask & X_OK)
  26. label.push('executable')
  27. return label.join(', ')
  28. }
  29. const BaseCommand = require('./base-command.js')
  30. class Doctor extends BaseCommand {
  31. /* istanbul ignore next - see test/lib/load-all-commands.js */
  32. static get description () {
  33. return 'Check your npm environment'
  34. }
  35. /* istanbul ignore next - see test/lib/load-all-commands.js */
  36. static get name () {
  37. return 'doctor'
  38. }
  39. /* istanbul ignore next - see test/lib/load-all-commands.js */
  40. static get params () {
  41. return ['registry']
  42. }
  43. exec (args, cb) {
  44. this.doctor(args).then(() => cb()).catch(cb)
  45. }
  46. async doctor (args) {
  47. this.npm.log.info('Running checkup')
  48. // each message is [title, ok, message]
  49. const messages = []
  50. const actions = [
  51. ['npm ping', 'checkPing', []],
  52. ['npm -v', 'getLatestNpmVersion', []],
  53. ['node -v', 'getLatestNodejsVersion', []],
  54. ['npm config get registry', 'checkNpmRegistry', []],
  55. ['which git', 'getGitPath', []],
  56. ...(isWindows ? [] : [
  57. ['Perms check on cached files', 'checkFilesPermission', [this.npm.cache, true, R_OK]],
  58. ['Perms check on local node_modules', 'checkFilesPermission', [this.npm.localDir, true]],
  59. ['Perms check on global node_modules', 'checkFilesPermission', [this.npm.globalDir, false]],
  60. ['Perms check on local bin folder', 'checkFilesPermission', [this.npm.localBin, false, R_OK | W_OK | X_OK]],
  61. ['Perms check on global bin folder', 'checkFilesPermission', [this.npm.globalBin, false, X_OK]],
  62. ]),
  63. ['Verify cache contents', 'verifyCachedFiles', [this.npm.flatOptions.cache]],
  64. // TODO:
  65. // - ensure arborist.loadActual() runs without errors and no invalid edges
  66. // - ensure package-lock.json matches loadActual()
  67. // - verify loadActual without hidden lock file matches hidden lockfile
  68. // - verify all local packages have bins linked
  69. ]
  70. // Do the actual work
  71. for (const [msg, fn, args] of actions) {
  72. const line = [msg]
  73. try {
  74. line.push(true, await this[fn](...args))
  75. } catch (er) {
  76. line.push(false, er)
  77. }
  78. messages.push(line)
  79. }
  80. const outHead = ['Check', 'Value', 'Recommendation/Notes']
  81. .map(!this.npm.color ? h => h : h => chalk.underline(h))
  82. let allOk = true
  83. const outBody = messages.map(!this.npm.color
  84. ? item => {
  85. allOk = allOk && item[1]
  86. item[1] = item[1] ? 'ok' : 'not ok'
  87. item[2] = String(item[2])
  88. return item
  89. }
  90. : item => {
  91. allOk = allOk && item[1]
  92. if (!item[1]) {
  93. item[0] = chalk.red(item[0])
  94. item[2] = chalk.magenta(String(item[2]))
  95. }
  96. item[1] = item[1] ? chalk.green('ok') : chalk.red('not ok')
  97. return item
  98. })
  99. const outTable = [outHead, ...outBody]
  100. const tableOpts = {
  101. stringLength: s => ansiTrim(s).length,
  102. }
  103. const silent = this.npm.log.levels[this.npm.log.level] >
  104. this.npm.log.levels.error
  105. if (!silent) {
  106. this.npm.output(table(outTable, tableOpts))
  107. if (!allOk)
  108. console.error('')
  109. }
  110. if (!allOk)
  111. throw 'Some problems found. See above for recommendations.'
  112. }
  113. async checkPing () {
  114. const tracker = this.npm.log.newItem('checkPing', 1)
  115. tracker.info('checkPing', 'Pinging registry')
  116. try {
  117. await ping(this.npm.flatOptions)
  118. return ''
  119. } catch (er) {
  120. if (/^E\d{3}$/.test(er.code || ''))
  121. throw er.code.substr(1) + ' ' + er.message
  122. else
  123. throw er.message
  124. } finally {
  125. tracker.finish()
  126. }
  127. }
  128. async getLatestNpmVersion () {
  129. const tracker = this.npm.log.newItem('getLatestNpmVersion', 1)
  130. tracker.info('getLatestNpmVersion', 'Getting npm package information')
  131. try {
  132. const latest = (await pacote.manifest('npm@latest', this.npm.flatOptions)).version
  133. if (semver.gte(this.npm.version, latest))
  134. return `current: v${this.npm.version}, latest: v${latest}`
  135. else
  136. throw `Use npm v${latest}`
  137. } finally {
  138. tracker.finish()
  139. }
  140. }
  141. async getLatestNodejsVersion () {
  142. // XXX get the latest in the current major as well
  143. const current = process.version
  144. const currentRange = `^${current}`
  145. const url = 'https://nodejs.org/dist/index.json'
  146. const tracker = this.npm.log.newItem('getLatestNodejsVersion', 1)
  147. tracker.info('getLatestNodejsVersion', 'Getting Node.js release information')
  148. try {
  149. const res = await fetch(url, { method: 'GET', ...this.npm.flatOptions })
  150. const data = await res.json()
  151. let maxCurrent = '0.0.0'
  152. let maxLTS = '0.0.0'
  153. for (const { lts, version } of data) {
  154. if (lts && semver.gt(version, maxLTS))
  155. maxLTS = version
  156. if (semver.satisfies(version, currentRange) &&
  157. semver.gt(version, maxCurrent))
  158. maxCurrent = version
  159. }
  160. const recommended = semver.gt(maxCurrent, maxLTS) ? maxCurrent : maxLTS
  161. if (semver.gte(process.version, recommended))
  162. return `current: ${current}, recommended: ${recommended}`
  163. else
  164. throw `Use node ${recommended} (current: ${current})`
  165. } finally {
  166. tracker.finish()
  167. }
  168. }
  169. async checkFilesPermission (root, shouldOwn, mask = null) {
  170. if (mask === null)
  171. mask = shouldOwn ? R_OK | W_OK : R_OK
  172. let ok = true
  173. const tracker = this.npm.log.newItem(root, 1)
  174. try {
  175. const uid = process.getuid()
  176. const gid = process.getgid()
  177. const files = new Set([root])
  178. for (const f of files) {
  179. tracker.silly('checkFilesPermission', f.substr(root.length + 1))
  180. const st = await lstat(f)
  181. .catch(er => {
  182. ok = false
  183. tracker.warn('checkFilesPermission', 'error getting info for ' + f)
  184. })
  185. tracker.completeWork(1)
  186. if (!st)
  187. continue
  188. if (shouldOwn && (uid !== st.uid || gid !== st.gid)) {
  189. tracker.warn('checkFilesPermission', 'should be owner of ' + f)
  190. ok = false
  191. }
  192. if (!st.isDirectory() && !st.isFile())
  193. continue
  194. try {
  195. await access(f, mask)
  196. } catch (er) {
  197. ok = false
  198. const msg = `Missing permissions on ${f} (expect: ${maskLabel(mask)})`
  199. tracker.error('checkFilesPermission', msg)
  200. continue
  201. }
  202. if (st.isDirectory()) {
  203. const entries = await readdir(f)
  204. .catch(er => {
  205. ok = false
  206. tracker.warn('checkFilesPermission', 'error reading directory ' + f)
  207. return []
  208. })
  209. for (const entry of entries)
  210. files.add(resolve(f, entry))
  211. }
  212. }
  213. } finally {
  214. tracker.finish()
  215. if (!ok) {
  216. throw `Check the permissions of files in ${root}` +
  217. (shouldOwn ? ' (should be owned by current user)' : '')
  218. } else
  219. return ''
  220. }
  221. }
  222. async getGitPath () {
  223. const tracker = this.npm.log.newItem('getGitPath', 1)
  224. tracker.info('getGitPath', 'Finding git in your PATH')
  225. try {
  226. return await which('git').catch(er => {
  227. tracker.warn(er)
  228. throw "Install git and ensure it's in your PATH."
  229. })
  230. } finally {
  231. tracker.finish()
  232. }
  233. }
  234. async verifyCachedFiles () {
  235. const tracker = this.npm.log.newItem('verifyCachedFiles', 1)
  236. tracker.info('verifyCachedFiles', 'Verifying the npm cache')
  237. try {
  238. const stats = await cacache.verify(this.npm.flatOptions.cache)
  239. const {
  240. badContentCount,
  241. reclaimedCount,
  242. missingContent,
  243. reclaimedSize,
  244. } = stats
  245. if (badContentCount || reclaimedCount || missingContent) {
  246. if (badContentCount)
  247. tracker.warn('verifyCachedFiles', `Corrupted content removed: ${badContentCount}`)
  248. if (reclaimedCount)
  249. tracker.warn('verifyCachedFiles', `Content garbage-collected: ${reclaimedCount} (${reclaimedSize} bytes)`)
  250. if (missingContent)
  251. tracker.warn('verifyCachedFiles', `Missing content: ${missingContent}`)
  252. tracker.warn('verifyCachedFiles', 'Cache issues have been fixed')
  253. }
  254. tracker.info('verifyCachedFiles', `Verification complete. Stats: ${
  255. JSON.stringify(stats, null, 2)
  256. }`)
  257. return `verified ${stats.verifiedContent} tarballs`
  258. } finally {
  259. tracker.finish()
  260. }
  261. }
  262. async checkNpmRegistry () {
  263. if (this.npm.flatOptions.registry !== defaultRegistry)
  264. throw `Try \`npm config set registry=${defaultRegistry}\``
  265. else
  266. return `using default registry (${defaultRegistry})`
  267. }
  268. }
  269. module.exports = Doctor