Skip to content

Commit

Permalink
doctor: add new subcommand
Browse files Browse the repository at this point in the history
This command will diagnose user's environment and let
the user know some recommended solutions if they
potentially have any problems related to npm.

Credit: @watilde
Reviewed-By: @othiym23
Reviewed-By: @iarna
PR-URL: npm/npm#14582
Fixes: npm#6756
  • Loading branch information
watilde authored and iarna committed Dec 15, 2016
1 parent 0209ee5 commit 2359505
Show file tree
Hide file tree
Showing 11 changed files with 406 additions and 2 deletions.
44 changes: 44 additions & 0 deletions doc/cli/npm-doctor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
npm-doctor(1) -- Check your environments
========================================================

## SYNOPSIS

npm doctor

## DESCRIPTION

npm command is just a single command, but depends on several things outside
the code base. Broadly speaking, it is the following three types:

+ Technology stack: Node.js, git
+ Registry: `registry.npmjs.com`
+ Files: `node_modules` in local/global, cached files in `npm config get cache`

Without all of these working properly, the npm command will not work properly.
Many issue reports that arrive under us are often attributable to things that
are outside the code base as described above, and it is necessary to confirm
that this is correctly set with one command would help you solve your issue.
Also, in addition to this, there are also very many issue reports due to using
old versions of npm. Since npm is constantly improving, in every aspect the
`latest` npm is better than the old version(it should be, and we are trying).

From the above reasons, `npm doctor` will investigate the following items in
your environment and if there are any other recommended settings, it will
display the recommended example.

|What to check|What we recommend|
|---|---|
|`npm ping`|It must be able to communicate with the registry|
|`npm -v`|It is preferable that the latest version of LTS is used|
|`node -v`|It is preferable that the latest version of LTS is used|
|`npm config get registry`|In order not to consider exceptions, it is better to set default values `registry.npmjs.org`|
|`which git`|Git has to be installed|
|`Perms check on cached files`|All cached module files must be readable.|
|`Perms check on global node_modules`|All global module files must be executable.|
|`Perms check on local node_modules`|All local module files must be executable.|
|`Checksum cached files`|All cached files must not be broken|

## SEE ALSO

* npm-bugs(1)
* npm-help(1)
3 changes: 2 additions & 1 deletion lib/config/cmd-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ var cmdList = [
'start',
'restart',
'run-script',
'completion'
'completion',
'doctor'
]

