diff --git a/util/io/redirect.go b/util/io/redirect.go new file mode 100644 index 00000000..ce9ff853 --- /dev/null +++ b/util/io/redirect.go @@ -0,0 +1,17 @@ +package io + +import ( + "os" + "sync" +) + +var osStderrMu sync.Mutex + +var OrigStderr = func() *os.File { + fd, err := dupFD(os.Stderr.Fd()) + if err != nil { + panic(err) + } + + return os.NewFile(fd, os.Stderr.Name()) +}() diff --git a/util/io/redirect_test.go b/util/io/redirect_test.go new file mode 100644 index 00000000..16b0c284 --- /dev/null +++ b/util/io/redirect_test.go @@ -0,0 +1,40 @@ +package io + +import ( + "bytes" + "fmt" + "io" + "os" + "regexp" + "testing" + "time" +) + +var ErrorPattern = regexp.MustCompile(`"error"`) + +func TestRedirect(t *testing.T) { + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + + ch := make(chan string) + + go func() { + buf := bytes.NewBuffer(nil) + _, _ = io.Copy(buf, r) + ch <- buf.String() + }() + + if err = RedirectStderr(w); err != nil { + t.Fatal(err) + } + + os.Stderr.Write([]byte(`test redirect`)) + time.Sleep(1 * time.Millisecond) + r.Close() + str := <-ch + if ErrorPattern.MatchString(str) { + t.Fatal(fmt.Errorf(str)) + } +} diff --git a/util/io/redirect_unix.go b/util/io/redirect_unix.go new file mode 100644 index 00000000..b9af2a88 --- /dev/null +++ b/util/io/redirect_unix.go @@ -0,0 +1,48 @@ +//go:build !windows +// +build !windows + +package io + +import ( + "os" + + "golang.org/x/sys/unix" +) + +// dupFD is used to initialize OrigStderr (see stderr_redirect.go). +func dupFD(fd uintptr) (uintptr, error) { + // Warning: failing to set FD_CLOEXEC causes the duplicated file descriptor + // to leak into subprocesses created by exec.Command. If the file descriptor + // is a pipe, these subprocesses will hold the pipe open (i.e., prevent + // EOF), potentially beyond the lifetime of this process. + // + // This can break go test's timeouts. go test usually spawns a test process + // with its stdin and stderr streams hooked up to pipes; if the test process + // times out, it sends a SIGKILL and attempts to read stdin and stderr to + // completion. If the test process has itself spawned long-lived + // subprocesses that hold references to the stdin or stderr pipes, go test + // will hang until the subprocesses exit, rather defeating the purpose of + // a timeout. + nfd, err := unix.FcntlInt(fd, unix.F_DUPFD_CLOEXEC, 0) + if err != nil { + return 0, err + } + return uintptr(nfd), nil +} + +// RedirectStderr is used to redirect internal writes to fd 2 to the +// specified file. This is needed to ensure that harcoded writes to fd +// 2 by e.g. the Go runtime are redirected to a log file of our +// choosing. +// +// We also override os.Stderr for those other parts of Go which use +// that and not fd 2 directly. +func RedirectStderr(f *os.File) error { + osStderrMu.Lock() + defer osStderrMu.Unlock() + if err := unix.Dup2(int(f.Fd()), unix.Stderr); err != nil { + return err + } + os.Stderr = f + return nil +} diff --git a/util/io/redirect_windows.go b/util/io/redirect_windows.go new file mode 100644 index 00000000..8d896570 --- /dev/null +++ b/util/io/redirect_windows.go @@ -0,0 +1,32 @@ +package io + +import ( + "os" + + "golang.org/x/sys/windows" +) + +// dupFD is used to initialize OrigStderr (see stderr_redirect.go). +func dupFD(fd uintptr) (uintptr, error) { + // Adapted from https://github.com/golang/go/blob/go1.8/src/syscall/exec_windows.go#L303. + p := windows.CurrentProcess() + var h windows.Handle + return uintptr(h), windows.DuplicateHandle(p, windows.Handle(fd), p, &h, 0, true, windows.DUPLICATE_SAME_ACCESS) +} + +// RedirectStderr is used to redirect internal writes to the error +// handle to the specified file. This is needed to ensure that +// harcoded writes to the error handle by e.g. the Go runtime are +// redirected to a log file of our choosing. +// +// We also override os.Stderr for those other parts of Go which use +// that and not fd 2 directly. +func RedirectStderr(f *os.File) error { + osStderrMu.Lock() + defer osStderrMu.Unlock() + if err := windows.SetStdHandle(windows.STD_ERROR_HANDLE, windows.Handle(f.Fd())); err != nil { + return err + } + os.Stderr = f + return nil +}