Migration to AGL gerrit (update go import)
[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
140         // Append client project dir to environment
141         execWS.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.ClientPath)
142
143         // Set command execution timeout
144         if args.CmdTimeout == 0 {
145                 // 0 : default timeout
146                 // TODO get default timeout from server-config.json file
147                 execWS.CmdExecTimeout = 24 * 60 * 60 // 1 day
148         } else {
149                 execWS.CmdExecTimeout = args.CmdTimeout
150         }
151
152         // Define callback for input (stdin)
153         execWS.InputEvent = xsapiv1.ExecInEvent
154         execWS.InputCB = func(e *eows.ExecOverWS, stdin string) (string, error) {
155                 s.Log.Debugf("STDIN <<%v>>", strings.Replace(stdin, "\n", "\\n", -1))
156
157                 // Handle Ctrl-D
158                 if len(stdin) == 1 && stdin == "\x04" {
159                         // Close stdin
160                         errMsg := fmt.Errorf("close stdin: %v", stdin)
161                         return "", errMsg
162                 }
163
164                 // Set correct path
165                 data := e.UserData
166                 prjID := (*data)["ID"].(string)
167                 f := s.mfolders.Get(prjID)
168                 if f == nil {
169                         s.Log.Errorf("InputCB: Cannot get folder ID %s", prjID)
170                 } else {
171                         // Translate paths from client to server
172                         stdin = (*f).ConvPathCli2Svr(stdin)
173                 }
174
175                 return stdin, nil
176         }
177
178         // Define callback for output (stdout+stderr)
179         execWS.OutputCB = func(e *eows.ExecOverWS, stdout, stderr string) {
180                 // IO socket can be nil when disconnected
181                 so := s.sessions.IOSocketGet(e.Sid)
182                 if so == nil {
183                         s.Log.Infof("%s not emitted: WS closed (sid:%s, msgid:%s)", xsapiv1.ExecOutEvent, e.Sid, e.CmdID)
184                         return
185                 }
186
187                 // Retrieve project ID and RootPath
188                 data := e.UserData
189                 prjID := (*data)["ID"].(string)
190                 gdbServerTTY := (*data)["gdbServerTTY"].(string)
191
192                 f := s.mfolders.Get(prjID)
193                 if f == nil {
194                         s.Log.Errorf("OutputCB: Cannot get folder ID %s", prjID)
195                 } else {
196                         // Translate paths from server to client
197                         stdout = (*f).ConvPathSvr2Cli(stdout)
198                         stderr = (*f).ConvPathSvr2Cli(stderr)
199                 }
200
201                 s.Log.Debugf("%s emitted - WS sid[4:] %s - id:%s - prjID:%s", xsapiv1.ExecOutEvent, e.Sid[4:], e.CmdID, prjID)
202                 if stdout != "" {
203                         s.Log.Debugf("STDOUT <<%v>>", strings.Replace(stdout, "\n", "\\n", -1))
204                 }
205                 if stderr != "" {
206                         s.Log.Debugf("STDERR <<%v>>", strings.Replace(stderr, "\n", "\\n", -1))
207                 }
208
209                 // FIXME replace by .BroadcastTo a room
210                 err := (*so).Emit(xsapiv1.ExecOutEvent, xsapiv1.ExecOutMsg{
211                         CmdID:     e.CmdID,
212                         Timestamp: time.Now().String(),
213                         Stdout:    stdout,
214                         Stderr:    stderr,
215                 })
216                 if err != nil {
217                         s.Log.Errorf("WS Emit : %v", err)
218                 }
219
220                 // XXX - Workaround due to gdbserver bug that doesn't redirect
221                 // inferior output (https://bugs.eclipse.org/bugs/show_bug.cgi?id=437532#c13)
222                 if gdbServerTTY == "workaround" && len(stdout) > 1 && stdout[0] == '&' {
223
224                         // Extract and cleanup string like &"bla bla\n"
225                         re := regexp.MustCompile("&\"(.*)\"")
226                         rer := re.FindAllStringSubmatch(stdout, -1)
227                         out := ""
228                         if rer != nil && len(rer) > 0 {
229                                 for _, o := range rer {
230                                         if len(o) >= 1 {
231                                                 out = strings.Replace(o[1], "\\n", "\n", -1)
232                                                 out = strings.Replace(out, "\\r", "\r", -1)
233                                                 out = strings.Replace(out, "\\t", "\t", -1)
234
235                                                 s.Log.Debugf("STDOUT INFERIOR: <<%v>>", out)
236                                                 err := (*so).Emit(xsapiv1.ExecInferiorOutEvent, xsapiv1.ExecOutMsg{
237                                                         CmdID:     e.CmdID,
238                                                         Timestamp: time.Now().String(),
239                                                         Stdout:    out,
240                                                         Stderr:    "",
241                                                 })
242                                                 if err != nil {
243                                                         s.Log.Errorf("WS Emit : %v", err)
244                                                 }
245                                         }
246                                 }
247                         } else {
248                                 s.Log.Errorf("INFERIOR out parsing error: stdout=<%v>", stdout)
249                         }
250                 }
251         }
252
253         // Define callback for output
254         execWS.ExitCB = func(e *eows.ExecOverWS, code int, err error) {
255                 s.Log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", e.CmdID, code, err)
256
257                 // Close client tty
258                 defer func() {
259                         if gdbPty != nil {
260                                 gdbPty.Close()
261                         }
262                         if gdbTty != nil {
263                                 gdbTty.Close()
264                         }
265                 }()
266
267                 // IO socket can be nil when disconnected
268                 so := s.sessions.IOSocketGet(e.Sid)
269                 if so == nil {
270                         s.Log.Infof("%s not emitted - WS closed (id:%s)", xsapiv1.ExecExitEvent, e.CmdID)
271                         return
272                 }
273
274                 // Retrieve project ID and RootPath
275                 data := e.UserData
276                 prjID := (*data)["ID"].(string)
277                 exitImm := (*data)["ExitImmediate"].(bool)
278
279                 // XXX - workaround to be sure that Syncthing detected all changes
280                 if err := s.mfolders.ForceSync(prjID); err != nil {
281                         s.Log.Errorf("Error while syncing folder %s: %v", prjID, err)
282                 }
283                 if !exitImm {
284                         // Wait end of file sync
285                         // FIXME pass as argument
286                         tmo := 60
287                         for t := tmo; t > 0; t-- {
288                                 s.Log.Debugf("Wait file in-sync for %s (%d/%d)", prjID, t, tmo)
289                                 if sync, err := s.mfolders.IsFolderInSync(prjID); sync || err != nil {
290                                         if err != nil {
291                                                 s.Log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err)
292                                         }
293                                         break
294                                 }
295                                 time.Sleep(time.Second)
296                         }
297                         s.Log.Debugf("OK file are synchronized.")
298                 }
299
300                 // FIXME replace by .BroadcastTo a room
301                 errSoEmit := (*so).Emit(xsapiv1.ExecExitEvent, xsapiv1.ExecExitMsg{
302                         CmdID:     e.CmdID,
303                         Timestamp: time.Now().String(),
304                         Code:      code,
305                         Error:     err,
306                 })
307                 if errSoEmit != nil {
308                         s.Log.Errorf("WS Emit : %v", errSoEmit)
309                 }
310         }
311
312         // User data (used within callbacks)
313         data := make(map[string]interface{})
314         data["ID"] = prj.ID
315         data["ExitImmediate"] = args.ExitImmediate
316         if args.TTY && args.TTYGdbserverFix {
317                 data["gdbServerTTY"] = "workaround"
318         } else {
319                 data["gdbServerTTY"] = ""
320         }
321         execWS.UserData = &data
322
323         // Start command execution
324         s.Log.Infof("Execute [Cmd ID %s]: %v %v", execWS.CmdID, execWS.Cmd, execWS.Args)
325
326         err = execWS.Start()
327         if err != nil {
328                 common.APIError(c, err.Error())
329                 return
330         }
331
332         c.JSON(http.StatusOK, xsapiv1.ExecResult{Status: "OK", CmdID: execWS.CmdID})
333 }
334
335 // ExecCmd executes remotely a command
336 func (s *APIService) execSignalCmd(c *gin.Context) {
337         var args xsapiv1.ExecSignalArgs
338
339         if c.BindJSON(&args) != nil {
340                 common.APIError(c, "Invalid arguments")
341                 return
342         }
343
344         s.Log.Debugf("Signal %s for command ID %s", args.Signal, args.CmdID)
345
346         e := eows.GetEows(args.CmdID)
347         if e == nil {
348                 common.APIError(c, "unknown cmdID")
349                 return
350         }
351
352         err := e.Signal(args.Signal)
353         if err != nil {
354                 common.APIError(c, err.Error())
355                 return
356         }
357
358         c.JSON(http.StatusOK, xsapiv1.ExecSigResult{Status: "OK", CmdID: args.CmdID})
359 }