feat: improved deploy progress reporting

See toolshed/abra#478
This commit is contained in:
2025-03-20 14:23:09 +01:00
committed by decentral1se
parent d0f982456e
commit 47045ca8f1
85 changed files with 8828 additions and 360 deletions

15
vendor/github.com/muesli/cancelreader/.gitignore generated vendored Normal file
View File

@ -0,0 +1,15 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/

View File

@ -0,0 +1,47 @@
run:
tests: false
issues:
include:
- EXC0001
- EXC0005
- EXC0011
- EXC0012
- EXC0013
max-issues-per-linter: 0
max-same-issues: 0
linters:
enable:
# - dupl
- exhaustive
# - exhaustivestruct
- goconst
- godot
- godox
- gomnd
- gomoddirectives
- goprintffuncname
- ifshort
# - lll
- misspell
- nakedret
- nestif
- noctx
- nolintlint
- prealloc
- wrapcheck
# disable default linters, they are already enabled in .golangci.yml
disable:
- deadcode
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- structcheck
- typecheck
- unused
- varcheck

29
vendor/github.com/muesli/cancelreader/.golangci.yml generated vendored Normal file
View File

@ -0,0 +1,29 @@
run:
tests: false
issues:
include:
- EXC0001
- EXC0005
- EXC0011
- EXC0012
- EXC0013
max-issues-per-linter: 0
max-same-issues: 0
linters:
enable:
- bodyclose
- exportloopref
- goimports
- gosec
- nilerr
- predeclared
- revive
- rowserrcheck
- sqlclosecheck
- tparallel
- unconvert
- unparam
- whitespace

21
vendor/github.com/muesli/cancelreader/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Erik Geiser and Christian Muehlhaeuser
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

64
vendor/github.com/muesli/cancelreader/README.md generated vendored Normal file
View File

