summaryrefslogtreecommitdiffstats
path: root/main.go
blob: b193676c7daf455462dba242a8028278c8253716 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
[match-endpoint]
priority = 75
direction = "source"
name = "bluealsa*"
media_class = "Stream/Output/Audio"

[endpoint-link]
keep = true
a> 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651
/*
 * 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.
 *
 *
 * xds-gdb: a wrapper on gdb tool for X(cross) Development System.
 */

package main

import (
	"bufio"
	"fmt"
	"io/ioutil"
	"os"
	"os/signal"
	"os/user"
	"syscall"
	"time"

	"strings"

	"path"

	common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib"
	"github.com/Sirupsen/logrus"
	"github.com/codegangsta/cli"
	"github.com/joho/godotenv"
)

var appAuthors = []cli.Author{
	cli.Author{Name: "Sebastien Douheret", Email: "sebastien@iot.bzh"},
}

// AppName name of this application
var AppName = "xds-gdb"

// AppVersion Version of this application
// (set by Makefile)
var AppVersion = "?.?.?"

// AppSubVersion is the git tag id added to version string
// Should be set by compilation -ldflags "-X main.AppSubVersion=xxx"
// (set by Makefile)
var AppSubVersion = "unknown-dev"

// Create logger
var log = logrus.New()
var logFileInitial = path.Join(os.TempDir(), "xds-gdb.log")

// Application details
const (
	appCopyright    = "Copyright (C) 2017-2018 IoT.bzh - Apache-2.0"
	defaultLogLevel = "warning"
)

// Exit events
type exitResult struct {
	error error
	code  int
}

// EnvVar - Environment variables used by application
type EnvVar struct {
	Name        string
	Usage       string
	Destination *string
}

// exitError terminates this program with the specified error
func exitError(code syscall.Errno, f string, a ...interface{}) {
	err := fmt.Sprintf(f, a...)
	fmt.Fprintf(os.Stderr, err+"\n")
	log.Debugf("Exit: code=%v, err=%s", code, err)

	os.Exit(int(code))
}

