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

Commit 6fc2c8c

Browse files
committed
Abstract cross-platform terminal syscalls
Change-Id: I409e93a6298d6c45ee96d47e65e4abef6e4fe503
1 parent 6a5fbc5 commit 6fc2c8c

File tree

8 files changed

+245
-134
lines changed

8 files changed

+245
-134
lines changed

Diff for: cmd/coder/resize_unix.go

-39
This file was deleted.

Diff for: cmd/coder/resize_windows.go

-46
This file was deleted.

Diff for: cmd/coder/shell.go

+40-44
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"go.coder.com/flog"
1818

1919
"cdr.dev/coder-cli/internal/activity"
20+
"cdr.dev/coder-cli/internal/xterminal"
2021
"cdr.dev/wsep"
2122
)
2223

@@ -31,49 +32,10 @@ func (cmd *shellCmd) Spec() cli.CommandSpec {
3132
}
3233
}
3334

34-
func enableTerminal(fd int) (restore func(), err error) {
35-
state, err := terminal.MakeRaw(fd)
36-
if err != nil {
37-
return restore, xerrors.Errorf("make raw term: %w", err)
38-
}
39-
return func() {
40-
err := terminal.Restore(fd, state)
41-
if err != nil {
42-
flog.Error("restore term state: %v", err)
43-
}
44-
}, nil
45-
}
46-
4735
type resizeEvent struct {
4836
height, width uint16
4937
}
5038

