Refit source files to have a public xs-apiv1 lib package.
[src/xds/xds-server.git] / lib / xdsserver / apiv1-exec.go
1 package xdsserver
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/iotbzh/xds-server/lib/xsapiv1"
16         "github.com/kr/pty"
17 )
18
19 var execCommandID = 1
20
21 // ExecCmd executes remotely a command
22 func (s *APIService) execCmd(c *gin.Context) {
23         var gdbPty, gdbTty *os.File
24         var err error
25         var args xsapiv1.ExecArgs
26         if c.BindJSON(&args) != nil {
27                 common.APIError(c, "Invalid arguments")
28                 return
29         }
30
31         // TODO: add permission ?
32
33         // Retrieve session info
34         sess := s.sessions.Get(c)
35         if sess == nil {
36                 common.APIError(c, "Unknown sessions")
37                 return
38         }
39         sop := sess.IOSocket
40         if sop == nil {
41                 common.APIError(c, "Websocket not established")
42                 return
43         }
44
45         // Allow to pass id in url (/exec/:id) or as JSON argument
46         idArg := c.Param("id")
47         if idArg == "" {
48                 idArg = args.ID
49         }
50         if idArg == "" {
51                 common.APIError(c, "Invalid id")
52                 return
53         }
54         id, err := s.mfolders.ResolveID(idArg)
55         if err != nil {
56                 common.APIError(c, err.Error())
57                 return
58         }
59         f := s.mfolders.Get(id)
60         if f == nil {
61                 common.APIError(c, "Unknown id")
62                 return
63         }
64         fld := *f
65         prj := fld.GetConfig()
66
67         // Build command line
68         cmd := []string{}
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, "&&")
73         } else {
74                 // It's an error if no envcmd found while a sdkid has been provided
75                 if args.SdkID != "" {
76                         common.APIError(c, "Unknown sdkid")
77                         return
78                 }
79         }
80
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)
88
89         // Process command arguments
90         cmdArgs := make([]string, len(args.Args)+1)
91
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))
96                 } else {
97                         cmdArgs = append(cmdArgs, aa)
98                 }
99         }
100
101         // Allocate pts if tty if used
102         if args.TTY {
103                 gdbPty, gdbTty, err = pty.Open()
104                 if err != nil {
105                         common.APIError(c, err.Error())
106                         return
107                 }
108
109                 s.Log.Debugf("Client command tty: %v %v\n", gdbTty.Name(), gdbTty.Name())
110                 cmdArgs = append(cmdArgs, "--tty="+gdbTty.Name())
111         }
112
113         // Unique ID for each commands
114         if args.CmdID == "" {
115                 args.CmdID = s.Config.ServerUID[:18] + "_" + strconv.Itoa(execCommandID)
116                 execCommandID++
117         }
118
119         // Create new execution over WS context
120         execWS := eows.New(strings.Join(cmd, " "), cmdArgs, sop, sess.ID, args.CmdID)
121         execWS.Log = s.Log
122
123         // Append client project dir to environment
124         execWS.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.ClientPath)
125
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
131         } else {
132                 execWS.CmdExecTimeout = args.CmdTimeout
133         }
134
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))
139
140                 // Handle Ctrl-D
141                 if len(stdin) == 1 && stdin == "\x04" {
142                         // Close stdin
143                         errMsg := fmt.Errorf("close stdin: %v", stdin)
144                         return "", errMsg
145                 }
146
147                 // Set correct path
148                 data := e.UserData
149                 prjID := (*data)["ID"].(string)
150                 f := s.mfolders.Get(prjID)
151                 if f == nil {
152                         s.Log.Errorf("InputCB: Cannot get folder ID %s", prjID)
153                 } else {
154                         // Translate paths from client to server
155                         stdin = (*f).ConvPathCli2Svr(stdin)
156                 }
157
158                 return stdin, nil
159         }
160
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)
165                 if so == nil {
166                         s.Log.Infof("%s not emitted: WS closed (sid:%s, msgid:%s)", xsapiv1.ExecOutEvent, e.Sid, e.CmdID)
167                         return
168                 }
169
170                 // Retrieve project ID and RootPath
171                 data := e.UserData
172                 prjID := (*data)["ID"].(string)
173                 gdbServerTTY := (*data)["gdbServerTTY"].(string)
174
175                 f := s.mfolders.Get(prjID)
176                 if f == nil {
177                         s.Log.Errorf("OutputCB: Cannot get folder ID %s", prjID)
178                 } else {
179                         // Translate paths from server to client
180                         stdout = (*f).ConvPathSvr2Cli(stdout)
181                         stderr = (*f).ConvPathSvr2Cli(stderr)
182                 }
183
184                 s.Log.Debugf("%s emitted - WS sid[4:] %s - id:%s - prjID:%s", xsapiv1.ExecOutEvent, e.Sid[4:], e.CmdID, prjID)
185                 if stdout != "" {
186                         s.Log.Debugf("STDOUT <<%v>>", strings.Replace(stdout, "\n", "\\n", -1))
187                 }
188                 if stderr != "" {
189                         s.Log.Debugf("STDERR <<%v>>", strings.Replace(stderr, "\n", "\\n", -1))
190                 }
191
192                 // FIXME replace by .BroadcastTo a room
193                 err := (*so).Emit(xsapiv1.ExecOutEvent, xsapiv1.ExecOutMsg{
194                         CmdID:     e.CmdID,
195                         Timestamp: time.Now().String(),
196                         Stdout:    stdout,
197                         Stderr:    stderr,
198                 })
199                 if err != nil {
200                         s.Log.Errorf("WS Emit : %v", err)
201                 }
202
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] == '&' {
206
207                         // Extract and cleanup string like &"bla bla\n"
208                         re := regexp.MustCompile("&\"(.*)\"")
209                         rer := re.FindAllStringSubmatch(stdout, -1)
210                         out := ""
211                         if rer != nil && len(rer) > 0 {
212                                 for _, o := range rer {
213                                         if len(o) >= 1 {
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)
217
218                                                 s.Log.Debugf("STDOUT INFERIOR: <<%v>>", out)
219                                                 err := (*so).Emit(xsapiv1.ExecInferiorOutEvent, xsapiv1.ExecOutMsg{
220                                                         CmdID:     e.CmdID,
221                                                         Timestamp: time.Now().String(),
222                                                         Stdout:    out,
223                                                         Stderr:    "",
224                                                 })
225                                                 if err != nil {
226                                                         s.Log.Errorf("WS Emit : %v", err)
227                                                 }
228                                         }
229                                 }
230                         } else {
231                                 s.Log.Errorf("INFERIOR out parsing error: stdout=<%v>", stdout)
232                         }
233                 }
234         }
235
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)
239
240                 // Close client tty
241                 defer func() {
242                         if gdbPty != nil {
243                                 gdbPty.Close()
244                         }
245                         if gdbTty != nil {
246                                 gdbTty.Close()
247                         }
248                 }()
249
250                 // IO socket can be nil when disconnected
251                 so := s.sessions.IOSocketGet(e.Sid)
252                 if so == nil {
253                         s.Log.Infof("%s not emitted - WS closed (id:%s)", xsapiv1.ExecExitEvent, e.CmdID)
254                         return
255                 }
256
257                 // Retrieve project ID and RootPath
258                 data := e.UserData
259                 prjID := (*data)["ID"].(string)
260                 exitImm := (*data)["ExitImmediate"].(bool)
261
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)
265                 }
266                 if !exitImm {
267                         // Wait end of file sync
268                         // FIXME pass as argument
269                         tmo := 60
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 {
273                                         if err != nil {
274                                                 s.Log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err)
275                                         }
276                                         break
277                                 }
278                                 time.Sleep(time.Second)
279                         }
280                         s.Log.Debugf("OK file are synchronized.")
281                 }
282
283                 // FIXME replace by .BroadcastTo a room
284                 errSoEmit := (*so).Emit(xsapiv1.ExecExitEvent, xsapiv1.ExecExitMsg{
285                         CmdID:     e.CmdID,
286                         Timestamp: time.Now().String(),
287                         Code:      code,
288                         Error:     err,
289                 })
290                 if errSoEmit != nil {
291                         s.Log.Errorf("WS Emit : %v", errSoEmit)
292                 }
293         }
294
295         // User data (used within callbacks)
296         data := make(map[string]interface{})
297         data["ID"] = prj.ID
298         data["ExitImmediate"] = args.ExitImmediate
299         if args.TTY && args.TTYGdbserverFix {
300                 data["gdbServerTTY"] = "workaround"
301         } else {
302                 data["gdbServerTTY"] = ""
303         }
304         execWS.UserData = &data
305
306         // Start command execution
307         s.Log.Infof("Execute [Cmd ID %s]: %v %v", execWS.CmdID, execWS.Cmd, execWS.Args)
308
309         err = execWS.Start()
310         if err != nil {
311                 common.APIError(c, err.Error())
312                 return
313         }
314
315         c.JSON(http.StatusOK, xsapiv1.ExecResult{Status: "OK", CmdID: execWS.CmdID})
316 }
317
318 // ExecCmd executes remotely a command
319 func (s *APIService) execSignalCmd(c *gin.Context) {
320         var args xsapiv1.ExecSignalArgs
321
322         if c.BindJSON(&args) != nil {
323                 common.APIError(c, "Invalid arguments")
324                 return
325         }
326
327         s.Log.Debugf("Signal %s for command ID %s", args.Signal, args.CmdID)
328
329         e := eows.GetEows(args.CmdID)
330         if e == nil {
331                 common.APIError(c, "unknown cmdID")
332                 return
333         }
334
335         err := e.Signal(args.Signal)
336         if err != nil {
337                 common.APIError(c, err.Error())
338                 return
339         }
340
341         c.JSON(http.StatusOK, xsapiv1.ExecSigResult{Status: "OK", CmdID: args.CmdID})
342 }