var plumbing = [
Expand Down
109 changes: 109 additions & 0 deletions lib/doctor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
var path = require('path')
var chain = require('slide').chain
var table = require('text-table')
var color = require('ansicolors')
var styles = require('ansistyles')
var semver = require('semver')
var npm = require('./npm.js')
var log = require('npmlog')
var ansiTrim = require('./utils/ansi-trim.js')
var output = require('./utils/output.js')
var defaultRegistry = require('./config/defaults.js').defaults.registry
var checkPing = require('./doctor/check-ping.js')
var getGitPath = require('./doctor/get-git-path.js')
var checksumCachedFiles = require('./doctor/checksum-cached-files.js')
var checkFilesPermission = require('./doctor/check-files-permission.js')
var getLatestNodejsVersion = require('./doctor/get-latest-nodejs-version.js')
var getLatestNpmVersion = require('./doctor/get-latest-npm-version')
var globalNodeModules = path.join(npm.config.globalPrefix, 'lib', 'node_modules')
var localNodeModules = path.join(npm.config.localPrefix, 'node_modules')

module.exports = doctor

doctor.usage = 'npm doctor'

function doctor (args, silent, cb) {
args = args || {}
if (typeof cb !== 'function') {
cb = silent
silent = false
}

var actionsToRun = [
[checkPing],
[getLatestNpmVersion],
[getLatestNodejsVersion, args['node-url']],
[getGitPath],
[checkFilesPermission, npm.cache, 6],
[checkFilesPermission, globalNodeModules, 4],
[checkFilesPermission, localNodeModules, 6],
[checksumCachedFiles]
]

log.info('doctor', 'Running checkup')
chain(actionsToRun, function (stderr, stdout) {
if (stderr && stderr.message !== 'not found: git') return cb(stderr)
var outHead = ['Check', 'Value', 'Recommendation']
var list = makePretty(stdout)
var outBody = list

if (npm.color) {
outHead = outHead.map(function (item) {
return styles.underline(item)
})
outBody = outBody.map(function (item) {
if (item[2]) {
item[0] = color.red(item[0])
item[2] = color.magenta(item[2])
}
return item
})
}

var outTable = [outHead].concat(outBody)
var tableOpts = {
stringLength: function (s) { return ansiTrim(s).length }
}

if (!silent) output(table(outTable, tableOpts))

cb(null, list)
})
}

function makePretty (p) {
var ping = p[0] ? 'ok' : 'notOk'
var npmLTS = p[1]
var nodeLTS = p[2].replace('v', '')
var whichGit = p[3] || 'not installed'
var readbleCaches = p[4] ? 'ok' : 'notOk'
var executableGlobalModules = p[5] ? 'ok' : 'notOk'
var executableLocalModules = p[6] ? 'ok' : 'notOk'
var checksumCachedFiles = p[7] ? 'ok' : 'notOk'
var npmV = npm.version
var nodeV = process.version.replace('v', '')
var registry = npm.config.get('registry')
var list = [
['npm ping', ping],
['npm -v', 'v' + npmV],
['node -v', 'v' + nodeV],
['npm config get registry', registry],
['which git', whichGit],
['Perms check on cached files', readbleCaches],
['Perms check on global node_modules', executableGlobalModules],
['Perms check on local node_modules', executableLocalModules],
['Checksum cached files', checksumCachedFiles]
]

if (ping !== 'ok') list[0][2] = 'Check your internet connection'
if (!semver.satisfies(npmV, '>=' + npmLTS)) list[1][2] = 'Use npm v' + npmLTS
if (!semver.satisfies(nodeV, '>=' + nodeLTS)) list[2][2] = 'Use node v' + nodeLTS
if (registry !== defaultRegistry) list[3][2] = 'Try `npm config set ' + defaultRegistry
if (whichGit === 'not installed') list[4][2] = 'Install git'
if (readbleCaches !== 'ok') list[5][2] = 'Check the permission of your files in ' + npm.config.get('cache')
if (executableGlobalModules !== 'ok') list[6][2] = 'Check the permission of your files in ' + globalNodeModules
if (executableLocalModules !== 'ok') list[7][2] = 'Check the permission of your files in ' + localNodeModules
if (checksumCachedFiles !== 'ok') list[8][2] = 'You have some broken packages in your cache'

return list
}
55 changes: 55 additions & 0 deletions lib/doctor/check-files-permission.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
var fs = require('fs')
var path = require('path')
var getUid = require('uid-number')
var chain = require('slide').chain
var log = require('npmlog')
var npm = require('../npm.js')
var fileCompletion = require('../utils/completion/file-completion.js')

function checkFilesPermission (root, mask, cb) {
if (process.platform === 'win32') return cb(null, true)
getUid(npm.config.get('user'), npm.config.get('group'), function (e, uid, gid) {
if (e) {
tracker.finish()
tracker.warn('checkFilePermissions', 'Error looking up user and group:', e)
return cb(e)
}
var tracker = log.newItem('checkFilePermissions', 1)
tracker.info('checkFilePermissions', 'Building file list of ' + root)
fileCompletion(root, '.', Infinity, function (e, files) {
if (e) {
tracker.warn('checkFilePermissions', 'Error building file list:', e)
tracker.finish()
return cb(e)
}
tracker.addWork(files.length)
tracker.completeWork(1)
chain(files.map(andCheckFile), function (er) {
tracker.finish()
cb(null, !er)
})
function andCheckFile (f) {
return [checkFile, f]
}
function checkFile (f, next) {
var file = path.join(root, f)
tracker.silly('checkFilePermissions', f)
fs.stat(file, function (e, stat) {
tracker.completeWork(1)
if (e) return next(e)
if (!stat.isFile()) return next()
var mode = stat.mode
var isGroup = stat.gid ? stat.gid === gid : true
var isUser = stat.uid ? stat.uid === uid : true
if ((mode & parseInt('000' + mask, 8))) return next()
if ((isGroup && mode & parseInt('00' + mask + '0', 8))) return next()
if ((isUser && mode & parseInt('0' + mask + '00', 8))) return next()
tracker.error('checkFilePermissions', 'Missing permissions on (' + isGroup + ', ' + isUser + ', ' + mode + ')', file)
return next(new Error('Missing permissions for ' + file))
})
}
})
})
}

module.exports = checkFilesPermission
13 changes: 13 additions & 0 deletions lib/doctor/check-ping.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
var log = require('npmlog')
var ping = require('../ping.js')

function checkPing (cb) {
var tracker = log.newItem('checkPing', 1)
tracker.info('checkPing', 'Pinging registry')
ping({}, true, function (err, pong) {
tracker.finish()
cb(err, pong)
})
}

module.exports = checkPing
62 changes: 62 additions & 0 deletions lib/doctor/checksum-cached-files.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
var crypto = require('crypto')
var fs = require('fs')
var path = require('path')
var chain = require('slide').chain
var log = require('npmlog')
var npm = require('../npm')
var fileCompletion = require('../utils/completion/file-completion.js')

function checksum (str) {
return crypto
.createHash('sha1')
.update(str, 'utf8')
.digest('hex')
}

function checksumCachedFiles (cb) {
var tracker = log.newItem('checksumCachedFiles', 1)
tracker.info('checksumCachedFiles', 'Building file list of ' + npm.cache)
fileCompletion(npm.cache, '.', Infinity, function (e, files) {
if (e) {
tracker.finish()
return cb(e)
}
tracker.addWork(files.length)
tracker.completeWork(1)
chain(files.map(andChecksumFile), function (er) {
tracker.finish()
cb(null, !er)
})
function andChecksumFile (f) {
return [function (next) { process.nextTick(function () { checksumFile(f, next) }) }]
}
function checksumFile (f, next) {
var file = path.join(npm.cache, f)
tracker.silly('checksumFile', f)
if (!/.tgz$/.test(file)) {
tracker.completeWork(1)
return next()
}
fs.readFile(file, function (err, tgz) {
tracker.completeWork(1)
if (err) return next(err)
try {
var pkgJSON = fs.readFileSync(path.join(path.dirname(file), 'package/package.json'))
} catch (e) {
return next() // no package.json in cche is ok
}
try {
var pkg = JSON.parse(pkgJSON)
var shasum = (pkg.dist && pkg.dist.shasum) || pkg._shasum
var actual = checksum(tgz)
if (actual !== shasum) return next(new Error('Checksum mismatch on ' + file + ', expected: ' + shasum + ', got: ' + shasum))
return next()
} catch (e) {
return next(new Error('Error parsing JSON in ' + file + ': ' + e))
}
})
}
})
}

module.exports = checksumCachedFiles
13 changes: 13 additions & 0 deletions lib/doctor/get-git-path.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
var log = require('npmlog')
var which = require('which')

function getGitPath (cb) {
var tracker = log.newItem('getGitPath', 1)
tracker.info('getGitPath', 'Finding git in your PATH')
which('git', function (err, path) {
tracker.finish()
cb(err, path)
})
}

module.exports = getGitPath
26 changes: 26 additions & 0 deletions lib/doctor/get-latest-nodejs-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
var log = require('npmlog')
var request = require('request')

function getLatestNodejsVersion (url, cb) {
var tracker = log.newItem('getLatestNodejsVersion', 1)
tracker.info('getLatestNodejsVersion', 'Getting Node.js release information')
var version = ''
url = url || 'https://nodejs.org/dist/index.json'
request(url, function (e, res, index) {
tracker.finish()
if (e) return cb(e)
if (res.statusCode !== 200) {
return cb(new Error('Status not 200, ' + res.statusCode))
}
try {
JSON.parse(index).forEach(function (item) {
if (item.lts && item.version > version) version = item.version
})
cb(null, version)
} catch (e) {
cb(e)
}
})
}

module.exports = getLatestNodejsVersion
13 changes: 13 additions & 0 deletions lib/doctor/get-latest-npm-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
var log = require('npmlog')
var fetchPackageMetadata = require('../fetch-package-metadata')

function getLatestNpmVersion (cb) {
var tracker = log.newItem('getLatestNpmVersion', 1)
tracker.info('getLatestNpmVersion', 'Getting npm package information')
fetchPackageMetadata('npm@latest', '.', function (e, d) {
tracker.finish()
cb(e, d.version)
})
}

module.exports = getLatestNpmVersion
1 change: 0 additions & 1 deletion node_modules/request/node_modules/.bin/uuid

This file was deleted.

Loading

0 comments on commit 2359505

Please sign in to comment.