// main
func main() {
	var agentURL, serverURL string
	var prjID, rPath, logLevel, logFile, sdkid, confFile, gdbNative string
	var listProject bool
	var err error

	// Init Logger and set temporary file and level for the 1st part
	// IOW while XDS_LOGLEVEL and XDS_LOGFILE options are not parsed
	logFile = logFileInitial
	fdL, err := os.OpenFile(logFileInitial, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
	if err != nil {
		fmt.Printf("WARNING: Cannot create initial log file %s\n", logFileInitial)
		log.Level = logrus.WarnLevel
	} else {
		log.Out = fdL
		log.Level = logrus.DebugLevel
	}
	log.Formatter = &logrus.TextFormatter{}

	agentURL = "localhost:8800"
	logLevel = defaultLogLevel

	// Create a new App instance
	app := cli.NewApp()
	app.Name = AppName
	app.Usage = "wrapper on gdb for X(cross) Development System."
	app.Version = AppVersion + " (" + AppSubVersion + ")"
	app.Authors = appAuthors
	app.Copyright = appCopyright
	app.Metadata = make(map[string]interface{})
	app.Metadata["version"] = AppVersion
	app.Metadata["git-tag"] = AppSubVersion
	app.Metadata["logger"] = log

	app.Flags = []cli.Flag{
		cli.BoolFlag{
			Name:        "list, ls",
			Usage:       "list existing xds projects",
			Destination: &listProject,
		},
	}

	appEnvVars := []EnvVar{
		EnvVar{
			Name:        "XDS_CONFIG",
			Usage:       "env config file to source on startup",
			Destination: &confFile,
		},
		EnvVar{
			Name:        "XDS_LOGLEVEL",
			Usage:       "logging level (supported levels: panic, fatal, error, warn, info, debug)",
			Destination: &logLevel,
		},
		EnvVar{
			Name:        "XDS_LOGFILE",
			Usage:       "logging file (default: " + logFileInitial + ")",
			Destination: &logFile,
		},
		EnvVar{
			Name:        "XDS_NATIVE_GDB",
			Usage:       "use native gdb instead of remote XDS server",
			Destination: &gdbNative,
		},
		EnvVar{
			Name:        "XDS_PROJECT_ID",
			Usage:       "project ID you want to build (mandatory variable)",
			Destination: &prjID,
		},
		EnvVar{
			Name:        "XDS_RPATH",
			Usage:       "relative path into project",
			Destination: &rPath,
		},
		EnvVar{
			Name:        "XDS_SDK_ID",
			Usage:       "Cross Sdk ID to use to build project",
			Destination: &sdkid,
		},
		EnvVar{
			Name:        "XDS_AGENT_URL",
			Usage:       "local XDS agent url",
			Destination: &agentURL,
		},
		EnvVar{
			Name:        "XDS_SERVER_URL",
			Usage:       "overwrite remote XDS server url (default value set in xds-agent-config.json file)",
			Destination: &serverURL,
		},
	}

	// Process gdb arguments
	log.Debugf("xds-gdb started with args: %v", os.Args)
	args := make([]string, len(os.Args))
	args[0] = os.Args[0]
	gdbArgs := make([]string, len(os.Args))

	// Split xds-xxx options from gdb options
	copy(gdbArgs, os.Args[1:])
	for idx, a := range os.Args[1:] {
		// Specific case to print help or version of xds-gdb
		switch a {
		case "--help", "-h", "--version", "-v", "--list", "-ls":
			args[1] = a
			goto endloop
		case "--":
			// Detect skip option (IOW '--') to split arguments
			copy(args, os.Args[0:idx+1])
			copy(gdbArgs, os.Args[idx+2:])
			goto endloop
		}
	}
endloop:

	// Parse gdb arguments to detect:
	//  --tty option: used for inferior/ tty of debugged program
	//  -x/--command option: XDS env vars may be set within gdb command file
	clientPty := ""
	gdbCmdFile := ""
	for idx, a := range gdbArgs {
		switch {
		case strings.HasPrefix(a, "--tty="):
			clientPty = a[len("--tty="):]
			gdbArgs[idx] = ""

		case a == "--tty":
		case strings.HasPrefix(a, "-tty"):
			clientPty = gdbArgs[idx+1]
			gdbArgs[idx] = ""
			gdbArgs[idx+1] = ""

		case strings.HasPrefix(a, "--command="):
			gdbCmdFile = a[len("--command="):]

		case a == "--command":
		case strings.HasPrefix(a, "-x"):
			gdbCmdFile = gdbArgs[idx+1]
		}
	}

	// Source config env file
	// (we cannot use confFile var because env variables setting is just after)
	envMap, confFile, err := loadConfigEnvFile(os.Getenv("XDS_CONFIG"), gdbCmdFile)
	log.Infof("Load env config: envMap=%v, confFile=%v, err=%v", envMap, confFile, err)

	// Only rise an error when args is not set (IOW when --help or --version is not set)
	if len(args) == 1 {
		if err != nil {
			exitError(syscall.ENOENT, err.Error())
		}
	}

	// Managed env vars and create help
	dynDesc := "\nENVIRONMENT VARIABLES:"
	for _, ev := range appEnvVars {
		dynDesc += fmt.Sprintf("\n %s \t\t %s", ev.Name, ev.Usage)
		if evVal, evExist := os.LookupEnv(ev.Name); evExist && ev.Destination != nil {
			*ev.Destination = evVal
		}
	}
	app.Description = "gdb wrapper for X(cross) Development System\n"
	app.Description += "\n"
	app.Description += " Two debugging models are supported:\n"
	app.Description += "  - xds remote debugging requiring an XDS server and allowing cross debug\n"
	app.Description += "  - native debugging\n"
	app.Description += " By default xds remote debug is used and you need to define XDS_NATIVE_GDB to\n"
	app.Description += " use native gdb debug mode instead.\n"
	app.Description += "\n"
	app.Description += " xds-gdb configuration (see variables list below) can be set using:\n"
	app.Description += "  - a config file (XDS_CONFIG)\n"
	app.Description += "  - or environment variables\n"
	app.Description += "  - or by setting variables within gdb ini file (commented line including :XDS-ENV: tag)\n"
	app.Description += "    Example of gdb ini file where we define project and sdk ID:\n"
	app.Description += "     # :XDS-ENV: XDS_PROJECT_ID=IW7B4EE-DBY4Z74_myProject\n"
	app.Description += "     # :XDS-ENV: XDS_SDK_ID=poky-agl_aarch64_3.99.1+snapshot\n"
	app.Description += "\n"
	app.Description += dynDesc + "\n"

	// only one action
	app.Action = func(ctx *cli.Context) error {
		var err error
		curDir, _ := os.Getwd()

		// Build env variables
		env := []string{}
		for k, v := range envMap {
			env = append(env, k+"="+v)
		}

		// Now set logger level and log file to correct/env var settings
		if log.Level, err = logrus.ParseLevel(logLevel); err != nil {
			msg := fmt.Sprintf("Invalid log level : \"%v\"\n", logLevel)
			return cli.NewExitError(msg, int(syscall.EINVAL))
		}
		log.Infof("Switch log level to %s", logLevel)

		if logFile != logFileInitial {
			log.Infof("Switch logging to log file %s", logFile)

			fdL, err := os.OpenFile(logFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
			if err != nil {
				msgErr := fmt.Sprintf("Cannot create log file %s", logFile)
				return cli.NewExitError(msgErr, int(syscall.EPERM))
			}
			defer fdL.Close()
			log.Out = fdL
		}

		// Create cross or native gdb interface
		var gdb IGDB
		if gdbNative != "" {
			gdb = NewGdbNative(log, gdbArgs, env)
		} else {
			gdb = NewGdbXds(log, gdbArgs, env)
			gdb.SetConfig("agentURL", agentURL)
			gdb.SetConfig("serverURL", serverURL)
			gdb.SetConfig("prjID", prjID)
			gdb.SetConfig("sdkID", sdkid)
			gdb.SetConfig("rPath", rPath)
			gdb.SetConfig("listProject", listProject)
		}

		// Log useful info
		log.Infof("Original arguments: %v", os.Args)
		log.Infof("Current directory : %v", curDir)
		log.Infof("Use confFile      : '%s'", confFile)
		log.Infof("Execute           : /exec %v %v", gdb.Cmd(), gdb.Args())

		// Properly report invalid init file error
		gdbCommandFileError := ""
		for i, a := range gdbArgs {
			if a == "-x" {
				gdbCommandFileError = gdbArgs[i+1] + ": No such file or directory."
				break
			} else if strings.HasPrefix(a, "--command=") {
				gdbCommandFileError = strings.TrimLeft(a, "--command=") + ": No such file or directory."
				break
			}
		}
		log.Infof("Add detection of error: <%s>", gdbCommandFileError)

		// Init gdb subprocess management
		if code, err := gdb.Init(); err != nil {
			return cli.NewExitError(err.Error(), code)
		}

		exitChan := make(chan exitResult, 1)

		gdb.OnError(func(err error) {
			fmt.Println("ERROR: ", err.Error())
		})

		gdb.OnDisconnect(func(err error) {
			errMsg := "\nXDS-Agent disconnected"
			if err != nil {
				fmt.Printf("%s: %v\n", errMsg, err.Error())
			} else {
				fmt.Println(errMsg)
			}

			exitChan <- exitResult{err, int(syscall.ESHUTDOWN)}
		})

		gdb.Read(func(timestamp, stdout, stderr string) {
			if stdout != "" {
				fmt.Printf("%s", stdout)
				log.Debugf("Recv OUT: <%s>", stdout)
			}
			if stderr != "" {
				// Filter-out ugly message (python error when cross gdb exited)
				if !strings.Contains(stderr, "readline.write_history_file") &&
					!(strings.Contains(stderr, "Traceback") && strings.Contains(stderr, "__exithandler")) {
					fmt.Fprintf(os.Stderr, "%s", stderr)
					log.Debugf("Recv ERR: <%s>", stderr)
				} else {
					log.Debugf("Recv ERR (FILTERED OUT): <%s>", stderr)
				}
			}

			// Correctly report error about init file
			if gdbCommandFileError != "" && strings.Contains(stdout, gdbCommandFileError) {
				fmt.Fprintf(os.Stderr, "ERROR: "+gdbCommandFileError)
				log.Errorf("ERROR: " + gdbCommandFileError)
				if err := gdb.SendSignal(syscall.SIGTERM); err != nil {
					log.Errorf("Error while sending signal: %s", err.Error())
				}
				exitChan <- exitResult{err, int(syscall.ENOENT)}
			}
		})

		gdb.OnExit(func(code int, err error) {
			exitChan <- exitResult{err, code}
		})

		// Handle client tty / pts
		if clientPty != "" {
			log.Infoln("Client tty detected: %v", clientPty)

			cpFd, err := os.OpenFile(clientPty, os.O_RDWR, 0)
			if err != nil {
				return cli.NewExitError(err.Error(), int(syscall.EPERM))
			}
			defer cpFd.Close()

			// client tty stdin
			/* XXX TODO - implement stdin to send data to debugged program
			go func() {
				reader := bufio.NewReader(cpFd)
				sc := bufio.NewScanner(reader)
				for sc.Scan() {
					data := sc.Text()
					iosk.Emit(xaapiv1.ExecInferiorInEvent, data+"\n")
					log.Debugf("Inferior IN: <%v>", data)
				}
				if sc.Err() != nil {
					log.Warnf("Inferior Stdin scanner exit, close stdin (err=%v)", sc.Err())
				}
			}()
			*/

			// client tty stdout
			gdb.InferiorRead(func(timestamp, stdout, stderr string) {
				if stdout != "" {
					fmt.Fprintf(cpFd, "%s", stdout)
					log.Debugf("Inferior OUT: <%s>", stdout)
				}
				if stderr != "" {
					fmt.Fprintf(cpFd, "%s", stderr)
					log.Debugf("Inferior ERR: <%s>", stderr)
				}
			})
		}

		// Allow to overwrite some gdb commands
		var overwriteMap = make(map[string]string)
		if overEnv, exist := os.LookupEnv("XDS_OVERWRITE_COMMANDS"); exist {
			overEnvS := strings.TrimSpace(overEnv)
			if len(overEnvS) > 0 {
				// Extract overwrite commands from env variable
				for _, def := range strings.Split(overEnvS, ",") {
					if kv := strings.Split(def, ":"); len(kv) == 2 {
						overwriteMap[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
					} else {
						return cli.NewExitError(
							fmt.Errorf("Invalid definition in XDS_OVERWRITE_COMMANDS (%s)", def),
							int(syscall.EINVAL))
					}
				}
			}
		} else {
			overwriteMap["-exec-run"] = "-exec-continue"
			overwriteMap["-file-exec-and-symbols"] = "-file-exec-file"
		}
		log.Debugf("overwriteMap = %v", overwriteMap)

		// Send stdin though WS
		go func() {
			paranoia := 600
			reader := bufio.NewReader(os.Stdin)

			// Enable workaround to correctly close connection
			// except if XDS_GDBSERVER_EXIT_NOFIX is defined
			_, gdbExitNoFix := os.LookupEnv("XDS_GDBSERVER_EXIT_NOFIX")

			for {
				sc := bufio.NewScanner(reader)
				for sc.Scan() {
					command := sc.Text()

					// overwrite some commands
					for key, value := range overwriteMap {
						if strings.Contains(command, key) {
							command = strings.Replace(command, key, value, 1)
							log.Debugf("OVERWRITE %s -> %s", key, value)
						}
					}

					// Send SIGINT to stop debugged process execution before sending -gdb-exit command
					if !gdbExitNoFix && strings.Contains(command, "-gdb-exit") {
						log.Infof("Detection of -gdb-exit, exiting...")
						if err := gdb.SendSignal(syscall.SIGINT); err != nil {
							log.Errorf("Error while sending signal SIGINT : %s", err.Error())
						}
						time.Sleep(time.Millisecond * 200)
					}

					log.Debugf("Send: <%v>", command)
					gdb.Write(command + "\n")
				}
				log.Infof("Stdin scanner exit, close stdin (err=%v)", sc.Err())

				// CTRL-D exited scanner, so send it explicitly
				gdb.Write("\x04")
				time.Sleep(time.Millisecond * 100)

				if paranoia--; paranoia <= 0 {
					msg := "Abnormal loop detected on stdin"
					log.Errorf("Abnormal loop detected on stdin")
					gdb.SendSignal(syscall.SIGTERM)
					exitChan <- exitResult{fmt.Errorf(msg), int(syscall.ELOOP)}
				}
			}
		}()

		// Handling all Signals
		sigs := make(chan os.Signal, 1)
		signal.Notify(sigs)

		go func() {
			for {
				sig := <-sigs

				if isIgnoredSignal(sig) {
					return
				}

				if err := gdb.SendSignal(sig); err != nil {
					log.Errorf("Error while sending signal %v : %s", sig, err.Error())
				}
			}
		}()

		// Start gdb
		if code, err := gdb.Start(clientPty != ""); err != nil {
			return cli.NewExitError(err.Error(), code)
		}

		// Wait exit
		select {
		case res := <-exitChan:
			errStr := ""
			if res.code == 0 {
				log.Infoln("Exit successfully")
			}
			if res.error != nil {
				log.Infoln("Exit with ERROR: ", res.error.Error())
				errStr = res.error.Error()
			}
			return cli.NewExitError(errStr, res.code)
		}
	}

	app.Run(args)
}

// loadConfigEnvFile
func loadConfigEnvFile(confFile, gdbCmdFile string) (map[string]string, string, error) {
	var err error
	envMap := make(map[string]string)

	// 1- if no confFile set, use setting from gdb command file is option
	//    --command/-x is set
	if confFile == "" && gdbCmdFile != "" {
		log.Infof("Try extract config from gdbCmdFile: %s", gdbCmdFile)
		confFile, err = extractEnvFromCmdFile(gdbCmdFile)
		if confFile != "" {
			defer os.Remove(confFile)
		}
		if err != nil {
			log.Infof("Extraction from gdbCmdFile failed: %v", err.Error())
		}
	}
	// 2- search xds-gdb.env file in various locations
	if confFile == "" {
		curDir, _ := os.Getwd()
		if u, err := user.Current(); err == nil {
			xdsEnvFile := "xds-gdb.env"
			for _, d := range []string{
				path.Join(curDir),
				path.Join(curDir, ".."),
				path.Join(curDir, "target"),
				path.Join(u.HomeDir, ".config", "xds"),
			} {
				cf := path.Join(d, xdsEnvFile)
				log.Infof("Search config in %s", cf)
				if common.Exists(cf) {
					confFile = cf
					break
				}
			}
		}
	}

	if confFile == "" {
		log.Infof("NO valid conf file found!")
		return envMap, "", nil
	}

	if !common.Exists(confFile) {
		return envMap, confFile, fmt.Errorf("Error no env config file not found")
	}
	if err = godotenv.Load(confFile); err != nil {
		return envMap, confFile, fmt.Errorf("Error loading env config file " + confFile)
	}
	if envMap, err = godotenv.Read(confFile); err != nil {
		return envMap, confFile, fmt.Errorf("Error reading env config file " + confFile)
	}

	return envMap, confFile, nil
}

/*
 extractEnvFromCmdFile: extract xds-gdb env variable from gdb command file
  All commented lines (#) in gdb command file that start with ':XDS-ENV:' prefix
  will be considered as XDS env commands. For example the 3 syntaxes below
  are supported:
  # :XDS-ENV: XDS_PROJECT_ID=4021617e-ced0-11e7-acd2-3c970e49ad9b
  #:XDS-ENV:XDS_SDK_ID=06c0e95a-e215-3a5a-b373-f677c0dabd3b
  # :XDS-ENV:  export XDS_AGENT_URL=localhost:8800
*/
func extractEnvFromCmdFile(cmdFile string) (string, error) {
	if !common.Exists(cmdFile) {
		return "", nil
	}
	cFd, err := os.Open(cmdFile)
	if err != nil {
		return "", fmt.Errorf("Cannot open %s : %s", cmdFile, err.Error())
	}
	defer cFd.Close()

	var lines []string
	scanner := bufio.NewScanner(cFd)
	for scanner.Scan() {
		lines = append(lines, scanner.Text())
	}
	if err = scanner.Err(); err != nil {
		return "", fmt.Errorf("Cannot parse %s : %s", cmdFile, err.Error())
	}

	envFile, err := ioutil.TempFile("", "xds-gdb_env.ini")
	if err != nil {
		return "", fmt.Errorf("Error while creating temporary env file: %s", err.Error())
	}
	envFileName := envFile.Name()
	defer envFile.Close()

	envFound := false
	for _, ln := range lines {
		ln = strings.TrimSpace(ln)
		if strings.HasPrefix(ln, "#") && strings.Contains(ln, ":XDS-ENV:") {
			env := strings.SplitAfterN(ln, ":XDS-ENV:", 2)
			if len(env) == 2 {
				envFound = true
				if _, err := envFile.WriteString(strings.TrimSpace(env[1]) + "\n"); err != nil {
					return "", fmt.Errorf("Error write into temporary env file: %s", err.Error())
				}
			} else {
				log.Warnf("Error while decoding line %s", ln)
			}
		}
	}

	if !envFound {
		ff := envFileName
		defer os.Remove(ff)
		envFileName = ""

	}

	return envFileName, nil
}