11 "github.com/gin-gonic/gin"
12 common "github.com/iotbzh/xds-common/golib"
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
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"`
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"`
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
49 // ExecOutEvent Event send in WS when characters are received
50 const ExecOutEvent = "exec:output"
52 // ExecExitEvent Event send in WS when program exited
53 const ExecExitEvent = "exec:exit"
57 // ExecCmd executes remotely a command
58 func (s *APIService) execCmd(c *gin.Context) {
60 if c.BindJSON(&args) != nil {
61 common.APIError(c, "Invalid arguments")
65 // TODO: add permission ?
67 // Retrieve session info
68 sess := s.sessions.Get(c)
70 common.APIError(c, "Unknown sessions")
75 common.APIError(c, "Websocket not established")
79 // Allow to pass id in url (/exec/:id) or as JSON argument
85 common.APIError(c, "Invalid id")
89 prj := s.mfolder.GetFolderFromID(id)
91 common.APIError(c, "Unknown id")
95 execTmo := args.CmdTimeout
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
105 // Define callback for input
107 var iCB common.OnInputCB
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)
119 s.log.Infof("%s not emitted: WS closed - sid: %s - msg id:%d", ExecOutEvent, sid, id)
123 // Retrieve project ID and RootPath
124 prjID := (*data)["ID"].(string)
125 prjRootPath := (*data)["RootPath"].(string)
127 // Cleanup any references to internal rootpath in stdout & stderr
128 stdout = strings.Replace(stdout, prjRootPath, "", -1)
129 stderr = strings.Replace(stderr, prjRootPath, "", -1)
131 s.log.Debugf("%s emitted - WS sid %s - id:%d - prjID:%s", ExecOutEvent, sid, id, prjID)
133 fmt.Printf("SEB SEND out <%v>, err <%v>\n", stdout, stderr)
135 // FIXME replace by .BroadcastTo a room
136 err := (*so).Emit(ExecOutEvent, ExecOutMsg{
138 Timestamp: time.Now().String(),
143 s.log.Errorf("WS Emit : %v", err)
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)
151 // IO socket can be nil when disconnected
152 so := s.sessions.IOSocketGet(sid)
154 s.log.Infof("%s not emitted - WS closed (id:%d", ExecExitEvent, id)
158 // Retrieve project ID and RootPath
159 prjID := (*data)["ID"].(string)
160 exitImm := (*data)["ExitImmediate"].(bool)
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)
167 // Wait end of file sync
168 // FIXME pass as argument
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 {
174 s.log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err)
178 time.Sleep(time.Second)
182 // FIXME replace by .BroadcastTo a room
183 e := (*so).Emit(ExecExitEvent, ExecExitMsg{
185 Timestamp: time.Now().String(),
190 s.log.Errorf("WS Emit : %v", e)
194 cmdID := strconv.Itoa(execCommandID)
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, "&&")
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")
210 cmd = append(cmd, "cd", prj.GetFullPath(args.RPath), "&&", args.Cmd)
211 if len(args.Args) > 0 {
212 cmd = append(cmd, args.Args...)
215 // SEB Workaround for stderr issue (order not respected with stdout)
216 cmd = append(cmd, " 2>&1")
218 // Append client project dir to environment
219 args.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.RelativePath)
221 s.log.Debugf("Execute [Cmd ID %s]: %v", cmdID, cmd)
223 data := make(map[string]interface{})
225 data["RootPath"] = prj.RootPath
226 data["ExitImmediate"] = args.ExitImmediate
228 err := common.ExecPipeWs(cmd, args.Env, sop, sess.ID, cmdID, execTmo, s.log, oCB, eCB, &data)
230 common.APIError(c, err.Error())
234 c.JSON(http.StatusOK,
241 // ExecCmd executes remotely a command
242 func (s *APIService) execSignalCmd(c *gin.Context) {
243 var args ExecSignalArgs
245 if c.BindJSON(&args) != nil {
246 common.APIError(c, "Invalid arguments")
250 s.log.Debugf("Signal %s for command ID %s", args.Signal, args.CmdID)
251 err := common.ExecSignal(args.CmdID, args.Signal)
253 common.APIError(c, err.Error())
257 c.JSON(http.StatusOK,