From 00b5b83dcff4904aeb18760caa193fa3393241e0 Mon Sep 17 00:00:00 2001 From: Sebastien Douheret Date: Fri, 9 Mar 2018 17:33:18 +0100 Subject: [PATCH] Fixed terminal output (support escape and control characters) Signed-off-by: Sebastien Douheret --- .vscode/settings.json | 6 ++- cmd-exec.go | 8 ++-- cmd-sdks.go | 14 +++--- cmd-target.go | 120 +++++++++++++++++++++++++++++++++----------------- glide.yaml | 7 ++- iosocket-client.go | 80 +++++++++++++++++++++++++++++++++ main.go | 27 ++++++------ 7 files changed, 195 insertions(+), 67 deletions(-) create mode 100644 iosocket-client.go diff --git a/.vscode/settings.json b/.vscode/settings.json index fc15f8b..9f7f5b2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -39,6 +39,10 @@ "gerrit", "EVTSDK", "tgts", - "sigs" + "sigs", + "rdfs", + "goselect", + "creack", + "Sillyf" ] } diff --git a/cmd-exec.go b/cmd-exec.go index 3f1ee97..c988f95 100644 --- a/cmd-exec.go +++ b/cmd-exec.go @@ -78,7 +78,7 @@ func execCmd(ctx *cli.Context) error { } exitChan := make(chan exitResult, 1) - IOsk.On("disconnection", func(err error) { + IOSkClient.On("disconnection", func(err error) { Log.Debugf("WS disconnection event with err: %v\n", err) exitChan <- exitResult{err, 2} }) @@ -96,15 +96,15 @@ func execCmd(ctx *cli.Context) error { } } - IOsk.On(xaapiv1.ExecOutEvent, func(ev xaapiv1.ExecOutMsg) { + IOSkClient.On(xaapiv1.ExecOutEvent, func(ev xaapiv1.ExecOutMsg) { outFunc(ev.Timestamp, ev.Stdout, ev.Stderr) }) - IOsk.On(xaapiv1.ExecExitEvent, func(ev xaapiv1.ExecExitMsg) { + IOSkClient.On(xaapiv1.ExecExitEvent, func(ev xaapiv1.ExecExitMsg) { exitChan <- exitResult{ev.Error, ev.Code} }) - IOsk.On(xaapiv1.EVTProjectChange, func(ev xaapiv1.EventMsg) { + IOSkClient.On(xaapiv1.EVTProjectChange, func(ev xaapiv1.EventMsg) { prj, _ := ev.DecodeProjectConfig() Log.Infof("Event %v (%v): %v", ev.Type, ev.Time, prj) }) diff --git a/cmd-sdks.go b/cmd-sdks.go index 3fc596f..eeebffa 100644 --- a/cmd-sdks.go +++ b/cmd-sdks.go @@ -242,7 +242,7 @@ func sdksInstall(ctx *cli.Context) error { } exitChan := make(chan exitResult, 1) - IOsk.On("disconnection", func(err error) { + IOSkClient.On("disconnection", func(err error) { Log.Debugf("WS disconnection event with err: %v\n", err) errMsg := "" if err != nil { @@ -251,7 +251,7 @@ func sdksInstall(ctx *cli.Context) error { exitChan <- exitResult{errMsg, 2} }) - IOsk.On(xaapiv1.EVTSDKManagement, func(ev xaapiv1.EventMsg) { + IOSkClient.On(xaapiv1.EVTSDKManagement, func(ev xaapiv1.EventMsg) { sdkEvt, _ := ev.DecodeSDKMgtMsg() if sdkEvt.Action != xaapiv1.SdkMgtActionInstall { @@ -259,11 +259,11 @@ func sdksInstall(ctx *cli.Context) error { return } - if !shortOut && sdkEvt.Stdout != "" { - fmt.Printf("%s", sdkEvt.Stdout) + if !shortOut && len(sdkEvt.Stdout) > 0 { + os.Stdout.Write([]byte(sdkEvt.Stdout)) } - if !shortOut && sdkEvt.Stderr != "" { - fmt.Fprintf(os.Stderr, "%s", sdkEvt.Stderr) + if !shortOut && len(sdkEvt.Stderr) > 0 { + os.Stderr.Write([]byte(sdkEvt.Stderr)) } if sdkEvt.Exited { @@ -271,7 +271,7 @@ func sdksInstall(ctx *cli.Context) error { } }) - IOsk.On(xaapiv1.EVTSDKStateChange, func(ev xaapiv1.EventMsg) { + IOSkClient.On(xaapiv1.EVTSDKStateChange, func(ev xaapiv1.EventMsg) { sdk, _ := ev.DecodeSDKEvent() Log.Debugf("EVTSDKStateChange: %v", sdk) }) diff --git a/cmd-target.go b/cmd-target.go index db97e91..cd96ed4 100644 --- a/cmd-target.go +++ b/cmd-target.go @@ -19,18 +19,17 @@ package main import ( - "bufio" "encoding/json" "fmt" + "io" "os" "sort" "strings" - "syscall" "time" - "github.com/golang/crypto/ssh/terminal" - "gerrit.automotivelinux.org/gerrit/src/xds/xds-agent.git/lib/xaapiv1" + "github.com/creack/goselect" + "github.com/golang/crypto/ssh/terminal" "github.com/urfave/cli" ) @@ -198,6 +197,7 @@ func _displayTargets(tgts []xaapiv1.TargetConfig, verbose bool) { for _, tt := range tgt.Terms { tmNfo += "\t ID:\t" + tt.ID + "\n" tmNfo += "\t Name:\t" + tt.Name + "\n" + tmNfo += "\t Type:\t" + string(tt.Type) + "\n" tmNfo += "\t Status:\t" + tt.Status + "\n" tmNfo += "\t User:\t" + tt.User + "\n" tmNfo += "\t Options:\t" + strings.Join(tt.Options, " ") + "\n" @@ -324,60 +324,88 @@ func terminalOpen(ctx *cli.Context) error { } exitChan := make(chan exitResult, 1) - IOsk.On("disconnection", func(err error) { + IOSkClient.On("disconnection", func(err error) { Log.Debugf("WS disconnection event with err: %v\n", err) exitChan <- exitResult{err, 2} }) - IOsk.On(xaapiv1.TerminalOutEvent, func(ev xaapiv1.TerminalOutMsg) { - if ev.Stdout != "" { - fmt.Printf(ev.Stdout) + IOSkClient.On(xaapiv1.TerminalOutEvent, func(ev xaapiv1.TerminalOutMsg) { + if len(ev.Stdout) > 0 { + os.Stdout.Write(ev.Stdout) } - if ev.Stderr != "" { - fmt.Fprintf(os.Stderr, ev.Stderr) + if len(ev.Stderr) > 0 { + os.Stderr.Write(ev.Stdout) } }) - IOsk.On(xaapiv1.TerminalExitEvent, func(ev xaapiv1.TerminalExitMsg) { + IOSkClient.On(xaapiv1.TerminalExitEvent, func(ev xaapiv1.TerminalExitMsg) { exitChan <- exitResult{ev.Error, ev.Code} }) - /* FIXME - use raw mode to support escape keys, arrows keys, control char... - // import "github.com/golang/crypto/ssh/terminal" - + // Setup terminal (raw mode to handle escape and control keys) + if !terminal.IsTerminal(0) || !terminal.IsTerminal(1) { + return cli.NewExitError("stdin/stdout should be terminal", 1) + } oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) - if err == nil { - defer terminal.Restore(int(os.Stdin.Fd()), oldState) + if err != nil { + return cli.NewExitError(err.Error(), 1) } - */ + defer terminal.Restore(int(os.Stdin.Fd()), oldState) // Send stdin though WS go func() { - paranoia := 600 - reader := bufio.NewReader(os.Stdin) + type exposeFd interface { + Fd() uintptr + } + buff := make([]byte, 128) + rdfs := &goselect.FDSet{} + reader := io.ReadCloser(os.Stdin) + defer reader.Close() + for { - sc := bufio.NewScanner(reader) - for sc.Scan() { - command := sc.Text() - Log.Debugf("Terminal Send command <%v>", command) - IOsk.Emit(xaapiv1.TerminalInEvent, command+"\n") - } - if sc.Err() != nil { - exitChan <- exitResult{sc.Err(), 3} + rdfs.Zero() + rdfs.Set(reader.(exposeFd).Fd()) + err := goselect.Select(1, rdfs, nil, nil, 50*time.Millisecond) + if err != nil { + terminal.Restore(int(os.Stdin.Fd()), oldState) + exitChan <- exitResult{err, 3} + return } + if rdfs.IsSet(reader.(exposeFd).Fd()) { + size, err := reader.Read(buff) + + if err != nil { + Log.Debugf("Read error %v; err %v", size, err) + if err == io.EOF { + // CTRL-D exited scanner, so send it explicitly + err := IOSkClient.Emit(xaapiv1.TerminalInEvent, "\x04\n") + + if err != nil { + terminal.Restore(int(os.Stdin.Fd()), oldState) + exitChan <- exitResult{err, 4} + return + } + time.Sleep(time.Millisecond * 100) + continue + } else { + terminal.Restore(int(os.Stdin.Fd()), oldState) + exitChan <- exitResult{err, 5} + return + } + } - // CTRL-D exited scanner, so send it explicitly - IOsk.Emit(xaapiv1.TerminalInEvent, "\x04\n") - time.Sleep(time.Millisecond * 100) - - if paranoia--; paranoia <= 0 { - msg := "Abnormal loop detected on stdin" - Log.Errorf("Abnormal loop detected on stdin") - - // Send signal to gently exit terminal session - TerminalSendSignal(tgt, term, syscall.SIGTERM) + if size <= 0 { + continue + } - exitChan <- exitResult{fmt.Errorf(msg), int(syscall.ELOOP)} + data := buff[:size] + LogSillyf("Terminal Send data <%v> (%s)", data, data) + err = IOSkClient.Emit(xaapiv1.TerminalInEvent, data) + if err != nil { + terminal.Restore(int(os.Stdin.Fd()), oldState) + exitChan <- exitResult{err, 6} + return + } } } }() @@ -388,7 +416,7 @@ func terminalOpen(ctx *cli.Context) error { if IsWinResizeSignal(sig) { TerminalResize(tgt, term) } else if IsInterruptSignal(sig) { - IOsk.Emit(xaapiv1.TerminalInEvent, "\x03\n") + IOSkClient.Emit(xaapiv1.TerminalInEvent, "\x03\n") } else { TerminalSendSignal(tgt, term, sig) } @@ -404,6 +432,9 @@ func terminalOpen(ctx *cli.Context) error { return cli.NewExitError(err.Error(), 1) } + // Send init size + TerminalResize(tgt, term) + // Wait exit - blocking select { case res := <-exitChan: @@ -452,7 +483,8 @@ func TerminalResize(tgt *xaapiv1.TargetConfig, term *xaapiv1.TerminalConfig) { if err != nil { Log.Errorf("Error cannot get terminal size: %v", err) } - Log.Debugf("Terminal resizing rows %v, cols %v", row, col) + + LogSillyf("Terminal resizing rows %v, cols %v", row, col) sz := xaapiv1.TerminalResizeArgs{Rows: uint16(row), Cols: uint16(col)} url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID + "/resize") if err := HTTPCli.Post(url, &sz, nil); err != nil { @@ -514,6 +546,14 @@ func GetTargetAndTerminalIDs(ctx *cli.Context, useFirstFree bool) (*xaapiv1.Targ for _, tt := range tgts { if compareID(tt.ID, idArg) { + if useFirstFree { + for _, ttm := range tt.Terms { + if ttm.Type == xaapiv1.TypeTermSSH && + (ttm.Status == xaapiv1.StatusTermEnable || ttm.Status == xaapiv1.StatusTermClose) { + return &tt, &ttm, nil + } + } + } return &tt, nil, nil } } diff --git a/glide.yaml b/glide.yaml index a0e7826..faa57f2 100644 --- a/glide.yaml +++ b/glide.yaml @@ -16,7 +16,7 @@ import: subpackages: - lib/xaapiv1 - package: gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git - version: ~0.2.0 + version: ~0.3.0 subpackages: - golib/common - package: github.com/joho/godotenv @@ -25,4 +25,7 @@ import: - cmd/godotenv - package: github.com/franciscocpg/reflectme version: ^0.1.9 -- package: github.com/golang/crypto/ssh/terminal +- package: github.com/golang/crypto + subpackages: + - ssh/terminal +- package: github.com/creack/goselect diff --git a/iosocket-client.go b/iosocket-client.go new file mode 100644 index 0000000..9115b10 --- /dev/null +++ b/iosocket-client.go @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2018 "IoT.bzh" + * Author Sebastien Douheret + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package main + +import ( + "fmt" + "sync" + + socketio_client "github.com/sebd71/go-socket.io-client" +) + +// IOSockClient . +type IOSockClient struct { + URL string + Conn *socketio_client.Client + Options *socketio_client.Options + EmitMutex *sync.Mutex + Connected bool + EscapeKeys []byte +} + +// NewIoSocketClient Create a new IOSockClient +func NewIoSocketClient(url, clientID string) (*IOSockClient, error) { + + var err error + + sCli := &IOSockClient{ + URL: url, + EmitMutex: &sync.Mutex{}, + Options: &socketio_client.Options{ + Transport: "websocket", + Header: make(map[string][]string), + }, + } + sCli.Options.Header["XDS-AGENT-SID"] = []string{clientID} + + sCli.Conn, err = socketio_client.NewClient(url, sCli.Options) + if err != nil { + return nil, fmt.Errorf("IO.socket connection error: " + err.Error()) + } + + sCli.Conn.On("connection", func() { + sCli.Connected = true + }) + + sCli.Conn.On("disconnection", func(err error) { + Log.Debugf("WS disconnection event with err: %v\n", err) + sCli.Connected = false + }) + + return sCli, nil +} + +// On record a callback on a specific message +func (c *IOSockClient) On(message string, f interface{}) (err error) { + return c.Conn.On(message, f) +} + +// Emit send a message +func (c *IOSockClient) Emit(message string, args ...interface{}) (err error) { + c.EmitMutex.Lock() + defer c.EmitMutex.Unlock() + return c.Conn.Emit(message, args...) +} diff --git a/main.go b/main.go index a6f8104..2dfd056 100644 --- a/main.go +++ b/main.go @@ -33,7 +33,6 @@ import ( "github.com/Sirupsen/logrus" "github.com/joho/godotenv" - socketio_client "github.com/sebd71/go-socket.io-client" "github.com/urfave/cli" ) @@ -71,8 +70,8 @@ var EnvConfFileMap map[string]string // HTTPCli Global variable that hold HTTP Client var HTTPCli *common.HTTPClient -// IOsk Global variable that hold SocketIo client -var IOsk *socketio_client.Client +// IOSkClient Global variable that hold SocketIo client +var IOSkClient *IOSockClient // exitError exists this program with the specified error func exitError(code int, f string, a ...interface{}) { @@ -96,6 +95,14 @@ func earlyDisplay() { earlyDebug = []string{} } +// LogSillyf Logging helper used for silly logging (printed on log.debug) +func LogSillyf(format string, args ...interface{}) { + sillyVal, sillyLog := os.LookupEnv("XDS_LOG_SILLY") + if sillyLog && sillyVal == "1" { + Log.Debugf("SILLY: "+format, args...) + } +} + // main func main() { @@ -382,23 +389,17 @@ func XdsConnInit(ctx *cli.Context) error { // Create io Websocket client Log.Debugln("Connecting IO.socket client on ", agentURL) - opts := &socketio_client.Options{ - Transport: "websocket", - Header: make(map[string][]string), - } - opts.Header["XDS-AGENT-SID"] = []string{HTTPCli.GetClientID()} - - IOsk, err = socketio_client.NewClient(agentURL, opts) + IOSkClient, err = NewIoSocketClient(agentURL, HTTPCli.GetClientID()) if err != nil { - return cli.NewExitError("IO.socket connection error: "+err.Error(), 1) + return cli.NewExitError(err.Error(), 1) } - IOsk.On("error", func(err error) { + IOSkClient.On("error", func(err error) { fmt.Println("ERROR Websocket: ", err.Error()) }) ctx.App.Metadata["httpCli"] = HTTPCli - ctx.App.Metadata["ioskCli"] = IOsk + ctx.App.Metadata["ioskCli"] = IOSkClient // Display version in logs (debug helpers) ver := xaapiv1.XDSVersion{} -- 2.16.6