Skip to content

Commit

Permalink
pkg/term: Add ways to change the state of a terminal device
Browse files Browse the repository at this point in the history
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.

containers#752
containers#1263
  • Loading branch information
debarshiray committed Dec 13, 2023
1 parent ddcd28c commit 3a6398d
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 0 deletions.
42 changes: 42 additions & 0 deletions src/pkg/term/term.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
}
95 changes: 95 additions & 0 deletions src/pkg/term/term_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"golang.org/x/sys/unix"
)

func TestIsTerminalTemporaryFile(t *testing.T) {
Expand All @@ -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)
}

0 comments on commit 3a6398d

Please sign in to comment.