Refit source files to have a public xs-apiv1 lib package.
[src/xds/xds-server.git] / lib / apiv1 / exec.go
diff --git a/lib/apiv1/exec.go b/lib/apiv1/exec.go
deleted file mode 100644 (file)
index baf431f..0000000
+++ /dev/null
@@ -1,416 +0,0 @@
-package apiv1
-
-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/kr/pty"
-)
-
-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
-       }
-
-       // ExecRes JSON result of /exec command
-       ExecRes struct {
-               Status string `json:"status"` // status OK
-               CmdID  string `json:"cmdID"`  // command unique ID
-       }
-
-       // ExecSigRes JSON result of /signal command
-       ExecSigRes struct {
-               Status string `json:"status"` // status OK
-               CmdID  string `json:"cmdID"`  // command unique ID
-       }
-
-       // 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"`
-       }
-
-       // 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"
-
-       // 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")
-               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.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
-
-       // 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 = 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)", 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", 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:     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(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
-       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)", 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(ExecExitEvent, 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, ExecRes{Status: "OK", CmdID: execWS.CmdID})
-}
-
-// ExecCmd executes remotely a command
-func (s *APIService) execSignalCmd(c *gin.Context) {
-       var args 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, ExecSigRes{Status: "OK", CmdID: args.CmdID})
-}