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