895807d5b89ad83a61e46e6f24cdea0076a80a9a
[src/xds/xds-server.git] / lib / apiv1 / exec.go
1 package apiv1
2
3 import (
4         "net/http"
5         "strconv"
6         "strings"
7         "time"
8
9         "github.com/gin-gonic/gin"
10         "github.com/iotbzh/xds-server/lib/common"
11 )
12
13 // ExecArgs JSON parameters of /exec command
14 type ExecArgs struct {
15         ID         string   `json:"id" binding:"required"`
16         SdkID      string   `json:"sdkid"` // sdk ID to use for setting env
17         Cmd        string   `json:"cmd" binding:"required"`
18         Args       []string `json:"args"`
19         Env        []string `json:"env"`
20         RPath      string   `json:"rpath"`   // relative path into project
21         CmdTimeout int      `json:"timeout"` // command completion timeout in Second
22 }
23
24 // ExecOutMsg Message send on each output (stdout+stderr) of executed command
25 type ExecOutMsg struct {
26         CmdID     string `json:"cmdID"`
27         Timestamp string `json:"timestamp"`
28         Stdout    string `json:"stdout"`
29         Stderr    string `json:"stderr"`
30 }
31
32 // ExecExitMsg Message send when executed command exited
33 type ExecExitMsg struct {
34         CmdID     string `json:"cmdID"`
35         Timestamp string `json:"timestamp"`
36         Code      int    `json:"code"`
37         Error     error  `json:"error"`
38 }
39
40 // ExecOutEvent Event send in WS when characters are received
41 const ExecOutEvent = "exec:output"
42
43 // ExecExitEvent Event send in WS when program exited
44 const ExecExitEvent = "exec:exit"
45
46 var execCommandID = 1
47
48 // ExecCmd executes remotely a command
49 func (s *APIService) execCmd(c *gin.Context) {
50         var args ExecArgs
51         if c.BindJSON(&args) != nil {
52                 common.APIError(c, "Invalid arguments")
53                 return
54         }
55
56         // TODO: add permission ?
57
58         // Retrieve session info
59         sess := s.sessions.Get(c)
60         if sess == nil {
61                 common.APIError(c, "Unknown sessions")
62                 return
63         }
64         sop := sess.IOSocket
65         if sop == nil {
66                 common.APIError(c, "Websocket not established")
67                 return
68         }
69
70         // Allow to pass id in url (/exec/:id) or as JSON argument
71         id := c.Param("id")
72         if id == "" {
73                 id = args.ID
74         }
75         if id == "" {
76                 common.APIError(c, "Invalid id")
77                 return
78         }
79
80         prj := s.mfolder.GetFolderFromID(id)
81         if prj == nil {
82                 common.APIError(c, "Unknown id")
83                 return
84         }
85
86         execTmo := args.CmdTimeout
87         if execTmo == 0 {
88                 // TODO get default timeout from config.json file
89                 execTmo = 24 * 60 * 60 // 1 day
90         }
91
92         // Define callback for output
93         var oCB common.EmitOutputCB
94         oCB = func(sid string, id int, stdout, stderr string, data *map[string]interface{}) {
95                 // IO socket can be nil when disconnected
96                 so := s.sessions.IOSocketGet(sid)
97                 if so == nil {
98                         s.log.Infof("%s not emitted: WS closed - sid: %s - msg id:%d", ExecOutEvent, sid, id)
99                         return
100                 }
101
102                 // Retrieve project ID and RootPath
103                 prjID := (*data)["ID"].(string)
104                 prjRootPath := (*data)["RootPath"].(string)
105
106                 // Cleanup any references to internal rootpath in stdout & stderr
107                 stdout = strings.Replace(stdout, prjRootPath, "", -1)
108                 stderr = strings.Replace(stderr, prjRootPath, "", -1)
109
110                 s.log.Debugf("%s emitted - WS sid %s - id:%d - prjID:%s", ExecOutEvent, sid, id, prjID)
111
112                 // FIXME replace by .BroadcastTo a room
113                 err := (*so).Emit(ExecOutEvent, ExecOutMsg{
114                         CmdID:     strconv.Itoa(id),
115                         Timestamp: time.Now().String(),
116                         Stdout:    stdout,
117                         Stderr:    stderr,
118                 })
119                 if err != nil {
120                         s.log.Errorf("WS Emit : %v", err)
121                 }
122         }
123
124         // Define callback for output
125         eCB := func(sid string, id int, code int, err error) {
126                 s.log.Debugf("Command [Cmd ID %d] exited: code %d, error: %v", id, code, err)
127
128                 // IO socket can be nil when disconnected
129                 so := s.sessions.IOSocketGet(sid)
130                 if so == nil {
131                         s.log.Infof("%s not emitted - WS closed (id:%d", ExecExitEvent, id)
132                         return
133                 }
134
135                 // FIXME replace by .BroadcastTo a room
136                 e := (*so).Emit(ExecExitEvent, ExecExitMsg{
137                         CmdID:     strconv.Itoa(id),
138                         Timestamp: time.Now().String(),
139                         Code:      code,
140                         Error:     err,
141                 })
142                 if e != nil {
143                         s.log.Errorf("WS Emit : %v", e)
144                 }
145         }
146
147         cmdID := execCommandID
148         execCommandID++
149         cmd := []string{}
150
151         // Setup env var regarding Sdk ID (used for example to setup cross toolchain)
152         if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); len(envCmd) > 0 {
153                 cmd = append(cmd, envCmd...)
154                 cmd = append(cmd, "&&")
155         }
156
157         cmd = append(cmd, "cd", prj.GetFullPath(args.RPath), "&&", args.Cmd)
158         if len(args.Args) > 0 {
159                 cmd = append(cmd, args.Args...)
160         }
161
162         s.log.Debugf("Execute [Cmd ID %d]: %v", cmdID, cmd)
163
164         data := make(map[string]interface{})
165         data["ID"] = prj.ID
166         data["RootPath"] = prj.RootPath
167
168         err := common.ExecPipeWs(cmd, args.Env, sop, sess.ID, cmdID, execTmo, s.log, oCB, eCB, &data)
169         if err != nil {
170                 common.APIError(c, err.Error())
171                 return
172         }
173
174         c.JSON(http.StatusOK,
175                 gin.H{
176                         "status": "OK",
177                         "cmdID":  cmdID,
178                 })
179 }