Skip to content

Commit c4fa25f

Browse files
committed
os/exec: make Cmd.Output include stderr in ExitError
Change-Id: I3c6649d2f2521ab0843b13308569867d2e5f02da Reviewed-on: https://go-review.googlesource.com/11415 Reviewed-by: Ian Lance Taylor <[email protected]> Run-TryBot: Brad Fitzpatrick <[email protected]> Reviewed-by: Andrew Gerrand <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent 72193c9 commit c4fa25f

File tree

3 files changed

+186
-4
lines changed

3 files changed

+186
-4
lines changed

src/os/exec/exec.go

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,18 @@ func (c *Cmd) Start() error {
347347
// An ExitError reports an unsuccessful exit by a command.
348348
type ExitError struct {
349349
*os.ProcessState
350+
351+
// Stderr holds a subset of the standard error output from the
352+
// Cmd.Output method if standard error was not otherwise being
353+
// collected.
354+
//
355+
// If the error output is long, Stderr may contain only a prefix
356+
// and suffix of the output, with the middle replaced with
357+
// text about the number of omitted bytes.
358+
//
359+
// Stderr is provided for debugging, for inclusion in error messages.
360+
// Users with other needs should redirect Cmd.Stderr as needed.
361+
Stderr []byte
350362
}
351363

352364
func (e *ExitError) Error() string {
@@ -392,21 +404,34 @@ func (c *Cmd) Wait() error {
392404
if err != nil {
393405
return err
394406
} else if !state.Success() {
395-
return &ExitError{state}
407+
return &ExitError{ProcessState: state}
396408
}
397409

398410
return copyError
399411
}
400412

401413
// Output runs the command and returns its standard output.
414+
// Any returned error will usually be of type *ExitError.
415+
// If c.Stderr was nil, Output populates ExitError.Stderr.
402416
func (c *Cmd) Output() ([]byte, error) {
403417
if c.Stdout != nil {
404418
return nil, errors.New("exec: Stdout already set")
405419
}
406-
var b bytes.Buffer
407-
c.Stdout = &b
420+
var stdout bytes.Buffer
421+
c.Stdout = &stdout
422+
423+
captureErr := c.Stderr == nil
424+
if captureErr {
425+
c.Stderr = &prefixSuffixSaver{N: 32 << 10}
426+
}
427+
408428
err := c.Run()
409-
return b.Bytes(), err
429+
if err != nil && captureErr {
430+
if ee, ok := err.(*ExitError); ok {
431+
ee.Stderr = c.Stderr.(*prefixSuffixSaver).Bytes()
432+
}
433+
}
434+
return stdout.Bytes(), err
410435
}
411436

412437
// CombinedOutput runs the command and returns its combined standard
@@ -514,3 +539,80 @@ func (c *Cmd) StderrPipe() (io.ReadCloser, error) {
514539
c.closeAfterWait = append(c.closeAfterWait, pr)
515540
return pr, nil
516541
}
542+
543+
// prefixSuffixSaver is an io.Writer which retains the first N bytes
544+
// and the last N bytes written to it. The Bytes() methods reconstructs
545+
// it with a pretty error message.
546+
type prefixSuffixSaver struct {
547+
N int // max size of prefix or suffix
548+
prefix []byte
549+
suffix []byte // ring buffer once len(suffix) == N
550+
suffixOff int // offset to write into suffix
551+
skipped int64
552+
553+
// TODO(bradfitz): we could keep one large []byte and use part of it for
554+
// the prefix, reserve space for the '... Omitting N bytes ...' message,
555+
// then the ring buffer suffix, and just rearrange the ring buffer
556+
// suffix when Bytes() is called, but it doesn't seem worth it for
557+
// now just for error messages. It's only ~64KB anyway.
558+
}
559+
560+
func (w *prefixSuffixSaver) Write(p []byte) (n int, err error) {
561+
lenp := len(p)
562+
p = w.fill(&w.prefix, p)
563+
564+
// Only keep the last w.N bytes of suffix data.
565+
if overage := len(p) - w.N; overage > 0 {
566+
p = p[overage:]
567+
w.skipped += int64(overage)
568+
}
569+
p = w.fill(&w.suffix, p)
570+
571+
// w.suffix is full now if p is non-empty. Overwrite it in a circle.
572+
for len(p) > 0 { // 0, 1, or 2 iterations.
573+
n := copy(w.suffix[w.suffixOff:], p)
574+
p = p[n:]
575+
w.skipped += int64(n)
576+
w.suffixOff += n
577+
if w.suffixOff == w.N {
578+
w.suffixOff = 0
579+
}
580+
}
581+
return lenp, nil
582+
}
583+
584+
// fill appends up to len(p) bytes of p to *dst, such that *dst does not
585+
// grow larger than w.N. It returns the un-appended suffix of p.
586+
func (w *prefixSuffixSaver) fill(dst *[]byte, p []byte) (pRemain []byte) {
587+
if remain := w.N - len(*dst); remain > 0 {
588+
add := minInt(len(p), remain)
589+
*dst = append(*dst, p[:add]...)
590+
p = p[add:]
591+
}
592+
return p
593+
}
594+
595+
func (w *prefixSuffixSaver) Bytes() []byte {
596+
if w.suffix == nil {
597+
return w.prefix
598+
}
599+
if w.skipped == 0 {
600+
return append(w.prefix, w.suffix...)
601+
}
602+
var buf bytes.Buffer
603+
buf.Grow(len(w.prefix) + len(w.suffix) + 50)
604+
buf.Write(w.prefix)
605+
buf.WriteString("\n... omitting ")
606+
buf.WriteString(strconv.FormatInt(w.skipped, 10))
607+
buf.WriteString(" bytes ...\n")
608+
buf.Write(w.suffix[w.suffixOff:])
609+
buf.Write(w.suffix[:w.suffixOff])
610+
return buf.Bytes()
611+
}
612+
613+
func minInt(a, b int) int {
614+
if a < b {
615+
return a
616+
}
617+
return b
618+
}

src/os/exec/exec_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,9 @@ func TestHelperProcess(*testing.T) {
760760
}
761761
fmt.Print(p)
762762
os.Exit(0)
763+
case "stderrfail":
764+
fmt.Fprintf(os.Stderr, "some stderr text\n")
765+
os.Exit(1)
763766
default:
764767
fmt.Fprintf(os.Stderr, "Unknown command %q\n", cmd)
765768
os.Exit(2)
@@ -816,3 +819,19 @@ func TestClosePipeOnCopyError(t *testing.T) {
816819
t.Fatalf("yes got stuck writing to bad writer")
817820
}
818821
}
822+
823+
func TestOutputStderrCapture(t *testing.T) {
824+
testenv.MustHaveExec(t)
825+
826+
cmd := helperCommand(t, "stderrfail")
827+
_, err := cmd.Output()
828+
ee, ok := err.(*exec.ExitError)
829+
if !ok {
830+
t.Fatalf("Output error type = %T; want ExitError", err)
831+
}
832+
got := string(ee.Stderr)
833+
want := "some stderr text\n"
834+
if got != want {
835+
t.Errorf("ExitError.Stderr = %q; want %q", got, want)
836+
}
837+
}

src/os/exec/internal_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright 2015 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package exec
6+
7+
import (
8+
"io"
9+
"testing"
10+
)
11+
12+
func TestPrefixSuffixSaver(t *testing.T) {
13+
tests := []struct {
14+
N int
15+
writes []string
16+
want string
17+
}{
18+
{
19+
N: 2,
20+
writes: nil,
21+
want: "",
22+
},
23+
{
24+
N: 2,
25+
writes: []string{"a"},
26+
want: "a",
27+
},
28+
{
29+
N: 2,
30+
writes: []string{"abc", "d"},
31+
want: "abcd",
32+
},
33+
{
34+
N: 2,
35+
writes: []string{"abc", "d", "e"},
36+
want: "ab\n... omitting 1 bytes ...\nde",
37+
},
38+
{
39+
N: 2,
40+
writes: []string{"ab______________________yz"},
41+
want: "ab\n... omitting 22 bytes ...\nyz",
42+
},
43+
{
44+
N: 2,
45+
writes: []string{"ab_______________________y", "z"},
46+
want: "ab\n... omitting 23 bytes ...\nyz",
47+
},
48+
}
49+
for i, tt := range tests {
50+
w := &prefixSuffixSaver{N: tt.N}
51+
for _, s := range tt.writes {
52+
n, err := io.WriteString(w, s)
53+
if err != nil || n != len(s) {
54+
t.Errorf("%d. WriteString(%q) = %v, %v; want %v, %v", i, s, n, err, len(s), nil)
55+
}
56+
}
57+
if got := string(w.Bytes()); got != tt.want {
58+
t.Errorf("%d. Bytes = %q; want %q", i, got, tt.want)
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)