2 * Copyright (C) 2017 "IoT.bzh"
3 * Author Sebastien Douheret <sebastien@iot.bzh>
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
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
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"
36 // GdbXds - Implementation of IGDB used to interfacing XDS
51 httpCli *common.HTTPClient
52 ioSock *sio_client.Client
54 projects []xaapiv1.ProjectConfig
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)
64 // NewGdbXds creates a new instance of GdbXds
65 func NewGdbXds(log *logrus.Logger, args []string, env []string) *GdbXds {
68 ccmd: "exec $GDB", // var set by environment-setup-xxx script
73 xGdbPid: strconv.Itoa(os.Getpid()),
77 // SetConfig set additional config fields
78 func (g *GdbXds) SetConfig(name string, value interface{}) error {
81 g.agentURL = value.(string)
83 g.serverURL = value.(string)
85 g.prjID = value.(string)
87 g.sdkID = value.(string)
89 g.rPath = value.(string)
91 g.listPrj = value.(bool)
93 return fmt.Errorf("Unknown %s field", name)
98 // Init initializes gdb XDS
99 func (g *GdbXds) Init() (int, error) {
101 // Reset command ID (also used to enable sending of signals)
104 // Define HTTP and WS url
105 baseURL := g.agentURL
107 // Allow to only set port number
108 if match, _ := regexp.MatchString("^([0-9]+)$", baseURL); match {
109 baseURL = "http://localhost:" + g.agentURL
111 // Add http prefix if missing
112 if baseURL != "" && !strings.HasPrefix(g.agentURL, "http://") {
113 baseURL = "http://" + g.agentURL
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",
123 LogPrefix: "XDSAGENT: ",
124 LogLevel: common.HTTPLogLevelDebug,
126 c, err := common.HTTPNewClient(baseURL, conf)
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
134 newErr += " (" + strings.TrimSpace(errmsg[i+1:]) + ")"
136 newErr += " (" + strings.TrimSpace(errmsg) + ")"
140 return int(syscallEBADE), fmt.Errorf(errmsg)
143 g.httpCli.SetLogLevel(g.log.Level.String())
144 g.log.Infoln("HTTP session ID:", g.httpCli.GetClientID())
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
151 g.log.Infoln("XDS agent & server version:", ver)
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
158 // FIXME: add multi-servers support
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
169 } else if !svrCfg.Connected {
170 return int(syscallEBADE), fmt.Errorf("XDS server not connected (url=%s)", svrCfg.URL)
173 // Get XDS projects list
175 if err := g.httpCli.HTTPGet("/projects", &data); err != nil {
176 return int(syscallEBADE), err
179 g.log.Infof("Result of /projects: %v", string(data[:]))
180 g.projects = []xaapiv1.ProjectConfig{}
181 errMar := json.Unmarshal(data, &g.projects)
183 g.log.Errorf("Cannot decode projects configuration: %s", errMar.Error())
186 // Check mandatory args
187 if g.prjID == "" || g.listPrj {
188 return g.printProjectsList()
191 // Create io Websocket client
192 g.log.Infoln("Connecting IO.socket client on ", baseURL)
194 opts := &sio_client.Options{
195 Transport: "websocket",
196 Header: make(map[string][]string),
198 opts.Header["XDS-AGENT-SID"] = []string{c.GetClientID()}
200 iosk, err := sio_client.NewClient(baseURL, opts)
202 e := fmt.Sprintf("IO.socket connection error: " + err.Error())
203 return int(syscall.ECONNABORTED), fmt.Errorf(e)
207 iosk.On("error", func(err error) {
208 if g.cbOnError != nil {
213 iosk.On("disconnection", func(err error) {
214 if g.cbOnDisconnect != nil {
215 g.cbOnDisconnect(err)
220 iosk.On(xaapiv1.ExecOutEvent, func(ev xaapiv1.ExecOutMsg) {
222 g.cbRead(ev.Timestamp, ev.Stdout, ev.Stderr)
227 if strings.Contains(stdout, "pid = ") {
228 re := regexp.MustCompile("pid = ([0-9]+)")
229 if res := re.FindAllStringSubmatch(stdout, -1); len(res) > 0 {
232 g.log.Errorf("SEB FOUND THREAD in '%s' => gdbPid=%s", stdout, gdbPid)
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)
240 g.cbRead(ev.Timestamp, stdout, ev.Stderr)
245 iosk.On(xaapiv1.ExecInferiorOutEvent, func(ev xaapiv1.ExecOutMsg) {
246 if g.cbInferiorRead != nil {
247 g.cbInferiorRead(ev.Timestamp, ev.Stdout, ev.Stderr)
251 iosk.On(xaapiv1.ExecExitEvent, func(ev xaapiv1.ExecExitMsg) {
252 if g.cbOnExit != nil {
253 g.cbOnExit(ev.Code, ev.Error)
260 // Close frees allocated objects and close opened connections
261 func (g *GdbXds) Close() error {
262 g.cbOnDisconnect = nil
266 g.cbInferiorRead = nil
272 // Start sends a request to start remotely gdb within xds-server
273 func (g *GdbXds) Start(inferiorTTY bool) (int, error) {
275 var project *xaapiv1.ProjectConfig
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) {
286 // Auto setup rPath if needed
287 if g.rPath == "" && project != nil {
288 cwd, err := os.Getwd()
290 fldRp := project.ClientPath
291 if !strings.HasPrefix(fldRp, "/") {
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)
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")
306 args := xaapiv1.ExecArgs{
314 TTYGdbserverFix: !gdbserverNoFix,
315 CmdTimeout: -1, // no timeout, end when stdin close or command exited normally
318 g.log.Infof("POST %s/exec %v", g.agentURL, args)
319 res := xaapiv1.ExecResult{}
320 err = g.httpCli.Post("/exec", args, &res)
322 return int(syscall.EAGAIN), err
325 return int(syscallEBADE), fmt.Errorf("null CmdID")
332 // Cmd returns the command name
333 func (g *GdbXds) Cmd() string {
337 // Args returns the list of arguments
338 func (g *GdbXds) Args() []string {
342 // Env returns the list of environment variables
343 func (g *GdbXds) Env() []string {
347 // OnError is called on a WebSocket error
348 func (g *GdbXds) OnError(f func(error)) {
352 // OnDisconnect is called when WebSocket disconnection
353 func (g *GdbXds) OnDisconnect(f func(error)) {
357 // OnExit calls when exit event is received
358 func (g *GdbXds) OnExit(f func(code int, err error)) {
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)) {
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)) {
372 // Write writes message/string into gdb stdin
373 func (g *GdbXds) Write(args ...interface{}) error {
374 return g.ioSock.Emit(xaapiv1.ExecInEvent, args...)
377 // SendSignal is used to send a signal to remote process/gdb
378 func (g *GdbXds) SendSignal(sig os.Signal) error {
380 return fmt.Errorf("cmdID not set")
383 sigArg := xaapiv1.ExecSignalArgs{
385 Signal: sig.String(),
387 g.log.Debugf("POST /signal %v", sigArg)
388 return g.httpCli.Post("/signal", sigArg, nil)
391 //***** Private functions *****
393 func (g *GdbXds) printProjectsList() (int, error) {
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)
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
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)
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)
425 return 0, fmt.Errorf(msg)