Fixed --help and --version when no config file found.
[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
194         // Only rise an error when args is not set (IOW when --help or --version is not set)
195         if len(args) == 1 {
196                 if err != nil {
197                         exitError(syscall.ENOENT, err.Error())
198                 }
199         }
200
201         // Managed env vars and create help
202         dynDesc := "\nENVIRONMENT VARIABLES:"
203         for _, ev := range appEnvVars {
204                 dynDesc += fmt.Sprintf("\n %s \t\t %s", ev.Name, ev.Usage)
205                 if evVal, evExist := os.LookupEnv(ev.Name); evExist && ev.Destination != nil {
206                         *ev.Destination = evVal
207                 }
208         }
209         app.Description = "gdb wrapper for X(cross) Development System\n"
210         app.Description += "\n"
211         app.Description += " Two debugging models are supported:\n"
212         app.Description += "  - xds remote debugging requiring an XDS server and allowing cross debug\n"
213         app.Description += "  - native debugging\n"
214         app.Description += " By default xds remote debug is used and you need to define XDS_NATIVE_GDB to\n"
215         app.Description += " use native gdb debug mode instead.\n"
216         app.Description += "\n"
217         app.Description += " xds-gdb configuration (see variables list below) can be set using:\n"
218         app.Description += "  - a config file (XDS_CONFIG)\n"
219         app.Description += "  - or environment variables\n"
220         app.Description += "  - or by setting variables within gdb ini file (commented line including :XDS-ENV: tag)\n"
221         app.Description += "    Example of gdb ini file where we define project and sdk ID:\n"
222         app.Description += "     # :XDS-ENV: XDS_PROJECT_ID=IW7B4EE-DBY4Z74_myProject\n"
223         app.Description += "     # :XDS-ENV: XDS_SDK_ID=poky-agl_aarch64_3.99.1+snapshot\n"
224         app.Description += "\n"
225         app.Description += dynDesc + "\n"
226
227         // only one action
228         app.Action = func(ctx *cli.Context) error {
229                 var err error
230                 curDir, _ := os.Getwd()
231
232                 // Set logger level, formatter and log file
233                 if log.Level, err = logrus.ParseLevel(logLevel); err != nil {
234                         msg := fmt.Sprintf("Invalid log level : \"%v\"\n", logLevel)
235                         return cli.NewExitError(msg, int(syscall.EINVAL))
236                 }
237                 log.Formatter = &logrus.TextFormatter{}
238
239                 // Always log into a file
240                 if logFile == "" {
241                         logFile = "/tmp/xds-gdb.log"
242                 }
243                 fdL, err := os.OpenFile(logFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
244                 if err != nil {
245                         msgErr := fmt.Sprintf("Cannot create log file %s", logFile)
246                         return cli.NewExitError(msgErr, int(syscall.EPERM))
247                 }
248                 log.Out = fdL
249
250                 // Build env variables
251                 env := []string{}
252                 for k, v := range envMap {
253                         env = append(env, k+"="+v)
254                 }
255
256                 // Create cross or native gdb interface
257                 var gdb IGDB
258                 if gdbNative != "" {
259                         gdb = NewGdbNative(log, gdbArgs, env)
260                 } else {
261                         gdb = NewGdbXds(log, gdbArgs, env)
262                         gdb.SetConfig("uri", uri)
263                         gdb.SetConfig("prjID", prjID)
264                         gdb.SetConfig("sdkID", sdkid)
265                         gdb.SetConfig("rPath", rPath)
266                         gdb.SetConfig("listProject", listProject)
267                 }
268
269                 // Log early print
270                 for _, msg := range earlyLog {
271                         log.Debugf(msg)
272                 }
273
274                 // Log useful info
275                 log.Infof("Original arguments: %v", os.Args)
276                 log.Infof("Current directory : %v", curDir)
277                 log.Infof("Use confFile      : '%s'", confFile)
278                 log.Infof("Execute           : /exec %v %v", gdb.Cmd(), gdb.Args())
279
280                 // Properly report invalid init file error
281                 gdbCommandFileError := ""
282                 for i, a := range gdbArgs {
283                         if a == "-x" {
284                                 gdbCommandFileError = gdbArgs[i+1] + ": No such file or directory."
285                                 break
286                         } else if strings.HasPrefix(a, "--command=") {
287                                 gdbCommandFileError = strings.TrimLeft(a, "--command=") + ": No such file or directory."
288                                 break
289                         }
290                 }
291                 log.Infof("Add detection of error: <%s>", gdbCommandFileError)
292
293                 // Init gdb subprocess management
294                 if code, err := gdb.Init(); err != nil {
295                         return cli.NewExitError(err.Error(), code)
296                 }
297
298                 exitChan := make(chan exitResult, 1)
299
300                 gdb.OnError(func(err error) {
301                         fmt.Println("ERROR: ", err.Error())
302                 })
303
304                 gdb.OnDisconnect(func(err error) {
305                         fmt.Println("Disconnection: ", err.Error())
306                         exitChan <- exitResult{err, int(syscall.ESHUTDOWN)}
307                 })
308
309                 gdb.Read(func(timestamp, stdout, stderr string) {
310                         if stdout != "" {
311                                 fmt.Printf("%s", stdout)
312                                 log.Debugf("Recv OUT: <%s>", stdout)
313                         }
314                         if stderr != "" {
315                                 fmt.Fprintf(os.Stderr, "%s", stderr)
316                                 log.Debugf("Recv ERR: <%s>", stderr)
317                         }
318
319                         // Correctly report error about init file
320                         if gdbCommandFileError != "" && strings.Contains(stdout, gdbCommandFileError) {
321                                 fmt.Fprintf(os.Stderr, "ERROR: "+gdbCommandFileError)
322                                 log.Errorf("ERROR: " + gdbCommandFileError)
323                                 if err := gdb.SendSignal(syscall.SIGTERM); err != nil {
324                                         log.Errorf("Error while sending signal: %s", err.Error())
325                                 }
326                                 exitChan <- exitResult{err, int(syscall.ENOENT)}
327                         }
328                 })
329
330                 gdb.OnExit(func(code int, err error) {
331                         exitChan <- exitResult{err, code}
332                 })
333
334                 // Handle client tty / pts
335                 if clientPty != "" {
336                         log.Infoln("Client tty detected: %v\n", clientPty)
337
338                         cpFd, err := os.OpenFile(clientPty, os.O_RDWR, 0)
339                         if err != nil {
340                                 return cli.NewExitError(err.Error(), int(syscall.EPERM))
341                         }
342                         defer cpFd.Close()
343
344                         // client tty stdin
345                         /* XXX TODO - implement stdin to send data to debugged program
346                         go func() {
347                                 reader := bufio.NewReader(cpFd)
348                                 sc := bufio.NewScanner(reader)
349                                 for sc.Scan() {
350                                         data := sc.Text()
351                                         iosk.Emit(apiv1.ExecInferiorInEvent, data+"\n")
352                                         log.Debugf("Inferior IN: <%v>", data)
353                                 }
354                                 if sc.Err() != nil {
355                                         log.Warnf("Inferior Stdin scanner exit, close stdin (err=%v)", sc.Err())
356                                 }
357                         }()
358                         */
359
360                         // client tty stdout
361                         gdb.InferiorRead(func(timestamp, stdout, stderr string) {
362                                 if stdout != "" {
363                                         fmt.Fprintf(cpFd, "%s", stdout)
364                                         log.Debugf("Inferior OUT: <%s>", stdout)
365                                 }
366                                 if stderr != "" {
367                                         fmt.Fprintf(cpFd, "%s", stderr)
368                                         log.Debugf("Inferior ERR: <%s>", stderr)
369                                 }
370                         })
371                 }
372
373                 // Allow to overwrite some gdb commands
374                 var overwriteMap = make(map[string]string)
375                 if overEnv, exist := os.LookupEnv("XDS_OVERWRITE_COMMANDS"); exist {
376                         overEnvS := strings.TrimSpace(overEnv)
377                         if len(overEnvS) > 0 {
378                                 // Extract overwrite commands from env variable
379                                 for _, def := range strings.Split(overEnvS, ",") {
380                                         if kv := strings.Split(def, ":"); len(kv) == 2 {
381                                                 overwriteMap[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
382                                         } else {
383                                                 return cli.NewExitError(
384                                                         fmt.Errorf("Invalid definition in XDS_OVERWRITE_COMMANDS (%s)", def),
385                                                         int(syscall.EINVAL))
386                                         }
387                                 }
388                         }
389                 } else {
390                         overwriteMap["-exec-run"] = "-exec-continue"
391                         overwriteMap["-file-exec-and-symbols"] = "-file-exec-file"
392                 }
393                 log.Debugf("overwriteMap = %v", overwriteMap)
394
395                 // Send stdin though WS
396                 go func() {
397                         paranoia := 600
398                         reader := bufio.NewReader(os.Stdin)
399
400                         for {
401                                 sc := bufio.NewScanner(reader)
402                                 for sc.Scan() {
403                                         command := sc.Text()
404
405                                         // overwrite some commands
406                                         for key, value := range overwriteMap {
407                                                 if strings.Contains(command, key) {
408                                                         command = strings.Replace(command, key, value, 1)
409                                                         log.Debugf("OVERWRITE %s -> %s", key, value)
410                                                 }
411                                         }
412                                         gdb.Write(command + "\n")
413                                         log.Debugf("Send: <%v>", command)
414                                 }
415                                 log.Infof("Stdin scanner exit, close stdin (err=%v)", sc.Err())
416
417                                 // CTRL-D exited scanner, so send it explicitly
418                                 gdb.Write("\x04")
419                                 time.Sleep(time.Millisecond * 100)
420
421                                 if paranoia--; paranoia <= 0 {
422                                         msg := "Abnormal loop detected on stdin"
423                                         log.Errorf("Abnormal loop detected on stdin")
424                                         gdb.SendSignal(syscall.SIGTERM)
425                                         exitChan <- exitResult{fmt.Errorf(msg), int(syscall.ELOOP)}
426                                 }
427                         }
428                 }()
429
430                 // Handling all Signals
431                 sigs := make(chan os.Signal, 1)
432                 signal.Notify(sigs)
433
434                 go func() {
435                         for {
436                                 sig := <-sigs
437                                 if err := gdb.SendSignal(sig); err != nil {
438                                         log.Errorf("Error while sending signal: %s", err.Error())
439                                 }
440                         }
441                 }()
442
443                 // Start gdb
444                 if code, err := gdb.Start(clientPty != ""); err != nil {
445                         return cli.NewExitError(err.Error(), code)
446                 }
447
448                 // Wait exit
449                 select {
450                 case res := <-exitChan:
451                         errStr := ""
452                         if res.code == 0 {
453                                 log.Infoln("Exit successfully")
454                         }
455                         if res.error != nil {
456                                 log.Infoln("Exit with ERROR: ", res.error.Error())
457                                 errStr = res.error.Error()
458                         }
459                         return cli.NewExitError(errStr, res.code)
460                 }
461         }
462
463         app.Run(args)
464 }
465
466 // loadConfigEnvFile
467 func loadConfigEnvFile(confFile, gdbCmdFile string) (map[string]string, string, error) {
468         var err error
469         envMap := make(map[string]string)
470
471         // 1- if no confFile set, use setting from gdb command file is option
472         //    --command/-x is set
473         if confFile == "" && gdbCmdFile != "" {
474                 logEarly("Try extract config from gdbCmdFile: %s", gdbCmdFile)
475                 confFile, err = extractEnvFromCmdFile(gdbCmdFile)
476                 if confFile != "" {
477                         defer os.Remove(confFile)
478                 }
479                 if err != nil {
480                         return envMap, confFile, fmt.Errorf(err.Error())
481                 }
482         }
483         // 2- search xds-gdb.env file in various locations
484         if confFile == "" {
485                 curDir, _ := os.Getwd()
486                 if u, err := user.Current(); err == nil {
487                         xdsEnvFile := "xds-gdb.env"
488                         for _, d := range []string{
489                                 path.Join(curDir),
490                                 path.Join(curDir, "..", ".."),
491                                 path.Join(curDir, "../../target"),
492                                 path.Join(u.HomeDir, ".xds"),
493                         } {
494                                 confFile = path.Join(d, xdsEnvFile)
495                                 logEarly("Search config in %s", confFile)
496                                 if common.Exists(confFile) {
497                                         break
498                                 }
499                         }
500                 }
501         }
502
503         if confFile == "" {
504                 return envMap, "", nil
505         }
506
507         if !common.Exists(confFile) {
508                 return envMap, confFile, fmt.Errorf("Error no env config file not found")
509         }
510         if err = godotenv.Load(confFile); err != nil {
511                 return envMap, confFile, fmt.Errorf("Error loading env config file " + confFile)
512         }
513         if envMap, err = godotenv.Read(confFile); err != nil {
514                 return envMap, confFile, fmt.Errorf("Error reading env config file " + confFile)
515         }
516         return envMap, confFile, nil
517 }
518
519 /*
520  extractEnvFromCmdFile: extract xds-gdb env variable from gdb command file
521   All commented lines (#) in gdb command file that start with ':XDS-ENV:' prefix
522   will be considered as XDS env commands. For example the 3 syntaxes below
523   are supported:
524   # :XDS-ENV: XDS_PROJECT_ID=IW7B4EE-DBY4Z74_myProject
525   #:XDS-ENV:XDS_SDK_ID=poky-agl_aarch64_3.99.1+snapshot
526   # :XDS-ENV:  export XDS_SERVER_URL=localhost:8800
527 */
528 func extractEnvFromCmdFile(cmdFile string) (string, error) {
529         if !common.Exists(cmdFile) {
530                 return "", nil
531         }
532         cFd, err := os.Open(cmdFile)
533         if err != nil {
534                 return "", fmt.Errorf("Cannot open %s : %s", cmdFile, err.Error())
535         }
536         defer cFd.Close()
537
538         var lines []string
539         scanner := bufio.NewScanner(cFd)
540         for scanner.Scan() {
541                 lines = append(lines, scanner.Text())
542         }
543         if err = scanner.Err(); err != nil {
544                 return "", fmt.Errorf("Cannot parse %s : %s", cmdFile, err.Error())
545         }
546
547         envFile, err := ioutil.TempFile("", "xds-gdb_env.ini")
548         if err != nil {
549                 return "", fmt.Errorf("Error while creating temporary env file: %s", err.Error())
550         }
551         envFileName := envFile.Name()
552         defer envFile.Close()
553
554         envFound := false
555         for _, ln := range lines {
556                 ln = strings.TrimSpace(ln)
557                 if strings.HasPrefix(ln, "#") && strings.Contains(ln, ":XDS-ENV:") {
558                         env := strings.SplitAfterN(ln, ":XDS-ENV:", 2)
559                         if len(env) == 2 {
560                                 envFound = true
561                                 if _, err := envFile.WriteString(strings.TrimSpace(env[1]) + "\n"); err != nil {
562                                         return "", fmt.Errorf("Error write into temporary env file: %s", err.Error())
563                                 }
564                         } else {
565                                 log.Warnf("Error while decoding line %s", ln)
566                         }
567                 }
568         }
569
570         if !envFound {
571                 ff := envFileName
572                 defer os.Remove(ff)
573                 envFileName = ""
574
575         }
576
577         return envFileName, nil
578 }
579
580 func logEarly(format string, a ...interface{}) {
581         earlyLog = append(earlyLog, fmt.Sprintf(format, a...))
582 }