Fix bug when load config file.
[src/xds/xds-gdb.git] / main.go
1 // xds-gdb: a wrapper on gdb tool for X(cross) Development System.
2 package main
3
4 import (
5         "bufio"
6         "fmt"
7         "io/ioutil"
8         "os"
9         "os/signal"
10         "os/user"
11         "syscall"
12         "time"
13
14         "strings"
15
16         "path"
17
18         "github.com/Sirupsen/logrus"
19         "github.com/codegangsta/cli"
20         common "github.com/iotbzh/xds-common/golib"
21         "github.com/joho/godotenv"
22 )
23
24 var appAuthors = []cli.Author{
25         cli.Author{Name: "Sebastien Douheret", Email: "sebastien@iot.bzh"},
26 }
27
28 // AppName name of this application
29 var AppName = "xds-gdb"
30
31 // AppVersion Version of this application
32 // (set by Makefile)
33 var AppVersion = "?.?.?"
34
35 // AppSubVersion is the git tag id added to version string
36 // Should be set by compilation -ldflags "-X main.AppSubVersion=xxx"
37 // (set by Makefile)
38 var AppSubVersion = "unknown-dev"
39
40 // Create logger
41 var log = logrus.New()
42 var earlyLog = []string{}
43
44 // Application details
45 const (
46         appCopyright    = "Apache-2.0"
47         defaultLogLevel = "warning"
48 )
49
50 // Exit events
51 type exitResult struct {
52         error error
53         code  int
54 }
55
56 // EnvVar - Environment variables used by application
57 type EnvVar struct {
58         Name        string
59         Usage       string
60         Destination *string
61 }
62
63 // exitError terminates this program with the specified error
64 func exitError(code syscall.Errno, f string, a ...interface{}) {
65         err := fmt.Sprintf(f, a...)
66         fmt.Fprintf(os.Stderr, err+"\n")
67         os.Exit(int(code))
68 }
69
70 // main
71 func main() {
72         var uri, prjID, rPath, logLevel, logFile, sdkid, confFile, gdbNative string
73         var listProject bool
74         var err error
75
76         uri = "localhost:8000"
77         logLevel = defaultLogLevel
78
79         // Create a new App instance
80         app := cli.NewApp()
81         app.Name = AppName
82         app.Usage = "wrapper on gdb for X(cross) Development System."
83         app.Version = AppVersion + " (" + AppSubVersion + ")"
84         app.Authors = appAuthors
85         app.Copyright = appCopyright
86         app.Metadata = make(map[string]interface{})
87         app.Metadata["version"] = AppVersion
88         app.Metadata["git-tag"] = AppSubVersion
89         app.Metadata["logger"] = log
90
91         app.Flags = []cli.Flag{
92                 cli.BoolFlag{
93                         Name:        "list, ls",
94                         Usage:       "list existing xds projects",
95                         Destination: &listProject,
96                 },
97         }
98
99         appEnvVars := []EnvVar{
100                 EnvVar{
101                         Name:        "XDS_CONFIG",
102                         Usage:       "env config file to source on startup",
103                         Destination: &confFile,
104                 },
105                 EnvVar{
106                         Name:        "XDS_LOGLEVEL",
107                         Usage:       "logging level (supported levels: panic, fatal, error, warn, info, debug)",
108                         Destination: &logLevel,
109                 },
110                 EnvVar{
111                         Name:        "XDS_LOGFILE",
112                         Usage:       "logging file",
113                         Destination: &logFile,
114                 },
115                 EnvVar{
116                         Name:        "XDS_NATIVE_GDB",
117                         Usage:       "use native gdb instead of remote XDS server",
118                         Destination: &gdbNative,
119                 },
120                 EnvVar{
121                         Name:        "XDS_PROJECT_ID",
122                         Usage:       "project ID you want to build (mandatory variable)",
123                         Destination: &prjID,
124                 },
125                 EnvVar{
126                         Name:        "XDS_RPATH",
127                         Usage:       "relative path into project",
128                         Destination: &rPath,
129                 },
130                 EnvVar{
131                         Name:        "XDS_SDK_ID",
132                         Usage:       "Cross Sdk ID to use to build project",
133                         Destination: &sdkid,
134                 },
135                 EnvVar{
136                         Name:        "XDS_SERVER_URL",
137                         Usage:       "remote XDS server url",
138                         Destination: &uri,
139                 },
140         }
141
142         // Process gdb arguments
143         args := make([]string, len(os.Args))
144         args[0] = os.Args[0]
145         gdbArgs := make([]string, len(os.Args))
146
147         // Split xds-xxx options from gdb options
148         copy(gdbArgs, os.Args[1:])
149         for idx, a := range os.Args[1:] {
150                 // Specific case to print help or version of xds-gdb
151                 switch a {
152                 case "--help", "-h", "--version", "-v":
153                         args[1] = a
154                         goto endloop
155                 case "--":
156                         // Detect skip option (IOW '--') to split arguments
157                         copy(args, os.Args[0:idx+1])
158                         copy(gdbArgs, os.Args[idx+2:])
159                         goto endloop
160                 }
161         }
162 endloop:
163
164         // Parse gdb arguments to detect:
165         //  --tty option: used for inferior/ tty of debugged program
166         //  -x/--command option: XDS env vars may be set within gdb command file
167         clientPty := ""
168         gdbCmdFile := ""
169         for idx, a := range gdbArgs {
170                 switch {
171                 case strings.HasPrefix(a, "--tty="):
172                         clientPty = a[len("--tty="):]
173                         gdbArgs[idx] = ""
174
175                 case a == "--tty":
176                 case strings.HasPrefix(a, "-tty"):
177                         clientPty = gdbArgs[idx+1]
178                         gdbArgs[idx] = ""
179                         gdbArgs[idx+1] = ""
180
181                 case strings.HasPrefix(a, "--command="):
182                         gdbCmdFile = a[len("--command="):]
183
184                 case a == "--command":
185                 case strings.HasPrefix(a, "-x"):
186                         gdbCmdFile = gdbArgs[idx+1]
187                 }
188         }
189
190         // Source config env file
191         // (we cannot use confFile var because env variables setting is just after)
192         envMap, confFile, err := loadConfigEnvFile(os.Getenv("XDS_CONFIG"), gdbCmdFile)
193         if err != nil {
194                 exitError(syscall.ENOENT, err.Error())
195         }
196
197         // Managed env vars and create help
198         dynDesc := "\nENVIRONMENT VARIABLES:"
199         for _, ev := range appEnvVars {
200                 dynDesc += fmt.Sprintf("\n %s \t\t %s", ev.Name, ev.Usage)
201                 if evVal, evExist := os.LookupEnv(ev.Name); evExist && ev.Destination != nil {
202                         *ev.Destination = evVal
203                 }
204         }
205         app.Description = "gdb wrapper for X(cross) Development System\n"
206         app.Description += "\n"
207         app.Description += " Two debugging models are supported:\n"
208         app.Description += "  - xds remote debugging requiring an XDS server and allowing cross debug\n"
209         app.Description += "  - native debugging\n"
210         app.Description += " By default xds remote debug is used and you need to define XDS_NATIVE_GDB to\n"
211         app.Description += " use native gdb debug mode instead.\n"
212         app.Description += "\n"
213         app.Description += " xds-gdb configuration (see variables list below) can be set using:\n"
214         app.Description += "  - a config file (XDS_CONFIG)\n"
215         app.Description += "  - or environment variables\n"
216         app.Description += "  - or by setting variables within gdb ini file (commented line including :XDS-ENV: tag)\n"
217         app.Description += "    Example of gdb ini file where we define project and sdk ID:\n"
218         app.Description += "     # :XDS-ENV: XDS_PROJECT_ID=IW7B4EE-DBY4Z74_myProject\n"
219         app.Description += "     # :XDS-ENV: XDS_SDK_ID=poky-agl_aarch64_3.99.1+snapshot\n"
220         app.Description += "\n"
221         app.Description += dynDesc + "\n"
222
223         // only one action
224         app.Action = func(ctx *cli.Context) error {
225                 var err error
226                 curDir, _ := os.Getwd()
227
228                 // Set logger level, formatter and log file
229                 if log.Level, err = logrus.ParseLevel(logLevel); err != nil {
230                         msg := fmt.Sprintf("Invalid log level : \"%v\"\n", logLevel)
231                         return cli.NewExitError(msg, int(syscall.EINVAL))
232                 }
233                 log.Formatter = &logrus.TextFormatter{}
234
235                 // Always log into a file
236                 if logFile == "" {
237                         logFile = "/tmp/xds-gdb.log"
238                 }
239                 fdL, err := os.OpenFile(logFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
240                 if err != nil {
241                         msgErr := fmt.Sprintf("Cannot create log file %s", logFile)
242                         return cli.NewExitError(msgErr, int(syscall.EPERM))
243                 }
244                 log.Out = fdL
245
246                 // Build env variables
247                 env := []string{}
248                 for k, v := range envMap {
249                         env = append(env, k+"="+v)
250                 }
251
252                 // Create cross or native gdb interface
253                 var gdb IGDB
254                 if gdbNative != "" {
255                         gdb = NewGdbNative(log, gdbArgs, env)
256                 } else {
257                         gdb = NewGdbXds(log, gdbArgs, env)
258                         gdb.SetConfig("uri", uri)
259                         gdb.SetConfig("prjID", prjID)
260                         gdb.SetConfig("sdkID", sdkid)
261                         gdb.SetConfig("rPath", rPath)
262                         gdb.SetConfig("listProject", listProject)
263                 }
264
265                 // Log early print
266                 for _, msg := range earlyLog {
267                         log.Debugf(msg)
268                 }
269
270                 // Log useful info
271                 log.Infof("Original arguments: %v", os.Args)
272                 log.Infof("Current directory : %v", curDir)
273                 log.Infof("Use confFile      : '%s'", confFile)
274                 log.Infof("Execute           : /exec %v %v", gdb.Cmd(), gdb.Args())
275
276                 // Properly report invalid init file error
277                 gdbCommandFileError := ""
278                 for i, a := range gdbArgs {
279                         if a == "-x" {
280                                 gdbCommandFileError = gdbArgs[i+1] + ": No such file or directory."
281                                 break
282                         } else if strings.HasPrefix(a, "--command=") {
283                                 gdbCommandFileError = strings.TrimLeft(a, "--command=") + ": No such file or directory."
284                                 break
285                         }
286                 }
287                 log.Infof("Add detection of error: <%s>", gdbCommandFileError)
288
289                 // Init gdb subprocess management
290                 if code, err := gdb.Init(); err != nil {
291                         return cli.NewExitError(err.Error(), code)
292                 }
293
294                 exitChan := make(chan exitResult, 1)
295
296                 gdb.OnError(func(err error) {
297                         fmt.Println("ERROR: ", err.Error())
298                 })
299
300                 gdb.OnDisconnect(func(err error) {
301                         fmt.Println("Disconnection: ", err.Error())
302                         exitChan <- exitResult{err, int(syscall.ESHUTDOWN)}
303                 })
304
305                 gdb.Read(func(timestamp, stdout, stderr string) {
306                         if stdout != "" {
307                                 fmt.Printf("%s", stdout)
308                                 log.Debugf("Recv OUT: <%s>", stdout)
309                         }
310                         if stderr != "" {
311                                 fmt.Fprintf(os.Stderr, "%s", stderr)
312                                 log.Debugf("Recv ERR: <%s>", stderr)
313                         }
314
315                         // Correctly report error about init file
316                         if gdbCommandFileError != "" && strings.Contains(stdout, gdbCommandFileError) {
317                                 fmt.Fprintf(os.Stderr, "ERROR: "+gdbCommandFileError)
318                                 log.Errorf("ERROR: " + gdbCommandFileError)
319                                 if err := gdb.SendSignal(syscall.SIGTERM); err != nil {
320                                         log.Errorf("Error while sending signal: %s", err.Error())
321                                 }
322                                 exitChan <- exitResult{err, int(syscall.ENOENT)}
323                         }
324                 })
325
326                 gdb.OnExit(func(code int, err error) {
327                         exitChan <- exitResult{err, code}
328                 })
329
330                 // Handle client tty / pts
331                 if clientPty != "" {
332                         log.Infoln("Client tty detected: %v\n", clientPty)
333
334                         cpFd, err := os.OpenFile(clientPty, os.O_RDWR, 0)
335                         if err != nil {
336                                 return cli.NewExitError(err.Error(), int(syscall.EPERM))
337                         }
338                         defer cpFd.Close()
339
340                         // client tty stdin
341                         /* XXX TODO - implement stdin to send data to debugged program
342                         go func() {
343                                 reader := bufio.NewReader(cpFd)
344                                 sc := bufio.NewScanner(reader)
345                                 for sc.Scan() {
346                                         data := sc.Text()
347                                         iosk.Emit(apiv1.ExecInferiorInEvent, data+"\n")
348                                         log.Debugf("Inferior IN: <%v>", data)
349                                 }
350                                 if sc.Err() != nil {
351                                         log.Warnf("Inferior Stdin scanner exit, close stdin (err=%v)", sc.Err())
352                                 }
353                         }()
354                         */
355
356                         // client tty stdout
357                         gdb.InferiorRead(func(timestamp, stdout, stderr string) {
358                                 if stdout != "" {
359                                         fmt.Fprintf(cpFd, "%s", stdout)
360                                         log.Debugf("Inferior OUT: <%s>", stdout)
361                                 }
362                                 if stderr != "" {
363                                         fmt.Fprintf(cpFd, "%s", stderr)
364                                         log.Debugf("Inferior ERR: <%s>", stderr)
365                                 }
366                         })
367                 }
368
369                 // Allow to overwrite some gdb commands
370                 var overwriteMap = make(map[string]string)
371                 if overEnv, exist := os.LookupEnv("XDS_OVERWRITE_COMMANDS"); exist {
372                         overEnvS := strings.TrimSpace(overEnv)
373                         if len(overEnvS) > 0 {
374                                 // Extract overwrite commands from env variable
375                                 for _, def := range strings.Split(overEnvS, ",") {
376                                         if kv := strings.Split(def, ":"); len(kv) == 2 {
377                                                 overwriteMap[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
378                                         } else {
379                                                 return cli.NewExitError(
380                                                         fmt.Errorf("Invalid definition in XDS_OVERWRITE_COMMANDS (%s)", def),
381                                                         int(syscall.EINVAL))
382                                         }
383                                 }
384                         }
385                 } else {
386                         overwriteMap["-exec-run"] = "-exec-continue"
387                         overwriteMap["-file-exec-and-symbols"] = "-file-exec-file"
388                 }
389                 log.Debugf("overwriteMap = %v", overwriteMap)
390
391                 // Send stdin though WS
392                 go func() {
393                         paranoia := 600
394                         reader := bufio.NewReader(os.Stdin)
395
396                         for {
397                                 sc := bufio.NewScanner(reader)
398                                 for sc.Scan() {
399                                         command := sc.Text()
400
401                                         // overwrite some commands
402                                         for key, value := range overwriteMap {
403                                                 if strings.Contains(command, key) {
404                                                         command = strings.Replace(command, key, value, 1)
405                                                         log.Debugf("OVERWRITE %s -> %s", key, value)
406                                                 }
407                                         }
408                                         gdb.Write(command + "\n")
409                                         log.Debugf("Send: <%v>", command)
410                                 }
411                                 log.Infof("Stdin scanner exit, close stdin (err=%v)", sc.Err())
412
413                                 // CTRL-D exited scanner, so send it explicitly
414                                 gdb.Write("\x04")
415                                 time.Sleep(time.Millisecond * 100)
416
417                                 if paranoia--; paranoia <= 0 {
418                                         msg := "Abnormal loop detected on stdin"
419                                         log.Errorf("Abnormal loop detected on stdin")
420                                         gdb.SendSignal(syscall.SIGTERM)
421                                         exitChan <- exitResult{fmt.Errorf(msg), int(syscall.ELOOP)}
422                                 }
423                         }
424                 }()
425
426                 // Handling all Signals
427                 sigs := make(chan os.Signal, 1)
428                 signal.Notify(sigs)
429
430                 go func() {
431                         for {
432                                 sig := <-sigs
433                                 if err := gdb.SendSignal(sig); err != nil {
434                                         log.Errorf("Error while sending signal: %s", err.Error())
435                                 }
436                         }
437                 }()
438
439                 // Start gdb
440                 if code, err := gdb.Start(clientPty != ""); err != nil {
441                         return cli.NewExitError(err.Error(), code)
442                 }
443
444                 // Wait exit
445                 select {
446                 case res := <-exitChan:
447                         errStr := ""
448                         if res.code == 0 {
449                                 log.Infoln("Exit successfully")
450                         }
451                         if res.error != nil {
452                                 log.Infoln("Exit with ERROR: ", res.error.Error())
453                                 errStr = res.error.Error()
454                         }
455                         return cli.NewExitError(errStr, res.code)
456                 }
457         }
458
459         app.Run(args)
460 }
461
462 // loadConfigEnvFile
463 func loadConfigEnvFile(confFile, gdbCmdFile string) (map[string]string, string, error) {
464         var err error
465         envMap := make(map[string]string)
466
467         // 1- if no confFile set, use setting from gdb command file is option
468         //    --command/-x is set
469         if confFile == "" && gdbCmdFile != "" {
470                 logEarly("Try extract config from gdbCmdFile: %s", gdbCmdFile)
471                 confFile, err = extractEnvFromCmdFile(gdbCmdFile)
472                 if confFile != "" {
473                         defer os.Remove(confFile)
474                 }
475                 if err != nil {
476                         return envMap, confFile, fmt.Errorf(err.Error())
477                 }
478         }
479         // 2- search xds-gdb.env file in various locations
480         if confFile == "" {
481                 curDir, _ := os.Getwd()
482                 if u, err := user.Current(); err == nil {
483                         xdsEnvFile := "xds-gdb.env"
484                         for _, d := range []string{
485                                 path.Join(curDir),
486                                 path.Join(curDir, "..", ".."),
487                                 path.Join(curDir, "../../target"),
488                                 path.Join(u.HomeDir, ".xds"),
489                         } {
490                                 confFile = path.Join(d, xdsEnvFile)
491                                 logEarly("Search config in %s", confFile)
492                                 if common.Exists(confFile) {
493                                         break
494                                 }
495                         }
496                 }
497         }
498
499         if confFile == "" {
500                 return envMap, "", nil
501         }
502
503         if !common.Exists(confFile) {
504                 return envMap, confFile, fmt.Errorf("Error no env config file not found")
505         }
506         if err = godotenv.Load(confFile); err != nil {
507                 return envMap, confFile, fmt.Errorf("Error loading env config file " + confFile)
508         }
509         if envMap, err = godotenv.Read(confFile); err != nil {
510                 return envMap, confFile, fmt.Errorf("Error reading env config file " + confFile)
511         }
512         return envMap, confFile, nil
513 }
514
515 /*
516  extractEnvFromCmdFile: extract xds-gdb env variable from gdb command file
517   All commented lines (#) in gdb command file that start with ':XDS-ENV:' prefix
518   will be considered as XDS env commands. For example the 3 syntaxes below
519   are supported:
520   # :XDS-ENV: XDS_PROJECT_ID=IW7B4EE-DBY4Z74_myProject
521   #:XDS-ENV:XDS_SDK_ID=poky-agl_aarch64_3.99.1+snapshot
522   # :XDS-ENV:  export XDS_SERVER_URL=localhost:8800
523 */
524 func extractEnvFromCmdFile(cmdFile string) (string, error) {
525         if !common.Exists(cmdFile) {
526                 return "", nil
527         }
528         cFd, err := os.Open(cmdFile)
529         if err != nil {
530                 return "", fmt.Errorf("Cannot open %s : %s", cmdFile, err.Error())
531         }
532         defer cFd.Close()
533
534         var lines []string
535         scanner := bufio.NewScanner(cFd)
536         for scanner.Scan() {
537                 lines = append(lines, scanner.Text())
538         }
539         if err = scanner.Err(); err != nil {
540                 return "", fmt.Errorf("Cannot parse %s : %s", cmdFile, err.Error())
541         }
542
543         envFile, err := ioutil.TempFile("", "xds-gdb_env.ini")
544         if err != nil {
545                 return "", fmt.Errorf("Error while creating temporary env file: %s", err.Error())
546         }
547         envFileName := envFile.Name()
548         defer envFile.Close()
549
550         envFound := false
551         for _, ln := range lines {
552                 ln = strings.TrimSpace(ln)
553                 if strings.HasPrefix(ln, "#") && strings.Contains(ln, ":XDS-ENV:") {
554                         env := strings.SplitAfterN(ln, ":XDS-ENV:", 2)
555                         if len(env) == 2 {
556                                 envFound = true
557                                 if _, err := envFile.WriteString(strings.TrimSpace(env[1]) + "\n"); err != nil {
558                                         return "", fmt.Errorf("Error write into temporary env file: %s", err.Error())
559                                 }
560                         } else {
561                                 log.Warnf("Error while decoding line %s", ln)
562                         }
563                 }
564         }
565
566         if !envFound {
567                 ff := envFileName
568                 defer os.Remove(ff)
569                 envFileName = ""
570
571         }
572
573         return envFileName, nil
574 }
575
576 func logEarly(format string, a ...interface{}) {
577         earlyLog = append(earlyLog, fmt.Sprintf(format, a...))
578 }