aboutsummaryrefslogtreecommitdiffstats
path: root/lib/xdsserver/apiv1-exec.go
diff options
context:
space:
mode:
Diffstat (limited to 'lib/xdsserver/apiv1-exec.go')
-rw-r--r--lib/xdsserver/apiv1-exec.go342
1 files changed, 342 insertions, 0 deletions
diff --git a/lib/xdsserver/apiv1-exec.go b/lib/xdsserver/apiv1-exec.go
new file mode 100644
index 0000000..ce5e7b7
--- /dev/null
+++ b/lib/xdsserver/apiv1-exec.go
@@ -0,0 +1,342 @@
+package xdsserver
+
+import (
+ "fmt"
+ "net/http"
+ "os"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ common "github.com/iotbzh/xds-common/golib"
+ "github.com/iotbzh/xds-common/golib/eows"
+ "github.com/iotbzh/xds-server/lib/xsapiv1"
+ "github.com/kr/pty"
+)
+
+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 xsapiv1.ExecArgs
+ if c.BindJSON(&args) != nil {
+ common.APIError(c, "Invalid arguments")
+ return
+ }
+
+ // TODO: add permission ?
+
+ // Retrieve session info
+ sess := s.sessions.Get(c)
+ if sess == nil {
+ common.APIError(c, "Unknown sessions")
+ return
+ }
+ sop := sess.IOSocket
+ if sop == nil {
+ common.APIError(c, "Websocket not established")
+ return
+ }
+
+ // Allow to pass id in url (/exec/:id) or as JSON argument
+ idArg := c.Param("id")
+ if idArg == "" {
+ idArg = args.ID
+ }
+ if idArg == "" {
+ common.APIError(c, "Invalid id")
+ return
+ }
+ 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.Config.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
+
+ // 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
+ execWS.CmdExecTimeout = 24 * 60 * 60 // 1 day
+ } else {
+ execWS.CmdExecTimeout = args.CmdTimeout
+ }
+
+ // Define callback for input (stdin)
+ execWS.InputEvent = xsapiv1.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 (stdout+stderr)
+ execWS.OutputCB = func(e *eows.ExecOverWS, stdout, stderr string) {
+ // IO socket can be nil when disconnected
+ so := s.sessions.IOSocketGet(e.Sid)
+ if so == nil {
+ s.Log.Infof("%s not emitted: WS closed (sid:%s, msgid:%s)", xsapiv1.ExecOutEvent, e.Sid, e.CmdID)
+ return
+ }
+
+ // Retrieve project ID and RootPath
+ data := e.UserData
+ prjID := (*data)["ID"].(string)
+ 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)
+ }
+
+ s.Log.Debugf("%s emitted - WS sid[4:] %s - id:%s - prjID:%s", xsapiv1.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(xsapiv1.ExecOutEvent, xsapiv1.ExecOutMsg{
+ CmdID: e.CmdID,
+ Timestamp: time.Now().String(),
+ Stdout: stdout,
+ Stderr: stderr,
+ })
+ 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(xsapiv1.ExecInferiorOutEvent, xsapiv1.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
+ 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(e.Sid)
+ if so == nil {
+ s.Log.Infof("%s not emitted - WS closed (id:%s)", xsapiv1.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.mfolders.ForceSync(prjID); err != nil {
+ s.Log.Errorf("Error while syncing folder %s: %v", prjID, err)
+ }
+ if !exitImm {
+ // Wait end of file sync
+ // FIXME pass as argument
+ tmo := 60
+ for t := tmo; t > 0; t-- {
+ 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)
+ }
+ break
+ }
+ time.Sleep(time.Second)
+ }
+ s.Log.Debugf("OK file are synchronized.")
+ }
+
+ // FIXME replace by .BroadcastTo a room
+ errSoEmit := (*so).Emit(xsapiv1.ExecExitEvent, xsapiv1.ExecExitMsg{
+ CmdID: e.CmdID,
+ Timestamp: time.Now().String(),
+ Code: code,
+ Error: err,
+ })
+ if errSoEmit != nil {
+ s.Log.Errorf("WS Emit : %v", errSoEmit)
+ }
+ }
+
+ // User data (used within callbacks)
+ data := make(map[string]interface{})
+ data["ID"] = prj.ID
+ 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 = execWS.Start()
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+
+ c.JSON(http.StatusOK, xsapiv1.ExecResult{Status: "OK", CmdID: execWS.CmdID})
+}
+
+// ExecCmd executes remotely a command
+func (s *APIService) execSignalCmd(c *gin.Context) {
+ var args xsapiv1.ExecSignalArgs
+
+ if c.BindJSON(&args) != nil {
+ common.APIError(c, "Invalid arguments")
+ return
+ }
+
+ s.Log.Debugf("Signal %s for command ID %s", args.Signal, args.CmdID)
+
+ 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, xsapiv1.ExecSigResult{Status: "OK", CmdID: args.CmdID})
+}