Disable completion (not working well)
[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         "syscall"
30         "text/tabwriter"
31
32         "gerrit.automotivelinux.org/gerrit/src/xds/xds-agent.git/lib/xaapiv1"
33         common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib"
34         "github.com/Sirupsen/logrus"
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         // FIXME: Disable completion for now, because it's not working with options
152         // (eg. --label) and prevents to complete local path
153         // (IOW current function only completes command and sub-commands)
154         app.EnableBashCompletion = false
155
156         // Create env vars help
157         dynDesc := "\nENVIRONMENT VARIABLES:"
158         for _, f := range app.Flags {
159                 var env, usage string
160                 switch f.(type) {
161                 case cli.StringFlag:
162                         fs := f.(cli.StringFlag)
163                         env = fs.EnvVar
164                         usage = fs.Usage
165                 case cli.BoolFlag:
166                         fb := f.(cli.BoolFlag)
167                         env = fb.EnvVar
168                         usage = fb.Usage
169                 default:
170                         exitError(1, "Un-implemented option type")
171                 }
172                 if env != "" {
173                         dynDesc += fmt.Sprintf("\n %s \t\t %s", env, usage)
174                 }
175         }
176         app.Description = appDescription + dynDesc
177
178         // Declare global flags
179         app.Flags = []cli.Flag{
180                 cli.StringFlag{
181                         Name:   "config, c",
182                         EnvVar: "XDS_CONFIG",
183                         Usage:  "env config file to source on startup",
184                 },
185                 cli.StringFlag{
186                         Name:   "log, l",
187                         EnvVar: "XDS_LOGLEVEL",
188                         Usage:  "logging level (supported levels: panic, fatal, error, warn, info, debug)",
189                         Value:  defaultLogLevel,
190                 },
191                 cli.StringFlag{
192                         Name:   "logfile",
193                         Value:  "stderr",
194                         Usage:  "filename where logs will be redirected (default stderr)\n\t",
195                         EnvVar: "XDS_LOGFILENAME",
196                 },
197                 cli.StringFlag{
198                         Name:   "url, u",
199                         EnvVar: "XDS_AGENT_URL",
200                         Value:  "localhost:8800",
201                         Usage:  "local XDS agent url",
202                 },
203                 cli.StringFlag{
204                         Name:   "url-server, us",
205                         EnvVar: "XDS_SERVER_URL",
206                         Value:  "",
207                         Usage:  "overwrite remote XDS server url (default value set in xds-agent-config.json file)",
208                 },
209                 cli.BoolFlag{
210                         Name:   "timestamp, ts",
211                         EnvVar: "XDS_TIMESTAMP",
212                         Usage:  "prefix output with timestamp",
213                 },
214         }
215
216         // Declare commands
217         app.Commands = []cli.Command{}
218
219         initCmdProjects(&app.Commands)
220         initCmdSdks(&app.Commands)
221         initCmdExec(&app.Commands)
222         initCmdTargets(&app.Commands)
223         initCmdMisc(&app.Commands)
224
225         // Add --config option to all commands to support --config option either before or after command verb
226         // IOW support following both syntaxes:
227         //   xds-cli exec --config myPrj.conf ...
228         //   xds-cli --config myPrj.conf exec ...
229         for i, cmd := range app.Commands {
230                 if len(cmd.Flags) > 0 {
231                         app.Commands[i].Flags = append(cmd.Flags, cli.StringFlag{Hidden: true, Name: "config, c"})
232                 }
233                 for j, subCmd := range cmd.Subcommands {
234                         app.Commands[i].Subcommands[j].Flags = append(subCmd.Flags, cli.StringFlag{Hidden: true, Name: "config, c"})
235                 }
236         }
237
238         sort.Sort(cli.FlagsByName(app.Flags))
239         sort.Sort(cli.CommandsByName(app.Commands))
240
241         // Early and manual processing of --config option in order to set XDS_xxx
242         // variables before parsing of option by app cli
243         confFile := os.Getenv("XDS_CONFIG")
244         for idx, a := range os.Args[1:] {
245                 if a == "-c" || a == "--config" || a == "-config" {
246                         confFile = os.Args[idx+2]
247                         break
248                 }
249         }
250
251         // Load config file if requested
252         if confFile != "" {
253                 earlyPrintf("confFile detected: %v", confFile)
254                 confFile, err := common.ResolveEnvVar(confFile)
255                 if err != nil {
256                         exitError(1, "Error while resolving confFile: %v", err)
257                 }
258                 earlyPrintf("Resolved confFile: %v", confFile)
259                 if !common.Exists(confFile) {
260                         exitError(1, "Error env config file not found")
261                 }
262                 // Load config file variables that will overwrite env variables
263                 err = godotenv.Overload(confFile)
264                 if err != nil {
265                         exitError(1, "Error loading env config file "+confFile)
266                 }
267
268                 // Keep confFile settings in a map
269                 EnvConfFileMap, err = godotenv.Read(confFile)
270                 if err != nil {
271                         exitError(1, "Error reading env config file "+confFile)
272                 }
273                 earlyPrintf("EnvConfFileMap: %v", EnvConfFileMap)
274         }
275
276         app.Before = func(ctx *cli.Context) error {
277                 var err error
278
279                 // Don't init anything when no argument or help option is set
280                 if ctx.NArg() == 0 {
281                         return nil
282                 }
283                 for _, a := range ctx.Args() {
284                         switch a {
285                         case "-h", "--h", "-help", "--help":
286                                 return nil
287                         }
288                 }
289
290                 loglevel := ctx.String("log")
291                 // Set logger level and formatter
292                 if Log.Level, err = logrus.ParseLevel(loglevel); err != nil {
293                         msg := fmt.Sprintf("Invalid log level : \"%v\"\n", loglevel)
294                         return cli.NewExitError(msg, 1)
295                 }
296                 Log.Formatter = &logrus.TextFormatter{}
297
298                 if ctx.String("logfile") != "stderr" {
299                         logFile, _ := common.ResolveEnvVar(ctx.String("logfile"))
300                         fdL, err := os.OpenFile(logFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
301                         if err != nil {
302                                 msgErr := fmt.Sprintf("Cannot create log file %s", logFile)
303                                 return cli.NewExitError(msgErr, 1)
304                         }
305                         Log.Infof("Logging to file: %s", logFile)
306                         Log.Out = fdL
307                 }
308
309                 Log.Infof("%s version: %s", AppName, app.Version)
310                 earlyDisplay()
311                 Log.Debugf("\nEnvironment: %v\n", os.Environ())
312
313                 if err = XdsConnInit(ctx); err != nil {
314                         // Directly call HandleExitCoder to avoid to print help (ShowAppHelp)
315                         // Note that this function wil never return and program will exit
316                         cli.HandleExitCoder(err)
317                 }
318
319                 return nil
320         }
321
322         // Close HTTP client and WS connection on exit
323         defer func() {
324                 XdsConnClose()
325         }()
326
327         // Start signals monitoring routine
328         MonitorSignals()
329
330         // Default callback to handle interrupt signal
331         // Maybe be overwritten by some subcommand (eg. targets commands)
332         err := OnSignals(func(sig os.Signal) {
333                 Log.Debugf("Send signal %v (from main)", sig)
334                 if IsInterruptSignal(sig) {
335                         err := cli.NewExitError("Interrupted\n", int(syscall.EINTR))
336                         cli.HandleExitCoder(err)
337                 }
338         })
339         if err != nil {
340                 cli.NewExitError(err.Error(), 1)
341                 return
342         }
343
344         // Run the cli app
345         app.Run(os.Args)
346 }
347
348 // XdsConnInit Initialized HTTP and WebSocket connection to XDS agent
349 func XdsConnInit(ctx *cli.Context) error {
350         var err error
351
352         // Define HTTP and WS url
353         agentURL := ctx.String("url")
354         serverURL := ctx.String("url-server")
355
356         // Allow to only set port number
357         if match, _ := regexp.MatchString("^([0-9]+)$", agentURL); match {
358                 agentURL = "http://localhost:" + ctx.String("url")
359         }
360         if match, _ := regexp.MatchString("^([0-9]+)$", serverURL); match {
361                 serverURL = "http://localhost:" + ctx.String("url-server")
362         }
363         // Add http prefix if missing
364         if agentURL != "" && !strings.HasPrefix(agentURL, "http://") {
365                 agentURL = "http://" + agentURL
366         }
367         if serverURL != "" && !strings.HasPrefix(serverURL, "http://") {
368                 serverURL = "http://" + serverURL
369         }
370
371         lvl := common.HTTPLogLevelWarning
372         if Log.Level == logrus.DebugLevel {
373                 lvl = common.HTTPLogLevelDebug
374         }
375
376         // Create HTTP client
377         Log.Debugln("Connect HTTP client on ", agentURL)
378         conf := common.HTTPClientConfig{
379                 URLPrefix:           "/api/v1",
380                 HeaderClientKeyName: "Xds-Agent-Sid",
381                 CsrfDisable:         true,
382                 LogOut:              Log.Out,
383                 LogPrefix:           "XDSAGENT: ",
384                 LogLevel:            lvl,
385         }
386
387         HTTPCli, err = common.HTTPNewClient(agentURL, conf)
388         if err != nil {
389                 errmsg := err.Error()
390                 m, err := regexp.MatchString("Get http.?://", errmsg)
391                 if (m && err == nil) || strings.Contains(errmsg, "Failed to get device ID") {
392                         i := strings.LastIndex(errmsg, ":")
393                         newErr := "Cannot connection to " + agentURL
394                         if i > 0 {
395                                 newErr += " (" + strings.TrimSpace(errmsg[i+1:]) + ")"
396                         } else {
397                                 newErr += " (" + strings.TrimSpace(errmsg) + ")"
398                         }
399                         errmsg = newErr
400                 }
401                 return cli.NewExitError(errmsg, 1)
402         }
403         HTTPCli.SetLogLevel(ctx.String("loglevel"))
404         Log.Infoln("HTTP session ID : ", HTTPCli.GetClientID())
405
406         // Create io Websocket client
407         Log.Debugln("Connecting IO.socket client on ", agentURL)
408
409         IOSkClient, err = NewIoSocketClient(agentURL, HTTPCli.GetClientID())
410         if err != nil {
411                 return cli.NewExitError(err.Error(), 1)
412         }
413
414         IOSkClient.On("error", func(err error) {
415                 fmt.Println("ERROR Websocket: ", err.Error())
416         })
417
418         ctx.App.Metadata["httpCli"] = HTTPCli
419         ctx.App.Metadata["ioskCli"] = IOSkClient
420
421         // Display version in logs (debug helpers)
422         ver := xaapiv1.XDSVersion{}
423         if err := XdsVersionGet(&ver); err != nil {
424                 return cli.NewExitError("ERROR while retrieving XDS version: "+err.Error(), 1)
425         }
426         Log.Infof("XDS Agent/Server version: %v", ver)
427
428         // Get current config and update connection to server when needed
429         xdsConf := xaapiv1.APIConfig{}
430         if err := XdsConfigGet(&xdsConf); err != nil {
431                 return cli.NewExitError("ERROR while getting XDS config: "+err.Error(), 1)
432         }
433         if len(xdsConf.Servers) < 1 {
434                 return cli.NewExitError("No XDS Server connected", 1)
435         }
436         svrCfg := xdsConf.Servers[XdsServerIndexGet()]
437         if (serverURL != "" && svrCfg.URL != serverURL) || !svrCfg.Connected {
438                 Log.Infof("Update XDS Server config: serverURL=%v, svrCfg=%v", serverURL, svrCfg)
439                 if serverURL != "" {
440                         svrCfg.URL = serverURL
441                 }
442                 svrCfg.ConnRetry = 10
443                 if err := XdsConfigSet(xdsConf); err != nil {
444                         return cli.NewExitError("ERROR while updating XDS server URL: "+err.Error(), 1)
445                 }
446         }
447
448         return nil
449 }
450
451 // XdsConnClose Terminate connection to XDS agent
452 func XdsConnClose() {
453         Log.Debugf("Closing HTTP client session...")
454         /* TODO
455         if httpCli, ok := app.Metadata["httpCli"]; ok {
456                 c := httpCli.(*common.HTTPClient)
457         }
458         */
459
460         Log.Debugf("Closing WebSocket connection...")
461         /*
462                 if ioskCli, ok := app.Metadata["ioskCli"]; ok {
463                         c := ioskCli.(*socketio_client.Client)
464                 }
465         */
466 }
467
468 // NewTableWriter Create a writer that inserts padding around tab-delimited
469 func NewTableWriter() *tabwriter.Writer {
470         writer := new(tabwriter.Writer)
471         writer.Init(os.Stdout, 0, 8, 0, '\t', 0)
472         return writer
473 }