Fixed terminal output (support escape and control characters)
authorSebastien Douheret <sebastien.douheret@iot.bzh>
Fri, 9 Mar 2018 16:33:18 +0000 (17:33 +0100)
committerSebastien Douheret <sebastien.douheret@iot.bzh>
Fri, 9 Mar 2018 16:33:18 +0000 (17:33 +0100)
Signed-off-by: Sebastien Douheret <sebastien.douheret@iot.bzh>
.vscode/settings.json
cmd-exec.go
cmd-sdks.go
cmd-target.go
glide.yaml
iosocket-client.go [new file with mode: 0644]
main.go

index fc15f8b..9f7f5b2 100644 (file)
         "gerrit",
         "EVTSDK",
         "tgts",
-        "sigs"
+        "sigs",
+        "rdfs",
+        "goselect",
+        "creack",
+        "Sillyf"
     ]
 }
index 3f1ee97..c988f95 100644 (file)
@@ -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)
        })
index 3fc596f..eeebffa 100644 (file)
@@ -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)
        })
index db97e91..cd96ed4 100644 (file)
 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
                }
        }
index a0e7826..faa57f2 100644 (file)
@@ -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 (file)
index 0000000..9115b10
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2018 "IoT.bzh"
+ * Author Sebastien Douheret <sebastien@iot.bzh>
+ *
+ * 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 (file)
--- 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{}