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