Skip to content
This repository has been archived by the owner on Feb 12, 2024. It is now read-only.

feat: improved error handling on the CLI #1335

Closed
wants to merge 10 commits into from

Conversation

hugomrdias
Copy link
Member

@hugomrdias hugomrdias commented May 2, 2018

This PR should improve error handling and help messages in the cli. All messages printed to the console follow the go implementation.

In order to do this i used the yargs fail() method to catch and handle errors from the commands (https://github.com/ipfs/js-ipfs/blob/feat/better-errors-cli/src/cli/bin.js#L81-L104).
Previously we were using the parse() method which isn't made to handle errors, its for running yargs in headless mode to build cli tests.

The fail() method handles promises returned from the commands handler, so its the perfect top-level spot to catch errors and output help specific to the cli. Like this:
https://github.com/ipfs/js-ipfs/blob/feat/better-errors-cli/src/cli/bin.js#L87-L90
Another nice thing is that we can hide the stack trace behind a flag --debug or -D.

In the PR i keep an hybrid solution only daemon, init and id commands are ported to return promises and use fail(). Everything else works almost the same, only two things changed:

  • moved away from yargs singleton mode, now we actually have two different instances here and here
  • all errors are actually outputted correctly

The ultimate goal if everyone agrees would be to port all commands to something like the 'id' command included in the PR and start creating errors with codes like this

Object.assign(new Error('repo is not initialized yet'),
{
    code: 'ERR_REPO_NOT_INITIALIZED',
    path: node._repo.path
})

Basically just adding error codes just like node does, so we can do this

if (err.code === 'ERR_REPO_NOT_INITIALIZED') {
     return `No IPFS repo found in ${err.path}. 
please run: 'ipfs init'`
}

instead of this

if (err.message.match(/not found/) || // indexeddb
    err.message.match(/ENOENT/) || // fs
    err.message.match(/No value/) // memory
) {
    return cb(null, false)
}

In the end we could delete all this and do all the cli specific error handling inside fail().

Needs:

Fixes #1180

@ghost ghost assigned hugomrdias May 2, 2018
@ghost ghost added the status/in-progress In progress label May 2, 2018
@hugomrdias hugomrdias added the status/blocked Unable to be worked further until needs are met label May 2, 2018
@hugomrdias hugomrdias requested a review from daviddias May 2, 2018 16:03
@ghost ghost assigned daviddias May 4, 2018
Copy link
Member

@daviddias daviddias left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dismiss my review. wrong button

@daviddias daviddias removed the status/blocked Unable to be worked further until needs are met label May 6, 2018
@daviddias daviddias removed their assignment May 6, 2018
@daviddias
Copy link
Member

Needed PRs have been reviewed and merged. Not blocked anymore

@hugomrdias hugomrdias force-pushed the feat/better-errors-cli branch from 49571a2 to c649a98 Compare May 14, 2018 14:36
@hugomrdias hugomrdias changed the title WIP: Feat/better errors cli Feat/better errors cli May 14, 2018
@hugomrdias
Copy link
Member Author

@diasdavid this PR is complete and with ipfs/js-ipfsd-ctl#241 linked tests pass locally.

@daviddias
Copy link
Member

@alanshaw mind doing the last review?

@daviddias
Copy link
Member

I'm puzzled on why the "better CLI errors" is the fix for changing the error on ipfs-repo introduced in -- ipfs/js-ipfs-repo@755b5c6 --.

src/cli/bin.js Outdated
const { disablePrinting, print, getNodeOrAPI } = require('./utils')
const addCmd = require('./commands/files/add')
const catCmd = require('./commands/files/cat')
const getCmd = require('./commands/files/get')
Copy link
Member

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed with last commit

src/cli/bin.js Outdated
ipfs - Global p2p merkle-dag filesystem.

ipfs [options] <command> ...`
const MSG_EPILOGUE = `Use 'ipfs <command> --help' to learn more about each command.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor - too much indentation!

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i see now, its const eslint auto fixes that wrong.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed with last commit

* @param {object} stateOptions - Node state options to reject with error
* @returns {IPFS}
*/
IPFS.createNodePromise = (options = {}, stateOptions = {}) => {
Copy link
Member

@alanshaw alanshaw May 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name suggests that the only difference between it and createNode is that it returns a promise, but actually it creates a node and waits for it to be ready. What about IPFS.startNode()?

Secondly, since you're adding a new public API function here, is it better to offer a dual promise/callback function like the rest of the API? I'd promisify this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, if this is going to add a new public API, it also needs to add documentation for it in the README: https://github.com/ipfs/js-ipfs/blob/master/README.md#api

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes also!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a static factory method, with the objective of creating a node if it is started or initialised depends on the options.
A startNode factory would imply options = {start:true}.
I can just move this to utils.js and the scope would be cli only.

.then(node => Promise.all([Promise.resolve(node), node.id()]))
.then(([node, id]) => {
print(JSON.stringify(id, '', 2))
node.stop()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come stop() is called here but not in version.js?

Do all our commands have to remember to call stop() after they're done, and also catch any error and call stop() to clean up correctly? Would it be better to have IPFS created and destroyed once in bin.js like it is at the moment? Is there another benefit to moving the IPFS node getter into each command?

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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.
But still to answer your questions:

How come stop() is called here but not in version.js?

you r right every cmd needs to do cleanup (easier cleanup coming in the next commits)

Do all our commands have to remember to call stop() after they're done, and also catch any error and call stop() to clean up correctly? Would it be better to have IPFS created and destroyed once in bin.js like it is at the moment? Is there another benefit to moving the IPFS node getter into each command?

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 fail() and proper stack traces, as we do now for daemon, init, id and version.

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.

It works exactly like that now

{
  "id": "QmYHif2UK4aFaMYBiNZYrA7xLvYJajYSQt14H5tmHyagvV",
  "publicKey": "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBJRc5+YxkfPX1LfwUXGY18ezEUpsUmWLAQxReOdeedbEHly8O7WYfYW1iDV1obVttDAbsYLMLh2gmoPqyMjvcAtA/JFnkCjTJ1GCAJFPvrHly5ZFO67knMTHXfVTaKJRBs98JnwrVgFRAbZAwoeVBRbPeP53FOmiw3pXoraOXU7YvGV3vc29PUTn4+1g4seoznyHAx5Ezp52MspiBvb0QmkEGvqZEKFRBUSz8pqMPCDYtItXMzOoBJG29TxxUztGTHvfogdxY9EOpPxVW6Nm0YRWbs9Vc8m/Vjtb88VQGrXXiplgag8OzyT8qM7Axx44nqOkAnQM3L50lOJ/ezGr9AgMBAAE=",
  "addresses": [],
  "agentVersion": "js-ipfs/0.28.2",
  "protocolVersion": "9000"
}

repo: new Repo(path),
init: false,
start: false
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does createNodePromise resolve when start: false?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, ready is always emitted if an error doesn't happen
https://github.com/ipfs/js-ipfs/blob/feat/better-errors-cli/src/core/boot.js#L53

}

// Required inline to reduce startup time
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please leave this comment

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed with last commit

const IPFS = require('../core')
const node = new IPFS({
return IPFS.createNodePromise({
repo: exports.getRepoPath(),
init: false,
start: false,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also here, does createNodePromise resolve when start is false?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, ready is always emitted if an error doesn't happen
https://github.com/ipfs/js-ipfs/blob/feat/better-errors-cli/src/core/boot.js#L53

fix indentation in bin.js
add back inline require comment in utils.js
…eRepoInitialized

forceRepoInitialized represents better to objective of the option forceInitialized could confuse we node initiliazed instead of repo
boot always runs first and puts state in stopped state, this check doesnt make sense
self.state.init() does nothing fsm even prints an error, check with DEBUG=* js-ipfs init
@daviddias daviddias changed the title Feat/better errors cli feat: improved error handling on the CLI May 29, 2018
@victorb
Copy link
Member

victorb commented Jul 10, 2018

This is marked as "blocked" but it's unclear why it's blocked.

@victorb
Copy link
Member

victorb commented Aug 6, 2018

What's the progress here or why is it blocked?

Being able to tell when a CLI command have finished running enables us to implement ipfs-exec for the CLI tests differently, where we can massively speed up the nodejs tests for js-ipfs.

@alanshaw
Copy link
Member

@hugomrdias I think #1834 ended capturing a lot (if not all) of what this PR was trying to address. Shall we close this PR (it's super out of date now) or do you think there are parts of this PR that would still be useful and that aren't already address or about to be addressed by near future PRs (I'm thinking async/await endeavour)?

@hugomrdias
Copy link
Member Author

i still need this PR, ill close it when i finish the error codes PR

@daviddias
Copy link
Member

Hi! js-ipfs master just got a whole new set of automated tests with #2528, #2440 and also running some of the test suites from our early testers (hi5 to @achingbrain for setting it all up!). Would you mind rebasing the master branch on this PR to ensure it runs all the latest tests? Thank you!

@hugomrdias hugomrdias closed this Feb 4, 2020
@hugomrdias hugomrdias deleted the feat/better-errors-cli branch February 4, 2020 14:47
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
status/blocked Unable to be worked further until needs are met status/in-progress In progress
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Better error message when not initialized
5 participants