-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: improved error handling on the CLI #1335
Changes from 6 commits
43c0c13
6924e35
40111af
6dbb834
c649a98
2bdfeee
43bcafa
34bab95
7209da3
7f6fccc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,8 @@ | |
test/repo-tests* | ||
**/bundle.js | ||
docs | ||
.vscode | ||
.eslintrc | ||
# Logs | ||
logs | ||
*.log | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,104 +2,161 @@ | |
|
||
'use strict' | ||
|
||
const yargs = require('yargs') | ||
const updateNotifier = require('update-notifier') | ||
const readPkgUp = require('read-pkg-up') | ||
const fs = require('fs') | ||
const path = require('path') | ||
const utils = require('./utils') | ||
const print = utils.print | ||
const yargs = require('yargs/yargs') | ||
const updateNotifier = require('update-notifier') | ||
const readPkgUp = require('read-pkg-up') | ||
const { disablePrinting, print, getNodeOrAPI } = require('./utils') | ||
const addCmd = require('./commands/files/add') | ||
const catCmd = require('./commands/files/cat') | ||
const getCmd = require('./commands/files/get') | ||
|
||
const pkg = readPkgUp.sync({cwd: __dirname}).pkg | ||
|
||
updateNotifier({ | ||
pkg, | ||
updateCheckInterval: 1000 * 60 * 60 * 24 * 7 // 1 week | ||
}).notify() | ||
|
||
const args = process.argv.slice(2) | ||
const MSG_USAGE = `Usage: | ||
ipfs - Global p2p merkle-dag filesystem. | ||
|
||
ipfs [options] <command> ...` | ||
const MSG_EPILOGUE = `Use 'ipfs <command> --help' to learn more about each command. | ||
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. Minor - too much indentation! 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. Sorry where? 26 has no identation. It's exactly the same as the go impl. 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. i see now, its 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. fixed with last commit |
||
|
||
ipfs uses a repository in the local file system. By default, the repo is | ||
located at ~/.ipfs. To change the repo location, set the $IPFS_PATH | ||
environment variable: | ||
|
||
export IPFS_PATH=/path/to/ipfsrepo | ||
|
||
// Determine if the first argument is a sub-system command | ||
EXIT STATUS | ||
|
||
The CLI will exit with one of the following values: | ||
|
||
0 Successful execution. | ||
1 Failed executions. | ||
` | ||
const MSG_NO_CMD = 'You need at least one command before moving on' | ||
|
||
const argv = process.argv.slice(2) | ||
const commandNames = fs.readdirSync(path.join(__dirname, 'commands')) | ||
const isCommand = commandNames.includes(`${args[0]}.js`) | ||
const isCommand = commandNames.includes(`${argv[0]}.js`) | ||
|
||
const cli = yargs | ||
let args = {} | ||
let cli = yargs(argv) | ||
.usage(MSG_USAGE) | ||
.option('silent', { | ||
desc: 'Write no output', | ||
type: 'boolean', | ||
default: false, | ||
coerce: ('silent', silent => silent ? utils.disablePrinting() : silent) | ||
coerce: disablePrinting | ||
}) | ||
.option('debug', { | ||
desc: 'Show debug output', | ||
type: 'boolean', | ||
default: false, | ||
alias: 'D' | ||
}) | ||
.option('pass', { | ||
desc: 'Pass phrase for the keys', | ||
type: 'string', | ||
default: '' | ||
}) | ||
.option('api', { | ||
desc: 'Use a specific API instance.', | ||
type: 'string' | ||
}) | ||
.commandDir('commands', { | ||
// Only include the commands for the sub-system we're using, or include all | ||
// if no sub-system command has been passed. | ||
include (path, filename) { | ||
if (!isCommand) return true | ||
return `${args[0]}.js` === filename | ||
return `${argv[0]}.js` === filename | ||
} | ||
}) | ||
.epilog(utils.ipfsPathHelp) | ||
.demandCommand(1) | ||
.fail((msg, err, yargs) => { | ||
if (err) { | ||
throw err // preserve stack | ||
|
||
if(!isCommand){ | ||
cli | ||
// NOTE: This creates an alias of | ||
// `jsipfs files {add, get, cat}` to `jsipfs {add, get, cat}`. | ||
// This will stay until https://github.com/ipfs/specs/issues/98 is resolved. | ||
.command(addCmd) | ||
.command(catCmd) | ||
.command(getCmd) | ||
} | ||
cli | ||
.demandCommand(1, MSG_NO_CMD) | ||
.alias('help', 'h') | ||
.epilogue(MSG_EPILOGUE) | ||
.strict() | ||
// .recommendCommands() | ||
.completion() | ||
|
||
if (['daemon', 'init', 'id', 'version'].includes(argv[0])) { | ||
args = cli.fail((msg, err, yargs) => { | ||
if (err instanceof Error && err.message && !msg) { | ||
msg = err.message | ||
} | ||
|
||
if (args.length > 0) { | ||
print(msg) | ||
// Cli specific error messages | ||
if (err && err.code === 'ERR_REPO_NOT_INITIALIZED') { | ||
msg = `No IPFS repo found in ${err.path}. | ||
please run: 'ipfs init'` | ||
} | ||
|
||
yargs.showHelp() | ||
}) | ||
// Show help and error message | ||
if (!args.silent) { | ||
yargs.showHelp() | ||
console.error('Error: ' + msg) | ||
} | ||
|
||
// If not a sub-system command then load the top level aliases | ||
if (!isCommand) { | ||
// NOTE: This creates an alias of | ||
// `jsipfs files {add, get, cat}` to `jsipfs {add, get, cat}`. | ||
// This will stay until https://github.com/ipfs/specs/issues/98 is resolved. | ||
const addCmd = require('./commands/files/add') | ||
const catCmd = require('./commands/files/cat') | ||
const getCmd = require('./commands/files/get') | ||
const aliases = [addCmd, catCmd, getCmd] | ||
aliases.forEach((alias) => { | ||
cli.command(alias.command, alias.describe, alias.builder, alias.handler) | ||
}) | ||
} | ||
// Write to stderr when debug is on | ||
if (err && args.debug) { | ||
console.error(err) | ||
} | ||
|
||
// Need to skip to avoid locking as these commands | ||
// don't require a daemon | ||
if (args[0] === 'daemon' || args[0] === 'init') { | ||
cli | ||
.help() | ||
.strict() | ||
.completion() | ||
.parse(args) | ||
process.exit(1) | ||
}).argv | ||
} else { | ||
// here we have to make a separate yargs instance with | ||
// only the `api` option because we need this before doing | ||
// the final yargs parse where the command handler is invoked.. | ||
yargs().option('api').parse(process.argv, (err, argv, output) => { | ||
if (err) { | ||
throw err | ||
} | ||
utils.getIPFS(argv, (err, ipfs, cleanup) => { | ||
if (err) { throw err } | ||
|
||
cli | ||
.help() | ||
.strict() | ||
.completion() | ||
.parse(args, { ipfs: ipfs }, (err, argv, output) => { | ||
if (output) { print(output) } | ||
|
||
cleanup(() => { | ||
if (err) { throw err } | ||
yargs() | ||
.option('pass', { | ||
desc: 'Pass phrase for the keys', | ||
type: 'string', | ||
default: '' | ||
}) | ||
.option('api', { | ||
desc: 'Use a specific API instance.', | ||
type: 'string' | ||
}) | ||
.parse(argv, (err, parsedArgv, output) => { | ||
if (err) { | ||
console.error(err) | ||
} else { | ||
getNodeOrAPI(parsedArgv) | ||
.then(node => { | ||
args = cli | ||
.parse(argv, { ipfs: node }, (err, parsedArgv, output) => { | ||
if (output) { | ||
print(output) | ||
} | ||
if (node && node._repo && !node._repo.closed) { | ||
node._repo.close(err => { | ||
if (err) { | ||
console.error(err) | ||
} | ||
}) | ||
} | ||
if (err && parsedArgv.debug) { | ||
console.error(err) | ||
} | ||
}) | ||
}) | ||
.catch(err => { | ||
console.error(err) | ||
process.exit(1) | ||
}) | ||
}) | ||
} | ||
}) | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
'use strict' | ||
const print = require('../utils').print | ||
|
||
const {print, getNodeOrAPI} = require('../utils') | ||
|
||
module.exports = { | ||
command: 'id', | ||
|
@@ -15,12 +16,11 @@ module.exports = { | |
|
||
handler (argv) { | ||
// TODO: handle argv.format | ||
argv.ipfs.id((err, id) => { | ||
if (err) { | ||
throw err | ||
} | ||
|
||
print(JSON.stringify(id, '', 2)) | ||
}) | ||
return getNodeOrAPI(argv) | ||
.then(node => Promise.all([Promise.resolve(node), node.id()])) | ||
.then(([node, id]) => { | ||
print(JSON.stringify(id, '', 2)) | ||
node.stop() | ||
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. How come Do all our commands have to remember to call 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. I'm not sure why this command is requiring a online node? Shouldn't this just work and return the ID without having to start a node? Feels wildly inefficient. Looking at what go-ipfs does: when you call ID with a online daemon, it returns the ID object but with swarm address. When offline, it returns the same but with addresses empty. In contrast, what this command seems to be doing is to either grab the API if daemon is running but if none is running, start a node. 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. That too, but I think that should be fixed outside of this PR. 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. The more i look, more code paths i see, events, cb(err), fsm, etc. This is tricky. I'm gonna push couple commits to improve further on this.
you r right every cmd needs to do cleanup (easier cleanup coming in the next commits)
I think being explicit and each cmd control their own lifecycle is better. Also we could remove all this code https://github.com/ipfs/js-ipfs/blob/master/src/cli/bin.js#L73-L104, which got even bigger in this PR after adding more safe guards and error handling. In the end we would only have one code path ending in
It works exactly like that now {
"id": "QmYHif2UK4aFaMYBiNZYrA7xLvYJajYSQt14H5tmHyagvV",
"publicKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBJRc5+YxkfPX1LfwUXGY18ezEUpsUmWLAQxReOdeedbEHly8O7WYfYW1iDV1obVttDAbsYLMLh2gmoPqyMjvcAtA/JFnkCjTJ1GCAJFPvrHly5ZFO67knMTHXfVTaKJRBs98JnwrVgFRAbZAwoeVBRbPeP53FOmiw3pXoraOXU7YvGV3vc29PUTn4+1g4seoznyHAx5Ezp52MspiBvb0QmkEGvqZEKFRBUSz8pqMPCDYtItXMzOoBJG29TxxUztGTHvfogdxY9EOpPxVW6Nm0YRWbs9Vc8m/Vjtb88VQGrXXiplgag8OzyT8qM7Axx44nqOkAnQM3L50lOJ/ezGr9AgMBAAE=",
"addresses": [],
"agentVersion": "js-ipfs/0.28.2",
"protocolVersion": "9000"
} |
||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,8 +2,7 @@ | |
|
||
const Repo = require('ipfs-repo') | ||
const IPFS = require('../../core') | ||
const utils = require('../utils') | ||
const print = utils.print | ||
const { ipfsPathHelp, getRepoPath, print } = require('../utils') | ||
|
||
module.exports = { | ||
command: 'init', | ||
|
@@ -12,7 +11,7 @@ module.exports = { | |
|
||
builder (yargs) { | ||
return yargs | ||
.epilog(utils.ipfsPathHelp) | ||
.epilog(ipfsPathHelp) | ||
.option('bits', { | ||
type: 'number', | ||
alias: 'b', | ||
|
@@ -22,33 +21,26 @@ module.exports = { | |
.option('emptyRepo', { | ||
alias: 'e', | ||
type: 'boolean', | ||
describe: "Don't add and pin help files to the local storage" | ||
describe: 'Don\'t add and pin help files to the local storage' | ||
}) | ||
}, | ||
|
||
handler (argv) { | ||
const path = utils.getRepoPath() | ||
const path = getRepoPath() | ||
|
||
print(`initializing ipfs node at ${path}`) | ||
|
||
const node = new IPFS({ | ||
return IPFS.createNodePromise({ | ||
repo: new Repo(path), | ||
init: false, | ||
start: false | ||
}) | ||
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. Does 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. yes, ready is always emitted if an error doesn't happen |
||
|
||
node.init({ | ||
bits: argv.bits, | ||
emptyRepo: argv.emptyRepo, | ||
pass: argv.pass, | ||
log: print | ||
}, (err) => { | ||
if (err) { | ||
if (err.code === 'EACCES') { | ||
err.message = `EACCES: permission denied, stat $IPFS_PATH/version` | ||
} | ||
throw err | ||
} | ||
}).then(node => { | ||
return node.init({ | ||
bits: argv.bits, | ||
emptyRepo: argv.emptyRepo, | ||
pass: argv.pass, | ||
log: print | ||
}) | ||
}) | ||
} | ||
} |
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.
It's faster if these 3 are only required when needed
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.
fixed with last commit