Use go module as dependency tool instead of glide
[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"
30         "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/eows"
31         "gerrit.automotivelinux.org/gerrit/src/xds/xds-server.git/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
87         // Reset by default LD_LIBRARY_PATH
88         // With new AGL SDK, New environment-setup-*-agl-linux file checks if
89         // LD_LIBRARY_PATH is set or not and not empty LD_LIBRARY_PATH is considered
90         // as misconfigured and prevent to use SDK
91         if !args.LdLibPathNoReset {
92                 cmd = append(cmd, "unset LD_LIBRARY_PATH; ")
93         }
94
95         // Setup env var regarding Sdk ID (used for example to setup cross toolchain)
96         if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); len(envCmd) > 0 {
97                 cmd = append(cmd, envCmd...)
98                 cmd = append(cmd, "&&")
99         } else {
100                 // It's an error if no envcmd found while a sdkid has been provided
101                 if args.SdkID != "" {
102                         common.APIError(c, "Unknown sdkid")
103                         return
104                 }
105         }
106
107         cmd = append(cmd, "cd", "\""+fld.GetFullPath(args.RPath)+"\"")
108         // FIXME - add 'exec' prevents to use syntax:
109         //       xds-exec -l debug -c xds-config.env -- "cd build && cmake .."
110         //  but exec is mandatory to allow to pass correctly signals
111         //  As workaround, exec is set for now on client side (eg. in xds-gdb)
112         //cmd = append(cmd, "&&", "exec", args.Cmd)
113         cmd = append(cmd, "&&", args.Cmd)
114
115         // Process command arguments
116         cmdArgs := make([]string, len(args.Args)+1)
117
118         // Copy and Translate path from client to server
119         for _, aa := range args.Args {
120                 if strings.Contains(aa, prj.ClientPath) {
121                         cmdArgs = append(cmdArgs, fld.ConvPathCli2Svr(aa))
122                 } else {
123                         cmdArgs = append(cmdArgs, aa)
124                 }
125         }
126
127         // Allocate pts if tty if used
128         if args.TTY {
129                 gdbPty, gdbTty, err = pty.Open()
130                 if err != nil {
131                         common.APIError(c, err.Error())
132                         return
133                 }
134
135                 s.Log.Debugf("Client command tty: %v %v\n", gdbTty.Name(), gdbTty.Name())
136                 cmdArgs = append(cmdArgs, "--tty="+gdbTty.Name())
137         }
138
139         // Unique ID for each commands
140         if args.CmdID == "" {
141                 args.CmdID = s.Config.ServerUID[:18] + "_" + strconv.Itoa(execCommandID)
142                 execCommandID++
143         }
144
145         // Create new execution over WS context
146         execWS := eows.New(strings.Join(cmd, " "), cmdArgs, sop, sess.ID, args.CmdID)
147         execWS.Log = s.Log
148         execWS.OutSplit = eows.SplitChar
149
150         // Append client project dir to environment
151         execWS.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.ClientPath)
152
153         // Set command execution timeout
154         if args.CmdTimeout == 0 {
155                 // 0 : default timeout
156                 // TODO get default timeout from server-config.json file
157                 execWS.CmdExecTimeout = 24 * 60 * 60 // 1 day
158         } else {
159                 execWS.CmdExecTimeout = args.CmdTimeout
160         }
161
162         // Define callback for input (stdin)
163         execWS.InputEvent = xsapiv1.ExecInEvent
164         execWS.InputCB = func(e *eows.ExecOverWS, bStdin []byte) ([]byte, error) {
165
166                 stdin := string(bStdin)
167
168                 s.Log.Debugf("STDIN <<%v>>", strings.Replace(stdin, "\n", "\\n", -1))
169
170                 // Handle Ctrl-D
171                 if len(stdin) == 1 && stdin == "\x04" {
172                         // Close stdin
173                         errMsg := fmt.Errorf("close stdin: %v", stdin)
174                         return []byte{}, errMsg
175                 }
176
177                 // Set correct path
178                 data := e.UserData
179                 prjID := (*data)["ID"].(string)
180                 f := s.mfolders.Get(prjID)
181                 if f == nil {
182                         s.Log.Errorf("InputCB: Cannot get folder ID %s", prjID)
183                 } else {
184                         // Translate paths from client to server
185                         stdin = (*f).ConvPathCli2Svr(stdin)
186                 }
187                 return []byte(stdin), nil
188         }
189
190         // Define callback for output (stdout+stderr)
191         execWS.OutputCB = func(e *eows.ExecOverWS, bStdout, bStderr []byte) {
192
193                 stdout := string(bStdout)
194                 stderr := string(bStderr)
195
196                 // IO socket can be nil when disconnected
197                 so := s.sessions.IOSocketGet(e.Sid)
198                 if so == nil {
199                         s.Log.Infof("%s not emitted: WS closed (sid:%s, CmdID:%s)", xsapiv1.ExecOutEvent, e.Sid, e.CmdID)
200                         return
201                 }
202
203                 // Retrieve project ID and RootPath
204                 data := e.UserData
205                 prjID := (*data)["ID"].(string)
206                 gdbServerTTY := (*data)["gdbServerTTY"].(string)
207
208                 f := s.mfolders.Get(prjID)
209                 if f == nil {
210                         s.Log.Errorf("OutputCB: Cannot get folder ID %s", prjID)
211                 } else {
212                         // Translate paths from server to client
213                         stdout = (*f).ConvPathSvr2Cli(stdout)
214                         stderr = (*f).ConvPathSvr2Cli(stderr)
215                 }
216
217                 s.Log.Debugf("%s emitted - WS sid[4:] %s - id:%s - prjID:%s", xsapiv1.ExecOutEvent, e.Sid[4:], e.CmdID, prjID)
218                 if stdout != "" {
219                         s.Log.Debugf("STDOUT <<%v>>", strings.Replace(stdout, "\n", "\\n", -1))
220                 }
221                 if stderr != "" {
222                         s.Log.Debugf("STDERR <<%v>>", strings.Replace(stderr, "\n", "\\n", -1))
223                 }
224
225                 // FIXME replace by .BroadcastTo a room
226                 err := (*so).Emit(xsapiv1.ExecOutEvent, xsapiv1.ExecOutMsg{
227                         CmdID:     e.CmdID,
228                         Timestamp: time.Now().String(),
229                         Stdout:    stdout,
230                         Stderr:    stderr,
231                 })
232                 if err != nil {
233                         s.Log.Errorf("WS Emit : %v", err)
234                 }
235
236                 // XXX - Workaround due to gdbserver bug that doesn't redirect
237                 // inferior output (https://bugs.eclipse.org/bugs/show_bug.cgi?id=437532#c13)
238                 if gdbServerTTY == "workaround" && len(stdout) > 1 && stdout[0] == '&' {
239
240                         // Extract and cleanup string like &"bla bla\n"
241                         re := regexp.MustCompile("&\"(.*)\"")
242                         rer := re.FindAllStringSubmatch(stdout, -1)
243                         out := ""
244                         if rer != nil && len(rer) > 0 {
245                                 for _, o := range rer {
246                                         if len(o) >= 1 {
247                                                 out = strings.Replace(o[1], "\\n", "\n", -1)
248                                                 out = strings.Replace(out, "\\r", "\r", -1)
249                                                 out = strings.Replace(out, "\\t", "\t", -1)
250
251                                                 s.Log.Debugf("STDOUT INFERIOR: <<%v>>", out)
252                                                 err := (*so).Emit(xsapiv1.ExecInferiorOutEvent, xsapiv1.ExecOutMsg{
253                                                         CmdID:     e.CmdID,
254                                                         Timestamp: time.Now().String(),
255                                                         Stdout:    out,
256                                                         Stderr:    "",
257                                                 })
258                                                 if err != nil {
259                                                         s.Log.Errorf("WS Emit : %v", err)
260                                                 }
261                                         }
262                                 }
263                         } else {
264                                 s.Log.Errorf("INFERIOR out parsing error: stdout=<%v>", stdout)
265                         }
266                 }
267         }
268
269         // Define callback for output
270         execWS.ExitCB = func(e *eows.ExecOverWS, code int, err error) {
271                 s.Log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", e.CmdID, code, err)
272
273                 defer LockXdsUpdateCounter(s.Context, false)
274                 // Close client tty
275                 defer func() {
276                         if gdbPty != nil {
277                                 gdbPty.Close()
278                         }
279                         if gdbTty != nil {
280                                 gdbTty.Close()
281                         }
282                 }()
283
284                 // IO socket can be nil when disconnected
285                 so := s.sessions.IOSocketGet(e.Sid)
286                 if so == nil {
287                         s.Log.Infof("%s not emitted - WS closed (id:%s)", xsapiv1.ExecExitEvent, e.CmdID)
288                         return
289                 }
290
291                 // Retrieve project ID and RootPath
292                 data := e.UserData
293                 prjID := (*data)["ID"].(string)
294                 exitImm := (*data)["ExitImmediate"].(bool)
295
296                 // XXX - workaround to be sure that Syncthing detected all changes
297                 if err := s.mfolders.ForceSync(prjID); err != nil {
298                         s.Log.Errorf("Error while syncing folder %s: %v", prjID, err)
299                 }
300                 if !exitImm {
301                         // Wait end of file sync
302                         // FIXME pass as argument
303                         tmo := 60
304                         for t := tmo; t > 0; t-- {
305                                 s.Log.Debugf("Wait file in-sync for %s (%d/%d)", prjID, t, tmo)
306                                 if sync, err := s.mfolders.IsFolderInSync(prjID); sync || err != nil {
307                                         if err != nil {
308                                                 s.Log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err)
309                                         }
310                                         break
311                                 }
312                                 time.Sleep(time.Second)
313                         }
314                         s.Log.Debugf("OK file are synchronized.")
315                 }
316
317                 // FIXME replace by .BroadcastTo a room
318                 errSoEmit := (*so).Emit(xsapiv1.ExecExitEvent, xsapiv1.ExecExitMsg{
319                         CmdID:     e.CmdID,
320                         Timestamp: time.Now().String(),
321                         Code:      code,
322                         Error:     err,
323                 })
324                 if errSoEmit != nil {
325                         s.Log.Errorf("WS Emit : %v", errSoEmit)
326                 }
327         }
328
329         // User data (used within callbacks)
330         data := make(map[string]interface{})
331         data["ID"] = prj.ID
332         data["ExitImmediate"] = args.ExitImmediate
333         if args.TTY && args.TTYGdbserverFix {
334                 data["gdbServerTTY"] = "workaround"
335         } else {
336                 data["gdbServerTTY"] = ""
337         }
338         execWS.UserData = &data
339
340         // Start command execution
341         s.Log.Infof("Execute [Cmd ID %s]: %v %v", execWS.CmdID, execWS.Cmd, execWS.Args)
342
343         LockXdsUpdateCounter(s.Context, true)
344         err = execWS.Start()
345         if err != nil {
346                 LockXdsUpdateCounter(s.Context, false)
347                 common.APIError(c, err.Error())
348                 return
349         }
350
351         c.JSON(http.StatusOK, xsapiv1.ExecResult{Status: "OK", CmdID: execWS.CmdID})
352 }
353
354 // ExecCmd executes remotely a command
355 func (s *APIService) execSignalCmd(c *gin.Context) {
356         var args xsapiv1.ExecSignalArgs
357
358         if c.BindJSON(&args) != nil {
359                 common.APIError(c, "Invalid arguments")
360                 return
361         }
362
363         s.Log.Debugf("Signal %s for command ID %s", args.Signal, args.CmdID)
364
365         e := eows.GetEows(args.CmdID)
366         if e == nil {
367                 common.APIError(c, "unknown cmdID")
368                 return
369         }
370
371         err := e.Signal(args.Signal)
372         if err != nil {
373                 common.APIError(c, err.Error())
374                 return
375         }
376
377         c.JSON(http.StatusOK, xsapiv1.ExecSigResult{Status: "OK", CmdID: args.CmdID})
378 }