Fixed terminal output (support escape and control characters)
[src/xds/xds-cli.git] / main.go
1 /*
2  * Copyright (C) 2017-2018 "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-cli: command line tool used to control / interface X(cross) Development System.
19  */
20
21 package main
22
23 import (
24         "fmt"
25         "os"
26         "regexp"
27         "sort"
28         "strings"
29         "text/tabwriter"
30
31         "gerrit.automotivelinux.org/gerrit/src/xds/xds-agent.git/lib/xaapiv1"
32         common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib"
33         "github.com/Sirupsen/logrus"
34
35         "github.com/joho/godotenv"
36         "github.com/urfave/cli"
37 )
38
39 var appAuthors = []cli.Author{
40         cli.Author{Name: "Sebastien Douheret", Email: "sebastien@iot.bzh"},
41 }
42
43 // AppName name of this application
44 var AppName = "xds-cli"
45
46 // AppNativeName native command name that this application can overload
47 var AppNativeName = "cli"
48
49 // AppVersion Version of this application
50 // (set by Makefile)
51 var AppVersion = "?.?.?"
52
53 // AppSubVersion is the git tag id added to version string
54 // Should be set by compilation -ldflags "-X main.AppSubVersion=xxx"
55 // (set by Makefile)
56 var AppSubVersion = "unknown-dev"
57
58 // Application details
59 const (
60         appCopyright    = "Copyright (C) 2017-2018 IoT.bzh - Apache-2.0"
61         defaultLogLevel = "error"
62 )
63
64 // Log Global variable that hold logger
65 var Log = logrus.New()
66
67 // EnvConfFileMap Global variable that hold environment vars loaded from config file
68 var EnvConfFileMap map[string]string
69
70 // HTTPCli Global variable that hold HTTP Client
71 var HTTPCli *common.HTTPClient
72
73 // IOSkClient Global variable that hold SocketIo client
74 var IOSkClient *IOSockClient
75
76 // exitError exists this program with the specified error
77 func exitError(code int, f string, a ...interface{}) {
78         earlyDisplay()
79         err := fmt.Sprintf(f, a...)
80         fmt.Fprintf(os.Stderr, err+"\n")
81         os.Exit(code)
82 }
83
84 // earlyDebug Used to log info before logger has been initialized
85 var earlyDebug []string
86
87 func earlyPrintf(format string, args ...interface{}) {
88         earlyDebug = append(earlyDebug, fmt.Sprintf(format, args...))
89 }
90
91 func earlyDisplay() {
92         for _, str := range earlyDebug {
93                 Log.Infof("%s", str)
94         }
95         earlyDebug = []string{}
96 }
97
98 // LogSillyf Logging helper used for silly logging (printed on log.debug)
99 func LogSillyf(format string, args ...interface{}) {
100         sillyVal, sillyLog := os.LookupEnv("XDS_LOG_SILLY")
101         if sillyLog && sillyVal == "1" {
102                 Log.Debugf("SILLY: "+format, args...)
103         }
104 }
105
106 // main
107 func main() {
108
109         // Allow to set app name from cli (useful for debugging)
110         if AppName == "" {
111                 AppName = os.Getenv("XDS_APPNAME")
112         }
113         if AppName == "" {
114                 panic("Invalid setup, AppName not define !")
115         }
116         if AppNativeName == "" {
117                 AppNativeName = AppName[4:]
118         }
119         appUsage := fmt.Sprintf("command line tool for X(cross) Development System.")
120         appDescription := fmt.Sprintf("%s utility for X(cross) Development System\n", AppName)
121         appDescription += `
122     Setting of global options is driven either by environment variables or by command
123     line options or using a config file knowning that the following priority order is used:
124       1. use option value (for example --url option),
125       2. else use variable 'XDS_xxx' (for example 'XDS_AGENT_URL' variable) when a
126          config file is specified with '--config|-c' option,
127       3. else use 'XDS_xxx' (for example 'XDS_AGENT_URL') environment variable.
128
129     Examples:
130     # Get help of 'projects' sub-command
131     ` + AppName + ` projects --help
132
133     # List all SDKs
134     ` + AppName + ` sdks ls
135
136     # Add a new project
137     ` + AppName + ` prj add --label="myProject" --type=cs --path=$HOME/xds-workspace/myProject
138 `
139
140         // Create a new App instance
141         app := cli.NewApp()
142         app.Name = AppName
143         app.Usage = appUsage
144         app.Version = AppVersion + " (" + AppSubVersion + ")"
145         app.Authors = appAuthors
146         app.Copyright = appCopyright
147         app.Metadata = make(map[string]interface{})
148         app.Metadata["version"] = AppVersion
149         app.Metadata["git-tag"] = AppSubVersion
150         app.Metadata["logger"] = Log
151         app.EnableBashCompletion = true
152
153         // Create env vars help
154         dynDesc := "\nENVIRONMENT VARIABLES:"
155         for _, f := range app.Flags {
156                 var env, usage string
157                 switch f.(type) {
158                 case cli.StringFlag:
159                         fs := f.(cli.StringFlag)
160                         env = fs.EnvVar
161                         usage = fs.Usage
162                 case cli.BoolFlag:
163                         fb := f.(cli.BoolFlag)
164                         env = fb.EnvVar
165                         usage = fb.Usage
166                 default:
167                         exitError(1, "Un-implemented option type")
168                 }
169                 if env != "" {
170                         dynDesc += fmt.Sprintf("\n %s \t\t %s", env, usage)
171                 }
172         }
173         app.Description = appDescription + dynDesc
174
175         // Declare global flags
176         app.Flags = []cli.Flag{
177                 cli.StringFlag{
178                         Name:   "config, c",
179                         EnvVar: "XDS_CONFIG",
180                         Usage:  "env config file to source on startup",
181                 },
182                 cli.StringFlag{
183                         Name:   "log, l",
184                         EnvVar: "XDS_LOGLEVEL",
185                         Usage:  "logging level (supported levels: panic, fatal, error, warn, info, debug)",
186                         Value:  defaultLogLevel,
187                 },
188                 cli.StringFlag{
189                         Name:   "logfile",
190                         Value:  "stderr",
191                         Usage:  "filename where logs will be redirected (default stderr)\n\t",
192                         EnvVar: "XDS_LOGFILENAME",
193                 },
194                 cli.StringFlag{
195                         Name:   "url, u",
196                         EnvVar: "XDS_AGENT_URL",
197                         Value:  "localhost:8800",
198                         Usage:  "local XDS agent url",
199                 },
200                 cli.StringFlag{
201                         Name:   "url-server, us",
202                         EnvVar: "XDS_SERVER_URL",
203                         Value:  "",
204                         Usage:  "overwrite remote XDS server url (default value set in xds-agent-config.json file)",
205                 },
206                 cli.BoolFlag{
207                         Name:   "timestamp, ts",
208                         EnvVar: "XDS_TIMESTAMP",
209                         Usage:  "prefix output with timestamp",
210                 },
211         }
212
213         // Declare commands
214         app.Commands = []cli.Command{}
215
216         initCmdProjects(&app.Commands)
217         initCmdSdks(&app.Commands)
218         initCmdExec(&app.Commands)
219         initCmdTargets(&app.Commands)
220         initCmdMisc(&app.Commands)
221
222         // Add --config option to all commands to support --config option either before or after command verb
223         // IOW support following both syntaxes:
224         //   xds-cli exec --config myPrj.conf ...
225         //   xds-cli --config myPrj.conf exec ...
226         for i, cmd := range app.Commands {
227                 if len(cmd.Flags) > 0 {
228                         app.Commands[i].Flags = append(cmd.Flags, cli.StringFlag{Hidden: true, Name: "config, c"})
229                 }
230                 for j, subCmd := range cmd.Subcommands {
231                         app.Commands[i].Subcommands[j].Flags = append(subCmd.Flags, cli.StringFlag{Hidden: true, Name: "config, c"})
232                 }
233         }
234
235         sort.Sort(cli.FlagsByName(app.Flags))
236         sort.Sort(cli.CommandsByName(app.Commands))
237
238         // Early and manual processing of --config option in order to set XDS_xxx
239         // variables before parsing of option by app cli
240         confFile := os.Getenv("XDS_CONFIG")
241         for idx, a := range os.Args[1:] {
242                 if a == "-c" || a == "--config" || a == "-config" {
243                         confFile = os.Args[idx+2]
244                         break
245                 }
246         }
247
248         // Load config file if requested
249         if confFile != "" {
250                 earlyPrintf("confFile detected: %v", confFile)
251                 confFile, err := common.ResolveEnvVar(confFile)
252                 if err != nil {
253                         exitError(1, "Error while resolving confFile: %v", err)
254                 }
255                 earlyPrintf("Resolved confFile: %v", confFile)
256                 if !common.Exists(confFile) {
257                         exitError(1, "Error env config file not found")
258                 }
259                 // Load config file variables that will overwrite env variables
260                 err = godotenv.Overload(confFile)
261                 if err != nil {
262                         exitError(1, "Error loading env config file "+confFile)
263                 }
264
265                 // Keep confFile settings in a map
266                 EnvConfFileMap, err = godotenv.Read(confFile)
267                 if err != nil {
268                         exitError(1, "Error reading env config file "+confFile)
269                 }
270                 earlyPrintf("EnvConfFileMap: %v", EnvConfFileMap)
271         }
272
273         app.Before = func(ctx *cli.Context) error {
274                 var err error
275
276                 // Don't init anything when no argument or help option is set
277                 if ctx.NArg() == 0 {
278                         return nil
279                 }
280                 for _, a := range ctx.Args() {
281                         switch a {
282                         case "-h", "--h", "-help", "--help":
283                                 return nil
284                         }
285                 }
286
287                 loglevel := ctx.String("log")
288                 // Set logger level and formatter
289                 if Log.Level, err = logrus.ParseLevel(loglevel); err != nil {
290                         msg := fmt.Sprintf("Invalid log level : \"%v\"\n", loglevel)
291                         return cli.NewExitError(msg, 1)
292                 }
293                 Log.Formatter = &logrus.TextFormatter{}
294
295                 if ctx.String("logfile") != "stderr" {
296                         logFile, _ := common.ResolveEnvVar(ctx.String("logfile"))
297                         fdL, err := os.OpenFile(logFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
298                         if err != nil {
299                                 msgErr := fmt.Sprintf("Cannot create log file %s", logFile)
300                                 return cli.NewExitError(msgErr, 1)
301                         }
302                         Log.Infof("Logging to file: %s", logFile)
303                         Log.Out = fdL
304                 }
305
306                 Log.Infof("%s version: %s", AppName, app.Version)
307                 earlyDisplay()
308                 Log.Debugf("\nEnvironment: %v\n", os.Environ())
309
310                 if err = XdsConnInit(ctx); err != nil {
311                         // Directly call HandleExitCoder to avoid to print help (ShowAppHelp)
312                         // Note that this function wil never return and program will exit
313                         cli.HandleExitCoder(err)
314                 }
315
316                 return nil
317         }
318
319         // Close HTTP client and WS connection on exit
320         defer func() {
321                 XdsConnClose()
322         }()
323
324         // Start signals monitoring routine
325         MonitorSignals()
326
327         // Run the cli app
328         app.Run(os.Args)
329 }
330
331 // XdsConnInit Initialized HTTP and WebSocket connection to XDS agent
332 func XdsConnInit(ctx *cli.Context) error {
333         var err error
334
335         // Define HTTP and WS url
336         agentURL := ctx.String("url")
337         serverURL := ctx.String("url-server")
338
339         // Allow to only set port number
340         if match, _ := regexp.MatchString("^([0-9]+)$", agentURL); match {
341                 agentURL = "http://localhost:" + ctx.String("url")
342         }
343         if match, _ := regexp.MatchString("^([0-9]+)$", serverURL); match {
344                 serverURL = "http://localhost:" + ctx.String("url-server")
345         }
346         // Add http prefix if missing
347         if agentURL != "" && !strings.HasPrefix(agentURL, "http://") {
348                 agentURL = "http://" + agentURL
349         }
350         if serverURL != "" && !strings.HasPrefix(serverURL, "http://") {
351                 serverURL = "http://" + serverURL
352         }
353
354         lvl := common.HTTPLogLevelWarning
355         if Log.Level == logrus.DebugLevel {
356                 lvl = common.HTTPLogLevelDebug
357         }
358
359         // Create HTTP client
360         Log.Debugln("Connect HTTP client on ", agentURL)
361         conf := common.HTTPClientConfig{
362                 URLPrefix:           "/api/v1",
363                 HeaderClientKeyName: "Xds-Agent-Sid",
364                 CsrfDisable:         true,
365                 LogOut:              Log.Out,
366                 LogPrefix:           "XDSAGENT: ",
367                 LogLevel:            lvl,
368         }
369
370         HTTPCli, err = common.HTTPNewClient(agentURL, conf)
371         if err != nil {
372                 errmsg := err.Error()
373                 m, err := regexp.MatchString("Get http.?://", errmsg)
374                 if (m && err == nil) || strings.Contains(errmsg, "Failed to get device ID") {
375                         i := strings.LastIndex(errmsg, ":")
376                         newErr := "Cannot connection to " + agentURL
377                         if i > 0 {
378                                 newErr += " (" + strings.TrimSpace(errmsg[i+1:]) + ")"
379                         } else {
380                                 newErr += " (" + strings.TrimSpace(errmsg) + ")"
381                         }
382                         errmsg = newErr
383                 }
384                 return cli.NewExitError(errmsg, 1)
385         }
386         HTTPCli.SetLogLevel(ctx.String("loglevel"))
387         Log.Infoln("HTTP session ID : ", HTTPCli.GetClientID())
388
389         // Create io Websocket client
390         Log.Debugln("Connecting IO.socket client on ", agentURL)
391
392         IOSkClient, err = NewIoSocketClient(agentURL, HTTPCli.GetClientID())
393         if err != nil {
394                 return cli.NewExitError(err.Error(), 1)
395         }
396
397         IOSkClient.On("error", func(err error) {
398                 fmt.Println("ERROR Websocket: ", err.Error())
399         })
400
401         ctx.App.Metadata["httpCli"] = HTTPCli
402         ctx.App.Metadata["ioskCli"] = IOSkClient
403
404         // Display version in logs (debug helpers)
405         ver := xaapiv1.XDSVersion{}
406         if err := XdsVersionGet(&ver); err != nil {
407                 return cli.NewExitError("ERROR while retrieving XDS version: "+err.Error(), 1)
408         }
409         Log.Infof("XDS Agent/Server version: %v", ver)
410
411         // Get current config and update connection to server when needed
412         xdsConf := xaapiv1.APIConfig{}
413         if err := XdsConfigGet(&xdsConf); err != nil {
414                 return cli.NewExitError("ERROR while getting XDS config: "+err.Error(), 1)
415         }
416         if len(xdsConf.Servers) < 1 {
417                 return cli.NewExitError("No XDS Server connected", 1)
418         }
419         svrCfg := xdsConf.Servers[XdsServerIndexGet()]
420         if (serverURL != "" && svrCfg.URL != serverURL) || !svrCfg.Connected {
421                 Log.Infof("Update XDS Server config: serverURL=%v, svrCfg=%v", serverURL, svrCfg)
422                 if serverURL != "" {
423                         svrCfg.URL = serverURL
424                 }
425                 svrCfg.ConnRetry = 10
426                 if err := XdsConfigSet(xdsConf); err != nil {
427                         return cli.NewExitError("ERROR while updating XDS server URL: "+err.Error(), 1)
428                 }
429         }
430
431         return nil
432 }
433
434 // XdsConnClose Terminate connection to XDS agent
435 func XdsConnClose() {
436         Log.Debugf("Closing HTTP client session...")
437         /* TODO
438         if httpCli, ok := app.Metadata["httpCli"]; ok {
439                 c := httpCli.(*common.HTTPClient)
440         }
441         */
442
443         Log.Debugf("Closing WebSocket connection...")
444         /*
445                 if ioskCli, ok := app.Metadata["ioskCli"]; ok {
446                         c := ioskCli.(*socketio_client.Client)
447                 }
448         */
449 }
450
451 // NewTableWriter Create a writer that inserts padding around tab-delimited
452 func NewTableWriter() *tabwriter.Writer {
453         writer := new(tabwriter.Writer)
454         writer.Init(os.Stdout, 0, 8, 0, '\t', 0)
455         return writer
456 }