diff --git a/README.md b/README.md index 83609fb..377489c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # CancelReader -[![Latest Release](https://img.shields.io/github/release/muesli/cancelreader.svg?style=for-the-badge)](https://github.com/muesli/cancelreader/releases) -[![Go Doc](https://img.shields.io/badge/godoc-reference-blue.svg?style=for-the-badge)](https://pkg.go.dev/github.com/muesli/cancelreader) +[![Latest Release](https://img.shields.io/github/release/abakum/cancelreader.svg?style=for-the-badge)](https://github.com/abakum/cancelreader/releases) +[![Go Doc](https://img.shields.io/badge/godoc-reference-blue.svg?style=for-the-badge)](https://pkg.go.dev/github.com/abakum/cancelreader) [![Software License](https://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge)](/LICENSE) -[![Build Status](https://img.shields.io/github/workflow/status/muesli/cancelreader/build?style=for-the-badge)](https://github.com/muesli/cancelreader/actions) -[![Go ReportCard](https://goreportcard.com/badge/github.com/muesli/cancelreader?style=for-the-badge)](https://goreportcard.com/report/muesli/cancelreader) +[![Build Status](https://img.shields.io/github/workflow/status/abakum/cancelreader/build?style=for-the-badge)](https://github.com/abakum/cancelreader/actions) +[![Go ReportCard](https://goreportcard.com/badge/github.com/abakum/cancelreader?style=for-the-badge)](https://goreportcard.com/report/abakum/cancelreader) A cancelable reader for Go diff --git a/cancelreader_bsd.go b/cancelreader_bsd.go index 3ddb6cf..af767d7 100644 --- a/cancelreader_bsd.go +++ b/cancelreader_bsd.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "os" - "strings" "golang.org/x/sys/unix" ) @@ -92,30 +91,25 @@ func (r *kqueueCancelReader) Cancel() bool { } func (r *kqueueCancelReader) Close() error { - var errMsgs []string - + var e1, e2, e3 error // close kqueue err := unix.Close(r.kQueue) if err != nil { - errMsgs = append(errMsgs, fmt.Sprintf("closing kqueue: %v", err)) + e1 = fmt.Errorf("closing kqueue: %w", err) } // close pipe err = r.cancelSignalWriter.Close() if err != nil { - errMsgs = append(errMsgs, fmt.Sprintf("closing cancel signal writer: %v", err)) + e2 = fmt.Errorf("closing cancel signal writer: %w", err) } err = r.cancelSignalReader.Close() if err != nil { - errMsgs = append(errMsgs, fmt.Sprintf("closing cancel signal reader: %v", err)) - } - - if len(errMsgs) > 0 { - return fmt.Errorf(strings.Join(errMsgs, ", ")) + e3 = fmt.Errorf("closing cancel signal reader: %w", err) } - return nil + return errors.Join(e1, e2, e3) } func (r *kqueueCancelReader) wait() error { diff --git a/cancelreader_linux.go b/cancelreader_linux.go index 09f7369..8d1e289 100644 --- a/cancelreader_linux.go +++ b/cancelreader_linux.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "os" - "strings" "golang.org/x/sys/unix" ) @@ -101,30 +100,27 @@ func (r *epollCancelReader) Cancel() bool { } func (r *epollCancelReader) Close() error { - var errMsgs []string + var e1, e2, e3 error // close kqueue err := unix.Close(r.epoll) if err != nil { - errMsgs = append(errMsgs, fmt.Sprintf("closing epoll: %v", err)) + e1 = fmt.Errorf("closing epoll: %w", err) } // close pipe err = r.cancelSignalWriter.Close() if err != nil { - errMsgs = append(errMsgs, fmt.Sprintf("closing cancel signal writer: %v", err)) + e2 = fmt.Errorf("closing cancel signal writer: %w", err) } err = r.cancelSignalReader.Close() if err != nil { - errMsgs = append(errMsgs, fmt.Sprintf("closing cancel signal reader: %v", err)) + e3 = fmt.Errorf("closing cancel signal reader: %w", err) } - if len(errMsgs) > 0 { - return fmt.Errorf(strings.Join(errMsgs, ", ")) - } + return errors.Join(e1, e2, e3) - return nil } func (r *epollCancelReader) wait() error { diff --git a/cancelreader_select.go b/cancelreader_select.go index 03f2a3e..d8c789b 100644 --- a/cancelreader_select.go +++ b/cancelreader_select.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "os" - "strings" "golang.org/x/sys/unix" ) @@ -81,24 +80,20 @@ func (r *selectCancelReader) Cancel() bool { } func (r *selectCancelReader) Close() error { - var errMsgs []string + var e1, e2 error // close pipe err := r.cancelSignalWriter.Close() if err != nil { - errMsgs = append(errMsgs, fmt.Sprintf("closing cancel signal writer: %v", err)) + e1 = fmt.Errorf("closing cancel signal writer: %w", err) } err = r.cancelSignalReader.Close() if err != nil { - errMsgs = append(errMsgs, fmt.Sprintf("closing cancel signal reader: %v", err)) + e2 = fmt.Errorf("closing cancel signal reader: %w", err) } - if len(errMsgs) > 0 { - return fmt.Errorf(strings.Join(errMsgs, ", ")) - } - - return nil + return errors.Join(e1, e2) } func waitForRead(reader, abort File) error { diff --git a/cancelreader_windows.go b/cancelreader_windows.go index c1dc8d5..f0ae54e 100644 --- a/cancelreader_windows.go +++ b/cancelreader_windows.go @@ -4,6 +4,7 @@ package cancelreader import ( + "errors" "fmt" "io" "os" @@ -37,11 +38,6 @@ func NewReader(reader io.Reader) (CancelReader, error) { return nil, fmt.Errorf("open CONIN$ in overlapping mode: %w", err) } - resetConsole, err := prepareConsole(conin) - if err != nil { - return nil, fmt.Errorf("prepare console: %w", err) - } - // flush input, otherwise it can contain events which trigger // WaitForMultipleObjects but which ReadFile cannot read, resulting in an // un-cancelable read @@ -58,7 +54,6 @@ func NewReader(reader io.Reader) (CancelReader, error) { return &winCancelReader{ conin: conin, cancelEvent: cancelEvent, - resetConsole: resetConsole, blockingReadSignal: make(chan struct{}, 1), }, nil } @@ -68,7 +63,6 @@ type winCancelReader struct { cancelEvent windows.Handle cancelMixin - resetConsole func() error blockingReadSignal chan struct{} } @@ -116,22 +110,17 @@ func (r *winCancelReader) Cancel() bool { } func (r *winCancelReader) Close() error { - err := windows.CloseHandle(r.cancelEvent) - if err != nil { - return fmt.Errorf("closing cancel event handle: %w", err) - } + var e1, e2 error - err = r.resetConsole() - if err != nil { - return err + if err := windows.CloseHandle(r.cancelEvent); err != nil { + e1 = fmt.Errorf("closing cancel event handle: %w", err) } - err = windows.Close(r.conin) - if err != nil { - return fmt.Errorf("closing CONIN$") + if err := windows.Close(r.conin); err != nil { + e2 = fmt.Errorf("closing CONIN$: %w", err) } - return nil + return errors.Join(e1, e2) } func (r *winCancelReader) wait() error { @@ -186,48 +175,6 @@ func (r *winCancelReader) readAsync(data []byte) (int, error) { return int(n), nil } -func prepareConsole(input windows.Handle) (reset func() error, err error) { - var originalMode uint32 - - err = windows.GetConsoleMode(input, &originalMode) - if err != nil { - return nil, fmt.Errorf("get console mode: %w", err) - } - - var newMode uint32 - newMode &^= windows.ENABLE_ECHO_INPUT - newMode &^= windows.ENABLE_LINE_INPUT - newMode &^= windows.ENABLE_MOUSE_INPUT - newMode &^= windows.ENABLE_WINDOW_INPUT - newMode &^= windows.ENABLE_PROCESSED_INPUT - - newMode |= windows.ENABLE_EXTENDED_FLAGS - newMode |= windows.ENABLE_INSERT_MODE - newMode |= windows.ENABLE_QUICK_EDIT_MODE - - // Enabling virtual terminal input is necessary for processing certain - // types of input like X10 mouse events and arrows keys with the current - // bytes-based input reader. It does, however, prevent cancelReader from - // being able to cancel input. The planned solution for this is to read - // Windows events in a more native fashion, rather than the current simple - // bytes-based input reader which works well on unix systems. - newMode |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT - - err = windows.SetConsoleMode(input, newMode) - if err != nil { - return nil, fmt.Errorf("set console mode: %w", err) - } - - return func() error { - err := windows.SetConsoleMode(input, originalMode) - if err != nil { - return fmt.Errorf("reset console mode: %w", err) - } - - return nil - }, nil -} - var ( modkernel32 = windows.NewLazySystemDLL("kernel32.dll") procFlushConsoleInputBuffer = modkernel32.NewProc("FlushConsoleInputBuffer") diff --git a/command/etc.go b/command/etc.go new file mode 100644 index 0000000..73a08d9 --- /dev/null +++ b/command/etc.go @@ -0,0 +1,7 @@ +//go:build !windows +// +build !windows + +package main + +func ConsoleCP(*bool) {} +func IsCygwinTerminal(fd uintptr) bool { return false } diff --git a/command/main.go b/command/main.go new file mode 100644 index 0000000..a1e2800 --- /dev/null +++ b/command/main.go @@ -0,0 +1,185 @@ +package main + +import ( + "fmt" + "io" + "log" + "os" + "os/exec" + "runtime" + "strings" + + "github.com/abakum/cancelreader" + "github.com/containerd/console" + "github.com/mattn/go-isatty" + "github.com/xlab/closer" +) + +func main() { + var ( + raw bool + once bool + reset = func(*bool) {} + cmd *exec.Cmd + arg0 = "bash" + arg1 = "-c" + arg2 = "echo Press any key to continue . . .;read -rn1" + ) + + defer func() { + reset(&raw) + closer.Close() + }() + if IsCygwinTerminal(os.Stdin.Fd()) { + ConsoleCP(&once) + } else if runtime.GOOS == "windows" { + arg0 = "cmd" + arg1 = "/c" + + // arg0 = "powershell" + // arg1 = "-command" + + arg2 = "pause" + } + log.SetFlags(log.Lmicroseconds | log.Lshortfile) + log.SetPrefix("\r") + + var ( + cr cancelreader.CancelReader + err error + ) + + for i := 0; i < 8; i++ { + if i%4 > 1 { + reset(&raw) + cmd = exec.Command(arg0) + } else { + reset = setRaw(&raw, reset) + cmd = exec.Command(arg0, arg1, arg2) + } + log.Println(cmd) + cr, err = cancelreader.NewReader(os.Stdin) + if err != nil { + panic(err) + } + + if i < 4 { + // exit exit + log.Println("--without pipes", i) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + fmt.Print("\r") + if i%4 > 1 { + fmt.Println("Type exit\r") + } + cmd.Run() + } else { + // exit exit + log.Println("--with pipes", i) + ConsoleCP(&once) + + in, err := cmd.StdinPipe() + if err != nil { + panic(err) + } + + out, err := cmd.StdoutPipe() + if err != nil { + panic(err) + } + + fmt.Print("\r") + if i%4 > 1 { + fmt.Println("Type exit\r") + } + err = cmd.Start() + if err != nil { + panic(err) + } + + go func() { + _, err = io.Copy(os.Stdout, out) + log.Println("Stdout done", i, err) + log.Println("Cancel read stdin", i, cr.Cancel()) + }() + + _, err = io.Copy(in, cr) + log.Println("Stdin done", i, err) + + log.Println("Wait", cmd.Wait()) + log.Println("Close", cr.Close()) + } + } +} +func setRaw(raw *bool, old func(*bool)) (reset func(*bool)) { + reset = old + if *raw { + return + } + var ( + err error + current console.Console + settings string + ) + + current, err = console.ConsoleFromFile(os.Stdin) + if err == nil { + err = current.SetRaw() + if err == nil { + *raw = true + reset = func(raw *bool) { + if *raw { + err := current.Reset() + log.Println("Restores the console to its original state by go", err) + } + *raw = err != nil + } + log.Println("Sets the console in raw mode by go") + return + } + } + + if isatty.IsCygwinTerminal(os.Stdin.Fd()) { + settings, err = sttySettings() + if err == nil { + err = sttyMakeRaw() + if err == nil { + *raw = true + reset = func(raw *bool) { + if *raw { + sttyReset(settings) + log.Println("Restores the console to its original state by stty") + } + *raw = false + } + log.Println("Sets the console in raw mode by stty") + return + } + } + } + log.Println(err) + return +} + +func sttyMakeRaw() error { + cmd := exec.Command("stty", "raw", "-echo") + cmd.Stdin = os.Stdin + return cmd.Run() +} + +func sttySettings() (string, error) { + cmd := exec.Command("stty", "-g") + cmd.Stdin = os.Stdin + out, err := cmd.Output() + if err != nil { + return "", err + } + return strings.TrimSpace(string(out)), nil +} + +func sttyReset(settings string) { + cmd := exec.Command("stty", settings) + cmd.Stdin = os.Stdin + err := cmd.Run() + log.Println("sttyReset", settings, err) +} diff --git a/command/windows.go b/command/windows.go new file mode 100644 index 0000000..14cb9ad --- /dev/null +++ b/command/windows.go @@ -0,0 +1,48 @@ +//go:build windows +// +build windows + +package main + +import ( + "github.com/mattn/go-isatty" + "github.com/xlab/closer" + "golang.org/x/sys/windows" +) + +func ConsoleCP(once *bool) { + if *once { + return + } + *once = false + const CP_UTF8 uint32 = 65001 + var kernel32 = windows.NewLazyDLL("kernel32.dll") + + getConsoleCP := func() uint32 { + result, _, _ := kernel32.NewProc("GetConsoleCP").Call() + return uint32(result) + } + + getConsoleOutputCP := func() uint32 { + result, _, _ := kernel32.NewProc("GetConsoleOutputCP").Call() + return uint32(result) + } + + setConsoleCP := func(cp uint32) { + kernel32.NewProc("SetConsoleCP").Call(uintptr(cp)) + } + + setConsoleOutputCP := func(cp uint32) { + kernel32.NewProc("SetConsoleOutputCP").Call(uintptr(cp)) + } + + inCP := getConsoleCP() + outCP := getConsoleOutputCP() + setConsoleCP(CP_UTF8) + setConsoleOutputCP(CP_UTF8) + closer.Bind(func() { setConsoleCP(inCP) }) + closer.Bind(func() { setConsoleOutputCP(outCP) }) +} + +func IsCygwinTerminal(fd uintptr) bool { + return isatty.IsCygwinTerminal(fd) +} diff --git a/example/main.go b/example/main.go index 8e0febf..0477285 100644 --- a/example/main.go +++ b/example/main.go @@ -6,7 +6,7 @@ import ( "os" "time" - "github.com/muesli/cancelreader" + "github.com/abakum/cancelreader" ) func main() { diff --git a/go.mod b/go.mod index 79cb8f4..6788bbf 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,11 @@ -module github.com/muesli/cancelreader +module github.com/abakum/cancelreader go 1.17 -require golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a +require golang.org/x/sys v0.6.0 + +require ( + github.com/containerd/console v1.0.4 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/xlab/closer v1.1.0 // indirect +) diff --git a/go.sum b/go.sum index 097da21..6fc27d9 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,12 @@ +github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= +github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/xlab/closer v1.1.0 h1:yrDiOXjd/B7pZ3lZkl/EZ1gWrR2M2N5XpBnixynm4mc= +github.com/xlab/closer v1.1.0/go.mod h1:Ff8YcUPbn5jju6nClrMCmJHQABM0S/obEK0za/1yVMk= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a h1:ppl5mZgokTT8uPkmYOyEUmPTr3ypaKkg5eFOGrAmxxE= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=