pax_global_header00006660000000000000000000000064142545340270014520gustar00rootroot0000000000000052 comment=d11f1e77abf7f8d69d81553ccaaf0b81163541a6 cancelreader-0.2.2/000077500000000000000000000000001425453402700141315ustar00rootroot00000000000000cancelreader-0.2.2/.github/000077500000000000000000000000001425453402700154715ustar00rootroot00000000000000cancelreader-0.2.2/.github/FUNDING.yml000066400000000000000000000000171425453402700173040ustar00rootroot00000000000000github: muesli cancelreader-0.2.2/.github/dependabot.yml000066400000000000000000000004231425453402700203200ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" labels: - "dependencies" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" labels: - "dependencies" cancelreader-0.2.2/.github/workflows/000077500000000000000000000000001425453402700175265ustar00rootroot00000000000000cancelreader-0.2.2/.github/workflows/build.yml000066400000000000000000000011171425453402700213500ustar00rootroot00000000000000name: build on: [push, pull_request] jobs: build: strategy: matrix: go-version: [~1.13, ^1] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} env: GO111MODULE: "on" steps: - name: Install Go uses: actions/setup-go@v3 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v3 - name: Download Go modules run: go mod download - name: Build run: go build -v ./... - name: Test run: go test ./... cancelreader-0.2.2/.github/workflows/coverage.yml000066400000000000000000000013211425453402700220410ustar00rootroot00000000000000name: coverage on: [push, pull_request] jobs: coverage: strategy: matrix: go-version: [^1] os: [ubuntu-latest] runs-on: ${{ matrix.os }} env: GO111MODULE: "on" steps: - name: Install Go uses: actions/setup-go@v3 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v3 - name: Coverage env: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | go test -race -covermode atomic -coverprofile=profile.cov ./... GO111MODULE=off go get github.com/mattn/goveralls $(go env GOPATH)/bin/goveralls -coverprofile=profile.cov -service=github cancelreader-0.2.2/.github/workflows/lint-soft.yml000066400000000000000000000011731425453402700221720ustar00rootroot00000000000000name: lint-soft on: push: pull_request: permissions: contents: read # Optional: allow read access to pull request. Use with `only-new-issues` option. pull-requests: read jobs: golangci: name: lint-soft runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: # Optional: golangci-lint command line arguments. args: --config .golangci-soft.yml --issues-exit-code=0 # Optional: show only new issues if it's a pull request. The default value is `false`. only-new-issues: true cancelreader-0.2.2/.github/workflows/lint.yml000066400000000000000000000011011425453402700212100ustar00rootroot00000000000000name: lint on: push: pull_request: permissions: contents: read # Optional: allow read access to pull request. Use with `only-new-issues` option. pull-requests: read jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: # Optional: golangci-lint command line arguments. #args: # Optional: show only new issues if it's a pull request. The default value is `false`. only-new-issues: true cancelreader-0.2.2/.gitignore000066400000000000000000000004151425453402700161210ustar00rootroot00000000000000# 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/ cancelreader-0.2.2/.golangci-soft.yml000066400000000000000000000012701425453402700174660ustar00rootroot00000000000000run: 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 cancelreader-0.2.2/.golangci.yml000066400000000000000000000006051425453402700165160ustar00rootroot00000000000000run: 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 cancelreader-0.2.2/LICENSE000066400000000000000000000021071425453402700151360ustar00rootroot00000000000000MIT 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. cancelreader-0.2.2/README.md000066400000000000000000000041251425453402700154120ustar00rootroot00000000000000# 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`. cancelreader-0.2.2/cancelreader.go000066400000000000000000000043371425453402700170770ustar00rootroot00000000000000package 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 } cancelreader-0.2.2/cancelreader_bsd.go000066400000000000000000000063011425453402700177200ustar00rootroot00000000000000//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") } cancelreader-0.2.2/cancelreader_default.go000066400000000000000000000006701425453402700205770ustar00rootroot00000000000000//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) } cancelreader-0.2.2/cancelreader_default_test.go000066400000000000000000000026421425453402700216370ustar00rootroot00000000000000//go:build !windows // +build !windows package cancelreader import ( "os" "testing" "time" ) func TestReader(t *testing.T) { pr, pw, err := os.Pipe() if err != nil { t.Errorf("expected no error, but got %s", err) } defer pw.Close() defer pr.Close() cr, err := NewReader(pr) if err != nil { t.Errorf("expected no error, but got %s", err) } msg := "hello" n, err := pw.Write([]byte(msg)) if n != 5 { t.Errorf("expected 5 bytes written but got %d", n) } if err != nil { t.Errorf("expected no error, but got %s", err) } done := make(chan struct{}) go func() { defer close(done) p := make([]byte, 1) n, err = cr.Read(p) }() if !cr.Cancel() { t.Errorf("expected cancellation to be success") } select { case <-done: case <-time.After(100 * time.Millisecond): t.Errorf("expected cancellation to unblock reader") } if n != 0 { t.Errorf("expected 0 bytes read but got %d", n) } if err != ErrCanceled { t.Errorf("expected cancel error but got %s", err) } // Test that read is still possible after cancellation. cr, err = NewReader(pr) if err != nil { t.Errorf("expected no error, but got %s", err) } p := make([]byte, 5) n, err = cr.Read(p) if n != 5 { t.Errorf("expected 5 bytes written but got %d", n) } if err != nil { t.Errorf("expected no error, but got %s", err) } if string(p[:n]) != msg[:n] { t.Errorf("expected to read %q but got %q", msg[:n], string(p[:n])) } } cancelreader-0.2.2/cancelreader_fallback_test.go000066400000000000000000000035001425453402700217440ustar00rootroot00000000000000package cancelreader import ( "bytes" "fmt" "io/ioutil" "sync" "testing" ) type blockingReader struct { sync.Mutex read bool unblockCh chan bool startedCh chan bool } func (r *blockingReader) Read([]byte) (int, error) { defer func() { r.Lock() defer r.Unlock() r.read = true }() r.startedCh <- true <-r.unblockCh return 0, fmt.Errorf("this error should be ignored") } func TestFallbackReaderConcurrentCancel(t *testing.T) { doneCh := make(chan bool, 1) startedCh := make(chan bool, 1) unblockCh := make(chan bool, 1) r := blockingReader{ startedCh: startedCh, unblockCh: unblockCh, } cr, err := newFallbackCancelReader(&r) if err != nil { t.Errorf("expected no error, but got %s", err) } go func() { defer func() { doneCh <- true }() if _, err := ioutil.ReadAll(cr); err != ErrCanceled { t.Errorf("expected canceled error, got %v", err) } }() // make sure the read started before canceling the reader <-startedCh cr.Cancel() unblockCh <- true // wait for the read to end to ensure its assertions were made <-doneCh // make sure that it waited for the reader if !r.read { t.Error("seems like the reader was canceled before the read, this shouldn't happen") } } func TestFallbackReader(t *testing.T) { var r bytes.Buffer cr, err := newFallbackCancelReader(&r) if err != nil { t.Errorf("expected no error, but got %s", err) } txt := "first" _, _ = r.WriteString(txt) first, err := ioutil.ReadAll(cr) if err != nil { t.Errorf("expected no error, but got %s", err) } if string(first) != txt { t.Errorf("expected output to be %q, got %q", txt, string(first)) } cr.Cancel() second, err := ioutil.ReadAll(cr) if err != ErrCanceled { t.Errorf("expected ErrCanceled, got %v", err) } if len(second) > 0 { t.Errorf("expected an empty read, got %q", string(second)) } } cancelreader-0.2.2/cancelreader_linux.go000066400000000000000000000063151425453402700203140ustar00rootroot00000000000000//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") } cancelreader-0.2.2/cancelreader_select.go000066400000000000000000000060141425453402700204300ustar00rootroot00000000000000//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") } cancelreader-0.2.2/cancelreader_test.go000066400000000000000000000004331425453402700201270ustar00rootroot00000000000000package cancelreader import ( "strings" "testing" ) func TestReaderNonFile(t *testing.T) { cr, err := NewReader(strings.NewReader("")) if err != nil { t.Errorf("expected no error, but got %s", err) } if cr.Cancel() { t.Errorf("expected cancellation to be failure") } } cancelreader-0.2.2/cancelreader_unix.go000066400000000000000000000011701425453402700201320ustar00rootroot00000000000000//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) } cancelreader-0.2.2/cancelreader_windows.go000066400000000000000000000152201425453402700206420ustar00rootroot00000000000000//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 } cancelreader-0.2.2/example/000077500000000000000000000000001425453402700155645ustar00rootroot00000000000000cancelreader-0.2.2/example/main.go000066400000000000000000000010621425453402700170360ustar00rootroot00000000000000package main import ( "errors" "fmt" "os" "time" "github.com/muesli/cancelreader" ) func main() { r, err := cancelreader.NewReader(os.Stdin) if err != nil { panic(err) } // 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 panic(err) } // handle data fmt.Println("read:", string(buf[:])) } } cancelreader-0.2.2/go.mod000066400000000000000000000001541425453402700152370ustar00rootroot00000000000000module github.com/muesli/cancelreader go 1.17 require golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a cancelreader-0.2.2/go.sum000066400000000000000000000003171425453402700152650ustar00rootroot00000000000000golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a h1:ppl5mZgokTT8uPkmYOyEUmPTr3ypaKkg5eFOGrAmxxE= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=