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