Skip to content

Commit

Permalink
Start adding tests for the interruptable reader
Browse files Browse the repository at this point in the history
  • Loading branch information
walles committed Jul 16, 2024
1 parent f4fed36 commit 002d188
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 31 deletions.
4 changes: 0 additions & 4 deletions twin/screen-setup-windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,6 @@ func (r *interruptableReaderImpl) Interrupt() {
r.shutdownRequested.Store(true)
}

func (r *interruptableReaderImpl) Close() error {
return nil
}

func newInterruptableReader(base *os.File) (interruptableReader, error) {
return &interruptableReaderImpl{base: base}, nil
}
Expand Down
37 changes: 20 additions & 17 deletions twin/screen-setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"os"
"os/signal"
"sync"
"syscall"

log "github.com/sirupsen/logrus"
Expand All @@ -20,12 +21,21 @@ type interruptableReaderImpl struct {
shutdownPipeReader *os.File
shutdownPipeWriter *os.File

interruptionComplete chan struct{}
lock sync.Mutex
interrupted bool
}

func (r *interruptableReaderImpl) Read(p []byte) (n int, err error) {
for {
r.lock.Lock()
interrupted := r.interrupted
r.lock.Unlock()
if interrupted {
return 0, io.EOF
}

n, err = r.read(p)

if err == syscall.EINTR {
// Not really a problem, we can get this on window resizes for
// example, just try again.
Expand Down Expand Up @@ -67,9 +77,6 @@ func (r *interruptableReaderImpl) read(p []byte) (n int, err error) {

err = io.EOF

// Let Interrupt() know we're done
r.Close()

return
}

Expand All @@ -83,25 +90,21 @@ func (r *interruptableReaderImpl) read(p []byte) (n int, err error) {
}

func (r *interruptableReaderImpl) Interrupt() {
// This will make the select() call claim the read end is ready
r.shutdownPipeWriter.Close()
r.lock.Lock()
defer r.lock.Unlock()
r.interrupted = true

// Wait for reader to react
<-r.interruptionComplete
}

func (r *interruptableReaderImpl) Close() error {
select {
case r.interruptionComplete <- struct{}{}:
default:
err := r.shutdownPipeWriter.Close()
if err != nil {
// This should never happen, but if it does we should log it
log.Warn("Failed to close shutdown pipe writer: ", err)
}
return nil
}

func newInterruptableReader(base *os.File) (interruptableReader, error) {
reader := interruptableReaderImpl{
base: base,
interruptionComplete: make(chan struct{}, 1),
base: base,
lock: sync.Mutex{},
}
pr, pw, err := os.Pipe()
if err != nil {
Expand Down
11 changes: 1 addition & 10 deletions twin/screen.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,6 @@ type interruptableReader interface {

// Interrupt unblocks the read call, either now or eventually.
Interrupt()

// Close() should be called after you are done with the interruptableReader.
//
// It will not close the underlying reader, but it will prevent Interrupt()
// from hanging if called after a failure in the screen mainLoop().
Close() error
}

type UnixScreen struct {
Expand Down Expand Up @@ -371,10 +365,7 @@ func (screen *UnixScreen) ShowCursorAt(column int, row int) {
}

func (screen *UnixScreen) mainLoop() {
defer func() {
screen.ttyInReader.Close()
log.Debug("Twin screen main loop done")
}()
defer log.Debug("Twin screen main loop done")

// "1400" comes from me trying fling scroll operations on my MacBook
// trackpad and looking at the high watermark (logged below).
Expand Down
55 changes: 55 additions & 0 deletions twin/screen_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package twin

import (
"io"
"os"
"strings"
"testing"
"time"

"gotest.tools/v3/assert"
)
Expand Down Expand Up @@ -239,3 +242,55 @@ func TestMultiCharHyperlink(t *testing.T) {
strings.ReplaceAll(rendered, "", "ESC"),
`ESC[mESC]8;;`+url+`ESC\-X-ESC]8;;ESC\ESC[K`)
}

// Test the most basic form of interruptability. Interrupting and sending a byte
// should make the reader return EOF.
//
// What we really want is for the reader to return EOF immediately when
// interrupted, with no write needed.
func TestInterruptableReader_blockedOnRead(t *testing.T) {
// Make a pipe to read from and write to
pipeReader, pipeWriter, err := os.Pipe()
assert.NilError(t, err)

// Make an interruptable reader
testMe, err := newInterruptableReader(pipeReader)
assert.NilError(t, err)
assert.Assert(t, testMe != nil)

// Start a thread that reads from the pipe
type readResult struct {
n int
err error
}
readResultChan := make(chan readResult)
go func() {
buffer := make([]byte, 1)
n, err := testMe.Read(buffer)
readResultChan <- readResult{n, err}
}()

// Give the reader thread some time to start waiting
time.Sleep(100 * time.Millisecond)

// Interrupt the reader
testMe.Interrupt()

// Write a byte to the pipe
n, err := pipeWriter.Write([]byte{42})
assert.NilError(t, err)
assert.Equal(t, n, 1)

// Wait for the reader thread to finish
result := <-readResultChan

// Check the result
assert.Equal(t, result.n, 0)
assert.Equal(t, result.err, io.EOF)

// Another read should return EOF immediately
buffer := make([]byte, 1)
n, err = testMe.Read(buffer)
assert.Equal(t, err, io.EOF)
assert.Equal(t, n, 0)
}

0 comments on commit 002d188

Please sign in to comment.