From 500695dddae045d7da62f6fd6d5de5ec218aa848 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 17 Apr 2024 17:12:00 +0000 Subject: [PATCH 1/2] fix: make terminal raw in ssh command on windows We were making it raw, but not raw enough. It needs a few extra modes enabled to function with arrows etc. --- cli/ssh.go | 22 +++++++----- cli/xterminal/terminal.go | 40 +++++++++++++++++++++ cli/xterminal/terminal_windows.go | 59 +++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 cli/xterminal/terminal.go create mode 100644 cli/xterminal/terminal_windows.go diff --git a/cli/ssh.go b/cli/ssh.go index 7f40959a591f4..75ea246ee2263 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -25,19 +25,18 @@ import ( "golang.org/x/xerrors" "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" - "github.com/coder/retry" - "github.com/coder/serpent" - "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" - "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/cli/cliutil" + "github.com/coder/coder/v2/cli/xterminal" "github.com/coder/coder/v2/coderd/autobuild/notify" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/workspacesdk" "github.com/coder/coder/v2/cryptorand" + "github.com/coder/retry" + "github.com/coder/serpent" ) var ( @@ -341,15 +340,22 @@ func (r *RootCmd) ssh() *serpent.Command { } } - stdoutFile, validOut := inv.Stdout.(*os.File) stdinFile, validIn := inv.Stdin.(*os.File) - if validOut && validIn && isatty.IsTerminal(stdoutFile.Fd()) { - state, err := term.MakeRaw(int(stdinFile.Fd())) + stdoutFile, validOut := inv.Stdout.(*os.File) + if validIn && validOut && isatty.IsTerminal(stdinFile.Fd()) && isatty.IsTerminal(stdoutFile.Fd()) { + inState, err := xterminal.MakeInputRaw(stdinFile.Fd()) + if err != nil { + return err + } + defer func() { + _ = xterminal.Restore(stdinFile.Fd(), inState) + }() + outState, err := xterminal.MakeOutputRaw(stdoutFile.Fd()) if err != nil { return err } defer func() { - _ = term.Restore(int(stdinFile.Fd()), state) + _ = xterminal.Restore(stdoutFile.Fd(), outState) }() windowChange := listenWindowSize(ctx) diff --git a/cli/xterminal/terminal.go b/cli/xterminal/terminal.go new file mode 100644 index 0000000000000..c8142c18134bc --- /dev/null +++ b/cli/xterminal/terminal.go @@ -0,0 +1,40 @@ +//go:build !windows +// +build !windows + +package xterminal + +import ( + "golang.org/x/term" +) + +// State differs per-platform. +type State struct { + s *term.State +} + +// MakeInputRaw calls term.MakeRaw on non-Windows platforms. +func MakeInputRaw(fd uintptr) (*State, error) { + s, err := term.MakeRaw(int(fd)) + if err != nil { + return nil, err + } + return &State{ + s: s, + }, nil +} + +// MakeOutputRaw does nothing on non-Windows platforms. +func MakeOutputRaw(_ uintptr) (*State, error) { + return &State{ + s: nil, + }, nil +} + +// Restore terminal back to original state. +func Restore(fd uintptr, state *State) error { + if state == nil || state.s == nil { + return nil + } + + return term.Restore(int(fd), state.s) +} diff --git a/cli/xterminal/terminal_windows.go b/cli/xterminal/terminal_windows.go new file mode 100644 index 0000000000000..8eda2c57f5290 --- /dev/null +++ b/cli/xterminal/terminal_windows.go @@ -0,0 +1,59 @@ +//go:build windows +// +build windows + +package xterminal + +import ( + "golang.org/x/sys/windows" +) + +// State differs per-platform. +type State struct { + mode uint32 +} + +// makeRaw sets the terminal in raw mode and returns the previous state so it can be restored. +func makeRaw(handle windows.Handle, input bool) (uint32, error) { + var prevState uint32 + if err := windows.GetConsoleMode(handle, &prevState); err != nil { + return 0, err + } + + var raw uint32 + if input { + raw = prevState &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT) + raw |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT + } else { + raw = prevState | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING + } + + if err := windows.SetConsoleMode(handle, raw); err != nil { + return 0, err + } + return prevState, nil +} + +// MakeInputRaw sets an input terminal to raw and enables VT100 processing. +func MakeInputRaw(handle uintptr) (*State, error) { + prevState, err := makeRaw(windows.Handle(handle), true) + if err != nil { + return nil, err + } + + return &State{mode: prevState}, nil +} + +// MakeOutputRaw sets an output terminal to raw and enables VT100 processing. +func MakeOutputRaw(handle uintptr) (*State, error) { + prevState, err := makeRaw(windows.Handle(handle), false) + if err != nil { + return nil, err + } + + return &State{mode: prevState}, nil +} + +// Restore terminal back to original state. +func Restore(handle uintptr, state *State) error { + return windows.SetConsoleMode(windows.Handle(handle), state.mode) +} From 7d143751526769f23ee881e9738f29f8f935c8ed Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 17 Apr 2024 17:51:00 +0000 Subject: [PATCH 2/2] Move xterminal to pty --- cli/ssh.go | 10 ++--- cli/xterminal/terminal.go | 40 ------------------- cli/xterminal/terminal_windows.go | 59 ---------------------------- pty/terminal.go | 31 +++++++++++++++ pty/terminal_other.go | 36 +++++++++++++++++ pty/terminal_windows.go | 65 +++++++++++++++++++++++++++++++ 6 files changed, 137 insertions(+), 104 deletions(-) delete mode 100644 cli/xterminal/terminal.go delete mode 100644 cli/xterminal/terminal_windows.go create mode 100644 pty/terminal.go create mode 100644 pty/terminal_other.go create mode 100644 pty/terminal_windows.go diff --git a/cli/ssh.go b/cli/ssh.go index 75ea246ee2263..a291b3764b843 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -29,12 +29,12 @@ import ( "cdr.dev/slog/sloggers/sloghuman" "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/cli/cliutil" - "github.com/coder/coder/v2/cli/xterminal" "github.com/coder/coder/v2/coderd/autobuild/notify" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/workspacesdk" "github.com/coder/coder/v2/cryptorand" + "github.com/coder/coder/v2/pty" "github.com/coder/retry" "github.com/coder/serpent" ) @@ -343,19 +343,19 @@ func (r *RootCmd) ssh() *serpent.Command { stdinFile, validIn := inv.Stdin.(*os.File) stdoutFile, validOut := inv.Stdout.(*os.File) if validIn && validOut && isatty.IsTerminal(stdinFile.Fd()) && isatty.IsTerminal(stdoutFile.Fd()) { - inState, err := xterminal.MakeInputRaw(stdinFile.Fd()) + inState, err := pty.MakeInputRaw(stdinFile.Fd()) if err != nil { return err } defer func() { - _ = xterminal.Restore(stdinFile.Fd(), inState) + _ = pty.RestoreTerminal(stdinFile.Fd(), inState) }() - outState, err := xterminal.MakeOutputRaw(stdoutFile.Fd()) + outState, err := pty.MakeOutputRaw(stdoutFile.Fd()) if err != nil { return err } defer func() { - _ = xterminal.Restore(stdoutFile.Fd(), outState) + _ = pty.RestoreTerminal(stdoutFile.Fd(), outState) }() windowChange := listenWindowSize(ctx) diff --git a/cli/xterminal/terminal.go b/cli/xterminal/terminal.go deleted file mode 100644 index c8142c18134bc..0000000000000 --- a/cli/xterminal/terminal.go +++ /dev/null @@ -1,40 +0,0 @@ -//go:build !windows -// +build !windows - -package xterminal - -import ( - "golang.org/x/term" -) - -// State differs per-platform. -type State struct { - s *term.State -} - -// MakeInputRaw calls term.MakeRaw on non-Windows platforms. -func MakeInputRaw(fd uintptr) (*State, error) { - s, err := term.MakeRaw(int(fd)) - if err != nil { - return nil, err - } - return &State{ - s: s, - }, nil -} - -// MakeOutputRaw does nothing on non-Windows platforms. -func MakeOutputRaw(_ uintptr) (*State, error) { - return &State{ - s: nil, - }, nil -} - -// Restore terminal back to original state. -func Restore(fd uintptr, state *State) error { - if state == nil || state.s == nil { - return nil - } - - return term.Restore(int(fd), state.s) -} diff --git a/cli/xterminal/terminal_windows.go b/cli/xterminal/terminal_windows.go deleted file mode 100644 index 8eda2c57f5290..0000000000000 --- a/cli/xterminal/terminal_windows.go +++ /dev/null @@ -1,59 +0,0 @@ -//go:build windows -// +build windows - -package xterminal - -import ( - "golang.org/x/sys/windows" -) - -// State differs per-platform. -type State struct { - mode uint32 -} - -// makeRaw sets the terminal in raw mode and returns the previous state so it can be restored. -func makeRaw(handle windows.Handle, input bool) (uint32, error) { - var prevState uint32 - if err := windows.GetConsoleMode(handle, &prevState); err != nil { - return 0, err - } - - var raw uint32 - if input { - raw = prevState &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT) - raw |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT - } else { - raw = prevState | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING - } - - if err := windows.SetConsoleMode(handle, raw); err != nil { - return 0, err - } - return prevState, nil -} - -// MakeInputRaw sets an input terminal to raw and enables VT100 processing. -func MakeInputRaw(handle uintptr) (*State, error) { - prevState, err := makeRaw(windows.Handle(handle), true) - if err != nil { - return nil, err - } - - return &State{mode: prevState}, nil -} - -// MakeOutputRaw sets an output terminal to raw and enables VT100 processing. -func MakeOutputRaw(handle uintptr) (*State, error) { - prevState, err := makeRaw(windows.Handle(handle), false) - if err != nil { - return nil, err - } - - return &State{mode: prevState}, nil -} - -// Restore terminal back to original state. -func Restore(handle uintptr, state *State) error { - return windows.SetConsoleMode(windows.Handle(handle), state.mode) -} diff --git a/pty/terminal.go b/pty/terminal.go new file mode 100644 index 0000000000000..2c1a35c3ee35f --- /dev/null +++ b/pty/terminal.go @@ -0,0 +1,31 @@ +package pty + +// TerminalState differs per-platform. +type TerminalState struct { + state terminalState +} + +// MakeInputRaw calls term.MakeRaw on non-Windows platforms. On Windows it sets +// special terminal modes that enable VT100 emulation as well as setting the +// same modes that term.MakeRaw sets. +// +//nolint:revive +func MakeInputRaw(fd uintptr) (*TerminalState, error) { + return makeInputRaw(fd) +} + +// MakeOutputRaw does nothing on non-Windows platforms. On Windows it sets +// special terminal modes that enable VT100 emulation as well as setting the +// same modes that term.MakeRaw sets. +// +//nolint:revive +func MakeOutputRaw(fd uintptr) (*TerminalState, error) { + return makeOutputRaw(fd) +} + +// RestoreTerminal restores the terminal back to its original state. +// +//nolint:revive +func RestoreTerminal(fd uintptr, state *TerminalState) error { + return restoreTerminal(fd, state) +} diff --git a/pty/terminal_other.go b/pty/terminal_other.go new file mode 100644 index 0000000000000..9c04354715253 --- /dev/null +++ b/pty/terminal_other.go @@ -0,0 +1,36 @@ +//go:build !windows +// +build !windows + +package pty + +import "golang.org/x/term" + +type terminalState *term.State + +//nolint:revive +func makeInputRaw(fd uintptr) (*TerminalState, error) { + s, err := term.MakeRaw(int(fd)) + if err != nil { + return nil, err + } + return &TerminalState{ + state: s, + }, nil +} + +//nolint:revive +func makeOutputRaw(_ uintptr) (*TerminalState, error) { + // Does nothing. makeInputRaw does enough for both input and output. + return &TerminalState{ + state: nil, + }, nil +} + +//nolint:revive +func restoreTerminal(fd uintptr, state *TerminalState) error { + if state == nil || state.state == nil { + return nil + } + + return term.Restore(int(fd), state.state) +} diff --git a/pty/terminal_windows.go b/pty/terminal_windows.go new file mode 100644 index 0000000000000..1d8f99d5b9eb1 --- /dev/null +++ b/pty/terminal_windows.go @@ -0,0 +1,65 @@ +//go:build windows +// +build windows + +package pty + +import "golang.org/x/sys/windows" + +type terminalState uint32 + +// This is adapted from term.MakeRaw, but adds +// ENABLE_VIRTUAL_TERMINAL_PROCESSING to the output mode and +// ENABLE_VIRTUAL_TERMINAL_INPUT to the input mode. +// +// See: https://github.com/golang/term/blob/5b15d269ba1f54e8da86c8aa5574253aea0c2198/term_windows.go#L23 +// +// Copyright 2019 The Go Authors. BSD-3-Clause license. See: +// https://github.com/golang/term/blob/master/LICENSE +func makeRaw(handle windows.Handle, input bool) (uint32, error) { + var prevState uint32 + if err := windows.GetConsoleMode(handle, &prevState); err != nil { + return 0, err + } + + var raw uint32 + if input { + raw = prevState &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT) + raw |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT + } else { + raw = prevState | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING + } + + if err := windows.SetConsoleMode(handle, raw); err != nil { + return 0, err + } + return prevState, nil +} + +//nolint:revive +func makeInputRaw(handle uintptr) (*TerminalState, error) { + prevState, err := makeRaw(windows.Handle(handle), true) + if err != nil { + return nil, err + } + + return &TerminalState{ + state: terminalState(prevState), + }, nil +} + +//nolint:revive +func makeOutputRaw(handle uintptr) (*TerminalState, error) { + prevState, err := makeRaw(windows.Handle(handle), false) + if err != nil { + return nil, err + } + + return &TerminalState{ + state: terminalState(prevState), + }, nil +} + +//nolint:revive +func restoreTerminal(handle uintptr, state *TerminalState) error { + return windows.SetConsoleMode(windows.Handle(handle), uint32(state.state)) +}