Add gitreview file for guppy branch
[src/xds/xds-gdb.git] / gdb-xds.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
19 package main
20
21 import (
22         "encoding/json"
23         "fmt"
24         "os"
25         "regexp"
26         "runtime"
27         "strconv"
28         "strings"
29         "syscall"
30         "text/tabwriter"
31
32         "gerrit.automotivelinux.org/gerrit/src/xds/xds-agent.git/lib/xaapiv1"
33         common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib"
34         "github.com/Sirupsen/logrus"
35         sio_client "github.com/sebd71/go-socket.io-client"
36 )
37
38 // GdbXds - Implementation of IGDB used to interfacing XDS
39 type GdbXds struct {
40         log       *logrus.Logger
41         ccmd      string
42         aargs     []string
43         eenv      []string
44         agentURL  string
45         serverURL string
46         prjID     string
47         sdkID     string
48         rPath     string
49         listPrj   bool
50         cmdID     string
51         xGdbPid   string
52
53         httpCli *common.HTTPClient
54         ioSock  *sio_client.Client
55
56         projects []xaapiv1.ProjectConfig
57
58         // callbacks
59         cbOnError      func(error)
60         cbOnDisconnect func(error)
61         cbRead         func(timestamp, stdout, stderr string)
62         cbInferiorRead func(timestamp, stdout, stderr string)
63         cbOnExit       func(code int, err error)
64 }
65
66 // NewGdbXds creates a new instance of GdbXds
67 func NewGdbXds(log *logrus.Logger, args []string, env []string) *GdbXds {
68         return &GdbXds{
69                 log:     log,
70                 ccmd:    "exec $GDB", // var set by environment-setup-xxx script
71                 aargs:   args,
72                 eenv:    env,
73                 httpCli: nil,
74                 ioSock:  nil,
75                 xGdbPid: strconv.Itoa(os.Getpid()),
76         }
77 }
78
79 // SetConfig set additional config fields
80 func (g *GdbXds) SetConfig(name string, value interface{}) error {
81         var val string
82         if name != "listProject" {
83                 val = strings.TrimSpace(value.(string))
84         }
85         switch name {
86         case "agentURL":
87                 g.agentURL = val
88         case "serverURL":
89                 g.serverURL = val
90         case "prjID":
91                 g.prjID = val
92         case "sdkID":
93                 g.sdkID = val
94         case "rPath":
95                 g.rPath = val
96         case "listProject":
97                 g.listPrj = value.(bool)
98         default:
99                 return fmt.Errorf("Unknown %s field", name)
100         }
101         return nil
102 }
103
104 // Init initializes gdb XDS
105 func (g *GdbXds) Init() (int, error) {
106
107         // Reset command ID (also used to enable sending of signals)
108         g.cmdID = ""
109
110         // Define HTTP and WS url
111         baseURL := g.agentURL
112
113         // Allow to only set port number
114         if match, _ := regexp.MatchString("^([0-9]+)$", baseURL); match {
115                 baseURL = "http://localhost:" + g.agentURL
116         }
117         // Add http prefix if missing
118         if baseURL != "" && !strings.HasPrefix(g.agentURL, "http://") {
119                 baseURL = "http://" + g.agentURL
120         }
121
122         // Create HTTP client
123         g.log.Infoln("Connect HTTP client on ", baseURL)
124         conf := common.HTTPClientConfig{
125                 URLPrefix:           "/api/v1",
126                 HeaderClientKeyName: "Xds-Agent-Sid",
127                 CsrfDisable:         true,
128                 LogOut:              g.log.Out,
129                 LogPrefix:           "XDSAGENT: ",
130                 LogLevel:            common.HTTPLogLevelDebug,
131         }
132         c, err := common.HTTPNewClient(baseURL, conf)
133         if err != nil {
134                 errmsg := err.Error()
135                 m, err := regexp.MatchString("Get http.?://", errmsg)
136                 if (m && err == nil) || strings.Contains(errmsg, "Failed to get device ID") {
137                         i := strings.LastIndex(errmsg, ":")
138                         newErr := "Cannot connection to " + baseURL
139                         if i > 0 {
140                                 newErr += " (" + strings.TrimSpace(errmsg[i+1:]) + ")"
141                         } else {
142                                 newErr += " (" + strings.TrimSpace(errmsg) + ")"
143                         }
144                         errmsg = newErr
145                 }
146                 return int(syscallEBADE), fmt.Errorf(errmsg)
147         }
148         g.httpCli = c
149         g.httpCli.SetLogLevel(g.log.Level.String())
150         g.log.Infoln("HTTP session ID:", g.httpCli.GetClientID())
151
152         // First call to check that xds-agent and server are alive
153         ver := xaapiv1.XDSVersion{}
154         if err := g.httpCli.Get("/version", &ver); err != nil {
155                 return int(syscallEBADE), err
156         }
157         g.log.Infoln("XDS agent & server version:", ver)
158
159         // Get current config and update connection to server when needed
160         xdsConf := xaapiv1.APIConfig{}
161         if err := g.httpCli.Get("/config", &xdsConf); err != nil {
162                 return int(syscallEBADE), err
163         }
164         // FIXME: add multi-servers support
165         idx := 0
166         svrCfg := xdsConf.Servers[idx]
167         if g.serverURL != "" && (svrCfg.URL != g.serverURL || !svrCfg.Connected) {
168                 svrCfg.URL = g.serverURL
169                 svrCfg.ConnRetry = 10
170                 newCfg := xaapiv1.APIConfig{}
171                 if err := g.httpCli.Post("/config", xdsConf, &newCfg); err != nil {
172                         return int(syscallEBADE), err
173                 }
174
175         } else if !svrCfg.Connected {
176                 return int(syscallEBADE), fmt.Errorf("XDS server not connected (url=%s)", svrCfg.URL)
177         }
178
179         // Get XDS projects list
180         var data []byte
181         if err := g.httpCli.HTTPGet("/projects", &data); err != nil {
182                 return int(syscallEBADE), err
183         }
184
185         g.log.Infof("Result of /projects: %v", string(data[:]))
186         g.projects = []xaapiv1.ProjectConfig{}
187         errMar := json.Unmarshal(data, &g.projects)
188         if errMar != nil {
189                 g.log.Errorf("Cannot decode projects configuration: %s", errMar.Error())
190         }
191
192         // Check mandatory args
193         if g.prjID == "" || g.listPrj {
194                 return g.printProjectsList()
195         }
196
197         // Create io Websocket client
198         g.log.Infoln("Connecting IO.socket client on ", baseURL)
199
200         opts := &sio_client.Options{
201                 Transport: "websocket",
202                 Header:    make(map[string][]string),
203         }
204         opts.Header["XDS-AGENT-SID"] = []string{c.GetClientID()}
205
206         iosk, err := sio_client.NewClient(baseURL, opts)
207         if err != nil {
208                 e := fmt.Sprintf("IO.socket connection error: " + err.Error())
209                 return int(syscall.ECONNABORTED), fmt.Errorf(e)
210         }
211         g.ioSock = iosk
212
213         iosk.On("error", func(err error) {
214                 if g.cbOnError != nil {
215                         g.cbOnError(err)
216                 }
217         })
218
219         iosk.On("disconnection", func(err error) {
220                 if g.cbOnDisconnect != nil {
221                         g.cbOnDisconnect(err)
222                 }
223         })
224
225         // SEB gdbPid := ""
226         iosk.On(xaapiv1.ExecOutEvent, func(ev xaapiv1.ExecOutMsg) {
227                 if g.cbRead != nil {
228                         g.cbRead(ev.Timestamp, ev.Stdout, ev.Stderr)
229                         /*
230                                 stdout := ev.Stdout
231                                 // SEB
232                                 //New Thread 15139
233                                 if strings.Contains(stdout, "pid = ") {
234                                         re := regexp.MustCompile("pid = ([0-9]+)")
235                                         if res := re.FindAllStringSubmatch(stdout, -1); len(res) > 0 {
236                                                 gdbPid = res[0][1]
237                                         }
238                                         g.log.Errorf("SEB FOUND THREAD in '%s' => gdbPid=%s", stdout, gdbPid)
239                                 }
240                                 if gdbPid != "" && g.xGdbPid != "" && strings.Contains(stdout, gdbPid) {
241                                         g.log.Errorf("SEB THREAD REPLACE 1 stdout=%s", stdout)
242                                         stdout = strings.Replace(stdout, gdbPid, g.xGdbPid, -1)
243                                         g.log.Errorf("SEB THREAD REPLACE 2 stdout=%s", stdout)
244                                 }
245
246                                 g.cbRead(ev.Timestamp, stdout, ev.Stderr)
247                         */
248                 }
249         })
250
251         iosk.On(xaapiv1.ExecInferiorOutEvent, func(ev xaapiv1.ExecOutMsg) {
252                 if g.cbInferiorRead != nil {
253                         g.cbInferiorRead(ev.Timestamp, ev.Stdout, ev.Stderr)
254                 }
255         })
256
257         iosk.On(xaapiv1.ExecExitEvent, func(ev xaapiv1.ExecExitMsg) {
258                 if g.cbOnExit != nil {
259                         g.cbOnExit(ev.Code, ev.Error)
260                 }
261         })
262
263         // Monitor XDS server configuration changes (and specifically connected status)
264         iosk.On(xaapiv1.EVTServerConfig, func(ev xaapiv1.EventMsg) {
265                 svrCfg, err := ev.DecodeServerCfg()
266                 if err == nil && !svrCfg.Connected {
267                         // TODO: should wait that server will be connected back
268                         if g.cbOnExit != nil {
269                                 g.cbOnExit(-1, fmt.Errorf("XDS Server disconnected"))
270                         } else {
271                                 fmt.Printf("XDS Server disconnected")
272                                 os.Exit(-1)
273                         }
274                 }
275         })
276
277         args := xaapiv1.EventRegisterArgs{Name: xaapiv1.EVTServerConfig}
278         if err := g.httpCli.Post("/events/register", args, nil); err != nil {
279                 return 0, err
280         }
281
282         return 0, nil
283 }
284
285 // Close frees allocated objects and close opened connections
286 func (g *GdbXds) Close() error {
287         g.cbOnDisconnect = nil
288         g.cbOnError = nil
289         g.cbOnExit = nil
290         g.cbRead = nil
291         g.cbInferiorRead = nil
292         g.cmdID = ""
293
294         return nil
295 }
296
297 // Start sends a request to start remotely gdb within xds-server
298 func (g *GdbXds) Start(inferiorTTY bool) (int, error) {
299         var err error
300         var project *xaapiv1.ProjectConfig
301
302         // Retrieve the project definition
303         for _, f := range g.projects {
304                 // check as prefix to support short/partial id name
305                 if strings.HasPrefix(f.ID, g.prjID) {
306                         project = &f
307                         break
308                 }
309         }
310
311         // Auto setup rPath if needed
312         if g.rPath == "" && project != nil {
313                 cwd, err := os.Getwd()
314                 if err == nil {
315                         fldRp := project.ClientPath
316                         if !strings.HasPrefix(fldRp, "/") {
317                                 fldRp = "/" + fldRp
318                         }
319                         log.Debugf("Try to auto-setup rPath: cwd=%s ; ClientPath=%s", cwd, fldRp)
320                         if sp := strings.SplitAfter(cwd, fldRp); len(sp) == 2 {
321                                 g.rPath = strings.Trim(sp[1], "/")
322                                 g.log.Debugf("Auto-setup rPath to: '%s'", g.rPath)
323                         }
324                 }
325         }
326
327         // Enable workaround about inferior output with gdbserver connection
328         // except if XDS_GDBSERVER_OUTPUT_NOFIX is defined
329         _, gdbserverNoFix := os.LookupEnv("XDS_GDBSERVER_OUTPUT_NOFIX")
330
331         // SDK ID must be set else $GDB cannot be resolved
332         if g.sdkID == "" {
333                 return int(syscall.EINVAL), fmt.Errorf("sdkid must be set")
334         }
335
336         args := xaapiv1.ExecArgs{
337                 ID:              g.prjID,
338                 SdkID:           g.sdkID,
339                 Cmd:             g.ccmd,
340                 Args:            g.aargs,
341                 Env:             g.eenv,
342                 RPath:           g.rPath,
343                 TTY:             inferiorTTY,
344                 TTYGdbserverFix: !gdbserverNoFix,
345                 CmdTimeout:      -1, // no timeout, end when stdin close or command exited normally
346         }
347
348         g.log.Infof("POST %s/exec %v", g.agentURL, args)
349         res := xaapiv1.ExecResult{}
350         err = g.httpCli.Post("/exec", args, &res)
351         if err != nil {
352                 return int(syscall.EAGAIN), err
353         }
354         if res.CmdID == "" {
355                 return int(syscallEBADE), fmt.Errorf("null CmdID")
356         }
357         g.cmdID = res.CmdID
358
359         return 0, nil
360 }
361
362 // Cmd returns the command name
363 func (g *GdbXds) Cmd() string {
364         return g.ccmd
365 }
366
367 // Args returns the list of arguments
368 func (g *GdbXds) Args() []string {
369         return g.aargs
370 }
371
372 // Env returns the list of environment variables
373 func (g *GdbXds) Env() []string {
374         return g.eenv
375 }
376
377 // OnError is called on a WebSocket error
378 func (g *GdbXds) OnError(f func(error)) {
379         g.cbOnError = f
380 }
381
382 // OnDisconnect is called when WebSocket disconnection
383 func (g *GdbXds) OnDisconnect(f func(error)) {
384         g.cbOnDisconnect = f
385 }
386
387 // OnExit calls when exit event is received
388 func (g *GdbXds) OnExit(f func(code int, err error)) {
389         g.cbOnExit = f
390 }
391
392 // Read calls when a message/string event is received on stdout or stderr
393 func (g *GdbXds) Read(f func(timestamp, stdout, stderr string)) {
394         g.cbRead = f
395 }
396
397 // InferiorRead calls when a message/string event is received on stdout or stderr of the debugged program (IOW inferior)
398 func (g *GdbXds) InferiorRead(f func(timestamp, stdout, stderr string)) {
399         g.cbInferiorRead = f
400 }
401
402 // Write writes message/string into gdb stdin
403 func (g *GdbXds) Write(args ...interface{}) error {
404         s := fmt.Sprint(args...)
405         return g.ioSock.Emit(xaapiv1.ExecInEvent, []byte(s))
406 }
407
408 // SendSignal is used to send a signal to remote process/gdb
409 func (g *GdbXds) SendSignal(sig os.Signal) error {
410         if g.cmdID == "" {
411                 return fmt.Errorf("cmdID not set")
412         }
413
414         sigArg := xaapiv1.ExecSignalArgs{
415                 CmdID:  g.cmdID,
416                 Signal: sig.String(),
417         }
418         g.log.Debugf("POST /signal %v", sigArg)
419         return g.httpCli.Post("/signal", sigArg, nil)
420 }
421
422 //***** Private functions *****
423
424 func (g *GdbXds) printProjectsList() (int, error) {
425         var prjExample *xaapiv1.ProjectConfig
426         var sdkExample *xaapiv1.SDK
427
428         writer := new(tabwriter.Writer)
429         writer.Init(os.Stdout, 0, 8, 0, '\t', 0)
430         msg := ""
431         if len(g.projects) > 0 {
432                 fmt.Fprintln(writer, "List of existing projects (use: export XDS_PROJECT_ID=<< ID >>):")
433                 fmt.Fprintln(writer, "ID \t Label")
434                 for ii, f := range g.projects {
435                         fmt.Fprintf(writer, " %s \t  %s\n", f.ID, f.Label)
436                         prjExample = &g.projects[ii]
437                 }
438         }
439
440         // FIXME : support multiple servers
441         sdks := []xaapiv1.SDK{}
442         if err := g.httpCli.Get("/servers/0/sdks", &sdks); err != nil {
443                 return int(syscallEBADE), err
444         }
445         fmt.Fprintln(writer, "\nList of installed cross SDKs (use: export XDS_SDK_ID=<< ID >>):")
446         fmt.Fprintln(writer, "ID \t Name")
447         for ii, s := range sdks {
448                 if s.Status == xaapiv1.SdkStatusInstalled {
449                         fmt.Fprintf(writer, " %s \t  %s\n", s.ID, s.Name)
450                         sdkExample = &sdks[ii]
451                 }
452         }
453
454         if prjExample != nil && sdkExample != nil {
455                 fmt.Fprintln(writer, "")
456                 fmt.Fprintln(writer, "For example: ")
457                 if runtime.GOOS == "windows" {
458                         fmt.Fprintf(writer, "  SET XDS_PROJECT_ID=%s && SET XDS_SDK_ID=%s &&  %s -x myGdbConf.ini\n",
459                                 prjExample.ID[:8], sdkExample.ID[:8], AppName)
460                 } else {
461                         fmt.Fprintf(writer, "  XDS_PROJECT_ID=%s XDS_SDK_ID=%s  %s -x myGdbConf.ini\n",
462                                 prjExample.ID[:8], sdkExample.ID[:8], AppName)
463                 }
464         }
465         fmt.Fprintln(writer, "")
466         fmt.Fprintln(writer, "Or define settings within gdb configuration file (see help and :XDS-ENV: tag)")
467         writer.Flush()
468
469         return 0, fmt.Errorf(msg)
470 }