From 3a6398d7dda19aa67fc5bb323ae5568a9e16adac Mon Sep 17 00:00:00 2001 From: Debarshi Ray Date: Tue, 12 Dec 2023 00:47:53 +0100 Subject: [PATCH] pkg/term: Add ways to change the state of a terminal device A subsequent commit will use this to ensure that the user can still interact with the image download prompt while 'skopeo inspect' fetches the image size from the remote registry. To do this, at some point, the terminal device will be put into non-canonical mode input and the echoing of input characters will be disabled to retain full control of the cursor position. https://github.com/containers/toolbox/issues/752 https://github.com/containers/toolbox/issues/1263 --- src/pkg/term/term.go | 42 +++++++++++++++++ src/pkg/term/term_test.go | 95 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/src/pkg/term/term.go b/src/pkg/term/term.go index 43bbdc6b6..ed55d63de 100644 --- a/src/pkg/term/term.go +++ b/src/pkg/term/term.go @@ -22,6 +22,8 @@ import ( "golang.org/x/sys/unix" ) +type Option func(*unix.Termios) + func GetState(file *os.File) (*unix.Termios, error) { fileFD := file.Fd() fileFDInt := int(fileFD) @@ -36,3 +38,43 @@ func IsTerminal(file *os.File) bool { return true } + +func NewStateFrom(oldState *unix.Termios, options ...Option) *unix.Termios { + newState := *oldState + for _, option := range options { + option(&newState) + } + + return &newState +} + +func SetState(file *os.File, state *unix.Termios) error { + fileFD := file.Fd() + fileFDInt := int(fileFD) + err := unix.IoctlSetTermios(fileFDInt, unix.TCSETS, state) + return err +} + +func WithVMIN(vmin uint8) Option { + return func(state *unix.Termios) { + state.Cc[unix.VMIN] = vmin + } +} + +func WithVTIME(vtime uint8) Option { + return func(state *unix.Termios) { + state.Cc[unix.VTIME] = vtime + } +} + +func WithoutECHO() Option { + return func(state *unix.Termios) { + state.Lflag &^= unix.ECHO + } +} + +func WithoutICANON() Option { + return func(state *unix.Termios) { + state.Lflag &^= unix.ICANON + } +} diff --git a/src/pkg/term/term_test.go b/src/pkg/term/term_test.go index ad929f5fb..c45444476 100644 --- a/src/pkg/term/term_test.go +++ b/src/pkg/term/term_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "golang.org/x/sys/unix" ) func TestIsTerminalTemporaryFile(t *testing.T) { @@ -43,3 +44,97 @@ func TestIsTerminalTerminal(t *testing.T) { ok := IsTerminal(file) assert.True(t, ok) } + +func TestNewStateFromDifferent(t *testing.T) { + file, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0) + assert.NoError(t, err) + defer file.Close() + + oldState, err := GetState(file) + assert.NoError(t, err) + assert.Equal(t, uint32(unix.ECHO), oldState.Lflag&unix.ECHO) + assert.Equal(t, uint32(unix.ICANON), oldState.Lflag&unix.ICANON) + assert.NotEqual(t, uint8(13), oldState.Cc[unix.VMIN]) + assert.NotEqual(t, uint8(42), oldState.Cc[unix.VTIME]) + + newState := NewStateFrom(oldState, WithVMIN(13), WithVTIME(42), WithoutECHO(), WithoutICANON()) + assert.Empty(t, newState.Lflag&unix.ECHO) + assert.Empty(t, newState.Lflag&unix.ICANON) + assert.Equal(t, uint8(13), newState.Cc[unix.VMIN]) + assert.Equal(t, uint8(42), newState.Cc[unix.VTIME]) +} + +func TestNewStateFromNOP(t *testing.T) { + file, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0) + assert.NoError(t, err) + defer file.Close() + + oldState, err := GetState(file) + assert.NoError(t, err) + + newState := NewStateFrom(oldState) + assert.Equal(t, oldState.Cc, newState.Cc) + assert.Equal(t, oldState.Cflag, newState.Cflag) + assert.Equal(t, oldState.Iflag, newState.Iflag) + assert.Equal(t, oldState.Ispeed, newState.Ispeed) + assert.Equal(t, oldState.Lflag, newState.Lflag) + assert.Equal(t, oldState.Line, newState.Line) + assert.Equal(t, oldState.Oflag, newState.Oflag) + assert.Equal(t, oldState.Ospeed, newState.Ospeed) +} + +func TestSetStateDifferent(t *testing.T) { + file, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0) + assert.NoError(t, err) + defer file.Close() + + oldState, err := GetState(file) + assert.NoError(t, err) + assert.Equal(t, uint32(unix.ECHO), oldState.Lflag&unix.ECHO) + assert.Equal(t, uint32(unix.ICANON), oldState.Lflag&unix.ICANON) + assert.NotEqual(t, uint8(13), oldState.Cc[unix.VMIN]) + assert.NotEqual(t, uint8(42), oldState.Cc[unix.VTIME]) + + newState := NewStateFrom(oldState, WithVMIN(13), WithVTIME(42), WithoutECHO(), WithoutICANON()) + assert.Empty(t, newState.Lflag&unix.ECHO) + assert.Empty(t, newState.Lflag&unix.ICANON) + assert.Equal(t, uint8(13), newState.Cc[unix.VMIN]) + assert.Equal(t, uint8(42), newState.Cc[unix.VTIME]) + + err = SetState(file, newState) + assert.NoError(t, err) + + newState2, err := GetState(file) + assert.NoError(t, err) + assert.Equal(t, newState.Cc, newState2.Cc) + assert.Equal(t, newState.Cflag, newState2.Cflag) + assert.Equal(t, newState.Iflag, newState2.Iflag) + assert.Equal(t, newState.Ispeed, newState2.Ispeed) + assert.Equal(t, newState.Lflag, newState2.Lflag) + assert.Equal(t, newState.Line, newState2.Line) + assert.Equal(t, newState.Oflag, newState2.Oflag) + assert.Equal(t, newState.Ospeed, newState2.Ospeed) +} + +func TestSetStateNOP(t *testing.T) { + file, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0) + assert.NoError(t, err) + defer file.Close() + + oldState, err := GetState(file) + assert.NoError(t, err) + + err = SetState(file, oldState) + assert.NoError(t, err) + + newState, err := GetState(file) + assert.NoError(t, err) + assert.Equal(t, oldState.Cc, newState.Cc) + assert.Equal(t, oldState.Cflag, newState.Cflag) + assert.Equal(t, oldState.Iflag, newState.Iflag) + assert.Equal(t, oldState.Ispeed, newState.Ispeed) + assert.Equal(t, oldState.Lflag, newState.Lflag) + assert.Equal(t, oldState.Line, newState.Line) + assert.Equal(t, oldState.Oflag, newState.Oflag) + assert.Equal(t, oldState.Ospeed, newState.Ospeed) +}