Fixed spaces handling in RPath - SPEC1116
[src/xds/xds-server.git] / lib / apiv1 / exec.go
1 package apiv1
2
3 import (
4         "fmt"
5         "net/http"
6         "os"
7         "regexp"
8         "strconv"
9         "strings"
10         "time"
11
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/kr/pty"
16 )
17
18 type (
19         // ExecArgs JSON parameters of /exec command
20         ExecArgs struct {
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
31         }
32
33         // ExecInMsg Message used to received input characters (stdin)
34         ExecInMsg struct {
35                 CmdID     string `json:"cmdID"`
36                 Timestamp string `json:"timestamp"`
37                 Stdin     string `json:"stdin"`
38         }
39
40         // ExecOutMsg Message used to send output characters (stdout+stderr)
41         ExecOutMsg struct {
42                 CmdID     string `json:"cmdID"`
43                 Timestamp string `json:"timestamp"`
44                 Stdout    string `json:"stdout"`
45                 Stderr    string `json:"stderr"`
46         }
47
48         // ExecExitMsg Message sent when executed command exited
49         ExecExitMsg struct {
50                 CmdID     string `json:"cmdID"`
51                 Timestamp string `json:"timestamp"`
52                 Code      int    `json:"code"`
53                 Error     error  `json:"error"`
54         }
55
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
60         }
61 )
62
63 const (
64         // ExecInEvent Event send in WS when characters are sent (stdin)
65         ExecInEvent = "exec:input"
66
67         // ExecOutEvent Event send in WS when characters are received (stdout or stderr)
68         ExecOutEvent = "exec:output"
69
70         // ExecExitEvent Event send in WS when program exited
71         ExecExitEvent = "exec:exit"
72
73         // ExecInferiorInEvent Event send in WS when characters are sent to an inferior (used by gdb inferior/tty)
74         ExecInferiorInEvent = "exec:inferior-input"
75
76         // ExecInferiorOutEvent Event send in WS when characters are received by an inferior
77         ExecInferiorOutEvent = "exec:inferior-output"
78 )
79
80 var execCommandID = 1
81
82 // ExecCmd executes remotely a command
83 func (s *APIService) execCmd(c *gin.Context) {
84         var gdbPty, gdbTty *os.File
85         var err error
86         var args ExecArgs
87         if c.BindJSON(&args) != nil {
88                 common.APIError(c, "Invalid arguments")
89                 return
90         }
91
92         // TODO: add permission ?
93
94         // Retrieve session info
95         sess := s.sessions.Get(c)
96         if sess == nil {
97                 common.APIError(c, "Unknown sessions")
98                 return
99         }
100         sop := sess.IOSocket
101         if sop == nil {
102                 common.APIError(c, "Websocket not established")
103                 return
104         }
105
106         // Allow to pass id in url (/exec/:id) or as JSON argument
107         id := c.Param("id")
108         if id == "" {
109                 id = args.ID
110         }
111         if id == "" {
112                 common.APIError(c, "Invalid id")
113                 return
114         }
115
116         f := s.mfolders.Get(id)
117         if f == nil {
118                 common.APIError(c, "Unknown id")
119                 return
120         }
121         fld := *f
122         prj := fld.GetConfig()
123
124         // Build command line
125         cmd := []string{}
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, "&&")
130         } else {
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")
134                         return
135                 }
136         }
137
138         cmd = append(cmd, "cd", "\""+fld.GetFullPath(args.RPath)+"\"")
139         // FIXME - add 'exec' prevents to use syntax:
140         //       xds-exec -l debug -c xds-config.env -- "cd build && cmake .."
141         //  but exec is mandatory to allow to pass correctly signals
142         //  As workaround, exec is set for now on client side (eg. in xds-gdb)
143         //cmd = append(cmd, "&&", "exec", args.Cmd)
144         cmd = append(cmd, "&&", args.Cmd)
145
146         // Process command arguments
147         cmdArgs := make([]string, len(args.Args)+1)
148
149         // Copy and Translate path from client to server
150         for _, aa := range args.Args {
151                 if strings.Contains(aa, prj.ClientPath) {
152                         cmdArgs = append(cmdArgs, fld.ConvPathCli2Svr(aa))
153                 } else {
154                         cmdArgs = append(cmdArgs, aa)
155                 }
156         }
157
158         // Allocate pts if tty if used
159         if args.TTY {
160                 gdbPty, gdbTty, err = pty.Open()
161                 if err != nil {
162                         common.APIError(c, err.Error())
163                         return
164                 }
165
166                 s.log.Debugf("Client command tty: %v %v\n", gdbTty.Name(), gdbTty.Name())
167                 cmdArgs = append(cmdArgs, "--tty="+gdbTty.Name())
168         }
169
170         // Unique ID for each commands
171         cmdID := strconv.Itoa(execCommandID)
172         execCommandID++
173
174         // Create new execution over WS context
175         execWS := eows.New(strings.Join(cmd, " "), cmdArgs, sop, sess.ID, cmdID)
176         execWS.Log = s.log
177
178         // Append client project dir to environment
179         execWS.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.ClientPath)
180
181         // Set command execution timeout
182         if args.CmdTimeout == 0 {
183                 // 0 : default timeout
184                 // TODO get default timeout from config.json file
185                 execWS.CmdExecTimeout = 24 * 60 * 60 // 1 day
186         } else {
187                 execWS.CmdExecTimeout = args.CmdTimeout
188         }
189
190         // Define callback for input (stdin)
191         execWS.InputEvent = ExecInEvent
192         execWS.InputCB = func(e *eows.ExecOverWS, stdin string) (string, error) {
193                 s.log.Debugf("STDIN <<%v>>", strings.Replace(stdin, "\n", "\\n", -1))
194
195                 // Handle Ctrl-D
196                 if len(stdin) == 1 && stdin == "\x04" {
197                         // Close stdin
198                         errMsg := fmt.Errorf("close stdin: %v", stdin)
199                         return "", errMsg
200                 }
201
202                 // Set correct path
203                 data := e.UserData
204                 prjID := (*data)["ID"].(string)
205                 f := s.mfolders.Get(prjID)
206                 if f == nil {
207                         s.log.Errorf("InputCB: Cannot get folder ID %s", prjID)
208                 } else {
209                         // Translate paths from client to server
210                         stdin = (*f).ConvPathCli2Svr(stdin)
211                 }
212
213                 return stdin, nil
214         }
215
216         // Define callback for output (stdout+stderr)
217         execWS.OutputCB = func(e *eows.ExecOverWS, stdout, stderr string) {
218                 // IO socket can be nil when disconnected
219                 so := s.sessions.IOSocketGet(e.Sid)
220                 if so == nil {
221                         s.log.Infof("%s not emitted: WS closed (sid:%s, msgid:%s)", ExecOutEvent, e.Sid, e.CmdID)
222                         return
223                 }
224
225                 // Retrieve project ID and RootPath
226                 data := e.UserData
227                 prjID := (*data)["ID"].(string)
228                 gdbServerTTY := (*data)["gdbServerTTY"].(string)
229
230                 f := s.mfolders.Get(prjID)
231                 if f == nil {
232                         s.log.Errorf("OutputCB: Cannot get folder ID %s", prjID)
233                 } else {
234                         // Translate paths from server to client
235                         stdout = (*f).ConvPathSvr2Cli(stdout)
236                         stderr = (*f).ConvPathSvr2Cli(stderr)
237                 }
238
239                 s.log.Debugf("%s emitted - WS sid[4:] %s - id:%s - prjID:%s", ExecOutEvent, e.Sid[4:], e.CmdID, prjID)
240                 if stdout != "" {
241                         s.log.Debugf("STDOUT <<%v>>", strings.Replace(stdout, "\n", "\\n", -1))
242                 }
243                 if stderr != "" {
244                         s.log.Debugf("STDERR <<%v>>", strings.Replace(stderr, "\n", "\\n", -1))
245                 }
246
247                 // FIXME replace by .BroadcastTo a room
248                 err := (*so).Emit(ExecOutEvent, ExecOutMsg{
249                         CmdID:     e.CmdID,
250                         Timestamp: time.Now().String(),
251                         Stdout:    stdout,
252                         Stderr:    stderr,
253                 })
254                 if err != nil {
255                         s.log.Errorf("WS Emit : %v", err)
256                 }
257
258                 // XXX - Workaround due to gdbserver bug that doesn't redirect
259                 // inferior output (https://bugs.eclipse.org/bugs/show_bug.cgi?id=437532#c13)
260                 if gdbServerTTY == "workaround" && len(stdout) > 1 && stdout[0] == '&' {
261
262                         // Extract and cleanup string like &"bla bla\n"
263                         re := regexp.MustCompile("&\"(.*)\"")
264                         rer := re.FindAllStringSubmatch(stdout, -1)
265                         out := ""
266                         if rer != nil && len(rer) > 0 {
267                                 for _, o := range rer {
268                                         if len(o) >= 1 {
269                                                 out = strings.Replace(o[1], "\\n", "\n", -1)
270                                                 out = strings.Replace(out, "\\r", "\r", -1)
271                                                 out = strings.Replace(out, "\\t", "\t", -1)
272
273                                                 s.log.Debugf("STDOUT INFERIOR: <<%v>>", out)
274                                                 err := (*so).Emit(ExecInferiorOutEvent, ExecOutMsg{
275                                                         CmdID:     e.CmdID,
276                                                         Timestamp: time.Now().String(),
277                                                         Stdout:    out,
278                                                         Stderr:    "",
279                                                 })
280                                                 if err != nil {
281                                                         s.log.Errorf("WS Emit : %v", err)
282                                                 }
283                                         }
284                                 }
285                         } else {
286                                 s.log.Errorf("INFERIOR out parsing error: stdout=<%v>", stdout)
287                         }
288                 }
289         }
290
291         // Define callback for output
292         execWS.ExitCB = func(e *eows.ExecOverWS, code int, err error) {
293                 s.log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", e.CmdID, code, err)
294
295                 // IO socket can be nil when disconnected
296                 so := s.sessions.IOSocketGet(e.Sid)
297                 if so == nil {
298                         s.log.Infof("%s not emitted - WS closed (id:%s)", ExecExitEvent, e.CmdID)
299                         return
300                 }
301
302                 // Retrieve project ID and RootPath
303                 data := e.UserData
304                 prjID := (*data)["ID"].(string)
305                 exitImm := (*data)["ExitImmediate"].(bool)
306
307                 // XXX - workaround to be sure that Syncthing detected all changes
308                 if err := s.mfolders.ForceSync(prjID); err != nil {
309                         s.log.Errorf("Error while syncing folder %s: %v", prjID, err)
310                 }
311                 if !exitImm {
312                         // Wait end of file sync
313                         // FIXME pass as argument
314                         tmo := 60
315                         for t := tmo; t > 0; t-- {
316                                 s.log.Debugf("Wait file in-sync for %s (%d/%d)", prjID, t, tmo)
317                                 if sync, err := s.mfolders.IsFolderInSync(prjID); sync || err != nil {
318                                         if err != nil {
319                                                 s.log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err)
320                                         }
321                                         break
322                                 }
323                                 time.Sleep(time.Second)
324                         }
325                 }
326
327                 // Close client tty
328                 if gdbPty != nil {
329                         gdbPty.Close()
330                 }
331                 if gdbTty != nil {
332                         gdbTty.Close()
333                 }
334
335                 // FIXME replace by .BroadcastTo a room
336                 errSoEmit := (*so).Emit(ExecExitEvent, ExecExitMsg{
337                         CmdID:     e.CmdID,
338                         Timestamp: time.Now().String(),
339                         Code:      code,
340                         Error:     err,
341                 })
342                 if errSoEmit != nil {
343                         s.log.Errorf("WS Emit : %v", errSoEmit)
344                 }
345         }
346
347         // User data (used within callbacks)
348         data := make(map[string]interface{})
349         data["ID"] = prj.ID
350         data["ExitImmediate"] = args.ExitImmediate
351         if args.TTY && args.TTYGdbserverFix {
352                 data["gdbServerTTY"] = "workaround"
353         } else {
354                 data["gdbServerTTY"] = ""
355         }
356         execWS.UserData = &data
357
358         // Start command execution
359         s.log.Infof("Execute [Cmd ID %s]: %v %v", execWS.CmdID, execWS.Cmd, execWS.Args)
360
361         err = execWS.Start()
362         if err != nil {
363                 common.APIError(c, err.Error())
364                 return
365         }
366
367         c.JSON(http.StatusOK,
368                 gin.H{
369                         "status": "OK",
370                         "cmdID":  execWS.CmdID,
371                 })
372 }
373
374 // ExecCmd executes remotely a command
375 func (s *APIService) execSignalCmd(c *gin.Context) {
376         var args ExecSignalArgs
377
378         if c.BindJSON(&args) != nil {
379                 common.APIError(c, "Invalid arguments")
380                 return
381         }
382
383         s.log.Debugf("Signal %s for command ID %s", args.Signal, args.CmdID)
384
385         e := eows.GetEows(args.CmdID)
386         if e == nil {
387                 common.APIError(c, "unknown cmdID")
388                 return
389         }
390
391         err := e.Signal(args.Signal)
392         if err != nil {
393                 common.APIError(c, err.Error())
394                 return
395         }
396
397         c.JSON(http.StatusOK,
398                 gin.H{
399                         "status": "OK",
400                 })
401 }