Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[error] Better error message for ExitErrors #675

Merged
merged 4 commits into from
Feb 24, 2023
Merged

Conversation

savil
Copy link
Collaborator

@savil savil commented Feb 23, 2023

Summary

This PR makes three changes:

  1. Handles ExitErrors specially (for non usererr.ExitError) by printing the err.Stderr when DEVBOX_DEBUG=1.

  2. When DEVBOX_DEBUG=0, we now include the command and also suggest to the user to run with DEVBOX_DEBUG=1 for a more detailed error and suggest reporting the bug.

  3. Renames usererr.UserExecError to usererr.ExitError which more accurately represents what that struct is doing: the error is only wrapped if it is ExitError.

How was it tested?

in testdata/unfree-packages:

BEFORE:

❯ DEVBOX_FEATURE_FLAKES=1 DEVBOX_FEATURE_UNIFIED_ENV=1 DEVBOX_DEBUG=0 devbox shell
Ensuring packages are installed.
Ensuring nixpkgs registry is downloaded.
Downloaded 'github:NixOS/nixpkgs/52e3e80afff4b16ccb7c52e9f0f5220552f03d04' to '/nix/store/j5a7i3dvxslg2ychfy53wdhg1m3xfrwm-source' (hash 'sha256-FqZ7b2RpoHQ/jlG6JPcCNmG/DoUPCIvyaropUDFhF3Q=').
Ensuring nixpkgs registry is downloaded: Success
Starting a devbox shell...
Error: exit status 1

AFTER:

Screenshot 2023-02-23 at 1 21 30 PM

and AFTER with DEBUG=1:

Screenshot 2023-02-23 at 1 15 50 PM

Copy link
Collaborator Author

savil commented Feb 23, 2023

Current dependencies on/for this PR:

This comment was auto-generated by Graphite.

@savil savil marked this pull request as ready for review February 23, 2023 00:13
@savil
Copy link
Collaborator Author

savil commented Feb 23, 2023

Should we only display this detailed error message when DEVBOX_DEBUG=1 is true?

@savil
Copy link
Collaborator Author

savil commented Feb 23, 2023

I chose to include more information so that if a user reports it, then it is much easier to see the problem.

@savil
Copy link
Collaborator Author

savil commented Feb 23, 2023

But it could be overwhelming to a user who is new to nix, so we could show a simpler error like:

A nix command failed with error: exit status 1. Run with DEVBOX_DEBUG=1 for a detailed error message and please report to https://github.com/jetpack-io/devbox/issues/new/choose

Copy link
Contributor

@mikeland73 mikeland73 left a comment

Choose a reason for hiding this comment

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

This is really good (and needed!)

I do wonder if we can simplify a bit to avoid having custom error structs. Specifically, we could have

type UserExitError exec.ExitError

and not use any special error at all for generic exit errors. (this way, if we forget to wrap we still get good info!)

In the debug middleware we can have additional handling depending on the type:

var userExitError *UserExitError
var genericExitErr *exec.ExitError
if errors.As(err, userExitError) {
 //print very detailed stuff since it's their error
} else if errors.As(err, genericExitErr) {
 // print less but enough detail. If DEVBOX_DEBUG=1 is true, print everything
}

For printing the command, instead of using a custom struct you can wrap:

c := exec.Command("bad-echo", "hello")
return errors.Wrapf(c.Run(), "command: %s", c.String())

This should print something like Error: command: bad-echo hello: exec: "bad-echo": executable file not found in $PATH which is pretty helpful! I think this would be a more "golang" approach.

Anyway, don't want to block this PR so pre-approving. We can always follow up if you think the suggestions are valid.

)

// ExecCmdError is an error from an exec.Cmd for a devbox internal command
type ExecCmdError struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

Slightly simpler:

// ExecCmdError is an error from an exec.Cmd for a devbox internal command
type ExecCmdError struct {
	error
	cmd *exec.Cmd
}

