/* * Copyright (C) 2017-2018 "IoT.bzh" * Author Sebastien Douheret <sebastien@iot.bzh> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package main import ( "encoding/json" "fmt" "os" "regexp" "runtime" "strconv" "strings" "syscall" "text/tabwriter" "gerrit.automotivelinux.org/gerrit/src/xds/xds-agent.git/lib/xaapiv1" common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib" "github.com/Sirupsen/logrus" sio_client "github.com/sebd71/go-socket.io-client" ) // GdbXds - Implementation of IGDB used to interfacing XDS type GdbXds struct { log *logrus.Logger ccmd string aargs []string eenv []string agentURL string serverURL string prjID string sdkID string rPath string listPrj bool cmdID string xGdbPid string httpCli *common.HTTPClient ioSock *sio_client.Client projects []xaapiv1.ProjectConfig // 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, xGdbPid: strconv.Itoa(os.Getpid()), } } // SetConfig set additional config fields func (g *GdbXds) SetConfig(name string, value interface{}) error { var val string if name != "listProject" { val = strings.TrimSpace(value.(string)) } switch name { case "agentURL": g.agentURL = val case "serverURL": g.serverURL = val case "prjID": g.prjID = val case "sdkID": g.sdkID = val case "rPath": g.rPath = val 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.agentURL // Allow to only set port number if match, _ := regexp.MatchString("^([0-9]+)$", baseURL); match { baseURL = "http://localhost:" + g.agentURL } // Add http prefix if missing if baseURL != "" && !strings.HasPrefix(g.agentURL, "http://") { baseURL = "http://" + g.agentURL } // Create HTTP client g.log.Infoln("Connect HTTP client on ", baseURL) conf := common.HTTPClientConfig{ URLPrefix: "/api/v1", HeaderClientKeyName: "Xds-Agent-Sid", CsrfDisable: true, LogOut: g.log.Out, LogPrefix: "XDSAGENT: ", LogLevel: common.HTTPLogLevelDebug, } c, err := common.HTTPNewClient(baseURL, conf) if err != nil { errmsg := err.Error() m, err := regexp.MatchString("Get http.?://", errmsg) if (m && err == nil) || strings.Contains(errmsg, "Failed to get device ID") { i := strings.LastIndex(errmsg, ":") newErr := "Cannot connection to " + baseURL if i > 0 { newErr += " (" + strings.TrimSpace(errmsg[i+1:]) + ")" } else { newErr += " (" + strings.TrimSpace(errmsg) + ")" } errmsg = newErr } return int(syscallEBADE), fmt.Errorf(errmsg) } g.httpCli = c g.httpCli.SetLogLevel(g.log.Level.String()) g.log.Infoln("HTTP session ID:", g.httpCli.GetClientID()) // First call to check that xds-agent and server are alive ver := xaapiv1.XDSVersion{} if err := g.httpCli.Get("/version", &ver); err != nil { return int(syscallEBADE), err } g.log.Infoln("XDS agent & server version:", ver) // Get current config and update connection to server when needed xdsConf := xaapiv1.APIConfig{} if err := g.httpCli.Get("/config", &xdsConf); err != nil { return int(syscallEBADE), err } // FIXME: add multi-servers support idx := 0 svrCfg := xdsConf.Servers[idx] if g.serverURL != "" && (svrCfg.URL != g.serverURL || !svrCfg.Connected) { svrCfg.URL = g.serverURL svrCfg.ConnRetry = 10 newCfg := xaapiv1.APIConfig{} if err := g.httpCli.Post("/config", xdsConf, &newCfg); err != nil { return int(syscallEBADE), err } } else if !svrCfg.Connected { return int(syscallEBADE), fmt.Errorf("XDS server not connected (url=%s)", svrCfg.URL) } // Get XDS projects list var data []byte if err := g.httpCli.HTTPGet("/projects", &data); err != nil { return int(syscallEBADE), err } g.log.Infof("Result of /projects: %v", string(data[:])) g.projects = []xaapiv1.ProjectConfig{} errMar := json.Unmarshal(data, &g.projects) if errMar != nil { g.log.Errorf("Cannot decode projects 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-AGENT-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) } }) // SEB gdbPid := "" iosk.On(xaapiv1.ExecOutEvent, func(ev xaapiv1.ExecOutMsg) { if g.cbRead != nil { g.cbRead(ev.Timestamp, ev.Stdout, ev.Stderr) /* stdout := ev.Stdout // SEB //New Thread 15139 if strings.Contains(stdout, "pid = ") { re := regexp.MustCompile("pid = ([0-9]+)") if res := re.FindAllStringSubmatch(stdout, -1); len(res) > 0 { gdbPid = res[0][1] } g.log.Errorf("SEB FOUND THREAD in '%s' => gdbPid=%s", stdout, gdbPid) } if gdbPid != "" && g.xGdbPid != "" && strings.Contains(stdout, gdbPid) { g.log.Errorf("SEB THREAD REPLACE 1 stdout=%s", stdout) stdout = strings.Replace(stdout, gdbPid, g.xGdbPid, -1) g.log.Errorf("SEB THREAD REPLACE 2 stdout=%s", stdout) } g.cbRead(ev.Timestamp, stdout, ev.Stderr) */ } }) iosk.On(xaapiv1.ExecInferiorOutEvent, func(ev xaapiv1.ExecOutMsg) { if g.cbInferiorRead != nil { g.cbInferiorRead(ev.Timestamp, ev.Stdout, ev.Stderr) } }) iosk.On(xaapiv1.ExecExitEvent, func(ev xaapiv1.ExecExitMsg) { if g.cbOnExit != nil { g.cbOnExit(ev.Code, ev.Error) } }) // Monitor XDS server configuration changes (and specifically connected status) iosk.On(xaapiv1.EVTServerConfig, func(ev xaapiv1.EventMsg) { svrCfg, err := ev.DecodeServerCfg() if err == nil && !svrCfg.Connected { // TODO: should wait that server will be connected back if g.cbOnExit != nil { g.cbOnExit(-1, fmt.Errorf("XDS Server disconnected")) } else { fmt.Printf("XDS Server disconnected") os.Exit(-1) } } }) args := xaapiv1.EventRegisterArgs{Name: xaapiv1.EVTServerConfig} if err := g.httpCli.Post("/events/register", args, nil); err != nil { return 0, err } return 0, nil } // Close frees allocated objects and close opened connections 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 err error var project *xaapiv1.ProjectConfig // Retrieve the project definition for _, f := range g.projects { // check as prefix to support short/partial id name if strings.HasPrefix(f.ID, g.prjID) { project = &f break } } // Auto setup rPath if needed if g.rPath == "" && project != nil { cwd, err := os.Getwd() if err == nil { fldRp := project.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") // SDK ID must be set else $GDB cannot be resolved if g.sdkID == "" { return int(syscall.EINVAL), fmt.Errorf("sdkid must be set") } args := xaapiv1.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 } g.log.Infof("POST %s/exec %v", g.agentURL, args) res := xaapiv1.ExecResult{} err = g.httpCli.Post("/exec", args, &res) if err != nil { return int(syscall.EAGAIN), err } if res.CmdID == "" { return int(syscallEBADE), fmt.Errorf("null CmdID") } g.cmdID = res.CmdID 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(xaapiv1.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") } sigArg := xaapiv1.ExecSignalArgs{ CmdID: g.cmdID, Signal: sig.String(), } g.log.Debugf("POST /signal %v", sigArg) return g.httpCli.Post("/signal", sigArg, nil) } //***** Private functions ***** func (g *GdbXds) printProjectsList() (int, error) { writer := new(tabwriter.Writer) writer.Init(os.Stdout, 0, 8, 0, '\t', 0) msg := "" if len(g.projects) > 0 { fmt.Fprintln(writer, "List of existing projects (use: export XDS_PROJECT_ID=<< ID >>):") fmt.Fprintln(writer, "ID \t Label") for _, f := range g.projects { fmt.Fprintf(writer, " %s \t %s\n", f.ID, f.Label) } } // FIXME : support multiple servers sdks := []xaapiv1.SDK{} if err := g.httpCli.Get("/servers/0/sdks", &sdks); err != nil { return int(syscallEBADE), err } fmt.Fprintln(writer, "\nList of installed cross SDKs (use: export XDS_SDK_ID=<< ID >>):") fmt.Fprintln(writer, "ID \t Name") for _, s := range sdks { if s.Status == xaapiv1.SdkStatusInstalled { fmt.Fprintf(writer, " %s \t %s\n", s.ID, s.Name) } } if len(g.projects) > 0 && len(sdks) > 0 { fmt.Fprintln(writer, "") fmt.Fprintln(writer, "For example: ") if runtime.GOOS == "windows" { fmt.Fprintf(writer, " SET XDS_PROJECT_ID=%s && SET XDS_SDK_ID=%s && %s -x myGdbConf.ini\n", g.projects[0].ID[:8], sdks[0].ID[:8], AppName) } else { fmt.Fprintf(writer, " XDS_PROJECT_ID=%s XDS_SDK_ID=%s %s -x myGdbConf.ini\n", g.projects[0].ID[:8], sdks[0].ID[:8], AppName) } } fmt.Fprintln(writer, "") fmt.Fprintln(writer, "Or define settings within gdb configuration file (see help and :XDS-ENV: tag)") writer.Flush() return 0, fmt.Errorf(msg) }