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 Cmd string `json:"cmd" binding:"required"`
24 Args []string `json:"args"`
25 Env []string `json:"env"`
26 RPath string `json:"rpath"` // relative path into project
27 TTY bool `json:"tty"` // Use a tty, specific to gdb --tty option
28 TTYGdbserverFix bool `json:"ttyGdbserverFix"` // Set to true to activate gdbserver workaround about inferior output
29 ExitImmediate bool `json:"exitImmediate"` // when true, exit event sent immediately when command exited (IOW, don't wait file synchronization)
30 CmdTimeout int `json:"timeout"` // command completion timeout in Second
33 // ExecInMsg Message used to received input characters (stdin)
35 CmdID string `json:"cmdID"`
36 Timestamp string `json:"timestamp"`
37 Stdin string `json:"stdin"`
40 // ExecOutMsg Message used to send output characters (stdout+stderr)
42 CmdID string `json:"cmdID"`
43 Timestamp string `json:"timestamp"`
44 Stdout string `json:"stdout"`
45 Stderr string `json:"stderr"`
48 // ExecExitMsg Message sent when executed command exited
50 CmdID string `json:"cmdID"`
51 Timestamp string `json:"timestamp"`
52 Code int `json:"code"`
53 Error error `json:"error"`
56 // ExecSignalArgs JSON parameters of /exec/signal command
57 ExecSignalArgs struct {
58 CmdID string `json:"cmdID" binding:"required"` // command id
59 Signal string `json:"signal" binding:"required"` // signal number
64 // ExecInEvent Event send in WS when characters are sent (stdin)
65 ExecInEvent = "exec:input"
67 // ExecOutEvent Event send in WS when characters are received (stdout or stderr)
68 ExecOutEvent = "exec:output"
70 // ExecExitEvent Event send in WS when program exited
71 ExecExitEvent = "exec:exit"
73 // ExecInferiorInEvent Event send in WS when characters are sent to an inferior (used by gdb inferior/tty)
74 ExecInferiorInEvent = "exec:inferior-input"
76 // ExecInferiorOutEvent Event send in WS when characters are received by an inferior
77 ExecInferiorOutEvent = "exec:inferior-output"
82 // ExecCmd executes remotely a command
83 func (s *APIService) execCmd(c *gin.Context) {
84 var gdbPty, gdbTty *os.File
87 if c.BindJSON(&args) != nil {
88 common.APIError(c, "Invalid arguments")
92 // TODO: add permission ?
94 // Retrieve session info
95 sess := s.sessions.Get(c)
97 common.APIError(c, "Unknown sessions")
102 common.APIError(c, "Websocket not established")
106 // Allow to pass id in url (/exec/:id) or as JSON argument
112 common.APIError(c, "Invalid id")
116 f := s.mfolders.Get(id)
118 common.APIError(c, "Unknown id")
122 prj := folder.GetConfig()
124 // Build command line
126 // Setup env var regarding Sdk ID (used for example to setup cross toolchain)
127 if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); len(envCmd) > 0 {
128 cmd = append(cmd, envCmd...)
129 cmd = append(cmd, "&&")
131 // It's an error if no envcmd found while a sdkid has been provided
132 if args.SdkID != "" {
133 common.APIError(c, "Unknown sdkid")
138 // FIXME - SEB: exec prevents to use syntax:
139 // xds-exec -l debug -c xds-config.env -- "cd build && cmake .."
140 cmd = append(cmd, "cd", folder.GetFullPath(args.RPath))
141 cmd = append(cmd, "&&", "exec", args.Cmd)
143 // Process command arguments
144 cmdArgs := make([]string, len(args.Args)+1)
145 copy(cmdArgs, args.Args)
147 // Allocate pts if tty if used
149 gdbPty, gdbTty, err = pty.Open()
151 common.APIError(c, err.Error())
155 s.log.Debugf("Client command tty: %v %v\n", gdbTty.Name(), gdbTty.Name())
156 cmdArgs = append(cmdArgs, "--tty="+gdbTty.Name())
159 // Unique ID for each commands
160 cmdID := strconv.Itoa(execCommandID)
163 // Create new execution over WS context
164 execWS := eows.New(strings.Join(cmd, " "), cmdArgs, sop, sess.ID, cmdID)
167 // Append client project dir to environment
168 execWS.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.ClientPath)
170 // Set command execution timeout
171 if args.CmdTimeout == 0 {
172 // 0 : default timeout
173 // TODO get default timeout from config.json file
174 execWS.CmdExecTimeout = 24 * 60 * 60 // 1 day
176 execWS.CmdExecTimeout = args.CmdTimeout
179 // Define callback for input (stdin)
180 execWS.InputEvent = ExecInEvent
181 execWS.InputCB = func(e *eows.ExecOverWS, stdin string) (string, error) {
182 s.log.Debugf("STDIN <<%v>>", strings.Replace(stdin, "\n", "\\n", -1))
185 if len(stdin) == 1 && stdin == "\x04" {
187 errMsg := fmt.Errorf("close stdin: %v", stdin)
193 rootPath := (*data)["RootPath"].(string)
194 clientPath := (*data)["ClientPath"].(string)
195 stdin = strings.Replace(stdin, clientPath, rootPath+"/"+clientPath, -1)
200 // Define callback for output (stdout+stderr)
201 execWS.OutputCB = func(e *eows.ExecOverWS, stdout, stderr string) {
202 // IO socket can be nil when disconnected
203 so := s.sessions.IOSocketGet(e.Sid)
205 s.log.Infof("%s not emitted: WS closed (sid:%s, msgid:%s)", ExecOutEvent, e.Sid, e.CmdID)
209 // Retrieve project ID and RootPath
211 prjID := (*data)["ID"].(string)
212 prjRootPath := (*data)["RootPath"].(string)
213 gdbServerTTY := (*data)["gdbServerTTY"].(string)
215 // Cleanup any references to internal rootpath in stdout & stderr
216 stdout = strings.Replace(stdout, prjRootPath, "", -1)
217 stderr = strings.Replace(stderr, prjRootPath, "", -1)
219 s.log.Debugf("%s emitted - WS sid[4:] %s - id:%s - prjID:%s", ExecOutEvent, e.Sid[4:], e.CmdID, prjID)
221 s.log.Debugf("STDOUT <<%v>>", strings.Replace(stdout, "\n", "\\n", -1))
224 s.log.Debugf("STDERR <<%v>>", strings.Replace(stderr, "\n", "\\n", -1))
227 // FIXME replace by .BroadcastTo a room
228 err := (*so).Emit(ExecOutEvent, ExecOutMsg{
230 Timestamp: time.Now().String(),
235 s.log.Errorf("WS Emit : %v", err)
238 // XXX - Workaround due to gdbserver bug that doesn't redirect
239 // inferior output (https://bugs.eclipse.org/bugs/show_bug.cgi?id=437532#c13)
240 if gdbServerTTY == "workaround" && len(stdout) > 1 && stdout[0] == '&' {
242 // Extract and cleanup string like &"bla bla\n"
243 re := regexp.MustCompile("&\"(.*)\"")
244 rer := re.FindAllStringSubmatch(stdout, -1)
246 if rer != nil && len(rer) > 0 {
247 for _, o := range rer {
249 out = strings.Replace(o[1], "\\n", "\n", -1)
250 out = strings.Replace(out, "\\r", "\r", -1)
251 out = strings.Replace(out, "\\t", "\t", -1)
253 s.log.Debugf("STDOUT INFERIOR: <<%v>>", out)
254 err := (*so).Emit(ExecInferiorOutEvent, ExecOutMsg{
256 Timestamp: time.Now().String(),
261 s.log.Errorf("WS Emit : %v", err)
266 s.log.Errorf("INFERIOR out parsing error: stdout=<%v>", stdout)
271 // Define callback for output
272 execWS.ExitCB = func(e *eows.ExecOverWS, code int, err error) {
273 s.log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", e.CmdID, code, err)
275 // IO socket can be nil when disconnected
276 so := s.sessions.IOSocketGet(e.Sid)
278 s.log.Infof("%s not emitted - WS closed (id:%s)", ExecExitEvent, e.CmdID)
282 // Retrieve project ID and RootPath
284 prjID := (*data)["ID"].(string)
285 exitImm := (*data)["ExitImmediate"].(bool)
287 // XXX - workaround to be sure that Syncthing detected all changes
288 if err := s.mfolders.ForceSync(prjID); err != nil {
289 s.log.Errorf("Error while syncing folder %s: %v", prjID, err)
292 // Wait end of file sync
293 // FIXME pass as argument
295 for t := tmo; t > 0; t-- {
296 s.log.Debugf("Wait file in-sync for %s (%d/%d)", prjID, t, tmo)
297 if sync, err := s.mfolders.IsFolderInSync(prjID); sync || err != nil {
299 s.log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err)
303 time.Sleep(time.Second)
315 // FIXME replace by .BroadcastTo a room
316 errSoEmit := (*so).Emit(ExecExitEvent, ExecExitMsg{
318 Timestamp: time.Now().String(),
322 if errSoEmit != nil {
323 s.log.Errorf("WS Emit : %v", errSoEmit)
327 // User data (used within callbacks)
328 data := make(map[string]interface{})
330 data["RootPath"] = prj.RootPath
331 data["ClientPath"] = prj.ClientPath
332 data["ExitImmediate"] = args.ExitImmediate
333 if args.TTY && args.TTYGdbserverFix {
334 data["gdbServerTTY"] = "workaround"
336 data["gdbServerTTY"] = ""
338 execWS.UserData = &data
340 // Start command execution
341 s.log.Debugf("Execute [Cmd ID %s]: %v %v", execWS.CmdID, execWS.Cmd, execWS.Args)
345 common.APIError(c, err.Error())
349 c.JSON(http.StatusOK,
352 "cmdID": execWS.CmdID,
356 // ExecCmd executes remotely a command
357 func (s *APIService) execSignalCmd(c *gin.Context) {
358 var args ExecSignalArgs
360 if c.BindJSON(&args) != nil {
361 common.APIError(c, "Invalid arguments")
365 s.log.Debugf("Signal %s for command ID %s", args.Signal, args.CmdID)
367 e := eows.GetEows(args.CmdID)
369 common.APIError(c, "unknown cmdID")
373 err := e.Signal(args.Signal)
375 common.APIError(c, err.Error())
379 c.JSON(http.StatusOK,