summaryrefslogtreecommitdiffstats
path: root/lib/apiv1/exec.go
diff options
context:
space:
mode:
authorSebastien Douheret <sebastien.douheret@iot.bzh>2017-11-29 08:54:00 +0100
committerSebastien Douheret <sebastien.douheret@iot.bzh>2017-11-29 11:10:30 +0100
commit2f7828d01f4c4ca2909f95f098627cd5475ed225 (patch)
treeb5e71920b813b95cae3e32044be08b99223348ec /lib/apiv1/exec.go
parent5caebfb4b7c3b73988f067082b219ce5b7f409ba (diff)
Refit source files to have a public xs-apiv1 lib package.
Signed-off-by: Sebastien Douheret <sebastien.douheret@iot.bzh>
Diffstat (limited to 'lib/apiv1/exec.go')
-rw-r--r--lib/apiv1/exec.go416
1 files changed, 0 insertions, 416 deletions
diff --git a/lib/apiv1/exec.go b/lib/apiv1/exec.go
deleted file mode 100644
index baf431f..0000000
--- a/lib/apiv1/exec.go
+++ /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})
-}