51-
func (s resizeEvent) equal(s2 *resizeEvent) bool {
52-
if s2 == nil {
53-
return false
54-
}
55-
return s.height == s2.height && s.width == s2.width
56-
}
57-
58-
func sendResizeEvents(ctx context.Context, termfd int, process wsep.Process) {
59-
events := resizeEvents(ctx, termfd)
60-
61-
// Limit the frequency of resizes to prevent a stuttering effect.
62-
resizeLimiter := rate.NewLimiter(rate.Every(time.Millisecond*100), 1)
63-
for {
64-
select {
65-
case newsize := <-events:
66-
err := process.Resize(ctx, newsize.height, newsize.width)
67-
if err != nil {
68-
return
69-
}
70-
_ = resizeLimiter.Wait(ctx)
71-
case <-ctx.Done():
72-
return
73-
}
74-
}
75-
}
76-
7739
func (cmd *shellCmd) Run(fl *pflag.FlagSet) {
7840
if len(fl.Args()) < 1 {
7941
exitUsage(fl)
@@ -101,21 +63,46 @@ func (cmd *shellCmd) Run(fl *pflag.FlagSet) {
10163
}
10264
}
10365

66+
func sendResizeEvents(ctx context.Context, termfd uintptr, process wsep.Process) {
67+
events := xterminal.ResizeEvents(ctx, termfd)
68+
69+
// Limit the frequency of resizes to prevent a stuttering effect.
70+
resizeLimiter := rate.NewLimiter(rate.Every(time.Millisecond*100), 1)
71+
for {
72+
select {
73+
case newsize := <-events:
74+
err := process.Resize(ctx, newsize.Height, newsize.Width)
75+
if err != nil {
76+
return
77+
}
78+
_ = resizeLimiter.Wait(ctx)
79+
case <-ctx.Done():
80+
return
81+
}
82+
}
83+
}
84+
10485
func runCommand(ctx context.Context, envName string, command string, args []string) error {
10586
var (
10687
entClient = requireAuth()
10788
env = findEnv(entClient, envName)
10889
)
10990

110-
termfd := int(os.Stdin.Fd())
91+
termfd := os.Stdout.Fd()
11192

112-
tty := terminal.IsTerminal(termfd)
93+
tty := terminal.IsTerminal(int(termfd))
11394
if tty {
114-
restore, err := enableTerminal(termfd)
95+
stdinState, err := xterminal.MakeRaw(os.Stdin.Fd())
96+
if err != nil {
97+
return err
98+
}
99+
defer xterminal.Restore(os.Stdin.Fd(), stdinState)
100+
101+
stdoutState, err := xterminal.MakeOutputRaw(os.Stdout.Fd())
115102
if err != nil {
116103
return err
117104
}
118-
defer restore()
105+
defer xterminal.Restore(os.Stdout.Fd(), stdoutState)
119106
}
120107

121108
ctx, cancel := context.WithCancel(ctx)
@@ -127,13 +114,22 @@ func runCommand(ctx context.Context, envName string, command string, args []stri
127114
}
128115
go heartbeat(ctx, conn, 15*time.Second)
129116

117+
var cmdEnv []string
118+
if tty {
119+
term := os.Getenv("TERM")
120+
if term == "" {
121+
term = "xterm"
122+
}
123+
cmdEnv = append(cmdEnv, "TERM="+term)
124+
}
125+
130126
execer := wsep.RemoteExecer(conn)
131127
process, err := execer.Start(ctx, wsep.Command{
132128
Command: command,
133129
Args: args,
134130
TTY: tty,
135131
Stdin: true,
136-
Env: []string{"TERM=" + os.Getenv("TERM")},
132+
Env: cmdEnv,
137133
})
138134
if err != nil {
139135
return err

Diff for: go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module cdr.dev/coder-cli
33
go 1.14
44

55
require (
6-
cdr.dev/wsep v0.0.0-20200727194627-13ef1f8df965
6+
cdr.dev/wsep v0.0.0-20200728013649-82316a09813f
77
github.com/fatih/color v1.9.0 // indirect
88
github.com/gorilla/websocket v1.4.1
99
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f

Diff for: go.sum

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
cdr.dev/slog v1.3.0 h1:MYN1BChIaVEGxdS7I5cpdyMC0+WfJfK8BETAfzfLUGQ=
22
cdr.dev/slog v1.3.0/go.mod h1:C5OL99WyuOK8YHZdYY57dAPN1jK2WJlCdq2VP6xeQns=
3-
cdr.dev/wsep v0.0.0-20200727194627-13ef1f8df965 h1:zFpraxgmcgX60oWFs8r1moWFMJ0x945t4moxNb6fjJ8=
4-
cdr.dev/wsep v0.0.0-20200727194627-13ef1f8df965/go.mod h1:N20HJdMn6q9NG7sjxL4uYdBQBGOf8/6psfdMyuJnYw8=
3+
cdr.dev/wsep v0.0.0-20200728013649-82316a09813f h1:WnTUINBwXE11xjp5nTVt+H2qB2/KEymos1jKMcppG9U=
4+
cdr.dev/wsep v0.0.0-20200728013649-82316a09813f/go.mod h1:2VKClUml3gfmLez0gBxTJIjSKszpQotc2ZqPdApfK/Y=
55
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
66
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
77
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
107107
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
108108
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
109109
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
110-
github.com/iamacarpet/go-winpty v1.0.2 h1:jwPVTYrjAHZx6Mcm6K5i9G4opMp5TblEHH5EQCl/Gzw=
111-
github.com/iamacarpet/go-winpty v1.0.2/go.mod h1:/GHKJicG/EVRQIK1IQikMYBakBkhj/3hTjLgdzYsmpI=
112110
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
113111
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
114112
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=

Diff for: internal/xterminal/doc.go

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Package xterminal provides functions to change termios or console attributes
2+
// and restore them later on. It supports Unix and Windows.
3+
//
4+
// This does the same thing as x/crypto/ssh/terminal on Linux. On Windows, it
5+
// sets the same console modes as the terminal package but also sets
6+
// `ENABLE_VIRTUAL_TERMINAL_INPUT` and `ENABLE_VIRTUAL_TERMINAL_PROCESSING` to
7+
// allow for VT100 sequences in the console. This is important, otherwise Linux
8+
// apps (with colors or ncurses) that are run through SSH or wsep get
9+
// garbled in a Windows console.
10+
//
11+
// More details can be found out about Windows console modes here:
12+
// https://docs.microsoft.com/en-us/windows/console/setconsolemode
13+
package xterminal

Diff for: internal/xterminal/terminal.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// +build !windows
2+
3+
package xterminal
4+
5+
import (
6+
"context"
7+
"os"
8+
"os/signal"
9+
10+
"golang.org/x/crypto/ssh/terminal"
11+
"golang.org/x/sys/unix"
12+
)
13+
14+
// State differs per-platform.
15+
type State struct {
16+
s *terminal.State
17+
}
18+
19+
// MakeRaw sets the terminal to raw.
20+
func MakeRaw(fd uintptr) (*State, error) {
21+
s, err := terminal.MakeRaw(int(fd))
22+
return &State{s}, err
23+
}
24+
25+
// MakeOutputRaw does nothing on non-Windows platforms.
26+
func MakeOutputRaw(fd uintptr) (*State, error) {
27+
return nil, nil
28+
}
29+
30+
// Restore terminal back to original state.
31+
func Restore(fd uintptr, state *State) error {
32+
if state == nil {
33+
return nil
34+
}
35+
36+
return terminal.Restore(int(fd), state.s)
37+
}
38+
39+
// ColorEnabled returns true on Linux if handle is a terminal.
40+
func ColorEnabled(fd uintptr) (bool, error) {
41+
return terminal.IsTerminal(int(fd)), nil
42+
}
43+
44+
type ResizeEvent struct {
45+
Height, Width uint16
46+
}
47+
48+
// ResizeEvents sends terminal resize events
49+
func ResizeEvents(ctx context.Context, termfd uintptr) chan ResizeEvent {
50+
sigs := make(chan os.Signal, 16)
51+
signal.Notify(sigs, unix.SIGWINCH)
52+
53+
events := make(chan ResizeEvent)
54+
55+
go func() {
56+
for ctx.Err() == nil {
57+
width, height, err := terminal.GetSize(int(termfd))
58+
if err != nil {
59+
return
60+
}
61+
events <- ResizeEvent{
62+
Height: uint16(height),
63+
Width: uint16(width),
64+
}
65+
66+
<-sigs
67+
}
68+
}()
69+
70+
return events
71+
}

0 commit comments

Comments
 (0)