diff --git a/go.mod b/go.mod index 14bb2a5..069904d 100644 --- a/go.mod +++ b/go.mod @@ -9,11 +9,13 @@ require ( github.com/google/go-cmp v0.4.0 github.com/gorilla/handlers v1.4.2 github.com/gorilla/mux v1.7.4 + github.com/gorilla/websocket v1.4.2 github.com/spf13/cobra v0.0.5 github.com/stretchr/testify v1.4.0 go.uber.org/goleak v1.0.0 golang.org/x/net v0.0.0-20190628185345-da137c7871d7 golang.org/x/perf v0.0.0-20191209155426-36b577b0eb03 + golang.org/x/sync v0.0.0-20190423024810-112230192c58 golang.org/x/tools v0.0.0-20200125223703-d33eef8e6825 gopkg.in/yaml.v2 v2.2.8 ) diff --git a/go.sum b/go.sum index c497164..e7f6555 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YAR github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= diff --git a/wscat/README.md b/wscat/README.md new file mode 100644 index 0000000..253dea0 --- /dev/null +++ b/wscat/README.md @@ -0,0 +1,33 @@ +## wscat + +wscat - примитивный аналог npm пакета [wscat](https://www.npmjs.com/package/wscat). + +Websocket - это двусторониий канал поверх tcp. wscat - это websocket клиент. + +wscat принимает на вход единственный аргумент `-addr` - адрес websocket сервера. +После подключения программа начинает читать с stdin'а и отправлять пользовательские строки на сервер, +печатая все сообщения от сервера в stdout. + +Клиент должен обрабатывать SIGINT и SIGTERM и плавно завершаться с кодом 0 дожидаясь горутин. +Для этого может пригодиться [context](https://golang.org/pkg/context/). + +Обратите внимание на то, что exit code `go run` - это не exit code исполняемого файла. + +## Пример + +Публичный echo сервер: +``` +✗ $GOPATH/bin/wscat -addr ws://echo.websocket.org +abc +abcdef +def^C2020/04/04 05:01:32 received signal interrupt +``` +``` +✗ echo $? +0 +``` + +## Ссылки + +1. websocket: https://en.wikipedia.org/wiki/WebSocket +2. signal shutdown: https://p.go.manytask.org/06-http/lecture.slide#20 diff --git a/wscat/main.go b/wscat/main.go new file mode 100644 index 0000000..cab7d3a --- /dev/null +++ b/wscat/main.go @@ -0,0 +1,7 @@ +// +build !solution + +package main + +func main() { + +} diff --git a/wscat/main_test.go b/wscat/main_test.go new file mode 100644 index 0000000..cf15c20 --- /dev/null +++ b/wscat/main_test.go @@ -0,0 +1,140 @@ +package main + +import ( + "bytes" + "context" + "io" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "strings" + "syscall" + "testing" + "time" + + "github.com/gorilla/websocket" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "gitlab.com/slon/shad-go/tools/testtool" +) + +const importPath = "gitlab.com/slon/shad-go/wscat" + +var binCache testtool.BinCache + +func TestMain(m *testing.M) { + os.Exit(func() int { + var teardown testtool.CloseFunc + binCache, teardown = testtool.NewBinCache() + defer teardown() + + return m.Run() + }()) +} + +type Conn struct { + in io.WriteCloser + out *bytes.Buffer +} + +func startCommand(t *testing.T, addr string) (conn *Conn, stop func()) { + t.Helper() + + binary, err := binCache.GetBinary(importPath) + require.NoError(t, err) + + cmd := exec.Command(binary, "-addr", addr) + cmd.Stderr = os.Stderr + + out := &bytes.Buffer{} + cmd.Stdout = out + + stdin, err := cmd.StdinPipe() + require.NoError(t, err) + + require.NoError(t, cmd.Start()) + + conn = &Conn{ + in: stdin, + out: out, + } + + done := make(chan struct{}) + go func() { + assert.NoError(t, cmd.Wait()) + close(done) + }() + + stop = func() { + defer func() { + _ = cmd.Process.Kill() + <-done + }() + + // try killing softly + _ = cmd.Process.Signal(syscall.SIGTERM) + + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) + defer cancel() + + select { + case <-done: + case <-ctx.Done(): + t.Fatalf("client shutdown timed out") + } + } + + return +} + +func TestWScat(t *testing.T) { + upgrader := websocket.Upgrader{} + + var received, sent [][]byte + h := func(w http.ResponseWriter, r *http.Request) { + c, err := upgrader.Upgrade(w, r, nil) + require.NoError(t, err) + defer func() { _ = c.Close() }() + + for { + _, message, err := c.ReadMessage() + if err != nil { + t.Logf("error reading message: %s", err) + break + } + received = append(received, message) + + resp := []byte(testtool.RandomName()) + err = c.WriteMessage(websocket.TextMessage, resp) + require.NoError(t, err) + sent = append(sent, resp) + } + } + + s := httptest.NewServer(http.HandlerFunc(h)) + defer s.Close() + + wsURL := strings.Replace(s.URL, "http", "ws", 1) + t.Logf("starting ws server %s", wsURL) + + conn, stop := startCommand(t, wsURL) + defer stop() + + var in [][]byte + for i := 0; i < 100; i++ { + msg := []byte(testtool.RandomName()) + in = append(in, msg) + + _, err := conn.in.Write(append(msg, '\n')) + require.NoError(t, err) + } + + // give client time to make a request + time.Sleep(time.Millisecond * 100) + stop() + + require.Equal(t, bytes.Join(in, nil), bytes.Join(received, nil)) + require.Equal(t, bytes.Join(sent, nil), conn.out.Bytes()) +}