Defined structures for /exec and /signal response.
[src/xds/xds-server.git] / lib / apiv1 / exec.go
index ce0241a..baf431f 100644 (file)
 package apiv1
 
 import (
+       "fmt"
        "net/http"
+       "os"
+       "regexp"
        "strconv"
        "strings"
        "time"
 
-       "fmt"
-
        "github.com/gin-gonic/gin"
        common "github.com/iotbzh/xds-common/golib"
+       "github.com/iotbzh/xds-common/golib/eows"
+       "github.com/kr/pty"
 )
 
-// ExecArgs JSON parameters of /exec command
-type ExecArgs struct {
-       ID            string   `json:"id" binding:"required"`
-       SdkID         string   `json:"sdkid"` // sdk ID to use for setting env
-       Cmd           string   `json:"cmd" binding:"required"`
-       Args          []string `json:"args"`
-       Env           []string `json:"env"`
-       RPath         string   `json:"rpath"`         // relative path into project
-       ExitImmediate bool     `json:"exitImmediate"` // when true, exit event sent immediately when command exited (IOW, don't wait file synchronization)
-       CmdTimeout    int      `json:"timeout"`       // command completion timeout in Second
-}
+type (
+       // ExecArgs JSON parameters of /exec command
+       ExecArgs struct {
+               ID              string   `json:"id" binding:"required"`
+               SdkID           string   `json:"sdkID"` // sdk ID to use for setting env
+               CmdID           string   `json:"cmdID"` // command unique ID
+               Cmd             string   `json:"cmd" binding:"required"`
+               Args            []string `json:"args"`
+               Env             []string `json:"env"`
+               RPath           string   `json:"rpath"`           // relative path into project
+               TTY             bool     `json:"tty"`             // Use a tty, specific to gdb --tty option
+               TTYGdbserverFix bool     `json:"ttyGdbserverFix"` // Set to true to activate gdbserver workaround about inferior output
+               ExitImmediate   bool     `json:"exitImmediate"`   // when true, exit event sent immediately when command exited (IOW, don't wait file synchronization)
+               CmdTimeout      int      `json:"timeout"`         // command completion timeout in Second
+       }
 
-// ExecOutMsg Message send on each output (stdout+stderr) of executed command
-type ExecOutMsg struct {
-       CmdID     string `json:"cmdID"`
-       Timestamp string `json:"timestamp"`
-       Stdout    string `json:"stdout"`
-       Stderr    string `json:"stderr"`
-}
+       // ExecRes JSON result of /exec command
+       ExecRes struct {
+               Status string `json:"status"` // status OK
+               CmdID  string `json:"cmdID"`  // command unique ID
+       }
 
-// ExecExitMsg Message send when executed command exited
-type ExecExitMsg struct {
-       CmdID     string `json:"cmdID"`
-       Timestamp string `json:"timestamp"`
-       Code      int    `json:"code"`
-       Error     error  `json:"error"`
-}
+       // ExecSigRes JSON result of /signal command
+       ExecSigRes struct {
+               Status string `json:"status"` // status OK
+               CmdID  string `json:"cmdID"`  // command unique ID
+       }
 
-// ExecSignalArgs JSON parameters of /exec/signal command
-type ExecSignalArgs struct {
-       CmdID  string `json:"cmdID" binding:"required"`  // command id
-       Signal string `json:"signal" binding:"required"` // signal number
-}
+       // ExecInMsg Message used to received input characters (stdin)
+       ExecInMsg struct {
+               CmdID     string `json:"cmdID"`
+               Timestamp string `json:"timestamp"`
+               Stdin     string `json:"stdin"`
+       }
+
+       // ExecOutMsg Message used to send output characters (stdout+stderr)
+       ExecOutMsg struct {
+               CmdID     string `json:"cmdID"`
+               Timestamp string `json:"timestamp"`
+               Stdout    string `json:"stdout"`
+               Stderr    string `json:"stderr"`
+       }
 
-// ExecOutEvent Event send in WS when characters are received
-const ExecOutEvent = "exec:output"
+       // ExecExitMsg Message sent when executed command exited
+       ExecExitMsg struct {
+               CmdID     string `json:"cmdID"`
+               Timestamp string `json:"timestamp"`
+               Code      int    `json:"code"`
+               Error     error  `json:"error"`
+       }
+
+       // ExecSignalArgs JSON parameters of /exec/signal command
+       ExecSignalArgs struct {
+               CmdID  string `json:"cmdID" binding:"required"`  // command id
+               Signal string `json:"signal" binding:"required"` // signal number
+       }
+)
+
+const (
+       // ExecInEvent Event send in WS when characters are sent (stdin)
+       ExecInEvent = "exec:input"
+
+       // ExecOutEvent Event send in WS when characters are received (stdout or stderr)
+       ExecOutEvent = "exec:output"
+
+       // ExecExitEvent Event send in WS when program exited
+       ExecExitEvent = "exec:exit"
 
-// ExecExitEvent Event send in WS when program exited
-const ExecExitEvent = "exec:exit"
+       // ExecInferiorInEvent Event send in WS when characters are sent to an inferior (used by gdb inferior/tty)
+       ExecInferiorInEvent = "exec:inferior-input"
+
+       // ExecInferiorOutEvent Event send in WS when characters are received by an inferior
+       ExecInferiorOutEvent = "exec:inferior-output"
+)
 
 var execCommandID = 1
 
 // ExecCmd executes remotely a command
 func (s *APIService) execCmd(c *gin.Context) {
+       var gdbPty, gdbTty *os.File
+       var err error
        var args ExecArgs
        if c.BindJSON(&args) != nil {
                common.APIError(c, "Invalid arguments")
@@ -77,64 +117,155 @@ func (s *APIService) execCmd(c *gin.Context) {
        }
 
        // Allow to pass id in url (/exec/:id) or as JSON argument
-       id := c.Param("id")
-       if id == "" {
-               id = args.ID
+       idArg := c.Param("id")
+       if idArg == "" {
+               idArg = args.ID
        }
-       if id == "" {
+       if idArg == "" {
                common.APIError(c, "Invalid id")
                return
        }
-
-       prj := s.mfolder.GetFolderFromID(id)
-       if prj == nil {
+       id, err := s.mfolders.ResolveID(idArg)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+       f := s.mfolders.Get(id)
+       if f == nil {
                common.APIError(c, "Unknown id")
                return
        }
+       fld := *f
+       prj := fld.GetConfig()
+
+       // Build command line
+       cmd := []string{}
+       // Setup env var regarding Sdk ID (used for example to setup cross toolchain)
+       if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); len(envCmd) > 0 {
+               cmd = append(cmd, envCmd...)
+               cmd = append(cmd, "&&")
+       } else {
+               // It's an error if no envcmd found while a sdkid has been provided
+               if args.SdkID != "" {
+                       common.APIError(c, "Unknown sdkid")
+                       return
+               }
+       }
+
+       cmd = append(cmd, "cd", "\""+fld.GetFullPath(args.RPath)+"\"")
+       // FIXME - add 'exec' prevents to use syntax:
+       //       xds-exec -l debug -c xds-config.env -- "cd build && cmake .."
+       //  but exec is mandatory to allow to pass correctly signals
+       //  As workaround, exec is set for now on client side (eg. in xds-gdb)
+       //cmd = append(cmd, "&&", "exec", args.Cmd)
+       cmd = append(cmd, "&&", args.Cmd)
+
+       // Process command arguments
+       cmdArgs := make([]string, len(args.Args)+1)
+
+       // Copy and Translate path from client to server
+       for _, aa := range args.Args {
+               if strings.Contains(aa, prj.ClientPath) {
+                       cmdArgs = append(cmdArgs, fld.ConvPathCli2Svr(aa))
+               } else {
+                       cmdArgs = append(cmdArgs, aa)
+               }
+       }
+
+       // Allocate pts if tty if used
+       if args.TTY {
+               gdbPty, gdbTty, err = pty.Open()
+               if err != nil {
+                       common.APIError(c, err.Error())
+                       return
+               }
+
+               s.log.Debugf("Client command tty: %v %v\n", gdbTty.Name(), gdbTty.Name())
+               cmdArgs = append(cmdArgs, "--tty="+gdbTty.Name())
+       }
+
+       // Unique ID for each commands
+       if args.CmdID == "" {
+               args.CmdID = s.cfg.ServerUID[:18] + "_" + strconv.Itoa(execCommandID)
+               execCommandID++
+       }
+
+       // Create new execution over WS context
+       execWS := eows.New(strings.Join(cmd, " "), cmdArgs, sop, sess.ID, args.CmdID)
+       execWS.Log = s.log
 
-       execTmo := args.CmdTimeout
-       if execTmo == -1 {
-               // -1 : no timeout
-               execTmo = 365 * 24 * 60 * 60 // 1 year == no timeout
-       } else if execTmo == 0 {
+       // Append client project dir to environment
+       execWS.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.ClientPath)
+
+       // Set command execution timeout
+       if args.CmdTimeout == 0 {
                // 0 : default timeout
                // TODO get default timeout from config.json file
-               execTmo = 24 * 60 * 60 // 1 day
+               execWS.CmdExecTimeout = 24 * 60 * 60 // 1 day
+       } else {
+               execWS.CmdExecTimeout = args.CmdTimeout
        }
 
-       // Define callback for input
-       /* SEB TODO
-       var iCB common.OnInputCB
-       iCB = func() {
+       // Define callback for input (stdin)
+       execWS.InputEvent = ExecInEvent
+       execWS.InputCB = func(e *eows.ExecOverWS, stdin string) (string, error) {
+               s.log.Debugf("STDIN <<%v>>", strings.Replace(stdin, "\n", "\\n", -1))
 
+               // Handle Ctrl-D
+               if len(stdin) == 1 && stdin == "\x04" {
+                       // Close stdin
+                       errMsg := fmt.Errorf("close stdin: %v", stdin)
+                       return "", errMsg
+               }
+
+               // Set correct path
+               data := e.UserData
+               prjID := (*data)["ID"].(string)
+               f := s.mfolders.Get(prjID)
+               if f == nil {
+                       s.log.Errorf("InputCB: Cannot get folder ID %s", prjID)
+               } else {
+                       // Translate paths from client to server
+                       stdin = (*f).ConvPathCli2Svr(stdin)
+               }
+
+               return stdin, nil
        }
-       */
 
-       // Define callback for output
-       var oCB common.EmitOutputCB
-       oCB = func(sid string, id string, stdout, stderr string, data *map[string]interface{}) {
+       // Define callback for output (stdout+stderr)
+       execWS.OutputCB = func(e *eows.ExecOverWS, stdout, stderr string) {
                // IO socket can be nil when disconnected
-               so := s.sessions.IOSocketGet(sid)
+               so := s.sessions.IOSocketGet(e.Sid)
                if so == nil {
-                       s.log.Infof("%s not emitted: WS closed - sid: %s - msg id:%d", ExecOutEvent, sid, id)
+                       s.log.Infof("%s not emitted: WS closed (sid:%s, msgid:%s)", ExecOutEvent, e.Sid, e.CmdID)
                        return
                }
 
                // Retrieve project ID and RootPath
+               data := e.UserData
                prjID := (*data)["ID"].(string)
-               prjRootPath := (*data)["RootPath"].(string)
-
-               // Cleanup any references to internal rootpath in stdout & stderr
-               stdout = strings.Replace(stdout, prjRootPath, "", -1)
-               stderr = strings.Replace(stderr, prjRootPath, "", -1)
-
-               s.log.Debugf("%s emitted - WS sid %s - id:%d - prjID:%s", ExecOutEvent, sid, id, prjID)
+               gdbServerTTY := (*data)["gdbServerTTY"].(string)
+
+               f := s.mfolders.Get(prjID)
+               if f == nil {
+                       s.log.Errorf("OutputCB: Cannot get folder ID %s", prjID)
+               } else {
+                       // Translate paths from server to client
+                       stdout = (*f).ConvPathSvr2Cli(stdout)
+                       stderr = (*f).ConvPathSvr2Cli(stderr)
+               }
 
-               fmt.Printf("SEB SEND out <%v>, err <%v>\n", stdout, stderr)
+               s.log.Debugf("%s emitted - WS sid[4:] %s - id:%s - prjID:%s", ExecOutEvent, e.Sid[4:], e.CmdID, prjID)
+               if stdout != "" {
+                       s.log.Debugf("STDOUT <<%v>>", strings.Replace(stdout, "\n", "\\n", -1))
+               }
+               if stderr != "" {
+                       s.log.Debugf("STDERR <<%v>>", strings.Replace(stderr, "\n", "\\n", -1))
+               }
 
                // FIXME replace by .BroadcastTo a room
                err := (*so).Emit(ExecOutEvent, ExecOutMsg{
-                       CmdID:     id,
+                       CmdID:     e.CmdID,
                        Timestamp: time.Now().String(),
                        Stdout:    stdout,
                        Stderr:    stderr,
@@ -142,25 +273,68 @@ func (s *APIService) execCmd(c *gin.Context) {
                if err != nil {
                        s.log.Errorf("WS Emit : %v", err)
                }
+
+               // XXX - Workaround due to gdbserver bug that doesn't redirect
+               // inferior output (https://bugs.eclipse.org/bugs/show_bug.cgi?id=437532#c13)
+               if gdbServerTTY == "workaround" && len(stdout) > 1 && stdout[0] == '&' {
+
+                       // Extract and cleanup string like &"bla bla\n"
+                       re := regexp.MustCompile("&\"(.*)\"")
+                       rer := re.FindAllStringSubmatch(stdout, -1)
+                       out := ""
+                       if rer != nil && len(rer) > 0 {
+                               for _, o := range rer {
+                                       if len(o) >= 1 {
+                                               out = strings.Replace(o[1], "\\n", "\n", -1)
+                                               out = strings.Replace(out, "\\r", "\r", -1)
+                                               out = strings.Replace(out, "\\t", "\t", -1)
+
+                                               s.log.Debugf("STDOUT INFERIOR: <<%v>>", out)
+                                               err := (*so).Emit(ExecInferiorOutEvent, ExecOutMsg{
+                                                       CmdID:     e.CmdID,
+                                                       Timestamp: time.Now().String(),
+                                                       Stdout:    out,
+                                                       Stderr:    "",
+                                               })
+                                               if err != nil {
+                                                       s.log.Errorf("WS Emit : %v", err)
+                                               }
+                                       }
+                               }
+                       } else {
+                               s.log.Errorf("INFERIOR out parsing error: stdout=<%v>", stdout)
+                       }
+               }
        }
 
        // Define callback for output
-       eCB := func(sid string, id string, code int, err error, data *map[string]interface{}) {
-               s.log.Debugf("Command [Cmd ID %d] exited: code %d, error: %v", id, code, err)
+       execWS.ExitCB = func(e *eows.ExecOverWS, code int, err error) {
+               s.log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", e.CmdID, code, err)
+
+               // Close client tty
+               defer func() {
+                       if gdbPty != nil {
+                               gdbPty.Close()
+                       }
+                       if gdbTty != nil {
+                               gdbTty.Close()
+                       }
+               }()
 
                // IO socket can be nil when disconnected
-               so := s.sessions.IOSocketGet(sid)
+               so := s.sessions.IOSocketGet(e.Sid)
                if so == nil {
-                       s.log.Infof("%s not emitted - WS closed (id:%d", ExecExitEvent, id)
+                       s.log.Infof("%s not emitted - WS closed (id:%s)", ExecExitEvent, e.CmdID)
                        return
                }
 
                // Retrieve project ID and RootPath
+               data := e.UserData
                prjID := (*data)["ID"].(string)
                exitImm := (*data)["ExitImmediate"].(bool)
 
                // XXX - workaround to be sure that Syncthing detected all changes
-               if err := s.mfolder.ForceSync(prjID); err != nil {
+               if err := s.mfolders.ForceSync(prjID); err != nil {
                        s.log.Errorf("Error while syncing folder %s: %v", prjID, err)
                }
                if !exitImm {
@@ -168,8 +342,8 @@ func (s *APIService) execCmd(c *gin.Context) {
                        // FIXME pass as argument
                        tmo := 60
                        for t := tmo; t > 0; t-- {
-                               s.log.Debugf("Wait file insync for %s (%d/%d)", prjID, t, tmo)
-                               if sync, err := s.mfolder.IsFolderInSync(prjID); sync || err != nil {
+                               s.log.Debugf("Wait file in-sync for %s (%d/%d)", prjID, t, tmo)
+                               if sync, err := s.mfolders.IsFolderInSync(prjID); sync || err != nil {
                                        if err != nil {
                                                s.log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err)
                                        }
@@ -177,65 +351,42 @@ func (s *APIService) execCmd(c *gin.Context) {
                                }
                                time.Sleep(time.Second)
                        }
+                       s.log.Debugf("OK file are synchronized.")
                }
 
                // FIXME replace by .BroadcastTo a room
-               e := (*so).Emit(ExecExitEvent, ExecExitMsg{
-                       CmdID:     id,
+               errSoEmit := (*so).Emit(ExecExitEvent, ExecExitMsg{
+                       CmdID:     e.CmdID,
                        Timestamp: time.Now().String(),
                        Code:      code,
                        Error:     err,
                })
-               if e != nil {
-                       s.log.Errorf("WS Emit : %v", e)
+               if errSoEmit != nil {
+                       s.log.Errorf("WS Emit : %v", errSoEmit)
                }
        }
 
-       cmdID := strconv.Itoa(execCommandID)
-       execCommandID++
-       cmd := []string{}
-
-       // Setup env var regarding Sdk ID (used for example to setup cross toolchain)
-       if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); len(envCmd) > 0 {
-               cmd = append(cmd, envCmd...)
-               cmd = append(cmd, "&&")
-       } else {
-               // It's an error if no envcmd found while a sdkid has been provided
-               if args.SdkID != "" {
-                       common.APIError(c, "Unknown sdkid")
-                       return
-               }
-       }
-
-       cmd = append(cmd, "cd", prj.GetFullPath(args.RPath), "&&", args.Cmd)
-       if len(args.Args) > 0 {
-               cmd = append(cmd, args.Args...)
-       }
-
-       // SEB Workaround for stderr issue (order not respected with stdout)
-       cmd = append(cmd, " 2>&1")
-
-       // Append client project dir to environment
-       args.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.RelativePath)
-
-       s.log.Debugf("Execute [Cmd ID %s]: %v", cmdID, cmd)
-
+       // User data (used within callbacks)
        data := make(map[string]interface{})
        data["ID"] = prj.ID
-       data["RootPath"] = prj.RootPath
        data["ExitImmediate"] = args.ExitImmediate
+       if args.TTY && args.TTYGdbserverFix {
+               data["gdbServerTTY"] = "workaround"
+       } else {
+               data["gdbServerTTY"] = ""
+       }
+       execWS.UserData = &data
+
+       // Start command execution
+       s.log.Infof("Execute [Cmd ID %s]: %v %v", execWS.CmdID, execWS.Cmd, execWS.Args)
 
-       err := common.ExecPipeWs(cmd, args.Env, sop, sess.ID, cmdID, execTmo, s.log, oCB, eCB, &data)
+       err = execWS.Start()
        if err != nil {
                common.APIError(c, err.Error())
                return
        }
 
-       c.JSON(http.StatusOK,
-               gin.H{
-                       "status": "OK",
-                       "cmdID":  cmdID,
-               })
+       c.JSON(http.StatusOK, ExecRes{Status: "OK", CmdID: execWS.CmdID})
 }
 
 // ExecCmd executes remotely a command
@@ -248,14 +399,18 @@ func (s *APIService) execSignalCmd(c *gin.Context) {
        }
 
        s.log.Debugf("Signal %s for command ID %s", args.Signal, args.CmdID)
-       err := common.ExecSignal(args.CmdID, args.Signal)
+
+       e := eows.GetEows(args.CmdID)
+       if e == nil {
+               common.APIError(c, "unknown cmdID")
+               return
+       }
+
+       err := e.Signal(args.Signal)
        if err != nil {
                common.APIError(c, err.Error())
                return
        }
 
-       c.JSON(http.StatusOK,
-               gin.H{
-                       "status": "OK",
-               })
+       c.JSON(http.StatusOK, ExecSigRes{Status: "OK", CmdID: args.CmdID})
 }