Skip to content

Commit

Permalink
Add reconnecting ptys (#23)
Browse files Browse the repository at this point in the history
* Add pty reconnections

* Add reconnection to dev client

* Add reconnect tests

* Update go get to go install

This is failing CI.
  • Loading branch information
code-asher authored Apr 29, 2022
1 parent 1c0e51c commit 9120171
Show file tree
Hide file tree
Showing 11 changed files with 376 additions and 55 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<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.

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

## Examples
Expand Down Expand Up @@ -54,19 +54,19 @@ go run ./dev/server
Start a client:

```sh
go run ./dev/client tty bash
go run ./dev/client notty ls
go run ./dev/client tty --id 1 -- bash
go run ./dev/client notty -- ls -la
```

### Local performance cost

Local `sh` through a local `wsep` connection

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

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

Local `sh` directly
Expand Down
6 changes: 3 additions & 3 deletions ci/image/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ FROM golang:1
ENV GOFLAGS="-mod=readonly"
ENV CI=true

RUN go get golang.org/x/tools/cmd/goimports
RUN go get golang.org/x/lint/golint
RUN go get github.com/mattn/goveralls
RUN go install golang.org/x/tools/cmd/goimports@latest
RUN go install golang.org/x/lint/golint@latest
RUN go install github.com/mattn/goveralls@latest
3 changes: 3 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ func RemoteExecer(conn *websocket.Conn) Execer {

// Command represents an external command to be run
type Command struct {
// ID allows reconnecting commands that have a TTY.
ID string
Command string
Args []string
TTY bool
Expand All @@ -39,6 +41,7 @@ type Command struct {

func (r remoteExec) Start(ctx context.Context, c Command) (Process, error) {
header := proto.ClientStartHeader{
ID: c.ID,
Command: mapToProtoCmd(c),
Type: proto.TypeStart,
}
Expand Down
10 changes: 5 additions & 5 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,14 @@ func TestRemoteStdin(t *testing.T) {
}
}

func mockConn(ctx context.Context, t *testing.T) (*websocket.Conn, *httptest.Server) {
func mockConn(ctx context.Context, t *testing.T, options *Options) (*websocket.Conn, *httptest.Server) {
mockServerHandler := func(w http.ResponseWriter, r *http.Request) {
ws, err := websocket.Accept(w, r, nil)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
err = Serve(r.Context(), ws, LocalExecer{})
err = Serve(r.Context(), ws, LocalExecer{}, options)
if err != nil {
t.Errorf("failed to serve execer: %v", err)
ws.Close(websocket.StatusAbnormalClosure, "failed to serve execer")
Expand All @@ -77,7 +77,7 @@ func TestRemoteExec(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

ws, server := mockConn(ctx, t)
ws, server := mockConn(ctx, t, nil)
defer server.Close()

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

ws, server := mockConn(ctx, t)
ws, server := mockConn(ctx, t, nil)
defer server.Close()

execer := RemoteExecer(ws)
Expand Down Expand Up @@ -123,7 +123,7 @@ func TestStderrVsStdout(t *testing.T) {
stderr bytes.Buffer
)

ws, server := mockConn(ctx, t)
ws, server := mockConn(ctx, t, nil)
defer server.Close()

execer := RemoteExecer(ws)
Expand Down
26 changes: 15 additions & 11 deletions dev/client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,35 +23,38 @@ type notty struct {
}

func (c *notty) Run(fl *pflag.FlagSet) {
do(fl, false)
do(fl, false, "")
}

func (c *notty) Spec() cli.CommandSpec {
return cli.CommandSpec{
Name: "notty",
Usage: "[flags]",
Desc: `Run a command without tty enabled.`,
RawArgs: true,
Name: "notty",
Usage: "[flags]",
Desc: `Run a command without tty enabled.`,
}
}

type tty struct {
id string
}

func (c *tty) Run(fl *pflag.FlagSet) {
do(fl, true)
do(fl, true, c.id)
}

func (c *tty) Spec() cli.CommandSpec {
return cli.CommandSpec{
Name: "tty",
Usage: "[flags]",
Desc: `Run a command with tty enabled.`,
RawArgs: true,
Name: "tty",
Usage: "[id] [flags]",
Desc: `Run a command with tty enabled. Use the same ID to reconnect.`,
}
}

func do(fl *pflag.FlagSet, tty bool) {
func (c *tty) RegisterFlags(fl *pflag.FlagSet) {
fl.StringVar(&c.id, "id", "", "sets id for reconnection")
}

func do(fl *pflag.FlagSet, tty bool, id string) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

Expand All @@ -71,6 +74,7 @@ func do(fl *pflag.FlagSet, tty bool) {
args = fl.Args()[1:]
}
process, err := executor.Start(ctx, wsep.Command{
ID: id,
Command: fl.Arg(0),
Args: args,
TTY: tty,
Expand Down
2 changes: 1 addition & 1 deletion dev/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func serve(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
return
}
err = wsep.Serve(r.Context(), ws, wsep.LocalExecer{})
err = wsep.Serve(r.Context(), ws, wsep.LocalExecer{}, nil)
if err != nil {
flog.Error("failed to serve execer: %v", err)
ws.Close(websocket.StatusAbnormalClosure, "failed to serve execer")
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ go 1.14

require (
cdr.dev/slog v1.3.0
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
github.com/creack/pty v1.1.11
github.com/google/go-cmp v0.4.0
github.com/google/uuid v1.3.0
github.com/spf13/pflag v1.0.5
go.coder.com/cli v0.4.0
go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUS
github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs=
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
Expand Down Expand Up @@ -92,6 +94,8 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
Expand Down
1 change: 1 addition & 0 deletions internal/proto/clientmsg.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type ClientResizeHeader struct {
// ClientStartHeader specifies a request to start command
type ClientStartHeader struct {
Type string `json:"type"`
ID string `json:"id"`
Command Command `json:"command"`
}

Expand Down
Loading

0 comments on commit 9120171

Please sign in to comment.