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