Fixed terminal output (support escape and control characters)
[src/xds/xds-server.git] / lib / xdsserver / apiv1-exec.go
1 /*
2  * Copyright (C) 2017-2018 "IoT.bzh"
3  * Author Sebastien Douheret <sebastien@iot.bzh>
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *   http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17
18 package xdsserver
19
20 import (
21         "fmt"
22         "net/http"
23         "os"
24         "regexp"
25         "strconv"
26         "strings"
27         "time"
28
29         common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib"
30         "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib/eows"
31         "gerrit.automotivelinux.org/gerrit/src/xds/xds-server/lib/xsapiv1"
32         "github.com/gin-gonic/gin"
33         "github.com/kr/pty"
34 )
35
36 var execCommandID = 1
37
38 // ExecCmd executes remotely a command
39 func (s *APIService) execCmd(c *gin.Context) {
40         var gdbPty, gdbTty *os.File
41         var err error
42         var args xsapiv1.ExecArgs
43         if c.BindJSON(&args) != nil {
44                 common.APIError(c, "Invalid arguments")
45                 return
46         }
47
48         // TODO: add permission ?
49
50         // Retrieve session info
51         sess := s.sessions.Get(c)
52         if sess == nil {
53                 common.APIError(c, "Unknown sessions")
54                 return
55         }
56         sop := sess.IOSocket
57         if sop == nil {
58                 common.APIError(c, "Websocket not established")
59                 return
60         }
61
62         // Allow to pass id in url (/exec/:id) or as JSON argument
63         idArg := c.Param("id")
64         if idArg == "" {
65                 idArg = args.ID
66         }
67         if idArg == "" {
68                 common.APIError(c, "Invalid id")
69                 return
70         }
71         id, err := s.mfolders.ResolveID(idArg)
72         if err != nil {
73                 common.APIError(c, err.Error())
74                 return
75         }
76         f := s.mfolders.Get(id)
77         if f == nil {
78                 common.APIError(c, "Unknown id")
79                 return
80         }
81         fld := *f
82         prj := fld.GetConfig()
83
84         // Build command line
85         cmd := []string{}
86         // Setup env var regarding Sdk ID (used for example to setup cross toolchain)
87         if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); len(envCmd) > 0 {
88                 cmd = append(cmd, envCmd...)
89                 cmd = append(cmd, "&&")
90         } else {
91                 // It's an error if no envcmd found while a sdkid has been provided
92                 if args.SdkID != "" {
93                         common.APIError(c, "Unknown sdkid")
94                         return
95                 }
96         }
97
98         cmd = append(cmd, "cd", "\""+fld.GetFullPath(args.RPath)+"\"")
99         // FIXME - add 'exec' prevents to use syntax:
100         //       xds-exec -l debug -c xds-config.env -- "cd build && cmake .."
101         //  but exec is mandatory to allow to pass correctly signals
102         //  As workaround, exec is set for now on client side (eg. in xds-gdb)
103         //cmd = append(cmd, "&&", "exec", args.Cmd)
104         cmd = append(cmd, "&&", args.Cmd)
105
106         // Process command arguments
107         cmdArgs := make([]string, len(args.Args)+1)
108
109         // Copy and Translate path from client to server
110         for _, aa := range args.Args {
111                 if strings.Contains(aa, prj.ClientPath) {
112                         cmdArgs = append(cmdArgs, fld.ConvPathCli2Svr(aa))
113                 } else {
114                         cmdArgs = append(cmdArgs, aa)
115                 }
116         }
117
118         // Allocate pts if tty if used
119         if args.TTY {
120                 gdbPty, gdbTty, err = pty.Open()
121                 if err != nil {
122                         common.APIError(c, err.Error())
123                         return
124                 }
125
126                 s.Log.Debugf("Client command tty: %v %v\n", gdbTty.Name(), gdbTty.Name())
127                 cmdArgs = append(cmdArgs, "--tty="+gdbTty.Name())
128         }
129
130         // Unique ID for each commands
131         if args.CmdID == "" {
132                 args.CmdID = s.Config.ServerUID[:18] + "_" + strconv.Itoa(execCommandID)
133                 execCommandID++
134         }
135
136         // Create new execution over WS context
137         execWS := eows.New(strings.Join(cmd, " "), cmdArgs, sop, sess.ID, args.CmdID)
138         execWS.Log = s.Log
139         execWS.OutSplit = eows.SplitChar
140
141         // Append client project dir to environment
142         execWS.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.ClientPath)
143
144         // Set command execution timeout
145         if args.CmdTimeout == 0 {
146                 // 0 : default timeout
147                 // TODO get default timeout from server-config.json file
148                 execWS.CmdExecTimeout = 24 * 60 * 60 // 1 day
149         } else {
150                 execWS.CmdExecTimeout = args.CmdTimeout
151         }
152
153         // Define callback for input (stdin)
154         execWS.InputEvent = xsapiv1.ExecInEvent
155         execWS.InputCB = func(e *eows.ExecOverWS, bStdin []byte) ([]byte, error) {
156
157                 stdin := string(bStdin)
158
159                 s.Log.Debugf("STDIN <<%v>>", strings.Replace(stdin, "\n", "\\n", -1))
160
161                 // Handle Ctrl-D
162                 if len(stdin) == 1 && stdin == "\x04" {
163                         // Close stdin
164                         errMsg := fmt.Errorf("close stdin: %v", stdin)
165                         return []byte{}, errMsg
166                 }
167
168                 // Set correct path
169                 data := e.UserData
170                 prjID := (*data)["ID"].(string)
171                 f := s.mfolders.Get(prjID)
172                 if f == nil {
173                         s.Log.Errorf("InputCB: Cannot get folder ID %s", prjID)
174                 } else {
175                         // Translate paths from client to server
176                         stdin = (*f).ConvPathCli2Svr(stdin)
177                 }
178                 return []byte(stdin), nil
179         }
180
181         // Define callback for output (stdout+stderr)
182         execWS.OutputCB = func(e *eows.ExecOverWS, bStdout, bStderr []byte) {
183
184                 stdout := string(bStdout)
185                 stderr := string(bStderr)
186
187                 // IO socket can be nil when disconnected
188                 so := s.sessions.IOSocketGet(e.Sid)
189                 if so == nil {
190                         s.Log.Infof("%s not emitted: WS closed (sid:%s, CmdID:%s)", xsapiv1.ExecOutEvent, e.Sid, e.CmdID)
191                         return
192                 }
193
194                 // Retrieve project ID and RootPath
195                 data := e.UserData
196                 prjID := (*data)["ID"].(string)
197                 gdbServerTTY := (*data)["gdbServerTTY"].(string)
198
199                 f := s.mfolders.Get(prjID)
200                 if f == nil {
201                         s.Log.Errorf("OutputCB: Cannot get folder ID %s", prjID)
202                 } else {
203                         // Translate paths from server to client
204                         stdout = (*f).ConvPathSvr2Cli(stdout)
205                         stderr = (*f).ConvPathSvr2Cli(stderr)
206                 }
207
208                 s.Log.Debugf("%s emitted - WS sid[4:] %s - id:%s - prjID:%s", xsapiv1.ExecOutEvent, e.Sid[4:], e.CmdID, prjID)
209                 if stdout != "" {
210                         s.Log.Debugf("STDOUT <<%v>>", strings.Replace(stdout, "\n", "\\n", -1))
211                 }
212                 if stderr != "" {
213                         s.Log.Debugf("STDERR <<%v>>", strings.Replace(stderr, "\n", "\\n", -1))
214                 }
215
216                 // FIXME replace by .BroadcastTo a room
217                 err := (*so).Emit(xsapiv1.ExecOutEvent, xsapiv1.ExecOutMsg{
218                         CmdID:     e.CmdID,
219                         Timestamp: time.Now().String(),
220                         Stdout:    stdout,
221                         Stderr:    stderr,
222                 })
223                 if err != nil {
224                         s.Log.Errorf("WS Emit : %v", err)
225                 }
226
227                 // XXX - Workaround due to gdbserver bug that doesn't redirect
228                 // inferior output (https://bugs.eclipse.org/bugs/show_bug.cgi?id=437532#c13)
229                 if gdbServerTTY == "workaround" && len(stdout) > 1 && stdout[0] == '&' {
230
231                         // Extract and cleanup string like &"bla bla\n"
232                         re := regexp.MustCompile("&\"(.*)\"")
233                         rer := re.FindAllStringSubmatch(stdout, -1)
234                         out := ""
235                         if rer != nil && len(rer) > 0 {
236                                 for _, o := range rer {
237                                         if len(o) >= 1 {
238                                                 out = strings.Replace(o[1], "\\n", "\n", -1)
239                                                 out = strings.Replace(out, "\\r", "\r", -1)
240                                                 out = strings.Replace(out, "\\t", "\t", -1)
241
242                                                 s.Log.Debugf("STDOUT INFERIOR: <<%v>>", out)
243                                                 err := (*so).Emit(xsapiv1.ExecInferiorOutEvent, xsapiv1.ExecOutMsg{
244                                                         CmdID:     e.CmdID,
245                                                         Timestamp: time.Now().String(),
246                                                         Stdout:    out,
247                                                         Stderr:    "",
248                                                 })
249                                                 if err != nil {
250                                                         s.Log.Errorf("WS Emit : %v", err)
251                                                 }
252                                         }
253                                 }
254                         } else {
255                                 s.Log.Errorf("INFERIOR out parsing error: stdout=<%v>", stdout)
256                         }
257                 }
258         }
259
260         // Define callback for output
261         execWS.ExitCB = func(e *eows.ExecOverWS, code int, err error) {
262                 s.Log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", e.CmdID, code, err)
263
264                 // Close client tty
265                 defer func() {
266                         if gdbPty != nil {
267                                 gdbPty.Close()
268                         }
269                         if gdbTty != nil {
270                                 gdbTty.Close()
271                         }
272                 }()
273
274                 // IO socket can be nil when disconnected
275                 so := s.sessions.IOSocketGet(e.Sid)
276                 if so == nil {
277                         s.Log.Infof("%s not emitted - WS closed (id:%s)", xsapiv1.ExecExitEvent, e.CmdID)
278                         return
279                 }
280
281                 // Retrieve project ID and RootPath
282                 data := e.UserData
283                 prjID := (*data)["ID"].(string)
284                 exitImm := (*data)["ExitImmediate"].(bool)
285
286                 // XXX - workaround to be sure that Syncthing detected all changes
287                 if err := s.mfolders.ForceSync(prjID); err != nil {
288                         s.Log.Errorf("Error while syncing folder %s: %v", prjID, err)
289                 }
290                 if !exitImm {
291                         // Wait end of file sync
292                         // FIXME pass as argument
293                         tmo := 60
294                         for t := tmo; t > 0; t-- {
295                                 s.Log.Debugf("Wait file in-sync for %s (%d/%d)", prjID, t, tmo)
296                                 if sync, err := s.mfolders.IsFolderInSync(prjID); sync || err != nil {
297                                         if err != nil {
298                                                 s.Log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err)
299                                         }
300                                         break
301                                 }
302                                 time.Sleep(time.Second)
303                         }
304                         s.Log.Debugf("OK file are synchronized.")
305                 }
306
307                 // FIXME replace by .BroadcastTo a room
308                 errSoEmit := (*so).Emit(xsapiv1.ExecExitEvent, xsapiv1.ExecExitMsg{
309                         CmdID:     e.CmdID,
310                         Timestamp: time.Now().String(),
311                         Code:      code,
312                         Error:     err,
313                 })
314                 if errSoEmit != nil {
315                         s.Log.Errorf("WS Emit : %v", errSoEmit)
316                 }
317         }
318
319         // User data (used within callbacks)
320         data := make(map[string]interface{})
321         data["ID"] = prj.ID
322         data["ExitImmediate"] = args.ExitImmediate
323         if args.TTY && args.TTYGdbserverFix {
324                 data["gdbServerTTY"] = "workaround"
325         } else {
326                 data["gdbServerTTY"] = ""
327         }
328         execWS.UserData = &data
329
330         // Start command execution
331         s.Log.Infof("Execute [Cmd ID %s]: %v %v", execWS.CmdID, execWS.Cmd, execWS.Args)
332
333         err = execWS.Start()
334         if err != nil {
335                 common.APIError(c, err.Error())
336                 return
337         }
338
339         c.JSON(http.StatusOK, xsapiv1.ExecResult{Status: "OK", CmdID: execWS.CmdID})
340 }
341
342 // ExecCmd executes remotely a command
343 func (s *APIService) execSignalCmd(c *gin.Context) {
344         var args xsapiv1.ExecSignalArgs
345
346         if c.BindJSON(&args) != nil {
347                 common.APIError(c, "Invalid arguments")
348                 return
349         }
350
351         s.Log.Debugf("Signal %s for command ID %s", args.Signal, args.CmdID)
352
353         e := eows.GetEows(args.CmdID)
354         if e == nil {
355                 common.APIError(c, "unknown cmdID")
356                 return
357         }
358
359         err := e.Signal(args.Signal)
360         if err != nil {
361                 common.APIError(c, err.Error())
362                 return
363         }
364
365         c.JSON(http.StatusOK, xsapiv1.ExecSigResult{Status: "OK", CmdID: args.CmdID})
366 }