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