mirror of
https://github.com/peco/peco.git
synced 2026-09-10 07:16:29 -04:00
Merge pull request #681 from peco/fix-screen-init-resume-error
Propagate screen Init error on resume
This commit is contained in:
commit
c4e02ac1ce
|
|
@ -390,7 +390,10 @@ func doFinish(ctx context.Context, state *Peco, _ Event) {
|
||||||
state.screen.Suspend()
|
state.screen.Suspend()
|
||||||
|
|
||||||
err = cmd.Run()
|
err = cmd.Run()
|
||||||
state.screen.Resume(ctx)
|
if err := state.screen.Resume(ctx); err != nil {
|
||||||
|
state.Exit(fmt.Errorf("failed to resume screen: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
state.Hub().SendDraw(ctx, &hub.DrawOptions{DisableCache: true})
|
state.Hub().SendDraw(ctx, &hub.DrawOptions{DisableCache: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// bail out, or otherwise the user cannot know what happened
|
// bail out, or otherwise the user cannot know what happened
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ type Screen interface {
|
||||||
Flush() error
|
Flush() error
|
||||||
PollEvent(context.Context, *Config) chan Event
|
PollEvent(context.Context, *Config) chan Event
|
||||||
Print(PrintArgs) int
|
Print(PrintArgs) int
|
||||||
Resume(context.Context)
|
Resume(context.Context) error
|
||||||
SetCell(int, int, rune, Attribute, Attribute)
|
SetCell(int, int, rune, Attribute, Attribute)
|
||||||
SetCursor(int, int)
|
SetCursor(int, int)
|
||||||
Size() (int, int)
|
Size() (int, int)
|
||||||
|
|
|
||||||
|
|
@ -261,8 +261,8 @@ func (s *SimScreen) Size() (int, int) {
|
||||||
return s.screen.Size()
|
return s.screen.Size()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SimScreen) Resume(_ context.Context) {}
|
func (s *SimScreen) Resume(_ context.Context) error { return nil }
|
||||||
func (s *SimScreen) Suspend() {}
|
func (s *SimScreen) Suspend() {}
|
||||||
|
|
||||||
// Sync records a "Sync" event via the interceptor. This satisfies the
|
// Sync records a "Sync" event via the interceptor. This satisfies the
|
||||||
// optional syncer interface used by BasicLayout.DrawScreen when
|
// optional syncer interface used by BasicLayout.DrawScreen when
|
||||||
|
|
|
||||||
30
screen.go
30
screen.go
|
|
@ -20,7 +20,7 @@ import (
|
||||||
type TcellScreen struct {
|
type TcellScreen struct {
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
screen tcell.Screen
|
screen tcell.Screen
|
||||||
resumeCh chan chan struct{}
|
resumeCh chan chan error
|
||||||
suspendCh chan struct{}
|
suspendCh chan struct{}
|
||||||
doneCh chan struct{} // closed on permanent Close() to signal goroutines to exit
|
doneCh chan struct{} // closed on permanent Close() to signal goroutines to exit
|
||||||
closeOnce sync.Once // ensures doneCh is closed exactly once
|
closeOnce sync.Once // ensures doneCh is closed exactly once
|
||||||
|
|
@ -179,7 +179,7 @@ func (t *TcellScreen) Init(_ *Config) error {
|
||||||
func NewTcellScreen() *TcellScreen {
|
func NewTcellScreen() *TcellScreen {
|
||||||
return &TcellScreen{
|
return &TcellScreen{
|
||||||
suspendCh: make(chan struct{}),
|
suspendCh: make(chan struct{}),
|
||||||
resumeCh: make(chan chan struct{}),
|
resumeCh: make(chan chan error),
|
||||||
doneCh: make(chan struct{}),
|
doneCh: make(chan struct{}),
|
||||||
errWriter: os.Stderr,
|
errWriter: os.Stderr,
|
||||||
}
|
}
|
||||||
|
|
@ -293,8 +293,12 @@ func (t *TcellScreen) PollEvent(ctx context.Context, cfg *Config) chan Event {
|
||||||
case <-t.doneCh:
|
case <-t.doneCh:
|
||||||
return
|
return
|
||||||
case replyCh := <-t.resumeCh:
|
case replyCh := <-t.resumeCh:
|
||||||
_ = t.Init(cfg)
|
if err := t.Init(cfg); err != nil {
|
||||||
close(replyCh)
|
fmt.Fprintf(t.errWriter, "peco: failed to re-initialize screen on resume: %v\n", err)
|
||||||
|
replyCh <- err
|
||||||
|
} else {
|
||||||
|
replyCh <- nil
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -309,8 +313,12 @@ func (t *TcellScreen) PollEvent(ctx context.Context, cfg *Config) chan Event {
|
||||||
case <-t.doneCh:
|
case <-t.doneCh:
|
||||||
return
|
return
|
||||||
case replyCh := <-t.resumeCh:
|
case replyCh := <-t.resumeCh:
|
||||||
_ = t.Init(cfg)
|
if err := t.Init(cfg); err != nil {
|
||||||
close(replyCh)
|
fmt.Fprintf(t.errWriter, "peco: failed to re-initialize screen on resume: %v\n", err)
|
||||||
|
replyCh <- err
|
||||||
|
} else {
|
||||||
|
replyCh <- nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -328,7 +336,7 @@ func (t *TcellScreen) Suspend() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TcellScreen) Resume(ctx context.Context) {
|
func (t *TcellScreen) Resume(ctx context.Context) error {
|
||||||
// Resume must be a block operation, because we can't safely proceed
|
// Resume must be a block operation, because we can't safely proceed
|
||||||
// without actually knowing that the screen has been re-initialized.
|
// without actually knowing that the screen has been re-initialized.
|
||||||
// So we send a channel where we expect a reply back, and wait for that.
|
// So we send a channel where we expect a reply back, and wait for that.
|
||||||
|
|
@ -337,16 +345,18 @@ func (t *TcellScreen) Resume(ctx context.Context) {
|
||||||
// polling goroutine is not yet waiting on resumeCh, a non-blocking
|
// polling goroutine is not yet waiting on resumeCh, a non-blocking
|
||||||
// send would silently drop the message and the subsequent receive
|
// send would silently drop the message and the subsequent receive
|
||||||
// would block forever.
|
// would block forever.
|
||||||
ch := make(chan struct{})
|
ch := make(chan error, 1)
|
||||||
select {
|
select {
|
||||||
case t.resumeCh <- ch:
|
case t.resumeCh <- ch:
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ch:
|
case err := <-ch:
|
||||||
|
return err
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -225,4 +225,4 @@ func (s *InlineScreen) SendEvent(_ Event) {}
|
||||||
func (s *InlineScreen) Suspend() {}
|
func (s *InlineScreen) Suspend() {}
|
||||||
|
|
||||||
// Resume is a no-op for inline mode.
|
// Resume is a no-op for inline mode.
|
||||||
func (s *InlineScreen) Resume(_ context.Context) {}
|
func (s *InlineScreen) Resume(_ context.Context) error { return nil }
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package peco
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -28,7 +29,7 @@ func (s *recordingScreen) Close() error { retur
|
||||||
func (s *recordingScreen) Flush() error { return nil }
|
func (s *recordingScreen) Flush() error { return nil }
|
||||||
func (s *recordingScreen) PollEvent(context.Context, *Config) chan Event { return nil }
|
func (s *recordingScreen) PollEvent(context.Context, *Config) chan Event { return nil }
|
||||||
func (s *recordingScreen) Print(args PrintArgs) int { return screenPrint(s, args) }
|
func (s *recordingScreen) Print(args PrintArgs) int { return screenPrint(s, args) }
|
||||||
func (s *recordingScreen) Resume(context.Context) {}
|
func (s *recordingScreen) Resume(context.Context) error { return nil }
|
||||||
func (s *recordingScreen) SetCursor(int, int) {}
|
func (s *recordingScreen) SetCursor(int, int) {}
|
||||||
func (s *recordingScreen) SendEvent(Event) {}
|
func (s *recordingScreen) SendEvent(Event) {}
|
||||||
func (s *recordingScreen) Suspend() {}
|
func (s *recordingScreen) Suspend() {}
|
||||||
|
|
@ -240,7 +241,7 @@ func TestTcellScreenPollingGoroutineExitsOnClose(t *testing.T) {
|
||||||
case <-tb.doneCh:
|
case <-tb.doneCh:
|
||||||
return
|
return
|
||||||
case replyCh := <-tb.resumeCh:
|
case replyCh := <-tb.resumeCh:
|
||||||
close(replyCh)
|
replyCh <- nil
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -313,16 +314,16 @@ func TestTcellScreenResumeNoDeadlock(t *testing.T) {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Simulate the polling goroutine: receive from resumeCh after a short delay,
|
// Simulate the polling goroutine: receive from resumeCh after a short delay,
|
||||||
// then close the reply channel (as PollEvent does after re-init).
|
// then send nil error (as PollEvent does after successful re-init).
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
replyCh := <-tb.resumeCh
|
replyCh := <-tb.resumeCh
|
||||||
close(replyCh)
|
replyCh <- nil
|
||||||
}()
|
}()
|
||||||
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
tb.Resume(ctx)
|
require.NoError(t, tb.Resume(ctx))
|
||||||
close(done)
|
close(done)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -344,10 +345,10 @@ func TestTcellScreenResumeDoesNotDropSend(t *testing.T) {
|
||||||
go func() {
|
go func() {
|
||||||
replyCh := <-tb.resumeCh
|
replyCh := <-tb.resumeCh
|
||||||
close(received)
|
close(received)
|
||||||
close(replyCh)
|
replyCh <- nil
|
||||||
}()
|
}()
|
||||||
|
|
||||||
tb.Resume(ctx)
|
require.NoError(t, tb.Resume(ctx))
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-received:
|
case <-received:
|
||||||
|
|
@ -367,7 +368,8 @@ func TestTcellScreenResumeContextCancelled(t *testing.T) {
|
||||||
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
tb.Resume(ctx)
|
err := tb.Resume(ctx)
|
||||||
|
require.Error(t, err)
|
||||||
close(done)
|
close(done)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -393,7 +395,8 @@ func TestTcellScreenResumeContextCancelledWhileWaitingForReply(t *testing.T) {
|
||||||
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
tb.Resume(ctx)
|
err := tb.Resume(ctx)
|
||||||
|
require.Error(t, err)
|
||||||
close(done)
|
close(done)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -411,3 +414,23 @@ func TestTcellScreenResumeContextCancelledWhileWaitingForReply(t *testing.T) {
|
||||||
// Verify context was indeed cancelled.
|
// Verify context was indeed cancelled.
|
||||||
require.Error(t, ctx.Err())
|
require.Error(t, ctx.Err())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTcellScreenResumeInitError(t *testing.T) {
|
||||||
|
tb := NewTcellScreen()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
initErr := errors.New("simulated screen init failure")
|
||||||
|
|
||||||
|
// Simulate the polling goroutine: receive from resumeCh and send
|
||||||
|
// an error as if Init() failed.
|
||||||
|
go func() {
|
||||||
|
replyCh := <-tb.resumeCh
|
||||||
|
replyCh <- initErr
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := tb.Resume(ctx)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, initErr, err)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue