util/io: add RedirectStderr #214
							
								
								
									
										17
									
								
								util/io/redirect.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								util/io/redirect.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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()) | ||||||
|  | }() | ||||||
							
								
								
									
										40
									
								
								util/io/redirect_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								util/io/redirect_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										48
									
								
								util/io/redirect_unix.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								util/io/redirect_unix.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  | } | ||||||
							
								
								
									
										32
									
								
								util/io/redirect_windows.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								util/io/redirect_windows.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user