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
Improve shell command output handling #521
Conversation
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.
Although backwards incompatible, this does seem like a big improvement. Could you share some before / after log output to show what the difference looks like for (a) commands that succeed and (b) commands that fail?
modules/shell/command.go
Outdated
} | ||
|
||
log.Logf(t, line) | ||
if _, err := writer.WriteString(line); err != nil { |
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.
Since there's no more mutex, we're assuming this writer is goroutine-safe?
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.
No; as they don't need to be (unless they write to the same underlying store).
As long as the caller does not call this method in parallel with the same writer, we do not need a goroutine-safe one.
In this case, output.merged
is protected by a mutex. However only because of the structure of output
; there's no relation to this function here.
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.
As long as the caller does not call this method in parallel with the same writer, we do not need a goroutine-safe one.
And I guess since readData
is a private method, you've gone through all the existing callers to make sure they aren't using the same writer anywhere?
Either way, it's worth updating the comment on readData
that the method is NOT go-routine safe, so you should not re-use writers.
e69eb63
to
8bc14cc
Compare
The outputs do not really differ actually, and I would argue that this change is backwards compatible as there are no public API changes. Comparison: SuccessfulOld:
New:
FailingOld:
New:
If you want the exact same outputs as before, I could remove the However, what is now possible is to access the output of a failed command: out, err = shell.RunCommandAndGetOutputE(t, shell.Command{
Command: "sh",
Args: []string{"-c", "doesnotexist"},
})
t.Logf("Failure.\n\tOutput: %v\n\tError: %v", out, err)
if err != nil {
o, ok := err.(*shell.ErrWithCmdOutput)
if !ok {
t.Fatalf("did not get correct type. got=%T", err)
}
t.Logf("Outputs:\n\tStdout: %s\n\tStderr: %s", o.Output.Stdout(), o.Output.Stderr())
}
If desired, we could also add another (public) function that returns the |
return "" | ||
} | ||
|
||
return o.stdout.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.
Do you need to check if o.stdout == nil
as well?
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.
No, as even if o.stdout
is nil, we can call *outputStream.String()
with a nil pointer.
The actual String()
methods check if the pointer is nil and just return an empty string in this case
(I actually noticed that I missed *outputStream.String
, but I did this with *merged.String
already. Now fixed and implemented with outputStream too.)
return "" | ||
} | ||
|
||
return o.stderr.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.
Do you need to check if o.stderr == nil
as well?
return "" | ||
} | ||
|
||
return o.merged.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.
Do you need to check if o.merged == nil
as well?
Hm, I was expecting / referring to the inverse of this! That is, I thought the log output did change to make the error more obvious (rather than always saying "exit status 1"). This is a change I'd very much welcome. The backwards incompatibility I was referring to is in the API, but in a subtle way: the error type we return is now different, with an extra layer of wrapping. If anyone was expecting that error to be of a certain type (e.g., doing some sort of casting or type comparison), that will no longer work. I'm guessing it's a very rare case, so I'm OK with this breaking change, but I do think we have to call it out. |
b991016
to
3905e42
Compare
Actually, that is the case - it just really depends on the failure. For example: out, err = shell.RunCommandAndGetOutputE(t, shell.Command{
Command: "sh",
Args: []string{"-c", "echo \"hello world\" >&2 && exit 1"},
})
t.Logf("Failure.\n\tOutput: %v\n\tError: %v", out, err) Before:
After:
Previously, the This was not in my example above as I actually tested the "wrong" thing. In the example, I ran a command that did not exist, instead of running a command that exits with a non-0 exit code.
Oh yeah, that makes sense, that is a breaking change. I added an example to the GoDoc to show how it can be used. |
OK, great! |
Apologies for the delay! I'm super booked this week, but will try to respond in the next few days. |
Changes in `shell` package: While there are many public functions for grabbing a command's mixed output or stdout output only, there is no way to specifically get Stderr instead. Stderr is needed to debug issues, which are (almost) always accompanied by an error (non-0 exit status). This commit changes the handling of stdout and stderr to make it possible to return a "merged" string (stdout+stderr) or stdout only. As we can now grab stderr separately, we can also add it to the returned error, if any. This means that every error that is returned by public functions in this package is now of type `ErrWithCmdOutput`, which makes it possible for the callee to explicitely get the stderr stream, if needed, or just print the whole error including stderr. It also means that per default, an error will not just be "FatalError{Underlying: exit status 1}", but instead contain the whole Stderr stream. The underlying error is encapsulated in the new error type. Signed-off-by: Ramon Rüttimann <me@ramonr.ch>
3905e42
to
37812f2
Compare
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.
OK, this LGTM, thank you! I'll kick off tests now.
There was an unrelated test failure that looked like a transient issue. All other tests passed, so I'm going to merge. Thanks for the PR! |
Thank you for taking your time and looking at this! |
Changes in
shell
package:While there are many public functions for grabbing a command's mixed
output or stdout output only, there is no way to specifically get Stderr
instead. Stderr is needed to debug issues, which are (almost) always
accompanied by an error (non-0 exit status). This commit changes the
handling of stdout and stderr to make it possible to return a "merged"
string (stdout+stderr) or stdout only. As we can now grab stderr
separately, we can also add it to the returned error, if any.
This means that every error that is returned by public functions in this
package is now of type
ErrWithCmdOutput
, which makes it possible for thecallee to explicitely get the stderr stream, if needed, or just print
the whole error including stderr.
It also means that per default, an error will not just be
"FatalError{Underlying: exit status 1}", but instead contain the whole
Stderr stream. The underlying error is encapsulated in the new error
type.
Signed-off-by: Ramon Rüttimann me@ramonr.ch