From 6a5fbc59b5de413cc1d560080458ecb3635dbb7f Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Mon, 27 Jul 2020 15:29:05 -0500 Subject: [PATCH 1/3] Adds support for windows Change-Id: I2797929cfa72752932e334fc44614095eb1cdea6 --- .github/workflows/test.yaml | 0 ci/build.sh | 1 + cmd/coder/resize_unix.go | 39 +++++++++++++++++++++++++++++++ cmd/coder/resize_windows.go | 46 +++++++++++++++++++++++++++++++++++++ cmd/coder/shell.go | 42 ++++++++++++++++----------------- go.mod | 2 +- go.sum | 10 ++++---- 7 files changed, 112 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/test.yaml create mode 100644 cmd/coder/resize_unix.go create mode 100644 cmd/coder/resize_windows.go diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 00000000..e69de29b diff --git a/ci/build.sh b/ci/build.sh index 1930034b..8a6f540c 100755 --- a/ci/build.sh +++ b/ci/build.sh @@ -30,6 +30,7 @@ build(){ if [[ "$(uname)" == "Darwin" ]]; then GOOS=linux build CGO_ENABLED=1 GOOS=darwin build + GOOS=windows GOARCH=386 build exit 0 fi diff --git a/cmd/coder/resize_unix.go b/cmd/coder/resize_unix.go new file mode 100644 index 00000000..57ce74c7 --- /dev/null +++ b/cmd/coder/resize_unix.go @@ -0,0 +1,39 @@ +// +build !windows + +package main + +import ( + "context" + "os" + "os/signal" + + "golang.org/x/crypto/ssh/terminal" + "golang.org/x/sys/unix" + + "go.coder.com/flog" +) + +func resizeEvents(ctx context.Context, termfd int) chan resizeEvent { + sigs := make(chan os.Signal, 16) + signal.Notify(sigs, unix.SIGWINCH) + + events := make(chan resizeEvent) + + go func() { + for ctx.Err() == nil { + width, height, err := terminal.GetSize(termfd) + if err != nil { + flog.Error("get term size: %v", err) + break + } + events <- resizeEvent{ + height: uint16(height), + width: uint16(width), + } + + <-sigs + } + }() + + return events +} diff --git a/cmd/coder/resize_windows.go b/cmd/coder/resize_windows.go new file mode 100644 index 00000000..dd3826a5 --- /dev/null +++ b/cmd/coder/resize_windows.go @@ -0,0 +1,46 @@ +// +build windows + +package main + +import ( + "context" + "time" + + "golang.org/x/crypto/ssh/terminal" + + "go.coder.com/flog" +) + +// windows does have a unix.SIGWINCH equivalent, so we poll the terminal size +// and send resize events when needed +func resizeEvents(ctx context.Context, termfd int) chan resizeEvent { + events := make(chan resizeEvent) + t := time.Tick(time.Millisecond * 100) + + go func() { + var lastEvent *resizeEvent + + for { + select { + case <-ctx.Done(): + break + case <-t: + width, height, err := terminal.GetSize(termfd) + if err != nil { + flog.Error("get term size: %v", err) + break + } + event := resizeEvent{ + height: uint16(height), + width: uint16(width), + } + if !event.equal(lastEvent) { + events <- event + } + lastEvent = &event + } + } + }() + + return events +} diff --git a/cmd/coder/shell.go b/cmd/coder/shell.go index 644aa5c3..5c862869 100644 --- a/cmd/coder/shell.go +++ b/cmd/coder/shell.go @@ -4,13 +4,11 @@ import ( "context" "io" "os" - "os/signal" "strings" "time" "github.com/spf13/pflag" "golang.org/x/crypto/ssh/terminal" - "golang.org/x/sys/unix" "golang.org/x/time/rate" "golang.org/x/xerrors" "nhooyr.io/websocket" @@ -46,31 +44,33 @@ func enableTerminal(fd int) (restore func(), err error) { }, nil } +type resizeEvent struct { + height, width uint16 +} + +func (s resizeEvent) equal(s2 *resizeEvent) bool { + if s2 == nil { + return false + } + return s.height == s2.height && s.width == s2.width +} + func sendResizeEvents(ctx context.Context, termfd int, process wsep.Process) { - sigs := make(chan os.Signal, 16) - signal.Notify(sigs, unix.SIGWINCH) + events := resizeEvents(ctx, termfd) // Limit the frequency of resizes to prevent a stuttering effect. resizeLimiter := rate.NewLimiter(rate.Every(time.Millisecond*100), 1) - - for ctx.Err() == nil { - if ctx.Err() != nil { - return - } - width, height, err := terminal.GetSize(termfd) - if err != nil { - flog.Error("get term size: %v", err) - return - } - - err = process.Resize(ctx, uint16(height), uint16(width)) - if err != nil { + for { + select { + case newsize := <-events: + err := process.Resize(ctx, newsize.height, newsize.width) + if err != nil { + return + } + _ = resizeLimiter.Wait(ctx) + case <-ctx.Done(): return } - - // Do this last so the first resize is sent. - <-sigs - resizeLimiter.Wait(ctx) } } diff --git a/go.mod b/go.mod index 3fed388a..718bccdd 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module cdr.dev/coder-cli go 1.14 require ( - cdr.dev/wsep v0.0.0-20200616212001-0613cfe9a4ac + cdr.dev/wsep v0.0.0-20200727194627-13ef1f8df965 github.com/fatih/color v1.9.0 // indirect github.com/gorilla/websocket v1.4.1 github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f diff --git a/go.sum b/go.sum index e687d95d..c91a65da 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,7 @@ cdr.dev/slog v1.3.0 h1:MYN1BChIaVEGxdS7I5cpdyMC0+WfJfK8BETAfzfLUGQ= cdr.dev/slog v1.3.0/go.mod h1:C5OL99WyuOK8YHZdYY57dAPN1jK2WJlCdq2VP6xeQns= -cdr.dev/wsep v0.0.0-20200615020153-e2b1c576fc40 h1:f369880iSAZ3cXwvbdc9WIyy3FZ4yanusYZjaVHeis4= -cdr.dev/wsep v0.0.0-20200615020153-e2b1c576fc40/go.mod h1:2VKClUml3gfmLez0gBxTJIjSKszpQotc2ZqPdApfK/Y= -cdr.dev/wsep v0.0.0-20200616212001-0613cfe9a4ac h1:rl4O0qfxgNRWBUe5gQu4of2cdsclcpjGYmLQhSCHX7c= -cdr.dev/wsep v0.0.0-20200616212001-0613cfe9a4ac/go.mod h1:2VKClUml3gfmLez0gBxTJIjSKszpQotc2ZqPdApfK/Y= +cdr.dev/wsep v0.0.0-20200727194627-13ef1f8df965 h1:zFpraxgmcgX60oWFs8r1moWFMJ0x945t4moxNb6fjJ8= +cdr.dev/wsep v0.0.0-20200727194627-13ef1f8df965/go.mod h1:N20HJdMn6q9NG7sjxL4uYdBQBGOf8/6psfdMyuJnYw8= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -109,6 +107,8 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/iamacarpet/go-winpty v1.0.2 h1:jwPVTYrjAHZx6Mcm6K5i9G4opMp5TblEHH5EQCl/Gzw= +github.com/iamacarpet/go-winpty v1.0.2/go.mod h1:/GHKJicG/EVRQIK1IQikMYBakBkhj/3hTjLgdzYsmpI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -240,8 +240,6 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200610111108-226ff32320da h1:bGb80FudwxpeucJUjPYJXuJ8Hk91vNtfvrymzwiei38= -golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 6fc2c8ccaa66e128a0bc5932cb965643724b2ade Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Mon, 27 Jul 2020 19:31:55 -0500 Subject: [PATCH 2/3] Abstract cross-platform terminal syscalls Change-Id: I409e93a6298d6c45ee96d47e65e4abef6e4fe503 --- cmd/coder/resize_unix.go | 39 -------- cmd/coder/resize_windows.go | 46 ---------- cmd/coder/shell.go | 84 +++++++++--------- go.mod | 2 +- go.sum | 6 +- internal/xterminal/doc.go | 13 +++ internal/xterminal/terminal.go | 71 +++++++++++++++ internal/xterminal/terminal_windows.go | 118 +++++++++++++++++++++++++ 8 files changed, 245 insertions(+), 134 deletions(-) delete mode 100644 cmd/coder/resize_unix.go delete mode 100644 cmd/coder/resize_windows.go create mode 100644 internal/xterminal/doc.go create mode 100644 internal/xterminal/terminal.go create mode 100644 internal/xterminal/terminal_windows.go diff --git a/cmd/coder/resize_unix.go b/cmd/coder/resize_unix.go deleted file mode 100644 index 57ce74c7..00000000 --- a/cmd/coder/resize_unix.go +++ /dev/null @@ -1,39 +0,0 @@ -// +build !windows - -package main - -import ( - "context" - "os" - "os/signal" - - "golang.org/x/crypto/ssh/terminal" - "golang.org/x/sys/unix" - - "go.coder.com/flog" -) - -func resizeEvents(ctx context.Context, termfd int) chan resizeEvent { - sigs := make(chan os.Signal, 16) - signal.Notify(sigs, unix.SIGWINCH) - - events := make(chan resizeEvent) - - go func() { - for ctx.Err() == nil { - width, height, err := terminal.GetSize(termfd) - if err != nil { - flog.Error("get term size: %v", err) - break - } - events <- resizeEvent{ - height: uint16(height), - width: uint16(width), - } - - <-sigs - } - }() - - return events -} diff --git a/cmd/coder/resize_windows.go b/cmd/coder/resize_windows.go deleted file mode 100644 index dd3826a5..00000000 --- a/cmd/coder/resize_windows.go +++ /dev/null @@ -1,46 +0,0 @@ -// +build windows - -package main - -import ( - "context" - "time" - - "golang.org/x/crypto/ssh/terminal" - - "go.coder.com/flog" -) - -// windows does have a unix.SIGWINCH equivalent, so we poll the terminal size -// and send resize events when needed -func resizeEvents(ctx context.Context, termfd int) chan resizeEvent { - events := make(chan resizeEvent) - t := time.Tick(time.Millisecond * 100) - - go func() { - var lastEvent *resizeEvent - - for { - select { - case <-ctx.Done(): - break - case <-t: - width, height, err := terminal.GetSize(termfd) - if err != nil { - flog.Error("get term size: %v", err) - break - } - event := resizeEvent{ - height: uint16(height), - width: uint16(width), - } - if !event.equal(lastEvent) { - events <- event - } - lastEvent = &event - } - } - }() - - return events -} diff --git a/cmd/coder/shell.go b/cmd/coder/shell.go index 5c862869..6e8f791f 100644 --- a/cmd/coder/shell.go +++ b/cmd/coder/shell.go @@ -17,6 +17,7 @@ import ( "go.coder.com/flog" "cdr.dev/coder-cli/internal/activity" + "cdr.dev/coder-cli/internal/xterminal" "cdr.dev/wsep" ) @@ -31,49 +32,10 @@ func (cmd *shellCmd) Spec() cli.CommandSpec { } } -func enableTerminal(fd int) (restore func(), err error) { - state, err := terminal.MakeRaw(fd) - if err != nil { - return restore, xerrors.Errorf("make raw term: %w", err) - } - return func() { - err := terminal.Restore(fd, state) - if err != nil { - flog.Error("restore term state: %v", err) - } - }, nil -} - type resizeEvent struct { height, width uint16 } -func (s resizeEvent) equal(s2 *resizeEvent) bool { - if s2 == nil { - return false - } - return s.height == s2.height && s.width == s2.width -} - -func sendResizeEvents(ctx context.Context, termfd int, process wsep.Process) { - events := resizeEvents(ctx, termfd) - - // Limit the frequency of resizes to prevent a stuttering effect. - resizeLimiter := rate.NewLimiter(rate.Every(time.Millisecond*100), 1) - for { - select { - case newsize := <-events: - err := process.Resize(ctx, newsize.height, newsize.width) - if err != nil { - return - } - _ = resizeLimiter.Wait(ctx) - case <-ctx.Done(): - return - } - } -} - func (cmd *shellCmd) Run(fl *pflag.FlagSet) { if len(fl.Args()) < 1 { exitUsage(fl) @@ -101,21 +63,46 @@ func (cmd *shellCmd) Run(fl *pflag.FlagSet) { } } +func sendResizeEvents(ctx context.Context, termfd uintptr, process wsep.Process) { + events := xterminal.ResizeEvents(ctx, termfd) + + // Limit the frequency of resizes to prevent a stuttering effect. + resizeLimiter := rate.NewLimiter(rate.Every(time.Millisecond*100), 1) + for { + select { + case newsize := <-events: + err := process.Resize(ctx, newsize.Height, newsize.Width) + if err != nil { + return + } + _ = resizeLimiter.Wait(ctx) + case <-ctx.Done(): + return + } + } +} + func runCommand(ctx context.Context, envName string, command string, args []string) error { var ( entClient = requireAuth() env = findEnv(entClient, envName) ) - termfd := int(os.Stdin.Fd()) + termfd := os.Stdout.Fd() - tty := terminal.IsTerminal(termfd) + tty := terminal.IsTerminal(int(termfd)) if tty { - restore, err := enableTerminal(termfd) + stdinState, err := xterminal.MakeRaw(os.Stdin.Fd()) + if err != nil { + return err + } + defer xterminal.Restore(os.Stdin.Fd(), stdinState) + + stdoutState, err := xterminal.MakeOutputRaw(os.Stdout.Fd()) if err != nil { return err } - defer restore() + defer xterminal.Restore(os.Stdout.Fd(), stdoutState) } ctx, cancel := context.WithCancel(ctx) @@ -127,13 +114,22 @@ func runCommand(ctx context.Context, envName string, command string, args []stri } go heartbeat(ctx, conn, 15*time.Second) + var cmdEnv []string + if tty { + term := os.Getenv("TERM") + if term == "" { + term = "xterm" + } + cmdEnv = append(cmdEnv, "TERM="+term) + } + execer := wsep.RemoteExecer(conn) process, err := execer.Start(ctx, wsep.Command{ Command: command, Args: args, TTY: tty, Stdin: true, - Env: []string{"TERM=" + os.Getenv("TERM")}, + Env: cmdEnv, }) if err != nil { return err diff --git a/go.mod b/go.mod index 718bccdd..7d8feec2 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module cdr.dev/coder-cli go 1.14 require ( - cdr.dev/wsep v0.0.0-20200727194627-13ef1f8df965 + cdr.dev/wsep v0.0.0-20200728013649-82316a09813f github.com/fatih/color v1.9.0 // indirect github.com/gorilla/websocket v1.4.1 github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f diff --git a/go.sum b/go.sum index c91a65da..37756a28 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ cdr.dev/slog v1.3.0 h1:MYN1BChIaVEGxdS7I5cpdyMC0+WfJfK8BETAfzfLUGQ= cdr.dev/slog v1.3.0/go.mod h1:C5OL99WyuOK8YHZdYY57dAPN1jK2WJlCdq2VP6xeQns= -cdr.dev/wsep v0.0.0-20200727194627-13ef1f8df965 h1:zFpraxgmcgX60oWFs8r1moWFMJ0x945t4moxNb6fjJ8= -cdr.dev/wsep v0.0.0-20200727194627-13ef1f8df965/go.mod h1:N20HJdMn6q9NG7sjxL4uYdBQBGOf8/6psfdMyuJnYw8= +cdr.dev/wsep v0.0.0-20200728013649-82316a09813f h1:WnTUINBwXE11xjp5nTVt+H2qB2/KEymos1jKMcppG9U= +cdr.dev/wsep v0.0.0-20200728013649-82316a09813f/go.mod h1:2VKClUml3gfmLez0gBxTJIjSKszpQotc2ZqPdApfK/Y= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -107,8 +107,6 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/iamacarpet/go-winpty v1.0.2 h1:jwPVTYrjAHZx6Mcm6K5i9G4opMp5TblEHH5EQCl/Gzw= -github.com/iamacarpet/go-winpty v1.0.2/go.mod h1:/GHKJicG/EVRQIK1IQikMYBakBkhj/3hTjLgdzYsmpI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= diff --git a/internal/xterminal/doc.go b/internal/xterminal/doc.go new file mode 100644 index 00000000..21e0ae0e --- /dev/null +++ b/internal/xterminal/doc.go @@ -0,0 +1,13 @@ +// Package xterminal provides functions to change termios or console attributes +// and restore them later on. It supports Unix and Windows. +// +// This does the same thing as x/crypto/ssh/terminal on Linux. On Windows, it +// sets the same console modes as the terminal package but also sets +// `ENABLE_VIRTUAL_TERMINAL_INPUT` and `ENABLE_VIRTUAL_TERMINAL_PROCESSING` to +// allow for VT100 sequences in the console. This is important, otherwise Linux +// apps (with colors or ncurses) that are run through SSH or wsep get +// garbled in a Windows console. +// +// More details can be found out about Windows console modes here: +// https://docs.microsoft.com/en-us/windows/console/setconsolemode +package xterminal diff --git a/internal/xterminal/terminal.go b/internal/xterminal/terminal.go new file mode 100644 index 00000000..d2838725 --- /dev/null +++ b/internal/xterminal/terminal.go @@ -0,0 +1,71 @@ +// +build !windows + +package xterminal + +import ( + "context" + "os" + "os/signal" + + "golang.org/x/crypto/ssh/terminal" + "golang.org/x/sys/unix" +) + +// State differs per-platform. +type State struct { + s *terminal.State +} + +// MakeRaw sets the terminal to raw. +func MakeRaw(fd uintptr) (*State, error) { + s, err := terminal.MakeRaw(int(fd)) + return &State{s}, err +} + +// MakeOutputRaw does nothing on non-Windows platforms. +func MakeOutputRaw(fd uintptr) (*State, error) { + return nil, nil +} + +// Restore terminal back to original state. +func Restore(fd uintptr, state *State) error { + if state == nil { + return nil + } + + return terminal.Restore(int(fd), state.s) +} + +// ColorEnabled returns true on Linux if handle is a terminal. +func ColorEnabled(fd uintptr) (bool, error) { + return terminal.IsTerminal(int(fd)), nil +} + +type ResizeEvent struct { + Height, Width uint16 +} + +// ResizeEvents sends terminal resize events +func ResizeEvents(ctx context.Context, termfd uintptr) chan ResizeEvent { + sigs := make(chan os.Signal, 16) + signal.Notify(sigs, unix.SIGWINCH) + + events := make(chan ResizeEvent) + + go func() { + for ctx.Err() == nil { + width, height, err := terminal.GetSize(int(termfd)) + if err != nil { + return + } + events <- ResizeEvent{ + Height: uint16(height), + Width: uint16(width), + } + + <-sigs + } + }() + + return events +} diff --git a/internal/xterminal/terminal_windows.go b/internal/xterminal/terminal_windows.go new file mode 100644 index 00000000..a7e9eaef --- /dev/null +++ b/internal/xterminal/terminal_windows.go @@ -0,0 +1,118 @@ +// +build windows + +package xterminal + +import ( + "context" + "time" + + "golang.org/x/crypto/ssh/terminal" + "golang.org/x/sys/windows" +) + +// State differs per-platform. +type State struct { + mode uint32 +} + +func makeRaw(handle windows.Handle, input bool) (uint32, error) { + var st uint32 + if err := windows.GetConsoleMode(handle, &st); err != nil { + return 0, err + } + + var raw uint32 + if input { + raw = st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT) + raw |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT + } else { + raw = st | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING + } + + if err := windows.SetConsoleMode(handle, raw); err != nil { + return 0, err + } + return st, nil +} + +// MakeRaw sets an input terminal to raw and enables VT100 processing. +func MakeRaw(handle uintptr) (*State, error) { + inSt, err := makeRaw(windows.Handle(handle), true) + if err != nil { + return nil, err + } + + return &State{inSt}, nil +} + +// MakeOutputRaw sets an output terminal to raw and enables VT100 processing. +func MakeOutputRaw(handle uintptr) (*State, error) { + outSt, err := makeRaw(windows.Handle(handle), false) + if err != nil { + return nil, err + } + + return &State{outSt}, nil +} + +// Restore terminal back to original state. +func Restore(handle uintptr, state *State) error { + return windows.SetConsoleMode(windows.Handle(handle), state.mode) +} + +// ColorEnabled returns true if VT100 processing is enabled on the output +// console. +func ColorEnabled(handle uintptr) (bool, error) { + var st uint32 + if err := windows.GetConsoleMode(windows.Handle(handle), &st); err != nil { + return false, err + } + + return st&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0, nil +} + +type ResizeEvent struct { + Height, Width uint16 +} + +func (s ResizeEvent) equal(s2 *ResizeEvent) bool { + if s2 == nil { + return false + } + return s.Height == s2.Height && s.Width == s2.Width +} + +// ResizeEvents sends terminal resize events when the dimensions change. +// Windows does not have a unix.SIGWINCH equivalent, so we poll the terminal size +// at a fixed interval +func ResizeEvents(ctx context.Context, termfd uintptr) chan ResizeEvent { + events := make(chan ResizeEvent) + ticker := time.NewTicker(time.Millisecond * 100) + + go func() { + defer ticker.Stop() + var lastEvent *ResizeEvent + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + width, height, err := terminal.GetSize(int(windows.Handle(termfd))) + if err != nil { + return + } + event := ResizeEvent{ + Height: uint16(height), + Width: uint16(width), + } + if !event.equal(lastEvent) { + events <- event + } + lastEvent = &event + } + } + }() + + return events +} From 5a6b14e51eacf251a005eff8152f4fd85fa5e465 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Mon, 27 Jul 2020 22:23:01 -0500 Subject: [PATCH 3/3] Set raw output for all commands Change-Id: Id286904a99d3f3796a7679837ec4f168a273807c --- cmd/coder/main.go | 10 ++++++++++ cmd/coder/shell.go | 6 ------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/cmd/coder/main.go b/cmd/coder/main.go index 3cf2ec5e..604775b5 100644 --- a/cmd/coder/main.go +++ b/cmd/coder/main.go @@ -6,8 +6,11 @@ import ( _ "net/http/pprof" "os" + "cdr.dev/coder-cli/internal/xterminal" "github.com/spf13/pflag" + "go.coder.com/flog" + "go.coder.com/cli" ) @@ -48,5 +51,12 @@ func main() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() } + + stdoutState, err := xterminal.MakeOutputRaw(os.Stdout.Fd()) + if err != nil { + flog.Fatal("failed to set output to raw: %v", err) + } + defer xterminal.Restore(os.Stdout.Fd(), stdoutState) + cli.RunRoot(&rootCmd{}) } diff --git a/cmd/coder/shell.go b/cmd/coder/shell.go index 6e8f791f..82a31436 100644 --- a/cmd/coder/shell.go +++ b/cmd/coder/shell.go @@ -97,12 +97,6 @@ func runCommand(ctx context.Context, envName string, command string, args []stri return err } defer xterminal.Restore(os.Stdin.Fd(), stdinState) - - stdoutState, err := xterminal.MakeOutputRaw(os.Stdout.Fd()) - if err != nil { - return err - } - defer xterminal.Restore(os.Stdout.Fd(), stdoutState) } ctx, cancel := context.WithCancel(ctx)