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