Add stdin support to /exec
[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         "fmt"
10
11         "github.com/gin-gonic/gin"
12         common "github.com/iotbzh/xds-common/golib"
13 )
14
15 // ExecArgs JSON parameters of /exec command
16 type ExecArgs struct {
17         ID            string   `json:"id" binding:"required"`
18         SdkID         string   `json:"sdkid"` // sdk ID to use for setting env
19         Cmd           string   `json:"cmd" binding:"required"`
20         Args          []string `json:"args"`
21         Env           []string `json:"env"`
22         RPath         string   `json:"rpath"`         // relative path into project
23         ExitImmediate bool     `json:"exitImmediate"` // when true, exit event sent immediately when command exited (IOW, don't wait file synchronization)
24         CmdTimeout    int      `json:"timeout"`       // command completion timeout in Second
25 }
26
27 // ExecOutMsg Message send on each output (stdout+stderr) of executed command
28 type ExecOutMsg struct {
29         CmdID     string `json:"cmdID"`
30         Timestamp string `json:"timestamp"`
31         Stdout    string `json:"stdout"`
32         Stderr    string `json:"stderr"`
33 }
34
35 // ExecExitMsg Message send when executed command exited
36 type ExecExitMsg struct {
37         CmdID     string `json:"cmdID"`
38         Timestamp string `json:"timestamp"`
39         Code      int    `json:"code"`
40         Error     error  `json:"error"`
41 }
42
43 // ExecSignalArgs JSON parameters of /exec/signal command
44 type ExecSignalArgs struct {
45         CmdID  string `json:"cmdID" binding:"required"`  // command id
46         Signal string `json:"signal" binding:"required"` // signal number
47 }
48
49 // ExecOutEvent Event send in WS when characters are received
50 const ExecOutEvent = "exec:output"
51
52 // ExecExitEvent Event send in WS when program exited
53 const ExecExitEvent = "exec:exit"
54
55 var execCommandID = 1
56
57 // ExecCmd executes remotely a command
58 func (s *APIService) execCmd(c *gin.Context) {
59         var args ExecArgs
60         if c.BindJSON(&args) != nil {
61                 common.APIError(c, "Invalid arguments")
62                 return
63         }
64
65         // TODO: add permission ?
66
67         // Retrieve session info
68         sess := s.sessions.Get(c)
69         if sess == nil {
70                 common.APIError(c, "Unknown sessions")
71                 return
72         }
73         sop := sess.IOSocket
74         if sop == nil {
75                 common.APIError(c, "Websocket not established")
76                 return
77         }
78
79         // Allow to pass id in url (/exec/:id) or as JSON argument
80         id := c.Param("id")
81         if id == "" {
82                 id = args.ID
83         }
84         if id == "" {
85                 common.APIError(c, "Invalid id")
86                 return
87         }
88
89         prj := s.mfolder.GetFolderFromID(id)
90         if prj == nil {
91                 common.APIError(c, "Unknown id")
92                 return
93         }
94
95         execTmo := args.CmdTimeout
96         if execTmo == -1 {
97                 // -1 : no timeout
98                 execTmo = 365 * 24 * 60 * 60 // 1 year == no timeout
99         } else if execTmo == 0 {
100                 // 0 : default timeout
101                 // TODO get default timeout from config.json file
102                 execTmo = 24 * 60 * 60 // 1 day
103         }
104
105         // Define callback for input
106         /* SEB TODO
107         var iCB common.OnInputCB
108         iCB = func() {
109
110         }
111         */
112
113         // Define callback for output
114         var oCB common.EmitOutputCB
115         oCB = func(sid string, id string, stdout, stderr string, data *map[string]interface{}) {
116                 // IO socket can be nil when disconnected
117                 so := s.sessions.IOSocketGet(sid)
118                 if so == nil {
119                         s.log.Infof("%s not emitted: WS closed - sid: %s - msg id:%d", ExecOutEvent, sid, id)
120                         return
121                 }
122
123                 // Retrieve project ID and RootPath
124                 prjID := (*data)["ID"].(string)
125                 prjRootPath := (*data)["RootPath"].(string)
126
127                 // Cleanup any references to internal rootpath in stdout & stderr
128                 stdout = strings.Replace(stdout, prjRootPath, "", -1)
129                 stderr = strings.Replace(stderr, prjRootPath, "", -1)
130
131                 s.log.Debugf("%s emitted - WS sid %s - id:%d - prjID:%s", ExecOutEvent, sid, id, prjID)
132
133                 fmt.Printf("SEB SEND out <%v>, err <%v>\n", stdout, stderr)
134
135                 // FIXME replace by .BroadcastTo a room
136                 err := (*so).Emit(ExecOutEvent, ExecOutMsg{
137                         CmdID:     id,
138                         Timestamp: time.Now().String(),
139                         Stdout:    stdout,
140                         Stderr:    stderr,
141                 })
142                 if err != nil {
143                         s.log.Errorf("WS Emit : %v", err)
144                 }
145         }
146
147         // Define callback for output
148         eCB := func(sid string, id string, code int, err error, data *map[string]interface{}) {
149                 s.log.Debugf("Command [Cmd ID %d] exited: code %d, error: %v", id, code, err)
150
151                 // IO socket can be nil when disconnected
152                 so := s.sessions.IOSocketGet(sid)
153                 if so == nil {
154                         s.log.Infof("%s not emitted - WS closed (id:%d", ExecExitEvent, id)
155                         return
156                 }
157
158                 // Retrieve project ID and RootPath
159                 prjID := (*data)["ID"].(string)
160                 exitImm := (*data)["ExitImmediate"].(bool)
161
162                 // XXX - workaround to be sure that Syncthing detected all changes
163                 if err := s.mfolder.ForceSync(prjID); err != nil {
164                         s.log.Errorf("Error while syncing folder %s: %v", prjID, err)
165                 }
166                 if !exitImm {
167                         // Wait end of file sync
168                         // FIXME pass as argument
169                         tmo := 60
170                         for t := tmo; t > 0; t-- {
171                                 s.log.Debugf("Wait file insync for %s (%d/%d)", prjID, t, tmo)
172                                 if sync, err := s.mfolder.IsFolderInSync(prjID); sync || err != nil {
173                                         if err != nil {
174                                                 s.log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err)
175                                         }
176                                         break
177                                 }
178                                 time.Sleep(time.Second)
179                         }
180                 }
181
182                 // FIXME replace by .BroadcastTo a room
183                 e := (*so).Emit(ExecExitEvent, ExecExitMsg{
184                         CmdID:     id,
185                         Timestamp: time.Now().String(),
186                         Code:      code,
187                         Error:     err,
188                 })
189                 if e != nil {
190                         s.log.Errorf("WS Emit : %v", e)
191                 }
192         }
193
194         cmdID := strconv.Itoa(execCommandID)
195         execCommandID++
196         cmd := []string{}
197
198         // Setup env var regarding Sdk ID (used for example to setup cross toolchain)
199         if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); len(envCmd) > 0 {
200                 cmd = append(cmd, envCmd...)
201                 cmd = append(cmd, "&&")
202         } else {
203                 // It's an error if no envcmd found while a sdkid has been provided
204                 if args.SdkID != "" {
205                         common.APIError(c, "Unknown sdkid")
206                         return
207                 }
208         }
209
210         cmd = append(cmd, "cd", prj.GetFullPath(args.RPath), "&&", args.Cmd)
211         if len(args.Args) > 0 {
212                 cmd = append(cmd, args.Args...)
213         }
214
215         // SEB Workaround for stderr issue (order not respected with stdout)
216         cmd = append(cmd, " 2>&1")
217
218         // Append client project dir to environment
219         args.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.RelativePath)
220
221         s.log.Debugf("Execute [Cmd ID %s]: %v", cmdID, cmd)
222
223         data := make(map[string]interface{})
224         data["ID"] = prj.ID
225         data["RootPath"] = prj.RootPath
226         data["ExitImmediate"] = args.ExitImmediate
227
228         err := common.ExecPipeWs(cmd, args.Env, sop, sess.ID, cmdID, execTmo, s.log, oCB, eCB, &data)
229         if err != nil {
230                 common.APIError(c, err.Error())
231                 return
232         }
233
234         c.JSON(http.StatusOK,
235                 gin.H{
236                         "status": "OK",
237                         "cmdID":  cmdID,
238                 })
239 }
240
241 // ExecCmd executes remotely a command
242 func (s *APIService) execSignalCmd(c *gin.Context) {
243         var args ExecSignalArgs
244
245         if c.BindJSON(&args) != nil {
246                 common.APIError(c, "Invalid arguments")
247                 return
248         }
249
250         s.log.Debugf("Signal %s for command ID %s", args.Signal, args.CmdID)
251         err := common.ExecSignal(args.CmdID, args.Signal)
252         if err != nil {
253                 common.APIError(c, err.Error())
254                 return
255         }
256
257         c.JSON(http.StatusOK,
258                 gin.H{
259                         "status": "OK",
260                 })
261 }