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