-
Notifications
You must be signed in to change notification settings - Fork 184
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
Conversation
Current dependencies on/for this PR:
This comment was auto-generated by Graphite. |
Should we only display this detailed error message when |
I chose to include more information so that if a user reports it, then it is much easier to see the problem. |
But it could be overwhelming to a user who is new to nix, so we could show a simpler error like:
|
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.
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.
internal/boxcli/usererr/cmderr.go
Outdated
) | ||
|
||
// ExecCmdError is an error from an exec.Cmd for a devbox internal command | ||
type ExecCmdError struct { |
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.
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
}
internal/boxcli/usererr/cmderr.go
Outdated
var exitErr *exec.ExitError | ||
if errors.As(e.err, &exitErr) { |
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.
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.
internal/boxcli/midcobra/midcobra.go
Outdated
@@ -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 |
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.
To avoid repetition, I wonder if just usererr.ExitError
@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. |
f202816
to
4a07df0
Compare
) | ||
|
||
type UserExecError struct { | ||
// ExitError is an ExitError for a command run on behalf of a user | ||
type ExitError struct { |
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.
I'm curious why we have a new type for this. Why not just use exec.ExitError
directly?
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.
@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.
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.
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 { |
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.
Not from this PR, but if ExitError
is just an exec.ExitError you don't need to reimplement all this stuff.
internal/boxcli/midcobra/debug.go
Outdated
} | ||
|
||
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)) |
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.
This should go to cmd.ErrOrStderr()
instead.
internal/boxcli/midcobra/debug.go
Outdated
@@ -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") |
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.
This has been changed to go to cmd.ErrOrStderr() (you need to rebase)
4a07df0
to
911b40d
Compare
Summary
This PR makes three changes:
Handles ExitErrors specially (for non
usererr.ExitError
) by printing theerr.Stderr
whenDEVBOX_DEBUG=1
.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.Renames
usererr.UserExecError
tousererr.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:
AFTER:
and AFTER with DEBUG=1: