Skip to content

Commit 59a91a9

Browse files
committed
Add client/server exec test
- move proto docs to internal
1 parent f81b7d0 commit 59a91a9

File tree

4 files changed

+169
-101
lines changed

4 files changed

+169
-101
lines changed

README.md

Lines changed: 9 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ with WebSocket so it may be used directly by a browser frontend.
1010
### Client
1111

1212
```golang
13-
conn, _, err := websocket.Dial(ctx, "ws://remote.exec.addr", nil)
13+
ws, _, err := websocket.Dial(ctx, "ws://remote.exec.addr", nil)
1414
if err != nil {
1515
// handle error
1616
}
1717
defer conn.Close(websocket.StatusAbnormalClosure, "terminate process")
1818

19-
executor := wsep.RemoteExecer(conn)
19+
executor := wsep.RemoteExecer(ws)
2020
process, err := executor.Start(ctx, proto.Command{
2121
Command: "cat",
2222
Args: []string{"go.mod"},
@@ -41,18 +41,18 @@ conn.Close(websocket.StatusNormalClosure, "normal closure")
4141
### Server
4242

4343
```golang
44-
func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
45-
ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{InsecureSkipVerify: true})
44+
func (s server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
45+
ws, err := websocket.Accept(w, r, nil)
4646
if err != nil {
4747
w.WriteHeader(http.StatusInternalServerError)
4848
return
49-
}
50-
defer ws.Close()
51-
49+
}
5250
err = wsep.Serve(r.Context(), ws, wsep.LocalExecer{})
5351
if err != nil {
54-
// handle error
52+
ws.Close(websocket.StatusAbnormalClosure, "failed to serve execer")
53+
return
5554
}
55+
ws.Close(websocket.StatusNormalClosure, "normal closure")
5656
}
5757
```
5858

@@ -86,91 +86,4 @@ streams at
8686
15.34 real 2.16 user 1.54 sys
8787
```
8888

89-
The same command over wush stdout (which is high performance) streams at 17MB/s. This leads me to believe
90-
there's a large gap we can close.
91-
92-
## Protocol
93-
94-
Each Message is represented as a single WebSocket message. A newline character separates a JSON header from the binary body.
95-
96-
Some messages may omit the body.
97-
98-
The overhead of the additional frame is 2 to 6 bytes. In high throughput cases, messages contain ~32KB of data,
99-
so this overhead is negligible.
100-
101-
### Client Messages
102-
103-
#### Start
104-
105-
This must be the first Client message.
106-
107-
```json
108-
{
109-
"type": "start",
110-
"command": {
111-
"command": "cat",
112-
"args": ["/dev/urandom"],
113-
"tty": false
114-
}
115-
}
116-
```
117-
118-
#### Stdin
119-
120-
```json
121-
{ "type": "stdin" }
122-
```
123-
124-
and a body follows after a newline character.
125-
126-
#### Resize
127-
128-
```json
129-
{ "type": "resize", "cols": 80, "rows": 80 }
130-
```
131-
132-
Only valid on tty messages.
133-
134-
#### CloseStdin
135-
136-
No more Stdin messages may be sent after this.
137-
138-
```json
139-
{ "type": "close_stdin" }
140-
```
141-
142-
### Server Messages
143-
144-
#### Pid
145-
146-
This is sent immediately after the command starts.
147-
148-
```json
149-
{ "type": "pid", "pid": 0 }
150-
```
151-
152-
#### Stdout
153-
154-
```json
155-
{ "type": "stdout" }
156-
```
157-
158-
and a body follows after a newline character.
159-
160-
#### Stderr
161-
162-
```json
163-
{ "type": "stderr" }
164-
```
165-
166-
and a body follows after a newline character.
167-
168-
#### ExitCode
169-
170-
This is the last message sent by the server.
171-
172-
```json
173-
{ "type": "exit_code", "code": 255 }
174-
```
175-
176-
A normal closure follows.
89+
The same command over wush stdout (which is high performance) streams at 17MB/s.

client_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,23 @@ package wsep
22

33
import (
44
"bytes"
5+
"context"
56
"io"
67
"io/ioutil"
78
"net"
9+
"net/http"
10+
"net/http/httptest"
11+
"os"
12+
"strings"
13+
"sync"
814
"testing"
15+
"time"
916

1017
"cdr.dev/slog/sloggers/slogtest/assert"
1118
"cdr.dev/wsep/internal/proto"
1219
"github.com/google/go-cmp/cmp"
20+
"go.coder.com/flog"
21+
"nhooyr.io/websocket"
1322
)
1423

1524
func TestRemoteStdin(t *testing.T) {
@@ -41,3 +50,63 @@ func TestRemoteStdin(t *testing.T) {
4150
assert.Equal(t, "stdin header", header, []byte(`{"type":"stdin"}`), bytecmp)
4251
}
4352
}
53+
54+
func TestExec(t *testing.T) {
55+
mockServerHandler := func(w http.ResponseWriter, r *http.Request) {
56+
ws, err := websocket.Accept(w, r, nil)
57+
if err != nil {
58+
w.WriteHeader(http.StatusInternalServerError)
59+
return
60+
}
61+
err = Serve(r.Context(), ws, LocalExecer{})
62+
if err != nil {
63+
flog.Error("failed to serve execer: %v", err)
64+
ws.Close(websocket.StatusAbnormalClosure, "failed to serve execer")
65+
return
66+
}
67+
ws.Close(websocket.StatusNormalClosure, "normal closure")
68+
}
69+
70+
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
71+
defer cancel()
72+
73+
server := httptest.NewServer(http.HandlerFunc(mockServerHandler))
74+
defer server.Close()
75+
76+
ws, _, err := websocket.Dial(ctx, "ws"+strings.TrimPrefix(server.URL, "http"), nil)
77+
assert.Success(t, "dial websocket server", err)
78+
defer ws.Close(websocket.StatusAbnormalClosure, "abnormal closure")
79+
80+
execer := RemoteExecer(ws)
81+
process, err := execer.Start(ctx, Command{
82+
Command: "pwd",
83+
})
84+
assert.Success(t, "start process", err)
85+
86+
assert.Equal(t, "pid", false, process.Pid() == 0)
87+
88+
var wg sync.WaitGroup
89+
go func() {
90+
wg.Add(1)
91+
defer wg.Done()
92+
93+
stdout, err := ioutil.ReadAll(process.Stdout())
94+
assert.Success(t, "read stdout", err)
95+
wd, err := os.Getwd()
96+
assert.Success(t, "get real working dir", err)
97+
98+
assert.Equal(t, "stdout", wd, strings.TrimSuffix(string(stdout), "\n"))
99+
}()
100+
go func() {
101+
wg.Add(1)
102+
defer wg.Done()
103+
stderr, err := ioutil.ReadAll(process.Stderr())
104+
assert.Success(t, "read stderr", err)
105+
assert.Equal(t, "len stderr", 0, len(stderr))
106+
}()
107+
108+
err = process.Wait()
109+
assert.Success(t, "wait", err)
110+
111+
wg.Wait()
112+
}

dev/server/main.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,23 @@ import (
1111
func main() {
1212
server := http.Server{
1313
Addr: ":8080",
14-
Handler: server{},
14+
Handler: http.HandlerFunc(serve),
1515
}
1616
err := server.ListenAndServe()
1717
flog.Fatal("failed to listen: %v", err)
1818
}
1919

20-
type server struct{}
21-
22-
func (s server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
20+
func serve(w http.ResponseWriter, r *http.Request) {
2321
ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{InsecureSkipVerify: true})
2422
if err != nil {
2523
w.WriteHeader(http.StatusInternalServerError)
2624
return
2725
}
2826
err = wsep.Serve(r.Context(), ws, wsep.LocalExecer{})
2927
if err != nil {
30-
flog.Fatal("failed to serve local execer: %v", err)
28+
flog.Error("failed to serve execer: %v", err)
29+
ws.Close(websocket.StatusAbnormalClosure, "failed to serve execer")
30+
return
3131
}
32+
ws.Close(websocket.StatusNormalClosure, "normal closure")
3233
}

internal/proto/README.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Protocol
2+
3+
Each Message is represented as a single WebSocket message. A newline character separates a JSON header from the binary body.
4+
5+
Some messages may omit the body.
6+
7+
The overhead of the additional frame is 2 to 6 bytes. In high throughput cases, messages contain ~32KB of data,
8+
so this overhead is negligible.
9+
10+
### Client Messages
11+
12+
#### Start
13+
14+
This must be the first Client message.
15+
16+
```json
17+
{
18+
"type": "start",
19+
"command": {
20+
"command": "cat",
21+
"args": ["/dev/urandom"],
22+
"tty": false
23+
}
24+
}
25+
```
26+
27+
#### Stdin
28+
29+
```json
30+
{ "type": "stdin" }
31+
```
32+
33+
and a body follows after a newline character.
34+
35+
#### Resize
36+
37+
```json
38+
{ "type": "resize", "cols": 80, "rows": 80 }
39+
```
40+
41+
Only valid on tty messages.
42+
43+
#### CloseStdin
44+
45+
No more Stdin messages may be sent after this.
46+
47+
```json
48+
{ "type": "close_stdin" }
49+
```
50+
51+
### Server Messages
52+
53+
#### Pid
54+
55+
This is sent immediately after the command starts.
56+
57+
```json
58+
{ "type": "pid", "pid": 0 }
59+
```
60+
61+
#### Stdout
62+
63+
```json
64+
{ "type": "stdout" }
65+
```
66+
67+
and a body follows after a newline character.
68+
69+
#### Stderr
70+
71+
```json
72+
{ "type": "stderr" }
73+
```
74+
75+
and a body follows after a newline character.
76+
77+
#### ExitCode
78+
79+
This is the last message sent by the server.
80+
81+
```json
82+
{ "type": "exit_code", "code": 255 }
83+
```
84+
85+
A normal closure follows.

0 commit comments

Comments
 (0)