Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Adds support for windows #79

Merged
merged 3 commits into from
Jul 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added .github/workflows/test.yaml
Empty file.
1 change: 1 addition & 0 deletions ci/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions cmd/coder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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{})
}
82 changes: 36 additions & 46 deletions cmd/coder/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -19,6 +17,7 @@ import (
"go.coder.com/flog"

"cdr.dev/coder-cli/internal/activity"
"cdr.dev/coder-cli/internal/xterminal"
"cdr.dev/wsep"
)

Expand All @@ -33,45 +32,8 @@ 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
}

func sendResizeEvents(ctx context.Context, termfd int, process wsep.Process) {
sigs := make(chan os.Signal, 16)
signal.Notify(sigs, unix.SIGWINCH)

// 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 {
return
}

// Do this last so the first resize is sent.
<-sigs
resizeLimiter.Wait(ctx)
}
type resizeEvent struct {
height, width uint16
}

func (cmd *shellCmd) Run(fl *pflag.FlagSet) {
Expand Down Expand Up @@ -101,21 +63,40 @@ 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 restore()
defer xterminal.Restore(os.Stdin.Fd(), stdinState)
}

ctx, cancel := context.WithCancel(ctx)
Expand All @@ -127,13 +108,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
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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-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
Expand Down
8 changes: 2 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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-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=
Expand Down Expand Up @@ -240,8 +238,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=
Expand Down
13 changes: 13 additions & 0 deletions internal/xterminal/doc.go
Original file line number Diff line number Diff line change
@@ -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
71 changes: 71 additions & 0 deletions internal/xterminal/terminal.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading