12 "github.com/gin-gonic/gin"
13 common "github.com/iotbzh/xds-common/golib"
14 "github.com/iotbzh/xds-common/golib/eows"
19 // ExecArgs JSON parameters of /exec command
21 ID string `json:"id" binding:"required"`
22 SdkID string `json:"sdkID"` // sdk ID to use for setting env
23 CmdID string `json:"cmdID"` // command unique ID
24 Cmd string `json:"cmd" binding:"required"`
25 Args []string `json:"args"`
26 Env []string `json:"env"`
27 RPath string `json:"rpath"` // relative path into project
28 TTY bool `json:"tty"` // Use a tty, specific to gdb --tty option
29 TTYGdbserverFix bool `json:"ttyGdbserverFix"` // Set to true to activate gdbserver workaround about inferior output
30 ExitImmediate bool `json:"exitImmediate"` // when true, exit event sent immediately when command exited (IOW, don't wait file synchronization)
31 CmdTimeout int `json:"timeout"` // command completion timeout in Second
34 // ExecRes JSON result of /exec command
36 Status string `json:"status"` // status OK
37 CmdID string `json:"cmdID"` // command unique ID
40 // ExecSigRes JSON result of /signal command
42 Status string `json:"status"` // status OK
43 CmdID string `json:"cmdID"` // command unique ID
46 // ExecInMsg Message used to received input characters (stdin)
48 CmdID string `json:"cmdID"`
49 Timestamp string `json:"timestamp"`
50 Stdin string `json:"stdin"`
53 // ExecOutMsg Message used to send output characters (stdout+stderr)
55 CmdID string `json:"cmdID"`
56 Timestamp string `json:"timestamp"`
57 Stdout string `json:"stdout"`
58 Stderr string `json:"stderr"`
61 // ExecExitMsg Message sent when executed command exited
63 CmdID string `json:"cmdID"`
64 Timestamp string `json:"timestamp"`
65 Code int `json:"code"`
66 Error error `json:"error"`
69 // ExecSignalArgs JSON parameters of /exec/signal command
70 ExecSignalArgs struct {
71 CmdID string `json:"cmdID" binding:"required"` // command id
72 Signal string `json:"signal" binding:"required"` // signal number
77 // ExecInEvent Event send in WS when characters are sent (stdin)
78 ExecInEvent = "exec:input"
80 // ExecOutEvent Event send in WS when characters are received (stdout or stderr)
81 ExecOutEvent = "exec:output"
83 // ExecExitEvent Event send in WS when program exited
84 ExecExitEvent = "exec:exit"
86 // ExecInferiorInEvent Event send in WS when characters are sent to an inferior (used by gdb inferior/tty)
87 ExecInferiorInEvent = "exec:inferior-input"
89 // ExecInferiorOutEvent Event send in WS when characters are received by an inferior
90 ExecInferiorOutEvent = "exec:inferior-output"
95 // ExecCmd executes remotely a command
96 func (s *APIService) execCmd(c *gin.Context) {
97 var gdbPty, gdbTty *os.File
100 if c.BindJSON(&args) != nil {
101 common.APIError(c, "Invalid arguments")
105 // TODO: add permission ?
107 // Retrieve session info
108 sess := s.sessions.Get(c)
110 common.APIError(c, "Unknown sessions")
115 common.APIError(c, "Websocket not established")
119 // Allow to pass id in url (/exec/:id) or as JSON argument
120 idArg := c.Param("id")
125 common.APIError(c, "Invalid id")
128 id, err := s.mfolders.ResolveID(idArg)
130 common.APIError(c, err.Error())
133 f := s.mfolders.Get(id)
135 common.APIError(c, "Unknown id")
139 prj := fld.GetConfig()
141 // Build command line
143 // Setup env var regarding Sdk ID (used for example to setup cross toolchain)
144 if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); len(envCmd) > 0 {
145 cmd = append(cmd, envCmd...)
146 cmd = append(cmd, "&&")
148 // It's an error if no envcmd found while a sdkid has been provided
149 if args.SdkID != "" {
150 common.APIError(c, "Unknown sdkid")
155 cmd = append(cmd, "cd", "\""+fld.GetFullPath(args.RPath)+"\"")
156 // FIXME - add 'exec' prevents to use syntax:
157 // xds-exec -l debug -c xds-config.env -- "cd build && cmake .."
158 // but exec is mandatory to allow to pass correctly signals
159 // As workaround, exec is set for now on client side (eg. in xds-gdb)
160 //cmd = append(cmd, "&&", "exec", args.Cmd)
161 cmd = append(cmd, "&&", args.Cmd)
163 // Process command arguments
164 cmdArgs := make([]string, len(args.Args)+1)
166 // Copy and Translate path from client to server
167 for _, aa := range args.Args {
168 if strings.Contains(aa, prj.ClientPath) {
169 cmdArgs = append(cmdArgs, fld.ConvPathCli2Svr(aa))
171 cmdArgs = append(cmdArgs, aa)
175 // Allocate pts if tty if used
177 gdbPty, gdbTty, err = pty.Open()
179 common.APIError(c, err.Error())
183 s.log.Debugf("Client command tty: %v %v\n", gdbTty.Name(), gdbTty.Name())
184 cmdArgs = append(cmdArgs, "--tty="+gdbTty.Name())
187 // Unique ID for each commands
188 if args.CmdID == "" {
189 args.CmdID = s.cfg.ServerUID[:18] + "_" + strconv.Itoa(execCommandID)
193 // Create new execution over WS context
194 execWS := eows.New(strings.Join(cmd, " "), cmdArgs, sop, sess.ID, args.CmdID)
197 // Append client project dir to environment
198 execWS.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.ClientPath)
200 // Set command execution timeout
201 if args.CmdTimeout == 0 {
202 // 0 : default timeout
203 // TODO get default timeout from config.json file
204 execWS.CmdExecTimeout = 24 * 60 * 60 // 1 day
206 execWS.CmdExecTimeout = args.CmdTimeout
209 // Define callback for input (stdin)
210 execWS.InputEvent = ExecInEvent
211 execWS.InputCB = func(e *eows.ExecOverWS, stdin string) (string, error) {
212 s.log.Debugf("STDIN <<%v>>", strings.Replace(stdin, "\n", "\\n", -1))
215 if len(stdin) == 1 && stdin == "\x04" {
217 errMsg := fmt.Errorf("close stdin: %v", stdin)
223 prjID := (*data)["ID"].(string)
224 f := s.mfolders.Get(prjID)
226 s.log.Errorf("InputCB: Cannot get folder ID %s", prjID)
228 // Translate paths from client to server
229 stdin = (*f).ConvPathCli2Svr(stdin)
235 // Define callback for output (stdout+stderr)
236 execWS.OutputCB = func(e *eows.ExecOverWS, stdout, stderr string) {
237 // IO socket can be nil when disconnected
238 so := s.sessions.IOSocketGet(e.Sid)
240 s.log.Infof("%s not emitted: WS closed (sid:%s, msgid:%s)", ExecOutEvent, e.Sid, e.CmdID)
244 // Retrieve project ID and RootPath
246 prjID := (*data)["ID"].(string)
247 gdbServerTTY := (*data)["gdbServerTTY"].(string)
249 f := s.mfolders.Get(prjID)
251 s.log.Errorf("OutputCB: Cannot get folder ID %s", prjID)
253 // Translate paths from server to client
254 stdout = (*f).ConvPathSvr2Cli(stdout)
255 stderr = (*f).ConvPathSvr2Cli(stderr)
258 s.log.Debugf("%s emitted - WS sid[4:] %s - id:%s - prjID:%s", ExecOutEvent, e.Sid[4:], e.CmdID, prjID)
260 s.log.Debugf("STDOUT <<%v>>", strings.Replace(stdout, "\n", "\\n", -1))
263 s.log.Debugf("STDERR <<%v>>", strings.Replace(stderr, "\n", "\\n", -1))
266 // FIXME replace by .BroadcastTo a room
267 err := (*so).Emit(ExecOutEvent, ExecOutMsg{
269 Timestamp: time.Now().String(),
274 s.log.Errorf("WS Emit : %v", err)
277 // XXX - Workaround due to gdbserver bug that doesn't redirect
278 // inferior output (https://bugs.eclipse.org/bugs/show_bug.cgi?id=437532#c13)
279 if gdbServerTTY == "workaround" && len(stdout) > 1 && stdout[0] == '&' {
281 // Extract and cleanup string like &"bla bla\n"
282 re := regexp.MustCompile("&\"(.*)\"")
283 rer := re.FindAllStringSubmatch(stdout, -1)
285 if rer != nil && len(rer) > 0 {
286 for _, o := range rer {
288 out = strings.Replace(o[1], "\\n", "\n", -1)
289 out = strings.Replace(out, "\\r", "\r", -1)
290 out = strings.Replace(out, "\\t", "\t", -1)
292 s.log.Debugf("STDOUT INFERIOR: <<%v>>", out)
293 err := (*so).Emit(ExecInferiorOutEvent, ExecOutMsg{
295 Timestamp: time.Now().String(),
300 s.log.Errorf("WS Emit : %v", err)
305 s.log.Errorf("INFERIOR out parsing error: stdout=<%v>", stdout)
310 // Define callback for output
311 execWS.ExitCB = func(e *eows.ExecOverWS, code int, err error) {
312 s.log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", e.CmdID, code, err)
324 // IO socket can be nil when disconnected
325 so := s.sessions.IOSocketGet(e.Sid)
327 s.log.Infof("%s not emitted - WS closed (id:%s)", ExecExitEvent, e.CmdID)
331 // Retrieve project ID and RootPath
333 prjID := (*data)["ID"].(string)
334 exitImm := (*data)["ExitImmediate"].(bool)
336 // XXX - workaround to be sure that Syncthing detected all changes
337 if err := s.mfolders.ForceSync(prjID); err != nil {
338 s.log.Errorf("Error while syncing folder %s: %v", prjID, err)
341 // Wait end of file sync
342 // FIXME pass as argument
344 for t := tmo; t > 0; t-- {
345 s.log.Debugf("Wait file in-sync for %s (%d/%d)", prjID, t, tmo)
346 if sync, err := s.mfolders.IsFolderInSync(prjID); sync || err != nil {
348 s.log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err)
352 time.Sleep(time.Second)
354 s.log.Debugf("OK file are synchronized.")
357 // FIXME replace by .BroadcastTo a room
358 errSoEmit := (*so).Emit(ExecExitEvent, ExecExitMsg{
360 Timestamp: time.Now().String(),
364 if errSoEmit != nil {
365 s.log.Errorf("WS Emit : %v", errSoEmit)
369 // User data (used within callbacks)
370 data := make(map[string]interface{})
372 data["ExitImmediate"] = args.ExitImmediate
373 if args.TTY && args.TTYGdbserverFix {
374 data["gdbServerTTY"] = "workaround"
376 data["gdbServerTTY"] = ""
378 execWS.UserData = &data
380 // Start command execution
381 s.log.Infof("Execute [Cmd ID %s]: %v %v", execWS.CmdID, execWS.Cmd, execWS.Args)
385 common.APIError(c, err.Error())
389 c.JSON(http.StatusOK, ExecRes{Status: "OK", CmdID: execWS.CmdID})
392 // ExecCmd executes remotely a command
393 func (s *APIService) execSignalCmd(c *gin.Context) {
394 var args ExecSignalArgs
396 if c.BindJSON(&args) != nil {
397 common.APIError(c, "Invalid arguments")
401 s.log.Debugf("Signal %s for command ID %s", args.Signal, args.CmdID)
403 e := eows.GetEows(args.CmdID)
405 common.APIError(c, "unknown cmdID")
409 err := e.Signal(args.Signal)
411 common.APIError(c, err.Error())
415 c.JSON(http.StatusOK, ExecSigRes{Status: "OK", CmdID: args.CmdID})