Use xds-common go library.
[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         common "github.com/iotbzh/xds-common/golib"
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         ExitImmediate bool     `json:"exitImmediate"` // when true, exit event sent immediately when command exited (IOW, don't wait file synchronization)
22         CmdTimeout    int      `json:"timeout"`       // command completion timeout in Second
23 }
24
25 // ExecOutMsg Message send on each output (stdout+stderr) of executed command
26 type ExecOutMsg struct {
27         CmdID     string `json:"cmdID"`
28         Timestamp string `json:"timestamp"`
29         Stdout    string `json:"stdout"`
30         Stderr    string `json:"stderr"`
31 }
32
33 // ExecExitMsg Message send when executed command exited
34 type ExecExitMsg struct {
35         CmdID     string `json:"cmdID"`
36         Timestamp string `json:"timestamp"`
37         Code      int    `json:"code"`
38         Error     error  `json:"error"`
39 }
40
41 // ExecOutEvent Event send in WS when characters are received
42 const ExecOutEvent = "exec:output"
43
44 // ExecExitEvent Event send in WS when program exited
45 const ExecExitEvent = "exec:exit"
46
47 var execCommandID = 1
48
49 // ExecCmd executes remotely a command
50 func (s *APIService) execCmd(c *gin.Context) {
51         var args ExecArgs
52         if c.BindJSON(&args) != nil {
53                 common.APIError(c, "Invalid arguments")
54                 return
55         }
56
57         // TODO: add permission ?
58
59         // Retrieve session info
60         sess := s.sessions.Get(c)
61         if sess == nil {
62                 common.APIError(c, "Unknown sessions")
63                 return
64         }
65         sop := sess.IOSocket
66         if sop == nil {
67                 common.APIError(c, "Websocket not established")
68                 return
69         }
70
71         // Allow to pass id in url (/exec/:id) or as JSON argument
72         id := c.Param("id")
73         if id == "" {
74                 id = args.ID
75         }
76         if id == "" {
77                 common.APIError(c, "Invalid id")
78                 return
79         }
80
81         prj := s.mfolder.GetFolderFromID(id)
82         if prj == nil {
83                 common.APIError(c, "Unknown id")
84                 return
85         }
86
87         execTmo := args.CmdTimeout
88         if execTmo == 0 {
89                 // TODO get default timeout from config.json file
90                 execTmo = 24 * 60 * 60 // 1 day
91         }
92
93         // Define callback for output
94         var oCB common.EmitOutputCB
95         oCB = func(sid string, id int, stdout, stderr string, data *map[string]interface{}) {
96                 // IO socket can be nil when disconnected
97                 so := s.sessions.IOSocketGet(sid)
98                 if so == nil {
99                         s.log.Infof("%s not emitted: WS closed - sid: %s - msg id:%d", ExecOutEvent, sid, id)
100                         return
101                 }
102
103                 // Retrieve project ID and RootPath
104                 prjID := (*data)["ID"].(string)
105                 prjRootPath := (*data)["RootPath"].(string)
106
107                 // Cleanup any references to internal rootpath in stdout & stderr
108                 stdout = strings.Replace(stdout, prjRootPath, "", -1)
109                 stderr = strings.Replace(stderr, prjRootPath, "", -1)
110
111                 s.log.Debugf("%s emitted - WS sid %s - id:%d - prjID:%s", ExecOutEvent, sid, id, prjID)
112
113                 // FIXME replace by .BroadcastTo a room
114                 err := (*so).Emit(ExecOutEvent, ExecOutMsg{
115                         CmdID:     strconv.Itoa(id),
116                         Timestamp: time.Now().String(),
117                         Stdout:    stdout,
118                         Stderr:    stderr,
119                 })
120                 if err != nil {
121                         s.log.Errorf("WS Emit : %v", err)
122                 }
123         }
124
125         // Define callback for output
126         eCB := func(sid string, id int, code int, err error, data *map[string]interface{}) {
127                 s.log.Debugf("Command [Cmd ID %d] exited: code %d, error: %v", id, code, err)
128
129                 // IO socket can be nil when disconnected
130                 so := s.sessions.IOSocketGet(sid)
131                 if so == nil {
132                         s.log.Infof("%s not emitted - WS closed (id:%d", ExecExitEvent, id)
133                         return
134                 }
135
136                 // Retrieve project ID and RootPath
137                 prjID := (*data)["ID"].(string)
138                 exitImm := (*data)["ExitImmediate"].(bool)
139
140                 // XXX - workaround to be sure that Syncthing detected all changes
141                 if err := s.mfolder.ForceSync(prjID); err != nil {
142                         s.log.Errorf("Error while syncing folder %s: %v", prjID, err)
143                 }
144                 if !exitImm {
145                         // Wait end of file sync
146                         // FIXME pass as argument
147                         tmo := 60
148                         for t := tmo; t > 0; t-- {
149                                 s.log.Debugf("Wait file insync for %s (%d/%d)", prjID, t, tmo)
150                                 if sync, err := s.mfolder.IsFolderInSync(prjID); sync || err != nil {
151                                         if err != nil {
152                                                 s.log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err)
153                                         }
154                                         break
155                                 }
156                                 time.Sleep(time.Second)
157                         }
158                 }
159
160                 // FIXME replace by .BroadcastTo a room
161                 e := (*so).Emit(ExecExitEvent, ExecExitMsg{
162                         CmdID:     strconv.Itoa(id),
163                         Timestamp: time.Now().String(),
164                         Code:      code,
165                         Error:     err,
166                 })
167                 if e != nil {
168                         s.log.Errorf("WS Emit : %v", e)
169                 }
170         }
171
172         cmdID := execCommandID
173         execCommandID++
174         cmd := []string{}
175
176         // Setup env var regarding Sdk ID (used for example to setup cross toolchain)
177         if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); len(envCmd) > 0 {
178                 cmd = append(cmd, envCmd...)
179                 cmd = append(cmd, "&&")
180         }
181
182         cmd = append(cmd, "cd", prj.GetFullPath(args.RPath), "&&", args.Cmd)
183         if len(args.Args) > 0 {
184                 cmd = append(cmd, args.Args...)
185         }
186
187         // Append client project dir to environment
188         args.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.RelativePath)
189
190         s.log.Debugf("Execute [Cmd ID %d]: %v", cmdID, cmd)
191
192         data := make(map[string]interface{})
193         data["ID"] = prj.ID
194         data["RootPath"] = prj.RootPath
195         data["ExitImmediate"] = args.ExitImmediate
196
197         err := common.ExecPipeWs(cmd, args.Env, sop, sess.ID, cmdID, execTmo, s.log, oCB, eCB, &data)
198         if err != nil {
199                 common.APIError(c, err.Error())
200                 return
201         }
202
203         c.JSON(http.StatusOK,
204                 gin.H{
205                         "status": "OK",
206                         "cmdID":  cmdID,
207                 })
208 }