Skip to content
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

Fix support for redirected stdin on Windows #8

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@ for {

The Windows implementation is based on WaitForMultipleObject with overlapping
reads from CONIN$. At this point it only supports canceling reads from
`os.Stdin`.
`os.Stdin` when stdin has not been redirected from a pipe.
77 changes: 77 additions & 0 deletions cancelreader_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package cancelreader

import (
"fmt"
"os"
"os/exec"
"strings"
"testing"
"time"
)

const testStr = "hello"

func TestReaderNonFile(t *testing.T) {
cr, err := NewReader(strings.NewReader(""))
if err != nil {
Expand All @@ -15,3 +21,74 @@ func TestReaderNonFile(t *testing.T) {
t.Errorf("expected cancellation to be failure")
}
}

// Test that a redirected stdin still works when used with with NewReader().
func TestRedirectedStdin(t *testing.T) {
cmd := exec.Command(os.Args[0])
cmd.Env = []string{"GO_TEST_MODE=reader"}
writer, err := cmd.StdinPipe()
if err != nil {
t.Fatal("cmd.StdinPipe():", err)
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Start()
if err != nil {
t.Fatal("cmd.Start():", err)
}
_, err = fmt.Fprintln(writer, testStr)
if err != nil {
t.Fatal("fmt.Fprintln():", err)
}
err = cmd.Wait()
if err != nil {
// Will fail if child process returns nonzero
t.Fatal("cmd.Wait():", err)
}
}

func testChildReader() int {
r, err := NewReader(os.Stdin)
if err != nil {
panic(err)
}
ch := make(chan string)

go func() {
var str string
n, err := fmt.Fscanln(r, &str)
if err != nil {
panic(err)
}
if n != 1 {
panic("n != 1")
}
ch <- str
}()

// give up after two seconds
timer := time.NewTimer(2 * time.Second)
defer timer.Stop()
select {
case str := <-ch:
if str != testStr {
panic(fmt.Sprintf("[%s] != expected [%s]", str, testStr))
}
break
case <-timer.C:
panic("timeout")
}

return 0
}

func TestMain(m *testing.M) {
switch os.Getenv("GO_TEST_MODE") {
case "":
// Test as normal
os.Exit(m.Run())

case "reader":
os.Exit(testChildReader())
}
}
16 changes: 9 additions & 7 deletions cancelreader_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@ import (
var fileShareValidFlags uint32 = 0x00000007

// NewReader returns a reader and a cancel function. If the input reader is a
// File with the same file descriptor as os.Stdin, the cancel function can
// be used to interrupt a blocking read call. In this case, the cancel function
// returns true if the call was canceled successfully. If the input reader is
// not a File with the same file descriptor as os.Stdin, the cancel
// function does nothing and always returns false. The Windows implementation
// is based on WaitForMultipleObject with overlapping reads from CONIN$.
// File with the same file descriptor as os.Stdin, and os.Stdin has not been
// redirected, the cancel function can be used to interrupt a blocking read
// call. In this case, the cancel function returns true if the call was canceled
// successfully. If the input reader is not a File with the same file descriptor
// as os.Stdin, or os.Stdin has been redirected to a pipe, the cancel function
// does nothing and always returns false. The Windows implementation is based on
// WaitForMultipleObject with overlapping reads from CONIN$.
func NewReader(reader io.Reader) (CancelReader, error) {
if f, ok := reader.(File); !ok || f.Fd() != os.Stdin.Fd() {
var m uint32
if f, ok := reader.(File); !ok || f.Fd() != os.Stdin.Fd() || syscall.GetConsoleMode(syscall.Handle(f.Fd()), &m) != nil {
return newFallbackCancelReader(reader)
}

Expand Down