diff --git a/cli/ssh.go b/cli/ssh.go index 7f40959a59..a291b3764b 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -25,12 +25,8 @@ 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/coderd/autobuild/notify" @@ -38,6 +34,9 @@ import ( "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" ) 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 := pty.MakeInputRaw(stdinFile.Fd()) if err != nil { return err } defer func() { - _ = term.Restore(int(stdinFile.Fd()), state) + _ = pty.RestoreTerminal(stdinFile.Fd(), inState) + }() + outState, err := pty.MakeOutputRaw(stdoutFile.Fd()) + if err != nil { + return err + } + defer func() { + _ = pty.RestoreTerminal(stdoutFile.Fd(), outState) }() windowChange := listenWindowSize(ctx) diff --git a/pty/terminal.go b/pty/terminal.go new file mode 100644 index 0000000000..2c1a35c3ee --- /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 0000000000..9c04354715 --- /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 0000000000..1d8f99d5b9 --- /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)) +}