Skip to content

Commit 9120171

Browse files
authored
Add reconnecting ptys (#23)
* Add pty reconnections * Add reconnection to dev client * Add reconnect tests * Update go get to go install This is failing CI.
1 parent 1c0e51c commit 9120171

File tree

11 files changed

+376
-55
lines changed

11 files changed

+376
-55
lines changed

Diff for: README.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<strong style="font-size: 1.5em; text-decoration: underline;">w</strong>eb <strong style="font-size: 1.5em;text-decoration: underline;">s</strong>ocket command <strong style="font-size: 1.5em;text-decoration: underline;">e</strong>xecution <strong style="font-size: 1.5em;text-decoration: underline;">p</strong>rotocol. It can be thought of as SSH without encryption.
55

66
It's useful in cases where you want to provide a command exec interface into a remote environment. It's implemented
7-
with WebSocket so it may be used directly by a browser frontend. Its symmetric design satisfies
7+
with WebSocket so it may be used directly by a browser frontend. Its symmetric design satisfies
88
`wsep.Execer` for local and remote execution.
99

1010
## Examples
@@ -54,19 +54,19 @@ go run ./dev/server
5454
Start a client:
5555

5656
```sh
57-
go run ./dev/client tty bash
58-
go run ./dev/client notty ls
57+
go run ./dev/client tty --id 1 -- bash
58+
go run ./dev/client notty -- ls -la
5959
```
6060

6161
### Local performance cost
6262

6363
Local `sh` through a local `wsep` connection
6464

6565
```shell script
66-
$ head -c 100000000 /dev/urandom > /tmp/random; cat /tmp/random | pv | time ./bin/client notty sh -c "cat > /dev/null"
66+
$ head -c 100000000 /dev/urandom > /tmp/random; cat /tmp/random | pv | time ./bin/client notty -- sh -c "cat > /dev/null"
6767

6868
95.4MiB 0:00:00 [ 269MiB/s] [ <=> ]
69-
./bin/client notty sh -c "cat > /dev/null" 0.32s user 0.31s system 31% cpu 2.019 total
69+
./bin/client notty -- sh -c "cat > /dev/null" 0.32s user 0.31s system 31% cpu 2.019 total
7070
```
7171

7272
Local `sh` directly

Diff for: ci/image/Dockerfile

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ FROM golang:1
33
ENV GOFLAGS="-mod=readonly"
44
ENV CI=true
55

6-
RUN go get golang.org/x/tools/cmd/goimports
7-
RUN go get golang.org/x/lint/golint
8-
RUN go get github.com/mattn/goveralls
6+
RUN go install golang.org/x/tools/cmd/goimports@latest
7+
RUN go install golang.org/x/lint/golint@latest
8+
RUN go install github.com/mattn/goveralls@latest

Diff for: client.go

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ func RemoteExecer(conn *websocket.Conn) Execer {
2727

2828
// Command represents an external command to be run
2929
type Command struct {
30+
// ID allows reconnecting commands that have a TTY.
31+
ID string
3032
Command string
3133
Args []string
3234
TTY bool
@@ -39,6 +41,7 @@ type Command struct {
3941

4042
func (r remoteExec) Start(ctx context.Context, c Command) (Process, error) {
4143
header := proto.ClientStartHeader{
44+
ID: c.ID,
4245
Command: mapToProtoCmd(c),
4346
Type: proto.TypeStart,
4447
}

Diff for: client_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@ func TestRemoteStdin(t *testing.T) {
4949
}
5050
}
5151

52-
func mockConn(ctx context.Context, t *testing.T) (*websocket.Conn, *httptest.Server) {
52+
func mockConn(ctx context.Context, t *testing.T, options *Options) (*websocket.Conn, *httptest.Server) {
5353
mockServerHandler := func(w http.ResponseWriter, r *http.Request) {
5454
ws, err := websocket.Accept(w, r, nil)
5555
if err != nil {
5656
w.WriteHeader(http.StatusInternalServerError)
5757
return
5858
}
59-
err = Serve(r.Context(), ws, LocalExecer{})
59+
err = Serve(r.Context(), ws, LocalExecer{}, options)
6060
if err != nil {
6161
t.Errorf("failed to serve execer: %v", err)
6262
ws.Close(websocket.StatusAbnormalClosure, "failed to serve execer")
@@ -77,7 +77,7 @@ func TestRemoteExec(t *testing.T) {
7777
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
7878
defer cancel()
7979

80-
ws, server := mockConn(ctx, t)
80+
ws, server := mockConn(ctx, t, nil)
8181
defer server.Close()
8282

8383
execer := RemoteExecer(ws)
@@ -89,7 +89,7 @@ func TestRemoteExecFail(t *testing.T) {
8989
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
9090
defer cancel()
9191

92-
ws, server := mockConn(ctx, t)
92+
ws, server := mockConn(ctx, t, nil)
9393
defer server.Close()
9494

9595
execer := RemoteExecer(ws)
@@ -123,7 +123,7 @@ func TestStderrVsStdout(t *testing.T) {
123123
stderr bytes.Buffer
124124
)
125125

126-
ws, server := mockConn(ctx, t)
126+
ws, server := mockConn(ctx, t, nil)
127127
defer server.Close()
128128

129129
execer := RemoteExecer(ws)

Diff for: dev/client/main.go

+15-11
Original file line numberDiff line numberDiff line change
@@ -23,35 +23,38 @@ type notty struct {
2323
}
2424

2525
func (c *notty) Run(fl *pflag.FlagSet) {
26-
do(fl, false)
26+
do(fl, false, "")
2727
}
2828

2929
func (c *notty) Spec() cli.CommandSpec {
3030
return cli.CommandSpec{
31-
Name: "notty",
32-
Usage: "[flags]",
33-
Desc: `Run a command without tty enabled.`,
34-
RawArgs: true,
31+
Name: "notty",
32+
Usage: "[flags]",
33+
Desc: `Run a command without tty enabled.`,
3534
}
3635
}
3736

3837
type tty struct {
38+
id string
3939
}
4040

4141
func (c *tty) Run(fl *pflag.FlagSet) {
42-
do(fl, true)
42+
do(fl, true, c.id)
4343
}
4444

4545
func (c *tty) Spec() cli.CommandSpec {
4646
return cli.CommandSpec{
47-
Name: "tty",
48-
Usage: "[flags]",
49-
Desc: `Run a command with tty enabled.`,
50-
RawArgs: true,
47+
Name: "tty",
48+
Usage: "[id] [flags]",
49+
Desc: `Run a command with tty enabled. Use the same ID to reconnect.`,
5150
}
5251
}
5352

54-
func do(fl *pflag.FlagSet, tty bool) {
53+
func (c *tty) RegisterFlags(fl *pflag.FlagSet) {
54+
fl.StringVar(&c.id, "id", "", "sets id for reconnection")
55+
}
56+
57+
func do(fl *pflag.FlagSet, tty bool, id string) {
5558
ctx, cancel := context.WithCancel(context.Background())
5659
defer cancel()
5760

@@ -71,6 +74,7 @@ func do(fl *pflag.FlagSet, tty bool) {
7174
args = fl.Args()[1:]
7275
}
7376
process, err := executor.Start(ctx, wsep.Command{
77+
ID: id,
7478
Command: fl.Arg(0),
7579
Args: args,
7680
TTY: tty,

Diff for: dev/server/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func serve(w http.ResponseWriter, r *http.Request) {
2323
w.WriteHeader(http.StatusInternalServerError)
2424
return
2525
}
26-
err = wsep.Serve(r.Context(), ws, wsep.LocalExecer{})
26+
err = wsep.Serve(r.Context(), ws, wsep.LocalExecer{}, nil)
2727
if err != nil {
2828
flog.Error("failed to serve execer: %v", err)
2929
ws.Close(websocket.StatusAbnormalClosure, "failed to serve execer")

Diff for: go.mod

+2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ go 1.14
44

55
require (
66
cdr.dev/slog v1.3.0
7+
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
78
github.com/creack/pty v1.1.11
89
github.com/google/go-cmp v0.4.0
10+
github.com/google/uuid v1.3.0
911
github.com/spf13/pflag v1.0.5
1012
go.coder.com/cli v0.4.0
1113
go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512

Diff for: go.sum

+4
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUS
3030
github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA=
3131
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
3232
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
33+
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs=
34+
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
3335
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
3436
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
3537
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
@@ -92,6 +94,8 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
9294
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
9395
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
9496
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
97+
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
98+
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
9599
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
96100
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
97101
github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=

Diff for: internal/proto/clientmsg.go

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type ClientResizeHeader struct {
1818
// ClientStartHeader specifies a request to start command
1919
type ClientStartHeader struct {
2020
Type string `json:"type"`
21+
ID string `json:"id"`
2122
Command Command `json:"command"`
2223
}
2324

0 commit comments

Comments
 (0)