package main import ( "encoding/json" "fmt" "net/http" "os" "strings" "syscall" "github.com/Sirupsen/logrus" common "github.com/iotbzh/xds-common/golib" "github.com/iotbzh/xds-server/lib/apiv1" "github.com/iotbzh/xds-server/lib/crosssdk" "github.com/iotbzh/xds-server/lib/folder" sio_client "github.com/zhouhui8915/go-socket.io-client" ) // GdbXds - type GdbXds struct { log *logrus.Logger ccmd string aargs []string eenv []string uri string prjID string sdkID string rPath string listPrj bool cmdID string httpCli *common.HTTPClient ioSock *sio_client.Client folders []folder.FolderConfig // callbacks cbOnError func(error) cbOnDisconnect func(error) cbRead func(timestamp, stdout, stderr string) cbInferiorRead func(timestamp, stdout, stderr string) cbOnExit func(code int, err error) } // NewGdbXds creates a new instance of GdbXds func NewGdbXds(log *logrus.Logger, args []string, env []string) *GdbXds { return &GdbXds{ log: log, ccmd: "exec $GDB", // var set by environment-setup-xxx script aargs: args, eenv: env, httpCli: nil, ioSock: nil, } } // SetConfig set additional config fields func (g *GdbXds) SetConfig(name string, value interface{}) error { switch name { case "uri": g.uri = value.(string) case "prjID": g.prjID = value.(string) case "sdkID": g.sdkID = value.(string) case "rPath": g.rPath = value.(string) case "listProject": g.listPrj = value.(bool) default: return fmt.Errorf("Unknown %s field", name) } return nil } // Init initializes gdb XDS func (g *GdbXds) Init() (int, error) { // Reset command ID (also used to enable sending of signals) g.cmdID = "" // Define HTTP and WS url baseURL := g.uri if !strings.HasPrefix(g.uri, "http://") { baseURL = "http://" + g.uri } // Create HTTP client g.log.Infoln("Connect HTTP client on ", baseURL) conf := common.HTTPClientConfig{ URLPrefix: "/api/v1", HeaderClientKeyName: "XDS-SID", CsrfDisable: true, } c, err := common.HTTPNewClient(baseURL, conf) if err != nil { return int(syscallEBADE), err } g.httpCli = c // First call to check that xds-server is alive var data []byte if err := c.HTTPGet("/folders", &data); err != nil { return int(syscallEBADE), err } g.log.Infof("Result of /folders: %v", string(data[:])) g.folders = []folder.FolderConfig{} errMar := json.Unmarshal(data, &g.folders) if errMar != nil { g.log.Errorf("Cannot decode folders configuration: %s", errMar.Error()) } // Check mandatory args if g.prjID == "" || g.listPrj { return g.printProjectsList() } // Create io Websocket client g.log.Infoln("Connecting IO.socket client on ", baseURL) opts := &sio_client.Options{ Transport: "websocket", Header: make(map[string][]string), } opts.Header["XDS-SID"] = []string{c.GetClientID()} iosk, err := sio_client.NewClient(baseURL, opts) if err != nil { e := fmt.Sprintf("IO.socket connection error: " + err.Error()) return int(syscall.ECONNABORTED), fmt.Errorf(e) } g.ioSock = iosk iosk.On("error", func(err error) { if g.cbOnError != nil { g.cbOnError(err) } }) iosk.On("disconnection", func(err error) { if g.cbOnDisconnect != nil { g.cbOnDisconnect(err) } }) iosk.On(apiv1.ExecOutEvent, func(ev apiv1.ExecOutMsg) { if g.cbRead != nil { g.cbRead(ev.Timestamp, ev.Stdout, ev.Stderr) } }) iosk.On(apiv1.ExecInferiorOutEvent, func(ev apiv1.ExecOutMsg) { if g.cbInferiorRead != nil { g.cbInferiorRead(ev.Timestamp, ev.Stdout, ev.Stderr) } }) iosk.On(apiv1.ExecExitEvent, func(ev apiv1.ExecExitMsg) { if g.cbOnExit != nil { g.cbOnExit(ev.Code, ev.Error) } }) return 0, nil } func (g *GdbXds) Close() error { g.cbOnDisconnect = nil g.cbOnError = nil g.cbOnExit = nil g.cbRead = nil g.cbInferiorRead = nil g.cmdID = "" return nil } // Start sends a request to start remotely gdb within xds-server func (g *GdbXds) Start(inferiorTTY bool) (int, error) { var body []byte var err error var folder *folder.FolderConfig // Retrieve the folder definition for _, f := range g.folders { if f.ID == g.prjID { folder = &f break } } // Auto setup rPath if needed if g.rPath == "" && folder != nil { cwd, err := os.Getwd() if err == nil { fldRp := folder.ClientPath if !strings.HasPrefix(fldRp, "/") { fldRp = "/" + fldRp } log.Debugf("Try to auto-setup rPath: cwd=%s ; ClientPath=%s", cwd, fldRp) if sp := strings.SplitAfter(cwd, fldRp); len(sp) == 2 { g.rPath = strings.Trim(sp[1], "/") g.log.Debugf("Auto-setup rPath to: '%s'", g.rPath) } } } // Enable workaround about inferior output with gdbserver connection // except if XDS_GDBSERVER_OUTPUT_NOFIX is defined _, gdbserverNoFix := os.LookupEnv("XDS_GDBSERVER_OUTPUT_NOFIX") args := apiv1.ExecArgs{ ID: g.prjID, SdkID: g.sdkID, Cmd: g.ccmd, Args: g.aargs, Env: g.eenv, RPath: g.rPath, TTY: inferiorTTY, TTYGdbserverFix: !gdbserverNoFix, CmdTimeout: -1, // no timeout, end when stdin close or command exited normally } body, err = json.Marshal(args) if err != nil { return int(syscallEBADE), err } g.log.Infof("POST %s/exec %v", g.uri, string(body)) var res *http.Response var found bool res, err = g.httpCli.HTTPPostWithRes("/exec", string(body)) if err != nil { return int(syscall.EAGAIN), err } dRes := make(map[string]interface{}) json.Unmarshal(g.httpCli.ResponseToBArray(res), &dRes) if _, found = dRes["cmdID"]; !found { return int(syscallEBADE), err } g.cmdID = dRes["cmdID"].(string) return 0, nil } // Cmd returns the command name func (g *GdbXds) Cmd() string { return g.ccmd } // Args returns the list of arguments func (g *GdbXds) Args() []string { return g.aargs } // Env returns the list of environment variables func (g *GdbXds) Env() []string { return g.eenv } // OnError is called on a WebSocket error func (g *GdbXds) OnError(f func(error)) { g.cbOnError = f } // OnDisconnect is called when WebSocket disconnection func (g *GdbXds) OnDisconnect(f func(error)) { g.cbOnDisconnect = f } // OnExit calls when exit event is received func (g *GdbXds) OnExit(f func(code int, err error)) { g.cbOnExit = f } // Read calls when a message/string event is received on stdout or stderr func (g *GdbXds) Read(f func(timestamp, stdout, stderr string)) { g.cbRead = f } // InferiorRead calls when a message/string event is received on stdout or stderr of the debugged program (IOW inferior) func (g *GdbXds) InferiorRead(f func(timestamp, stdout, stderr string)) { g.cbInferiorRead = f } // Write writes message/string into gdb stdin func (g *GdbXds) Write(args ...interface{}) error { return g.ioSock.Emit(apiv1.ExecInEvent, args...) } // SendSignal is used to send a signal to remote process/gdb func (g *GdbXds) SendSignal(sig os.Signal) error { if g.cmdID == "" { return fmt.Errorf("cmdID not set") } var body []byte body, err := json.Marshal(apiv1.ExecSignalArgs{ CmdID: g.cmdID, Signal: sig.String(), }) if err != nil { g.log.Errorf(err.Error()) } g.log.Debugf("POST /signal %s", string(body)) return g.httpCli.HTTPPost("/signal", string(body)) } //***** Private functions ***** func (g *GdbXds) printProjectsList() (int, error) { msg := "" if len(g.folders) > 0 { msg += "List of existing projects (use: export XDS_PROJECT_ID=<< ID >>): \n" msg += " ID\t\t\t\t | Label" for _, f := range g.folders { msg += fmt.Sprintf("\n %s\t | %s", f.ID, f.Label) if f.DefaultSdk != "" { msg += fmt.Sprintf("\t(default SDK: %s)", f.DefaultSdk) } } msg += "\n" } var data []byte if err := g.httpCli.HTTPGet("/sdks", &data); err != nil { return int(syscallEBADE), err } g.log.Infof("Result of /sdks: %v", string(data[:])) sdks := []crosssdk.SDK{} errMar := json.Unmarshal(data, &sdks) if errMar == nil { msg += "\nList of installed cross SDKs (use: export XDS_SDK_ID=<< ID >>): \n" msg += " ID\t\t\t\t\t | NAME\n" for _, s := range sdks { msg += fmt.Sprintf(" %s\t | %s\n", s.ID, s.Name) } } if len(g.folders) > 0 && len(sdks) > 0 { msg += fmt.Sprintf("\n") msg += fmt.Sprintf("For example: \n") msg += fmt.Sprintf(" XDS_PROJECT_ID=%q XDS_SDK_ID=%q %s -x myGdbConf.ini\n", g.folders[0].ID, sdks[0].ID, AppName) } return 0, fmt.Errorf(msg) }