12 "github.com/gin-gonic/gin"
13 common "github.com/iotbzh/xds-common/golib"
14 "github.com/iotbzh/xds-common/golib/eows"
15 "github.com/iotbzh/xds-server/lib/xsapiv1"
21 // ExecCmd executes remotely a command
22 func (s *APIService) execCmd(c *gin.Context) {
23 var gdbPty, gdbTty *os.File
25 var args xsapiv1.ExecArgs
26 if c.BindJSON(&args) != nil {
27 common.APIError(c, "Invalid arguments")
31 // TODO: add permission ?
33 // Retrieve session info
34 sess := s.sessions.Get(c)
36 common.APIError(c, "Unknown sessions")
41 common.APIError(c, "Websocket not established")
45 // Allow to pass id in url (/exec/:id) or as JSON argument
46 idArg := c.Param("id")
51 common.APIError(c, "Invalid id")
54 id, err := s.mfolders.ResolveID(idArg)
56 common.APIError(c, err.Error())
59 f := s.mfolders.Get(id)
61 common.APIError(c, "Unknown id")
65 prj := fld.GetConfig()
69 // Setup env var regarding Sdk ID (used for example to setup cross toolchain)
70 if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); len(envCmd) > 0 {
71 cmd = append(cmd, envCmd...)
72 cmd = append(cmd, "&&")
74 // It's an error if no envcmd found while a sdkid has been provided
76 common.APIError(c, "Unknown sdkid")
81 cmd = append(cmd, "cd", "\""+fld.GetFullPath(args.RPath)+"\"")
82 // FIXME - add 'exec' prevents to use syntax:
83 // xds-exec -l debug -c xds-config.env -- "cd build && cmake .."
84 // but exec is mandatory to allow to pass correctly signals
85 // As workaround, exec is set for now on client side (eg. in xds-gdb)
86 //cmd = append(cmd, "&&", "exec", args.Cmd)
87 cmd = append(cmd, "&&", args.Cmd)
89 // Process command arguments
90 cmdArgs := make([]string, len(args.Args)+1)
92 // Copy and Translate path from client to server
93 for _, aa := range args.Args {
94 if strings.Contains(aa, prj.ClientPath) {
95 cmdArgs = append(cmdArgs, fld.ConvPathCli2Svr(aa))
97 cmdArgs = append(cmdArgs, aa)
101 // Allocate pts if tty if used
103 gdbPty, gdbTty, err = pty.Open()
105 common.APIError(c, err.Error())
109 s.Log.Debugf("Client command tty: %v %v\n", gdbTty.Name(), gdbTty.Name())
110 cmdArgs = append(cmdArgs, "--tty="+gdbTty.Name())
113 // Unique ID for each commands
114 if args.CmdID == "" {
115 args.CmdID = s.Config.ServerUID[:18] + "_" + strconv.Itoa(execCommandID)
119 // Create new execution over WS context
120 execWS := eows.New(strings.Join(cmd, " "), cmdArgs, sop, sess.ID, args.CmdID)
123 // Append client project dir to environment
124 execWS.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.ClientPath)
126 // Set command execution timeout
127 if args.CmdTimeout == 0 {
128 // 0 : default timeout
129 // TODO get default timeout from config.json file
130 execWS.CmdExecTimeout = 24 * 60 * 60 // 1 day
132 execWS.CmdExecTimeout = args.CmdTimeout
135 // Define callback for input (stdin)
136 execWS.InputEvent = xsapiv1.ExecInEvent
137 execWS.InputCB = func(e *eows.ExecOverWS, stdin string) (string, error) {
138 s.Log.Debugf("STDIN <<%v>>", strings.Replace(stdin, "\n", "\\n", -1))
141 if len(stdin) == 1 && stdin == "\x04" {
143 errMsg := fmt.Errorf("close stdin: %v", stdin)
149 prjID := (*data)["ID"].(string)
150 f := s.mfolders.Get(prjID)
152 s.Log.Errorf("InputCB: Cannot get folder ID %s", prjID)
154 // Translate paths from client to server
155 stdin = (*f).ConvPathCli2Svr(stdin)
161 // Define callback for output (stdout+stderr)
162 execWS.OutputCB = func(e *eows.ExecOverWS, stdout, stderr string) {
163 // IO socket can be nil when disconnected
164 so := s.sessions.IOSocketGet(e.Sid)
166 s.Log.Infof("%s not emitted: WS closed (sid:%s, msgid:%s)", xsapiv1.ExecOutEvent, e.Sid, e.CmdID)
170 // Retrieve project ID and RootPath
172 prjID := (*data)["ID"].(string)
173 gdbServerTTY := (*data)["gdbServerTTY"].(string)
175 f := s.mfolders.Get(prjID)
177 s.Log.Errorf("OutputCB: Cannot get folder ID %s", prjID)
179 // Translate paths from server to client
180 stdout = (*f).ConvPathSvr2Cli(stdout)
181 stderr = (*f).ConvPathSvr2Cli(stderr)
184 s.Log.Debugf("%s emitted - WS sid[4:] %s - id:%s - prjID:%s", xsapiv1.ExecOutEvent, e.Sid[4:], e.CmdID, prjID)
186 s.Log.Debugf("STDOUT <<%v>>", strings.Replace(stdout, "\n", "\\n", -1))
189 s.Log.Debugf("STDERR <<%v>>", strings.Replace(stderr, "\n", "\\n", -1))
192 // FIXME replace by .BroadcastTo a room
193 err := (*so).Emit(xsapiv1.ExecOutEvent, xsapiv1.ExecOutMsg{
195 Timestamp: time.Now().String(),
200 s.Log.Errorf("WS Emit : %v", err)
203 // XXX - Workaround due to gdbserver bug that doesn't redirect
204 // inferior output (https://bugs.eclipse.org/bugs/show_bug.cgi?id=437532#c13)
205 if gdbServerTTY == "workaround" && len(stdout) > 1 && stdout[0] == '&' {
207 // Extract and cleanup string like &"bla bla\n"
208 re := regexp.MustCompile("&\"(.*)\"")
209 rer := re.FindAllStringSubmatch(stdout, -1)
211 if rer != nil && len(rer) > 0 {
212 for _, o := range rer {
214 out = strings.Replace(o[1], "\\n", "\n", -1)
215 out = strings.Replace(out, "\\r", "\r", -1)
216 out = strings.Replace(out, "\\t", "\t", -1)
218 s.Log.Debugf("STDOUT INFERIOR: <<%v>>", out)
219 err := (*so).Emit(xsapiv1.ExecInferiorOutEvent, xsapiv1.ExecOutMsg{
221 Timestamp: time.Now().String(),
226 s.Log.Errorf("WS Emit : %v", err)
231 s.Log.Errorf("INFERIOR out parsing error: stdout=<%v>", stdout)
236 // Define callback for output
237 execWS.ExitCB = func(e *eows.ExecOverWS, code int, err error) {
238 s.Log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", e.CmdID, code, err)
250 // IO socket can be nil when disconnected
251 so := s.sessions.IOSocketGet(e.Sid)
253 s.Log.Infof("%s not emitted - WS closed (id:%s)", xsapiv1.ExecExitEvent, e.CmdID)
257 // Retrieve project ID and RootPath
259 prjID := (*data)["ID"].(string)
260 exitImm := (*data)["ExitImmediate"].(bool)
262 // XXX - workaround to be sure that Syncthing detected all changes
263 if err := s.mfolders.ForceSync(prjID); err != nil {
264 s.Log.Errorf("Error while syncing folder %s: %v", prjID, err)
267 // Wait end of file sync
268 // FIXME pass as argument
270 for t := tmo; t > 0; t-- {
271 s.Log.Debugf("Wait file in-sync for %s (%d/%d)", prjID, t, tmo)
272 if sync, err := s.mfolders.IsFolderInSync(prjID); sync || err != nil {
274 s.Log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err)
278 time.Sleep(time.Second)
280 s.Log.Debugf("OK file are synchronized.")
283 // FIXME replace by .BroadcastTo a room
284 errSoEmit := (*so).Emit(xsapiv1.ExecExitEvent, xsapiv1.ExecExitMsg{
286 Timestamp: time.Now().String(),
290 if errSoEmit != nil {
291 s.Log.Errorf("WS Emit : %v", errSoEmit)
295 // User data (used within callbacks)
296 data := make(map[string]interface{})
298 data["ExitImmediate"] = args.ExitImmediate
299 if args.TTY && args.TTYGdbserverFix {
300 data["gdbServerTTY"] = "workaround"
302 data["gdbServerTTY"] = ""
304 execWS.UserData = &data
306 // Start command execution
307 s.Log.Infof("Execute [Cmd ID %s]: %v %v", execWS.CmdID, execWS.Cmd, execWS.Args)
311 common.APIError(c, err.Error())
315 c.JSON(http.StatusOK, xsapiv1.ExecResult{Status: "OK", CmdID: execWS.CmdID})
318 // ExecCmd executes remotely a command
319 func (s *APIService) execSignalCmd(c *gin.Context) {
320 var args xsapiv1.ExecSignalArgs
322 if c.BindJSON(&args) != nil {
323 common.APIError(c, "Invalid arguments")
327 s.Log.Debugf("Signal %s for command ID %s", args.Signal, args.CmdID)
329 e := eows.GetEows(args.CmdID)
331 common.APIError(c, "unknown cmdID")
335 err := e.Signal(args.Signal)
337 common.APIError(c, err.Error())
341 c.JSON(http.StatusOK, xsapiv1.ExecSigResult{Status: "OK", CmdID: args.CmdID})