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