@ -0,0 +1,64 @@
# 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)
[![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)
A cancelable reader for Go
This package is based on the fantastic work of [Erik Geiser](https://github.com/erikgeiser)
in Charm's [Bubble Tea](https://github.com/charmbracelet/bubbletea) framework.
## Usage
`NewReader` returns a reader with a `Cancel` function. If the input reader is a
`File`, 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`, the cancel function does
nothing and always returns false.
```go
r, err := cancelreader.NewReader(file)
if err != nil {
// handle error
...
}
// cancel after five seconds
go func() {
time.Sleep(5 * time.Second)
r.Cancel()
}()
// keep reading
for {
var buf [1024]byte
_, err := r.Read(buf[:])
if errors.Is(err, cancelreader.ErrCanceled) {
fmt.Println("canceled!")
break
}
if err != nil {
// handle other errors
...
}
// handle data
...
}
```
## Implementations
- The Linux implementation is based on the epoll mechanism
- The BSD and macOS implementation is based on the kqueue mechanism
- The generic Unix implementation is based on the posix select syscall
## Caution
The Windows implementation is based on WaitForMultipleObject with overlapping
reads from CONIN$. At this point it only supports canceling reads from
`os.Stdin`.

93
vendor/github.com/muesli/cancelreader/cancelreader.go generated vendored Normal file
View File

@ -0,0 +1,93 @@
package cancelreader
import (
"fmt"
"io"
"sync"
)
// ErrCanceled gets returned when trying to read from a canceled reader.
var ErrCanceled = fmt.Errorf("read canceled")
// CancelReader is a io.Reader whose Read() calls can be canceled without data
// being consumed. The cancelReader has to be closed.
type CancelReader interface {
io.ReadCloser
// Cancel cancels ongoing and future reads an returns true if it succeeded.
Cancel() bool
}
// File represents an input/output resource with a file descriptor.
type File interface {
io.ReadWriteCloser
// Fd returns its file descriptor
Fd() uintptr
// Name returns its file name.
Name() string
}
// fallbackCancelReader implements cancelReader but does not actually support
// cancelation during an ongoing Read() call. Thus, Cancel() always returns
// false. However, after calling Cancel(), new Read() calls immediately return
// errCanceled and don't consume any data anymore.
type fallbackCancelReader struct {
r io.Reader
cancelMixin
}
// newFallbackCancelReader is a fallback for NewReader that cannot actually
// cancel an ongoing read but will immediately return on future reads if it has
// been canceled.
func newFallbackCancelReader(reader io.Reader) (CancelReader, error) {
return &fallbackCancelReader{r: reader}, nil
}
func (r *fallbackCancelReader) Read(data []byte) (int, error) {
if r.isCanceled() {
return 0, ErrCanceled
}
n, err := r.r.Read(data)
/*
If the underlying reader is a blocking reader (e.g. an open connection),
it might happen that 1 goroutine cancels the reader while its stuck in
the read call waiting for something.
If that happens, we should still cancel the read.
*/
if r.isCanceled() {
return 0, ErrCanceled
}
return n, err // nolint: wrapcheck
}
func (r *fallbackCancelReader) Cancel() bool {
r.setCanceled()
return false
}
func (r *fallbackCancelReader) Close() error {
return nil
}
// cancelMixin represents a goroutine-safe cancelation status.
type cancelMixin struct {
unsafeCanceled bool
lock sync.Mutex
}
func (c *cancelMixin) isCanceled() bool {
c.lock.Lock()
defer c.lock.Unlock()
return c.unsafeCanceled
}
func (c *cancelMixin) setCanceled() {
c.lock.Lock()
defer c.lock.Unlock()
c.unsafeCanceled = true
}

View File

@ -0,0 +1,146 @@
//go:build darwin || freebsd || netbsd || openbsd || dragonfly
// +build darwin freebsd netbsd openbsd dragonfly
package cancelreader
import (
"errors"
"fmt"
"io"
"os"
"strings"
"golang.org/x/sys/unix"
)
// NewReader returns a reader and a cancel function. If the input reader is a
// File, 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, the cancel function
// does nothing and always returns false. The BSD and macOS implementation is
// based on the kqueue mechanism.
func NewReader(reader io.Reader) (CancelReader, error) {
file, ok := reader.(File)
if !ok {
return newFallbackCancelReader(reader)
}
// kqueue returns instantly when polling /dev/tty so fallback to select
if file.Name() == "/dev/tty" {
return newSelectCancelReader(reader)
}
kQueue, err := unix.Kqueue()
if err != nil {
return nil, fmt.Errorf("create kqueue: %w", err)
}
r := &kqueueCancelReader{
file: file,
kQueue: kQueue,
}
r.cancelSignalReader, r.cancelSignalWriter, err = os.Pipe()
if err != nil {
_ = unix.Close(kQueue)
return nil, err
}
unix.SetKevent(&r.kQueueEvents[0], int(file.Fd()), unix.EVFILT_READ, unix.EV_ADD)
unix.SetKevent(&r.kQueueEvents[1], int(r.cancelSignalReader.Fd()), unix.EVFILT_READ, unix.EV_ADD)
return r, nil
}
type kqueueCancelReader struct {
file File
cancelSignalReader File
cancelSignalWriter File
cancelMixin
kQueue int
kQueueEvents [2]unix.Kevent_t
}
func (r *kqueueCancelReader) Read(data []byte) (int, error) {
if r.isCanceled() {
return 0, ErrCanceled
}
err := r.wait()
if err != nil {
if errors.Is(err, ErrCanceled) {
// remove signal from pipe
var b [1]byte
_, errRead := r.cancelSignalReader.Read(b[:])
if errRead != nil {
return 0, fmt.Errorf("reading cancel signal: %w", errRead)
}
}
return 0, err
}
return r.file.Read(data)
}
func (r *kqueueCancelReader) Cancel() bool {
r.setCanceled()
// send cancel signal
_, err := r.cancelSignalWriter.Write([]byte{'c'})
return err == nil
}
func (r *kqueueCancelReader) Close() error {
var errMsgs []string
// close kqueue
err := unix.Close(r.kQueue)
if err != nil {
errMsgs = append(errMsgs, fmt.Sprintf("closing kqueue: %v", err))
}
// close pipe
err = r.cancelSignalWriter.Close()
if err != nil {
errMsgs = append(errMsgs, fmt.Sprintf("closing cancel signal writer: %v", 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, ", "))
}
return nil
}
func (r *kqueueCancelReader) wait() error {
events := make([]unix.Kevent_t, 1)
for {
_, err := unix.Kevent(r.kQueue, r.kQueueEvents[:], events, nil)
if errors.Is(err, unix.EINTR) {
continue // try again if the syscall was interrupted
}
if err != nil {
return fmt.Errorf("kevent: %w", err)
}
break
}
ident := uint64(events[0].Ident)
switch ident {
case uint64(r.file.Fd()):
return nil
case uint64(r.cancelSignalReader.Fd()):
return ErrCanceled
}
return fmt.Errorf("unknown error")
}

View File

@ -0,0 +1,12 @@
//go:build !darwin && !windows && !linux && !solaris && !freebsd && !netbsd && !openbsd && !dragonfly
// +build !darwin,!windows,!linux,!solaris,!freebsd,!netbsd,!openbsd,!dragonfly
package cancelreader
import "io"
// NewReader returns a fallbackCancelReader that satisfies the CancelReader but
// does not actually support cancellation.
func NewReader(reader io.Reader) (CancelReader, error) {
return newFallbackCancelReader(reader)
}

View File

@ -0,0 +1,154 @@
//go:build linux
// +build linux
package cancelreader
import (
"errors"
"fmt"
"io"
"os"
"strings"
"golang.org/x/sys/unix"
)
// NewReader returns a reader and a cancel function. If the input reader is a
// File, 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, the cancel function
// does nothing and always returns false. The Linux implementation is based on
// the epoll mechanism.
func NewReader(reader io.Reader) (CancelReader, error) {
file, ok := reader.(File)
if !ok {
return newFallbackCancelReader(reader)
}
epoll, err := unix.EpollCreate1(0)
if err != nil {
return nil, fmt.Errorf("create epoll: %w", err)
}
r := &epollCancelReader{
file: file,
epoll: epoll,
}
r.cancelSignalReader, r.cancelSignalWriter, err = os.Pipe()
if err != nil {
_ = unix.Close(epoll)
return nil, err
}
err = unix.EpollCtl(epoll, unix.EPOLL_CTL_ADD, int(file.Fd()), &unix.EpollEvent{
Events: unix.EPOLLIN,
Fd: int32(file.Fd()),
})
if err != nil {
_ = unix.Close(epoll)
return nil, fmt.Errorf("add reader to epoll interest list")
}
err = unix.EpollCtl(epoll, unix.EPOLL_CTL_ADD, int(r.cancelSignalReader.Fd()), &unix.EpollEvent{
Events: unix.EPOLLIN,
Fd: int32(r.cancelSignalReader.Fd()),
})
if err != nil {
_ = unix.Close(epoll)
return nil, fmt.Errorf("add reader to epoll interest list")
}
return r, nil
}
type epollCancelReader struct {
file File
cancelSignalReader File
cancelSignalWriter File
cancelMixin
epoll int
}
func (r *epollCancelReader) Read(data []byte) (int, error) {
if r.isCanceled() {
return 0, ErrCanceled
}
err := r.wait()
if err != nil {
if errors.Is(err, ErrCanceled) {
// remove signal from pipe
var b [1]byte
_, readErr := r.cancelSignalReader.Read(b[:])
if readErr != nil {
return 0, fmt.Errorf("reading cancel signal: %w", readErr)
}
}
return 0, err
}
return r.file.Read(data)
}
func (r *epollCancelReader) Cancel() bool {
r.setCanceled()
// send cancel signal
_, err := r.cancelSignalWriter.Write([]byte{'c'})
return err == nil
}
func (r *epollCancelReader) Close() error {
var errMsgs []string
// close kqueue
err := unix.Close(r.epoll)
if err != nil {
errMsgs = append(errMsgs, fmt.Sprintf("closing epoll: %v", err))
}
// close pipe
err = r.cancelSignalWriter.Close()
if err != nil {
errMsgs = append(errMsgs, fmt.Sprintf("closing cancel signal writer: %v", 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, ", "))
}
return nil
}
func (r *epollCancelReader) wait() error {
events := make([]unix.EpollEvent, 1)
for {
_, err := unix.EpollWait(r.epoll, events, -1)
if errors.Is(err, unix.EINTR) {
continue // try again if the syscall was interrupted
}
if err != nil {
return fmt.Errorf("kevent: %w", err)
}
break
}
switch events[0].Fd {
case int32(r.file.Fd()):
return nil
case int32(r.cancelSignalReader.Fd()):
return ErrCanceled
}
return fmt.Errorf("unknown error")
}

View File

@ -0,0 +1,136 @@
//go:build solaris || darwin || freebsd || netbsd || openbsd || dragonfly
// +build solaris darwin freebsd netbsd openbsd dragonfly
package cancelreader
import (
"errors"
"fmt"
"io"
"os"
"strings"
"golang.org/x/sys/unix"
)
// newSelectCancelReader returns a reader and a cancel function. If the input
// reader is a File, the cancel function can be used to interrupt a
// blocking call read call. In this case, the cancel function returns true if
// the call was canceled successfully. If the input reader is not a File or
// the file descriptor is 1024 or larger, the cancel function does nothing and
// always returns false. The generic unix implementation is based on the posix
// select syscall.
func newSelectCancelReader(reader io.Reader) (CancelReader, error) {
file, ok := reader.(File)
if !ok || file.Fd() >= unix.FD_SETSIZE {
return newFallbackCancelReader(reader)
}
r := &selectCancelReader{file: file}
var err error
r.cancelSignalReader, r.cancelSignalWriter, err = os.Pipe()
if err != nil {
return nil, err
}
return r, nil
}
type selectCancelReader struct {
file File
cancelSignalReader File
cancelSignalWriter File
cancelMixin
}
func (r *selectCancelReader) Read(data []byte) (int, error) {
if r.isCanceled() {
return 0, ErrCanceled
}
for {
err := waitForRead(r.file, r.cancelSignalReader)
if err != nil {
if errors.Is(err, unix.EINTR) {
continue // try again if the syscall was interrupted
}
if errors.Is(err, ErrCanceled) {
// remove signal from pipe
var b [1]byte
_, readErr := r.cancelSignalReader.Read(b[:])
if readErr != nil {
return 0, fmt.Errorf("reading cancel signal: %w", readErr)
}
}
return 0, err
}
return r.file.Read(data)
}
}
func (r *selectCancelReader) Cancel() bool {
r.setCanceled()
// send cancel signal
_, err := r.cancelSignalWriter.Write([]byte{'c'})
return err == nil
}
func (r *selectCancelReader) Close() error {
var errMsgs []string
// close pipe
err := r.cancelSignalWriter.Close()
if err != nil {
errMsgs = append(errMsgs, fmt.Sprintf("closing cancel signal writer: %v", 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, ", "))
}
return nil
}
func waitForRead(reader, abort File) error {
readerFd := int(reader.Fd())
abortFd := int(abort.Fd())
maxFd := readerFd
if abortFd > maxFd {
maxFd = abortFd
}
// this is a limitation of the select syscall
if maxFd >= unix.FD_SETSIZE {
return fmt.Errorf("cannot select on file descriptor %d which is larger than 1024", maxFd)
}
fdSet := &unix.FdSet{}
fdSet.Set(int(reader.Fd()))
fdSet.Set(int(abort.Fd()))
_, err := unix.Select(maxFd+1, fdSet, nil, nil, nil)
if err != nil {
return fmt.Errorf("select: %w", err)
}
if fdSet.IsSet(abortFd) {
return ErrCanceled
}
if fdSet.IsSet(readerFd) {
return nil
}
return fmt.Errorf("select returned without setting a file descriptor")
}

View File

@ -0,0 +1,18 @@
//go:build solaris
// +build solaris
package cancelreader
import (
"io"
)
// NewReader returns a reader and a cancel function. If the input reader is a
// File, 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 or the file descriptor
// is 1024 or larger, the cancel function does nothing and always returns false.
// The generic unix implementation is based on the posix select syscall.
func NewReader(reader io.Reader) (CancelReader, error) {
return newSelectCancelReader(reader)
}

View File

@ -0,0 +1,244 @@
//go:build windows
// +build windows
package cancelreader
import (
"fmt"
"io"
"os"
"syscall"
"time"
"unicode/utf16"
"golang.org/x/sys/windows"
)
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$.
func NewReader(reader io.Reader) (CancelReader, error) {
if f, ok := reader.(File); !ok || f.Fd() != os.Stdin.Fd() {
return newFallbackCancelReader(reader)
}
// it is necessary to open CONIN$ (NOT windows.STD_INPUT_HANDLE) in
// overlapped mode to be able to use it with WaitForMultipleObjects.
conin, err := windows.CreateFile(
&(utf16.Encode([]rune("CONIN$\x00"))[0]), windows.GENERIC_READ|windows.GENERIC_WRITE,
fileShareValidFlags, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_OVERLAPPED, 0)
if err != nil {
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
err = flushConsoleInputBuffer(conin)
if err != nil {
return nil, fmt.Errorf("flush console input buffer: %w", err)
}
cancelEvent, err := windows.CreateEvent(nil, 0, 0, nil)
if err != nil {
return nil, fmt.Errorf("create stop event: %w", err)
}
return &winCancelReader{
conin: conin,
cancelEvent: cancelEvent,
resetConsole: resetConsole,
blockingReadSignal: make(chan struct{}, 1),
}, nil
}
type winCancelReader struct {
conin windows.Handle
cancelEvent windows.Handle
cancelMixin
resetConsole func() error
blockingReadSignal chan struct{}
}
func (r *winCancelReader) Read(data []byte) (int, error) {
if r.isCanceled() {
return 0, ErrCanceled
}
err := r.wait()
if err != nil {
return 0, err
}
if r.isCanceled() {
return 0, ErrCanceled
}
// windows.Read does not work on overlapping windows.Handles
return r.readAsync(data)
}
// Cancel cancels ongoing and future Read() calls and returns true if the
// cancelation of the ongoing Read() was successful. On Windows Terminal,
// WaitForMultipleObjects sometimes immediately returns without input being
// available. In this case, graceful cancelation is not possible and Cancel()
// returns false.
func (r *winCancelReader) Cancel() bool {
r.setCanceled()
select {
case r.blockingReadSignal <- struct{}{}:
err := windows.SetEvent(r.cancelEvent)
if err != nil {
return false
}
<-r.blockingReadSignal
case <-time.After(100 * time.Millisecond):
// Read() hangs in a GetOverlappedResult which is likely due to
// WaitForMultipleObjects returning without input being available
// so we cannot cancel this ongoing read.
return false
}
return true
}
func (r *winCancelReader) Close() error {
err := windows.CloseHandle(r.cancelEvent)
if err != nil {
return fmt.Errorf("closing cancel event handle: %w", err)
}
err = r.resetConsole()
if err != nil {
return err
}
err = windows.Close(r.conin)
if err != nil {
return fmt.Errorf("closing CONIN$")
}
return nil
}
func (r *winCancelReader) wait() error {
event, err := windows.WaitForMultipleObjects([]windows.Handle{r.conin, r.cancelEvent}, false, windows.INFINITE)
switch {
case windows.WAIT_OBJECT_0 <= event && event < windows.WAIT_OBJECT_0+2:
if event == windows.WAIT_OBJECT_0+1 {
return ErrCanceled
}
if event == windows.WAIT_OBJECT_0 {
return nil
}
return fmt.Errorf("unexpected wait object is ready: %d", event-windows.WAIT_OBJECT_0)
case windows.WAIT_ABANDONED <= event && event < windows.WAIT_ABANDONED+2:
return fmt.Errorf("abandoned")
case event == uint32(windows.WAIT_TIMEOUT):
return fmt.Errorf("timeout")
case event == windows.WAIT_FAILED:
return fmt.Errorf("failed")
default:
return fmt.Errorf("unexpected error: %w", error(err))
}
}
// readAsync is necessary to read from a windows.Handle in overlapping mode.
func (r *winCancelReader) readAsync(data []byte) (int, error) {
hevent, err := windows.CreateEvent(nil, 0, 0, nil)
if err != nil {
return 0, fmt.Errorf("create event: %w", err)
}
overlapped := windows.Overlapped{
HEvent: hevent,
}
var n uint32
err = windows.ReadFile(r.conin, data, &n, &overlapped)
if err != nil && err != windows.ERROR_IO_PENDING {
return int(n), err
}
r.blockingReadSignal <- struct{}{}
err = windows.GetOverlappedResult(r.conin, &overlapped, &n, true)
if err != nil {
return int(n), nil
}
<-r.blockingReadSignal
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")
)
func flushConsoleInputBuffer(consoleInput windows.Handle) error {
r, _, e := syscall.Syscall(procFlushConsoleInputBuffer.Addr(), 1,
uintptr(consoleInput), 0, 0)
if r == 0 {
return error(e)
}
return nil
}