fix image link
[src/xds/xds-gdb.git] / gdb-xds.go
1 package main
2
3 import (
4         "encoding/json"
5         "fmt"
6         "net/http"
7         "os"
8         "strings"
9         "syscall"
10
11         "github.com/Sirupsen/logrus"
12         common "github.com/iotbzh/xds-common/golib"
13         "github.com/iotbzh/xds-server/lib/apiv1"
14         "github.com/iotbzh/xds-server/lib/crosssdk"
15         "github.com/iotbzh/xds-server/lib/folder"
16         sio_client "github.com/zhouhui8915/go-socket.io-client"
17 )
18
19 // GdbXds -
20 type GdbXds struct {
21         log     *logrus.Logger
22         ccmd    string
23         aargs   []string
24         eenv    []string
25         uri     string
26         prjID   string
27         sdkID   string
28         rPath   string
29         listPrj bool
30         cmdID   string
31
32         httpCli *common.HTTPClient
33         ioSock  *sio_client.Client
34
35         folders []folder.FolderConfig
36
37         // callbacks
38         cbOnError      func(error)
39         cbOnDisconnect func(error)
40         cbRead         func(timestamp, stdout, stderr string)
41         cbInferiorRead func(timestamp, stdout, stderr string)
42         cbOnExit       func(code int, err error)
43 }
44
45 // NewGdbXds creates a new instance of GdbXds
46 func NewGdbXds(log *logrus.Logger, args []string, env []string) *GdbXds {
47         return &GdbXds{
48                 log:     log,
49                 ccmd:    "exec $GDB", // var set by environment-setup-xxx script
50                 aargs:   args,
51                 eenv:    env,
52                 httpCli: nil,
53                 ioSock:  nil,
54         }
55 }
56
57 // SetConfig set additional config fields
58 func (g *GdbXds) SetConfig(name string, value interface{}) error {
59         switch name {
60         case "uri":
61                 g.uri = value.(string)
62         case "prjID":
63                 g.prjID = value.(string)
64         case "sdkID":
65                 g.sdkID = value.(string)
66         case "rPath":
67                 g.rPath = value.(string)
68         case "listProject":
69                 g.listPrj = value.(bool)
70         default:
71                 return fmt.Errorf("Unknown %s field", name)
72         }
73         return nil
74 }
75
76 // Init initializes gdb XDS
77 func (g *GdbXds) Init() (int, error) {
78
79         // Define HTTP and WS url
80         baseURL := g.uri
81         if !strings.HasPrefix(g.uri, "http://") {
82                 baseURL = "http://" + g.uri
83         }
84
85         // Create HTTP client
86         g.log.Infoln("Connect HTTP client on ", baseURL)
87         conf := common.HTTPClientConfig{
88                 URLPrefix:           "/api/v1",
89                 HeaderClientKeyName: "XDS-SID",
90                 CsrfDisable:         true,
91         }
92         c, err := common.HTTPNewClient(baseURL, conf)
93         if err != nil {
94                 return int(syscallEBADE), err
95         }
96         g.httpCli = c
97
98         // First call to check that xds-server is alive
99         var data []byte
100         if err := c.HTTPGet("/folders", &data); err != nil {
101                 return int(syscallEBADE), err
102         }
103         g.log.Infof("Result of /folders: %v", string(data[:]))
104         g.folders = []folder.FolderConfig{}
105         errMar := json.Unmarshal(data, &g.folders)
106         if errMar != nil {
107                 g.log.Errorf("Cannot decode folders configuration: %s", errMar.Error())
108         }
109
110         // Check mandatory args
111         if g.prjID == "" || g.listPrj {
112                 return g.printProjectsList()
113         }
114
115         // Create io Websocket client
116         g.log.Infoln("Connecting IO.socket client on ", baseURL)
117
118         opts := &sio_client.Options{
119                 Transport: "websocket",
120                 Header:    make(map[string][]string),
121         }
122         opts.Header["XDS-SID"] = []string{c.GetClientID()}
123
124         iosk, err := sio_client.NewClient(baseURL, opts)
125         if err != nil {
126                 e := fmt.Sprintf("IO.socket connection error: " + err.Error())
127                 return int(syscall.ECONNABORTED), fmt.Errorf(e)
128         }
129         g.ioSock = iosk
130
131         iosk.On("error", func(err error) {
132                 if g.cbOnError != nil {
133                         g.cbOnError(err)
134                 }
135         })
136
137         iosk.On("disconnection", func(err error) {
138                 if g.cbOnDisconnect != nil {
139                         g.cbOnDisconnect(err)
140                 }
141         })
142
143         iosk.On(apiv1.ExecOutEvent, func(ev apiv1.ExecOutMsg) {
144                 if g.cbRead != nil {
145                         g.cbRead(ev.Timestamp, ev.Stdout, ev.Stderr)
146                 }
147         })
148
149         iosk.On(apiv1.ExecInferiorOutEvent, func(ev apiv1.ExecOutMsg) {
150                 if g.cbInferiorRead != nil {
151                         g.cbInferiorRead(ev.Timestamp, ev.Stdout, ev.Stderr)
152                 }
153         })
154
155         iosk.On(apiv1.ExecExitEvent, func(ev apiv1.ExecExitMsg) {
156                 if g.cbOnExit != nil {
157                         g.cbOnExit(ev.Code, ev.Error)
158                 }
159         })
160
161         return 0, nil
162 }
163
164 func (g *GdbXds) Close() error {
165         g.cbOnDisconnect = nil
166         g.cbOnError = nil
167         g.cbOnExit = nil
168         g.cbRead = nil
169         g.cbInferiorRead = nil
170
171         return nil
172 }
173
174 // Start sends a request to start remotely gdb within xds-server
175 func (g *GdbXds) Start(inferiorTTY bool) (int, error) {
176         var body []byte
177         var err error
178         var folder *folder.FolderConfig
179
180         // Retrieve the folder definition
181         for _, f := range g.folders {
182                 if f.ID == g.prjID {
183                         folder = &f
184                         break
185                 }
186         }
187
188         // Auto setup rPath if needed
189         if g.rPath == "" && folder != nil {
190                 cwd, err := os.Getwd()
191                 if err == nil {
192                         fldRp := folder.ClientPath
193                         if !strings.HasPrefix(fldRp, "/") {
194                                 fldRp = "/" + fldRp
195                         }
196                         log.Debugf("Try to auto-setup rPath: cwd=%s ; ClientPath=%s", cwd, fldRp)
197                         if sp := strings.SplitAfter(cwd, fldRp); len(sp) == 2 {
198                                 g.rPath = strings.Trim(sp[1], "/")
199                                 g.log.Debugf("Auto-setup rPath to: '%s'", g.rPath)
200                         }
201                 }
202         }
203
204         // Enable workaround about inferior output with gdbserver connection
205         // except if XDS_GDBSERVER_OUTPUT_NOFIX is defined
206         _, gdbserverNoFix := os.LookupEnv("XDS_GDBSERVER_OUTPUT_NOFIX")
207
208         args := apiv1.ExecArgs{
209                 ID:              g.prjID,
210                 SdkID:           g.sdkID,
211                 Cmd:             g.ccmd,
212                 Args:            g.aargs,
213                 Env:             g.eenv,
214                 RPath:           g.rPath,
215                 TTY:             inferiorTTY,
216                 TTYGdbserverFix: !gdbserverNoFix,
217                 CmdTimeout:      -1, // no timeout, end when stdin close or command exited normally
218         }
219         body, err = json.Marshal(args)
220         if err != nil {
221                 return int(syscallEBADE), err
222         }
223
224         g.log.Infof("POST %s/exec %v", g.uri, string(body))
225         var res *http.Response
226         var found bool
227         res, err = g.httpCli.HTTPPostWithRes("/exec", string(body))
228         if err != nil {
229                 return int(syscall.EAGAIN), err
230         }
231         dRes := make(map[string]interface{})
232         json.Unmarshal(g.httpCli.ResponseToBArray(res), &dRes)
233         if _, found = dRes["cmdID"]; !found {
234                 return int(syscallEBADE), err
235         }
236         g.cmdID = dRes["cmdID"].(string)
237
238         return 0, nil
239 }
240
241 // Cmd returns the command name
242 func (g *GdbXds) Cmd() string {
243         return g.ccmd
244 }
245
246 // Args returns the list of arguments
247 func (g *GdbXds) Args() []string {
248         return g.aargs
249 }
250
251 // Env returns the list of environment variables
252 func (g *GdbXds) Env() []string {
253         return g.eenv
254 }
255
256 // OnError is called on a WebSocket error
257 func (g *GdbXds) OnError(f func(error)) {
258         g.cbOnError = f
259 }
260
261 // OnDisconnect is called when WebSocket disconnection
262 func (g *GdbXds) OnDisconnect(f func(error)) {
263         g.cbOnDisconnect = f
264 }
265
266 // OnExit calls when exit event is received
267 func (g *GdbXds) OnExit(f func(code int, err error)) {
268         g.cbOnExit = f
269 }
270
271 // Read calls when a message/string event is received on stdout or stderr
272 func (g *GdbXds) Read(f func(timestamp, stdout, stderr string)) {
273         g.cbRead = f
274 }
275
276 // InferiorRead calls when a message/string event is received on stdout or stderr of the debugged program (IOW inferior)
277 func (g *GdbXds) InferiorRead(f func(timestamp, stdout, stderr string)) {
278         g.cbInferiorRead = f
279 }
280
281 // Write writes message/string into gdb stdin
282 func (g *GdbXds) Write(args ...interface{}) error {
283         return g.ioSock.Emit(apiv1.ExecInEvent, args...)
284 }
285
286 // SendSignal is used to send a signal to remote process/gdb
287 func (g *GdbXds) SendSignal(sig os.Signal) error {
288         var body []byte
289         body, err := json.Marshal(apiv1.ExecSignalArgs{
290                 CmdID:  g.cmdID,
291                 Signal: sig.String(),
292         })
293         if err != nil {
294                 g.log.Errorf(err.Error())
295         }
296         g.log.Debugf("POST /signal %s", string(body))
297         return g.httpCli.HTTPPost("/signal", string(body))
298 }
299
300 //***** Private functions *****
301
302 func (g *GdbXds) printProjectsList() (int, error) {
303         msg := ""
304         if len(g.folders) > 0 {
305                 msg += "List of existing projects (use: export XDS_PROJECT_ID=<< ID >>): \n"
306                 msg += "  ID\t\t\t\t | Label"
307                 for _, f := range g.folders {
308                         msg += fmt.Sprintf("\n  %s\t | %s", f.ID, f.Label)
309                         if f.DefaultSdk != "" {
310                                 msg += fmt.Sprintf("\t(default SDK: %s)", f.DefaultSdk)
311                         }
312                 }
313                 msg += "\n"
314         }
315
316         var data []byte
317         if err := g.httpCli.HTTPGet("/sdks", &data); err != nil {
318                 return int(syscallEBADE), err
319         }
320         g.log.Infof("Result of /sdks: %v", string(data[:]))
321
322         sdks := []crosssdk.SDK{}
323         errMar := json.Unmarshal(data, &sdks)
324         if errMar == nil {
325                 msg += "\nList of installed cross SDKs (use: export XDS_SDK_ID=<< ID >>): \n"
326                 msg += "  ID\t\t\t\t\t | NAME\n"
327                 for _, s := range sdks {
328                         msg += fmt.Sprintf("  %s\t | %s\n", s.ID, s.Name)
329                 }
330         }
331
332         if len(g.folders) > 0 && len(sdks) > 0 {
333                 msg += fmt.Sprintf("\n")
334                 msg += fmt.Sprintf("For example: \n")
335                 msg += fmt.Sprintf("  XDS_PROJECT_ID=%q XDS_SDK_ID=%q  %s -x myGdbConf.ini\n",
336                         g.folders[0].ID, sdks[0].ID, AppName)
337         }
338
339         return 0, fmt.Errorf(msg)
340 }