func NewExecCmdError(cmd *exec.Cmd, err error) error {
	if err == nil {
		return nil
	}
	return &ExecCmdError{
		error: err,
		cmd:   cmd,
	}
}

func (e *ExecCmdError) Error() string {
	var errorMsg string

	// ExitErrors can give us more information so handle that specially.
	var exitErr *exec.ExitError
	if errors.As(e, &exitErr) {
		errorMsg = fmt.Sprintf(
			"Error running command %s. Exit status is %d. Command stderr: %s",
			e.cmd, exitErr.ExitCode(), string(exitErr.Stderr),
		)
	} else {
		errorMsg = fmt.Sprintf("Error running command %s. Error: %v", e.cmd, e.error.Error())
	}
	return errorMsg
}

Comment on lines 29 to 30
var exitErr *exec.ExitError
if errors.As(e.err, &exitErr) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Question, if error is not a *exec.ExitError do we even want to wrap it? Could NewExecCmdError simply return error as is? or maybe return errors.Wrap() and adds the command as the message? That way we only wrap stuff that really needs it.

@@ -64,7 +64,7 @@ func (ex *midcobraExecutable) Execute(ctx context.Context, args []string) int {
err := ex.cmd.ExecuteContext(ctx)

var postRunErr error
var userExecErr *usererr.UserExecError
var userExecErr *usererr.UserExitError
Copy link
Contributor

Choose a reason for hiding this comment

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

To avoid repetition, I wonder if just usererr.ExitError

@savil
Copy link
Collaborator Author

savil commented Feb 23, 2023

@mikeland86 yes! I like your approach a lot. I was also feeling that this is a bit heavy for what I am trying to achieve. I'll update to that.

@savil savil changed the title [error] introduce usererr.ExecCmdError for better error message [error] Better error message for ExitErrors Feb 23, 2023
)

type UserExecError struct {
// ExitError is an ExitError for a command run on behalf of a user
type ExitError struct {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm curious why we have a new type for this. Why not just use exec.ExitError directly?

Copy link
Contributor

Choose a reason for hiding this comment

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

@gcurtis the idea is that a user exit error and a generic exit error are treated differently in terms of what we show the user. For errors that were a result of an error caused by user code we want to always show as much info as possible. For non-user exit errors we only show all info if DEVBOX_DEBUG is true.

Copy link
Collaborator Author

@savil savil Feb 24, 2023

Choose a reason for hiding this comment

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

This type is used for errors that are caused by the user's program, as opposed to a devbox CLI error. For example, when doing devbox run -- <program> the user's program may exit with an error. So, if we can detect that circumstance, we wrap the exec.Command's error in this special type and then render the error message accordingly (i.e. making it clear that the user needs to fix their own code)

err: exitErr,
}
}

func (e *UserExecError) Error() string {
func (e *ExitError) Error() string {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not from this PR, but if ExitError is just an exec.ExitError you don't need to reimplement all this stuff.

}

st := debug.EarliestStackTrace(runErr)
debug.Log("Error: %v\nExecutionID:%s\n%+v\n", runErr, d.executionID, st)
color.Red(fmt.Sprintf("Error: %v\n\n", runErr))
Copy link
Contributor

Choose a reason for hiding this comment

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

This should go to cmd.ErrOrStderr() instead.

@@ -57,14 +59,17 @@ func (d *DebugMiddleware) postRun(cmd *cobra.Command, args []string, runErr erro
if usererr.IsWarning(runErr) {
ux.Fwarning(cmd.ErrOrStderr(), runErr.Error())
} else {
color.Red("\nError: " + runErr.Error() + "\n\n")
color.Red(runErr.Error() + "\n\n")
Copy link
Contributor

Choose a reason for hiding this comment

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

This has been changed to go to cmd.ErrOrStderr() (you need to rebase)

@savil savil merged commit e5b0710 into main Feb 24, 2023
@savil savil deleted the savil/devbox-exec-error branch February 24, 2023 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants