diff --git a/cli/go.mod b/cli/go.mod index 3e7656a..6bd6b99 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -5,4 +5,6 @@ go 1.16 require ( github.com/alecthomas/kong v0.2.17 // indirect github.com/gorilla/websocket v1.4.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/term v1.1.0 // indirect ) diff --git a/cli/go.sum b/cli/go.sum index 58e9275..2362830 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -6,9 +6,13 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= +github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 h1:W0lCpv29Hv0UaM1LXb9QlBHLNP8UFfcKjblhVCWftOM= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cli/src/main.go b/cli/src/main.go index 479db5d..7172d00 100644 --- a/cli/src/main.go +++ b/cli/src/main.go @@ -8,10 +8,15 @@ import ( "fmt" "log" "net/url" + "os" + "os/signal" "path" + "syscall" "github.com/alecthomas/kong" "github.com/gorilla/websocket" + "github.com/pkg/errors" + "github.com/pkg/term" ) var cli struct { @@ -34,6 +39,18 @@ type terminalOutput struct { Output string `json:"output"` } +type serviceFailed struct { + message + Service string `json:"service"` + Error string `json:"error"` + Code int `json:"code"` +} + +type errorExit struct { + error + status int +} + func run() error { apiUrl, err := url.Parse(cli.Host) if err != nil { @@ -56,33 +73,89 @@ func run() error { return err } defer conn.Close() - done := make(chan struct{}) + tty, err := term.Open("/dev/tty") + if err != nil { + return errors.Wrap(err, "failed to open stdin tty") + } + if err := tty.SetRaw(); err != nil { + return errors.Wrap(err, "failed to set raw mode") + } + defer tty.Restore() + sigint := make(chan os.Signal, 1) + sigterm := make(chan os.Signal, 1) + signal.Notify(sigint, syscall.SIGINT) + signal.Notify(sigterm, syscall.SIGTERM) + done1 := make(chan error) go func() { - defer close(done) + defer close(done1) for { _, rawMsg, err := conn.ReadMessage() if err != nil { - log.Println("failed to read websocket message:", err) + done1 <- errors.Wrap(err, "failed to read websocket message") return } var genericMsg message if err := json.Unmarshal(rawMsg, &genericMsg); err != nil { - log.Println("failed to parse websocket message:", err) + done1 <- errors.Wrap(err, "failed to parse websocket message") + return } switch genericMsg.Event { case "terminalOutput": var msg terminalOutput if err := json.Unmarshal(rawMsg, &msg); err != nil { - log.Println("failed to parse websocket message:", err) + done1 <- errors.Wrap(err, "failed to parse websocket message") + return } fmt.Print(msg.Output) + case "serviceFailed": + var msg serviceFailed + if err := json.Unmarshal(rawMsg, &msg); err != nil { + done1 <- errors.Wrap(err, "failed to parse websocket message") + return + } + done1 <- errorExit{nil, msg.Code} + return } } }() + input := make(chan []byte) + done2 := make(chan error) + go func() { + defer close(done2) + for { + buf := make([]byte, 1024) + n, err := os.Stdin.Read(buf) + if err != nil { + done2 <- errors.Wrap(err, "failed to read from stdin") + return + } + input <- buf[:n] + } + }() for { select { - case <-done: - return nil + case err := <-done1: + return err + case err := <-done2: + return err + case <-sigint: + return errorExit{nil, int(syscall.SIGINT) + 128} + case <-sigterm: + return errorExit{nil, int(syscall.SIGTERM) + 128} + case data := <-input: + msg := terminalInput{ + message: message{ + Event: "terminalInput", + }, + Input: string(data), + } + rawMsg, err := json.Marshal(&msg) + if err != nil { + return errors.Wrap(err, "failed to create websocket message") + } + if err := conn.WriteMessage(websocket.TextMessage, rawMsg); err != nil { + return errors.Wrap(err, "failed to send websocket message") + } } } return nil @@ -90,7 +163,17 @@ func run() error { func main() { kong.Parse(&cli) + exitStatus := 0 if err := run(); err != nil { - log.Fatalln(err) + if typedErr, ok := err.(errorExit); ok { + err = typedErr.error + exitStatus = typedErr.status + } else { + exitStatus = 1 + } + if err != nil { + log.Println(err) + } } + os.Exit(exitStatus) }