2 * Copyright (C) 2017-2018 "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.
18 * xds-gdb: a wrapper on gdb tool for X(cross) Development System.
37 common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib"
38 "github.com/Sirupsen/logrus"
39 "github.com/codegangsta/cli"
40 "github.com/joho/godotenv"
43 var appAuthors = []cli.Author{
44 cli.Author{Name: "Sebastien Douheret", Email: "sebastien@iot.bzh"},
47 // AppName name of this application
48 var AppName = "xds-gdb"
50 // AppVersion Version of this application
52 var AppVersion = "?.?.?"
54 // AppSubVersion is the git tag id added to version string
55 // Should be set by compilation -ldflags "-X main.AppSubVersion=xxx"
57 var AppSubVersion = "unknown-dev"
60 var log = logrus.New()
61 var logFileInitial = path.Join(os.TempDir(), "xds-gdb.log")
63 // Application details
65 appCopyright = "Copyright (C) 2017-2018 IoT.bzh - Apache-2.0"
66 defaultLogLevel = "warning"
70 type exitResult struct {
75 // EnvVar - Environment variables used by application
82 // exitError terminates this program with the specified error
83 func exitError(code syscall.Errno, f string, a ...interface{}) {
84 err := fmt.Sprintf(f, a...)
85 fmt.Fprintf(os.Stderr, err+"\n")
86 log.Debugf("Exit: code=%v, err=%s", code, err)
93 var agentURL, serverURL string
94 var prjID, rPath, logLevel, logFile, sdkid, confFile, gdbNative string
98 // Init Logger and set temporary file and level for the 1st part
99 // IOW while XDS_LOGLEVEL and XDS_LOGFILE options are not parsed
100 logFile = logFileInitial
101 fdL, err := os.OpenFile(logFileInitial, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
103 fmt.Printf("WARNING: Cannot create initial log file %s\n", logFileInitial)
104 log.Level = logrus.WarnLevel
107 log.Level = logrus.DebugLevel
109 log.Formatter = &logrus.TextFormatter{}
111 agentURL = "localhost:8800"
112 logLevel = defaultLogLevel
114 // Create a new App instance
117 app.Usage = "wrapper on gdb for X(cross) Development System."
118 app.Version = AppVersion + " (" + AppSubVersion + ")"
119 app.Authors = appAuthors
120 app.Copyright = appCopyright
121 app.Metadata = make(map[string]interface{})
122 app.Metadata["version"] = AppVersion
123 app.Metadata["git-tag"] = AppSubVersion
124 app.Metadata["logger"] = log
126 app.Flags = []cli.Flag{
129 Usage: "list existing xds projects",
130 Destination: &listProject,
134 appEnvVars := []EnvVar{
137 Usage: "env config file to source on startup",
138 Destination: &confFile,
141 Name: "XDS_LOGLEVEL",
142 Usage: "logging level (supported levels: panic, fatal, error, warn, info, debug)",
143 Destination: &logLevel,
147 Usage: "logging file (default: " + logFileInitial + ")",
148 Destination: &logFile,
151 Name: "XDS_NATIVE_GDB",
152 Usage: "use native gdb instead of remote XDS server",
153 Destination: &gdbNative,
156 Name: "XDS_PROJECT_ID",
157 Usage: "project ID you want to build (mandatory variable)",
162 Usage: "relative path into project",
167 Usage: "Cross Sdk ID to use to build project",
171 Name: "XDS_AGENT_URL",
172 Usage: "local XDS agent url",
173 Destination: &agentURL,
176 Name: "XDS_SERVER_URL",
177 Usage: "overwrite remote XDS server url (default value set in xds-agent-config.json file)",
178 Destination: &serverURL,
182 // Process gdb arguments
183 log.Debugf("xds-gdb started with args: %v", os.Args)
184 args := make([]string, len(os.Args))
186 gdbArgs := make([]string, len(os.Args))
188 // Split xds-xxx options from gdb options
189 copy(gdbArgs, os.Args[1:])
190 for idx, a := range os.Args[1:] {
191 // Specific case to print help or version of xds-gdb
193 case "--help", "-h", "--version", "-v", "--list", "-ls":
197 // Detect skip option (IOW '--') to split arguments
198 copy(args, os.Args[0:idx+1])
199 copy(gdbArgs, os.Args[idx+2:])
205 // Parse gdb arguments to detect:
206 // --tty option: used for inferior/ tty of debugged program
207 // -x/--command option: XDS env vars may be set within gdb command file
210 for idx, a := range gdbArgs {
212 case strings.HasPrefix(a, "--tty="):
213 clientPty = a[len("--tty="):]
217 case strings.HasPrefix(a, "-tty"):
218 clientPty = gdbArgs[idx+1]
222 case strings.HasPrefix(a, "--command="):
223 gdbCmdFile = a[len("--command="):]
225 case a == "--command":
226 case strings.HasPrefix(a, "-x"):
227 gdbCmdFile = gdbArgs[idx+1]
231 // Source config env file
232 // (we cannot use confFile var because env variables setting is just after)
233 envMap, confFile, err := loadConfigEnvFile(os.Getenv("XDS_CONFIG"), gdbCmdFile)
234 log.Infof("Load env config: envMap=%v, confFile=%v, err=%v", envMap, confFile, err)
236 // Only rise an error when args is not set (IOW when --help or --version is not set)
239 exitError(syscall.ENOENT, err.Error())
243 // Managed env vars and create help
244 dynDesc := "\nENVIRONMENT VARIABLES:"
245 for _, ev := range appEnvVars {
246 dynDesc += fmt.Sprintf("\n %s \t\t %s", ev.Name, ev.Usage)
247 if evVal, evExist := os.LookupEnv(ev.Name); evExist && ev.Destination != nil {
248 *ev.Destination = evVal
251 app.Description = "gdb wrapper for X(cross) Development System\n"
252 app.Description += "\n"
253 app.Description += " Two debugging models are supported:\n"
254 app.Description += " - xds remote debugging requiring an XDS server and allowing cross debug\n"
255 app.Description += " - native debugging\n"
256 app.Description += " By default xds remote debug is used and you need to define XDS_NATIVE_GDB to\n"
257 app.Description += " use native gdb debug mode instead.\n"
258 app.Description += "\n"
259 app.Description += " xds-gdb configuration (see variables list below) can be set using:\n"
260 app.Description += " - a config file (XDS_CONFIG)\n"
261 app.Description += " - or environment variables\n"
262 app.Description += " - or by setting variables within gdb ini file (commented line including :XDS-ENV: tag)\n"
263 app.Description += " Example of gdb ini file where we define project and sdk ID:\n"
264 app.Description += " # :XDS-ENV: XDS_PROJECT_ID=IW7B4EE-DBY4Z74_myProject\n"
265 app.Description += " # :XDS-ENV: XDS_SDK_ID=poky-agl_aarch64_3.99.1+snapshot\n"
266 app.Description += "\n"
267 app.Description += dynDesc + "\n"
270 app.Action = func(ctx *cli.Context) error {
272 curDir, _ := os.Getwd()
274 // Build env variables
276 for k, v := range envMap {
277 env = append(env, k+"="+v)
280 // Now set logger level and log file to correct/env var settings
281 if log.Level, err = logrus.ParseLevel(logLevel); err != nil {
282 msg := fmt.Sprintf("Invalid log level : \"%v\"\n", logLevel)
283 return cli.NewExitError(msg, int(syscall.EINVAL))
285 log.Infof("Switch log level to %s", logLevel)
287 if logFile != logFileInitial {
288 log.Infof("Switch logging to log file %s", logFile)
290 fdL, err := os.OpenFile(logFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
292 msgErr := fmt.Sprintf("Cannot create log file %s", logFile)
293 return cli.NewExitError(msgErr, int(syscall.EPERM))
299 // Create cross or native gdb interface
302 gdb = NewGdbNative(log, gdbArgs, env)
304 gdb = NewGdbXds(log, gdbArgs, env)
305 gdb.SetConfig("agentURL", agentURL)
306 gdb.SetConfig("serverURL", serverURL)
307 gdb.SetConfig("prjID", prjID)
308 gdb.SetConfig("sdkID", sdkid)
309 gdb.SetConfig("rPath", rPath)
310 gdb.SetConfig("listProject", listProject)
314 log.Infof("Original arguments: %v", os.Args)
315 log.Infof("Current directory : %v", curDir)
316 log.Infof("Use confFile : '%s'", confFile)
317 log.Infof("Execute : /exec %v %v", gdb.Cmd(), gdb.Args())
319 // Properly report invalid init file error
320 gdbCommandFileError := ""
321 for i, a := range gdbArgs {
323 gdbCommandFileError = gdbArgs[i+1] + ": No such file or directory."
325 } else if strings.HasPrefix(a, "--command=") {
326 gdbCommandFileError = strings.TrimLeft(a, "--command=") + ": No such file or directory."
330 log.Infof("Add detection of error: <%s>", gdbCommandFileError)
332 // Init gdb subprocess management
333 if code, err := gdb.Init(); err != nil {
334 return cli.NewExitError(err.Error(), code)
337 exitChan := make(chan exitResult, 1)
339 gdb.OnError(func(err error) {
340 fmt.Println("ERROR: ", err.Error())
343 gdb.OnDisconnect(func(err error) {
344 errMsg := "\nXDS-Agent disconnected"
346 fmt.Printf("%s: %v\n", errMsg, err.Error())
351 exitChan <- exitResult{err, int(syscall.ESHUTDOWN)}
354 gdb.Read(func(timestamp, stdout, stderr string) {
356 fmt.Printf("%s", stdout)
357 log.Debugf("Recv OUT: <%s>", stdout)
360 // Filter-out ugly message (python error when cross gdb exited)
361 if !strings.Contains(stderr, "readline.write_history_file") &&
362 !(strings.Contains(stderr, "Traceback") && strings.Contains(stderr, "__exithandler")) {
363 fmt.Fprintf(os.Stderr, "%s", stderr)
364 log.Debugf("Recv ERR: <%s>", stderr)
366 log.Debugf("Recv ERR (FILTERED OUT): <%s>", stderr)
370 // Correctly report error about init file
371 if gdbCommandFileError != "" && strings.Contains(stdout, gdbCommandFileError) {
372 fmt.Fprintf(os.Stderr, "ERROR: "+gdbCommandFileError)
373 log.Errorf("ERROR: " + gdbCommandFileError)
374 if err := gdb.SendSignal(syscall.SIGTERM); err != nil {
375 log.Errorf("Error while sending signal: %s", err.Error())
377 exitChan <- exitResult{err, int(syscall.ENOENT)}
381 gdb.OnExit(func(code int, err error) {
382 exitChan <- exitResult{err, code}
385 // Handle client tty / pts
387 log.Infoln("Client tty detected: %v", clientPty)
389 cpFd, err := os.OpenFile(clientPty, os.O_RDWR, 0)
391 return cli.NewExitError(err.Error(), int(syscall.EPERM))
396 /* XXX TODO - implement stdin to send data to debugged program
398 reader := bufio.NewReader(cpFd)
399 sc := bufio.NewScanner(reader)
402 iosk.Emit(xaapiv1.ExecInferiorInEvent, data+"\n")
403 log.Debugf("Inferior IN: <%v>", data)
406 log.Warnf("Inferior Stdin scanner exit, close stdin (err=%v)", sc.Err())
412 gdb.InferiorRead(func(timestamp, stdout, stderr string) {
414 fmt.Fprintf(cpFd, "%s", stdout)
415 log.Debugf("Inferior OUT: <%s>", stdout)
418 fmt.Fprintf(cpFd, "%s", stderr)
419 log.Debugf("Inferior ERR: <%s>", stderr)
424 // Allow to overwrite some gdb commands
425 var overwriteMap = make(map[string]string)
426 if overEnv, exist := os.LookupEnv("XDS_OVERWRITE_COMMANDS"); exist {
427 overEnvS := strings.TrimSpace(overEnv)
428 if len(overEnvS) > 0 {
429 // Extract overwrite commands from env variable
430 for _, def := range strings.Split(overEnvS, ",") {
431 if kv := strings.Split(def, ":"); len(kv) == 2 {
432 overwriteMap[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
434 return cli.NewExitError(
435 fmt.Errorf("Invalid definition in XDS_OVERWRITE_COMMANDS (%s)", def),
441 overwriteMap["-exec-run"] = "-exec-continue"
442 overwriteMap["-file-exec-and-symbols"] = "-file-exec-file"
444 log.Debugf("overwriteMap = %v", overwriteMap)
446 // Send stdin though WS
449 reader := bufio.NewReader(os.Stdin)
451 // Enable workaround to correctly close connection
452 // except if XDS_GDBSERVER_EXIT_NOFIX is defined
453 _, gdbExitNoFix := os.LookupEnv("XDS_GDBSERVER_EXIT_NOFIX")
456 sc := bufio.NewScanner(reader)
460 // overwrite some commands
461 for key, value := range overwriteMap {
462 if strings.Contains(command, key) {
463 command = strings.Replace(command, key, value, 1)
464 log.Debugf("OVERWRITE %s -> %s", key, value)
468 // Send SIGINT to stop debugged process execution before sending -gdb-exit command
469 if !gdbExitNoFix && strings.Contains(command, "-gdb-exit") {
470 log.Infof("Detection of -gdb-exit, exiting...")
471 if err := gdb.SendSignal(syscall.SIGINT); err != nil {
472 log.Errorf("Error while sending signal SIGINT : %s", err.Error())
474 time.Sleep(time.Millisecond * 200)
477 log.Debugf("Send: <%v>", command)
478 gdb.Write(command + "\n")
480 log.Infof("Stdin scanner exit, close stdin (err=%v)", sc.Err())
482 // CTRL-D exited scanner, so send it explicitly
484 time.Sleep(time.Millisecond * 100)
486 if paranoia--; paranoia <= 0 {
487 msg := "Abnormal loop detected on stdin"
488 log.Errorf("Abnormal loop detected on stdin")
489 gdb.SendSignal(syscall.SIGTERM)
490 exitChan <- exitResult{fmt.Errorf(msg), int(syscall.ELOOP)}
495 // Handling all Signals
496 sigs := make(chan os.Signal, 1)
503 if isIgnoredSignal(sig) {
507 if err := gdb.SendSignal(sig); err != nil {
508 log.Errorf("Error while sending signal %v : %s", sig, err.Error())
514 if code, err := gdb.Start(clientPty != ""); err != nil {
515 return cli.NewExitError(err.Error(), code)
520 case res := <-exitChan:
523 log.Infoln("Exit successfully")
525 if res.error != nil {
526 log.Infoln("Exit with ERROR: ", res.error.Error())
527 errStr = res.error.Error()
529 return cli.NewExitError(errStr, res.code)
537 func loadConfigEnvFile(confFile, gdbCmdFile string) (map[string]string, string, error) {
539 envMap := make(map[string]string)
541 // 1- if no confFile set, use setting from gdb command file is option
542 // --command/-x is set
543 if confFile == "" && gdbCmdFile != "" {
544 log.Infof("Try extract config from gdbCmdFile: %s", gdbCmdFile)
545 confFile, err = extractEnvFromCmdFile(gdbCmdFile)
547 defer os.Remove(confFile)
550 log.Infof("Extraction from gdbCmdFile failed: %v", err.Error())
553 // 2- search xds-gdb.env file in various locations
555 curDir, _ := os.Getwd()
556 if u, err := user.Current(); err == nil {
557 xdsEnvFile := "xds-gdb.env"
558 for _, d := range []string{
560 path.Join(curDir, ".."),
561 path.Join(curDir, "target"),
562 path.Join(u.HomeDir, ".config", "xds"),
564 cf := path.Join(d, xdsEnvFile)
565 log.Infof("Search config in %s", cf)
566 if common.Exists(cf) {
575 log.Infof("NO valid conf file found!")
576 return envMap, "", nil
579 if !common.Exists(confFile) {
580 return envMap, confFile, fmt.Errorf("Error no env config file not found")
582 if err = godotenv.Load(confFile); err != nil {
583 return envMap, confFile, fmt.Errorf("Error loading env config file " + confFile)
585 if envMap, err = godotenv.Read(confFile); err != nil {
586 return envMap, confFile, fmt.Errorf("Error reading env config file " + confFile)
589 return envMap, confFile, nil
593 extractEnvFromCmdFile: extract xds-gdb env variable from gdb command file
594 All commented lines (#) in gdb command file that start with ':XDS-ENV:' prefix
595 will be considered as XDS env commands. For example the 3 syntaxes below
597 # :XDS-ENV: XDS_PROJECT_ID=4021617e-ced0-11e7-acd2-3c970e49ad9b
598 #:XDS-ENV:XDS_SDK_ID=06c0e95a-e215-3a5a-b373-f677c0dabd3b
599 # :XDS-ENV: export XDS_AGENT_URL=localhost:8800
601 func extractEnvFromCmdFile(cmdFile string) (string, error) {
602 if !common.Exists(cmdFile) {
605 cFd, err := os.Open(cmdFile)
607 return "", fmt.Errorf("Cannot open %s : %s", cmdFile, err.Error())
612 scanner := bufio.NewScanner(cFd)
614 lines = append(lines, scanner.Text())
616 if err = scanner.Err(); err != nil {
617 return "", fmt.Errorf("Cannot parse %s : %s", cmdFile, err.Error())
620 envFile, err := ioutil.TempFile("", "xds-gdb_env.ini")
622 return "", fmt.Errorf("Error while creating temporary env file: %s", err.Error())
624 envFileName := envFile.Name()
625 defer envFile.Close()
628 for _, ln := range lines {
629 ln = strings.TrimSpace(ln)
630 if strings.HasPrefix(ln, "#") && strings.Contains(ln, ":XDS-ENV:") {
631 env := strings.SplitAfterN(ln, ":XDS-ENV:", 2)
634 if _, err := envFile.WriteString(strings.TrimSpace(env[1]) + "\n"); err != nil {
635 return "", fmt.Errorf("Error write into temporary env file: %s", err.Error())
638 log.Warnf("Error while decoding line %s", ln)
650 return envFileName, nil