X-Git-Url: https://gerrit.automotivelinux.org/gerrit/gitweb?p=src%2Fxds%2Fxds-cli.git;a=blobdiff_plain;f=main.go;h=737c9b2e642f67b608ac1cd49781b581fe51a65b;hp=c8d20958894964fc6c0421dae4ced0ceb6c95c6c;hb=3b5e82b55433fd49cfe0cd0349756e0c2e9a9c12;hpb=c35d7a0fc8bbb1f9123bb41a7b66e45ea2564dd2 diff --git a/main.go b/main.go index c8d2095..737c9b2 100644 --- a/main.go +++ b/main.go @@ -1,17 +1,39 @@ -// xds-cli: command line tool used to control / interface X(cross) Development System. +/* + * Copyright (C) 2017-2019 "IoT.bzh" + * Author Sebastien Douheret + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * xds-cli: command line tool used to control / interface X(cross) Development System. + */ + package main import ( "fmt" "os" + "path" "regexp" "sort" "strings" + "syscall" "text/tabwriter" + "gerrit.automotivelinux.org/gerrit/src/xds/xds-agent.git/lib/xaapiv1" + common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git" "github.com/Sirupsen/logrus" - common "github.com/iotbzh/xds-common/golib" - socketio_client "github.com/sebd71/go-socket.io-client" + "github.com/joho/godotenv" "github.com/urfave/cli" ) @@ -36,26 +58,53 @@ var AppSubVersion = "unknown-dev" // Application details const ( - appCopyright = "Apache-2.0" - defaultLogLevel = "error" + appCopyright = "Copyright (C) 2017-2019 IoT.bzh - Apache-2.0" + defaultLogLevel = "error" + defaultConfigEnvFilename = "cli-config.env" ) // Log Global variable that hold logger var Log = logrus.New() +// EnvConfFileMap Global variable that hold environment vars loaded from config file +var EnvConfFileMap map[string]string + // HTTPCli Global variable that hold HTTP Client var HTTPCli *common.HTTPClient -// IOsk Global variable that hold SocketIo client -var IOsk *socketio_client.Client +// IOSkClient Global variable that hold SocketIo client +var IOSkClient *IOSockClient // exitError exists this program with the specified error func exitError(code int, f string, a ...interface{}) { + earlyDisplay() err := fmt.Sprintf(f, a...) fmt.Fprintf(os.Stderr, err+"\n") os.Exit(code) } +// earlyDebug Used to log info before logger has been initialized +var earlyDebug []string + +func earlyPrintf(format string, args ...interface{}) { + earlyDebug = append(earlyDebug, fmt.Sprintf(format, args...)) +} + +func earlyDisplay() { + for _, str := range earlyDebug { + Log.Infof("%s", str) + } + earlyDebug = []string{} +} + +// LogSillyf Logging helper used for silly logging (printed on log.debug) +func LogSillyf(format string, args ...interface{}) { + sillyVal, sillyLog := os.LookupEnv("XDS_LOG_SILLY") + if sillyLog && sillyVal == "1" { + Log.Debugf("SILLY: "+format, args...) + } +} + // main func main() { @@ -71,16 +120,25 @@ func main() { } appUsage := fmt.Sprintf("command line tool for X(cross) Development System.") appDescription := fmt.Sprintf("%s utility for X(cross) Development System\n", AppName) - /* SEB UPDATE DOC - appDescription += ` - xds-cli configuration is driven either by environment variables or by command line - options or using a config file knowning that the following priority order is used: - 1. use option value (for example use project ID set by --id option), - 2. else use variable 'XDS_xxx' (for example 'XDS_PROJECT_ID' variable) when a - config file is specified with '--config|-c' option, - 3. else use 'XDS_xxx' (for example 'XDS_PROJECT_ID') environment variable. - ` - */ + appDescription += ` + Setting of global options is driven either by environment variables or by command + line options or using a config file knowning that the following priority order is used: + 1. use option value (for example --url option), + 2. else use variable 'XDS_xxx' (for example 'XDS_AGENT_URL' variable) when a + config file is specified with '--config|-c' option, + 3. else use 'XDS_xxx' (for example 'XDS_AGENT_URL') environment variable. + + Examples: + # Get help of 'projects' sub-command + ` + AppName + ` projects --help + + # List all SDKs + ` + AppName + ` sdks ls + + # Add a new project + ` + AppName + ` prj add --label="myProject" --type=cs --path=$HOME/xds-workspace/myProject +` + // Create a new App instance app := cli.NewApp() app.Name = AppName @@ -92,6 +150,10 @@ func main() { app.Metadata["version"] = AppVersion app.Metadata["git-tag"] = AppSubVersion app.Metadata["logger"] = Log + // FIXME: Disable completion for now, because it's not working with options + // (eg. --label) and prevents to complete local path + // (IOW current function only completes command and sub-commands) + app.EnableBashCompletion = false // Create env vars help dynDesc := "\nENVIRONMENT VARIABLES:" @@ -129,10 +191,22 @@ func main() { Value: defaultLogLevel, }, cli.StringFlag{ - Name: "url", + Name: "logfile", + Value: "stderr", + Usage: "filename where logs will be redirected (default stderr)\n\t", + EnvVar: "XDS_LOGFILENAME", + }, + cli.StringFlag{ + Name: "url, u", + EnvVar: "XDS_AGENT_URL", + Value: "localhost:8800", + Usage: "local XDS agent url", + }, + cli.StringFlag{ + Name: "url-server, us", EnvVar: "XDS_SERVER_URL", - Value: "localhost:8000", - Usage: "remote XDS server url", + Value: "", + Usage: "overwrite remote XDS server url (default value set in xds-agent-config.json file)", }, cli.BoolFlag{ Name: "timestamp, ts", @@ -147,13 +221,87 @@ func main() { initCmdProjects(&app.Commands) initCmdSdks(&app.Commands) initCmdExec(&app.Commands) + initCmdTargets(&app.Commands) initCmdMisc(&app.Commands) + // Add --config option to all commands to support --config option either before or after command verb + // IOW support following both syntaxes: + // xds-cli exec --config myPrj.conf ... + // xds-cli --config myPrj.conf exec ... + for i, cmd := range app.Commands { + if len(cmd.Flags) > 0 { + app.Commands[i].Flags = append(cmd.Flags, cli.StringFlag{Hidden: true, Name: "config, c"}) + } + for j, subCmd := range cmd.Subcommands { + app.Commands[i].Subcommands[j].Flags = append(subCmd.Flags, cli.StringFlag{Hidden: true, Name: "config, c"}) + } + } + sort.Sort(cli.FlagsByName(app.Flags)) sort.Sort(cli.CommandsByName(app.Commands)) + // Early and manual processing of --config option in order to set XDS_xxx + // variables before parsing of option by app cli + // 1/ from command line option: "--config myConfig.json" + // 2/ from environment variable XDS_CONFIG + // 3/ $HOME/.xds/cli/cli-config.env file + // 4/ /etc/xds/cli/cli-config.env file + searchIn := make([]string, 0, 4) + for idx, a := range os.Args[1:] { + if a == "-c" || a == "--config" || a == "-config" { + searchIn = append(searchIn, os.Args[idx+2]) + break + } + } + searchIn = append(searchIn, os.Getenv("XDS_CONFIG")) + if usrHome := common.GetUserHome(); usrHome != "" { + searchIn = append(searchIn, path.Join(usrHome, ".xds", "cli", defaultConfigEnvFilename)) + } + searchIn = append(searchIn, path.Join("/etc", "xds", "cli", defaultConfigEnvFilename)) + + // Use the first existing env config file + confFile := "" + for _, p := range searchIn { + if pr, err := common.ResolveEnvVar(p); err == nil { + earlyPrintf("Check if confFile exists : %v", pr) + if common.Exists(pr) { + confFile = pr + break + } + } + } + + // Load config file if requested + if confFile != "" { + earlyPrintf("Used confFile: %v", confFile) + // Load config file variables that will overwrite env variables + err := godotenv.Overload(confFile) + if err != nil { + exitError(1, "Error loading env config file "+confFile) + } + + // Keep confFile settings in a map + EnvConfFileMap, err = godotenv.Read(confFile) + if err != nil { + exitError(1, "Error reading env config file "+confFile) + } + earlyPrintf("EnvConfFileMap: %v", EnvConfFileMap) + } + app.Before = func(ctx *cli.Context) error { var err error + + // Don't init anything when no argument or help option is set + if ctx.NArg() == 0 { + return nil + } + for _, a := range ctx.Args() { + switch a { + case "-h", "--h", "-help", "--help": + return nil + } + } + loglevel := ctx.String("log") // Set logger level and formatter if Log.Level, err = logrus.ParseLevel(loglevel); err != nil { @@ -162,8 +310,20 @@ func main() { } Log.Formatter = &logrus.TextFormatter{} + if ctx.String("logfile") != "stderr" { + logFile, _ := common.ResolveEnvVar(ctx.String("logfile")) + fdL, err := os.OpenFile(logFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) + if err != nil { + msgErr := fmt.Sprintf("Cannot create log file %s", logFile) + return cli.NewExitError(msgErr, 1) + } + Log.Infof("Logging to file: %s", logFile) + Log.Out = fdL + } + Log.Infof("%s version: %s", AppName, app.Version) - // SEB Add again Log.Debugf("Environment: %v", os.Environ()) + earlyDisplay() + Log.Debugf("\nEnvironment: %v\n", os.Environ()) if err = XdsConnInit(ctx); err != nil { // Directly call HandleExitCoder to avoid to print help (ShowAppHelp) @@ -179,6 +339,24 @@ func main() { XdsConnClose() }() + // Start signals monitoring routine + MonitorSignals() + + // Default callback to handle interrupt signal + // Maybe be overwritten by some subcommand (eg. targets commands) + err := OnSignals(func(sig os.Signal) { + Log.Debugf("Send signal %v (from main)", sig) + if IsInterruptSignal(sig) { + err := cli.NewExitError("Interrupted\n", int(syscall.EINTR)) + cli.HandleExitCoder(err) + } + }) + if err != nil { + cli.NewExitError(err.Error(), 1) + return + } + + // Run the cli app app.Run(os.Args) } @@ -187,53 +365,100 @@ func XdsConnInit(ctx *cli.Context) error { var err error // Define HTTP and WS url - baseURL := ctx.String("url") - if !strings.HasPrefix(ctx.String("url"), "http://") { - baseURL = "http://" + ctx.String("url") + agentURL := ctx.String("url") + serverURL := ctx.String("url-server") + + // Allow to only set port number + if match, _ := regexp.MatchString("^([0-9]+)$", agentURL); match { + agentURL = "http://localhost:" + ctx.String("url") + } + if match, _ := regexp.MatchString("^([0-9]+)$", serverURL); match { + serverURL = "http://localhost:" + ctx.String("url-server") + } + // Add http prefix if missing + if agentURL != "" && !strings.HasPrefix(agentURL, "http://") { + agentURL = "http://" + agentURL + } + if serverURL != "" && !strings.HasPrefix(serverURL, "http://") { + serverURL = "http://" + serverURL + } + + lvl := common.HTTPLogLevelWarning + if Log.Level == logrus.DebugLevel { + lvl = common.HTTPLogLevelDebug } // Create HTTP client - Log.Debugln("Connect HTTP client on ", baseURL) + Log.Debugln("Connect HTTP client on ", agentURL) conf := common.HTTPClientConfig{ URLPrefix: "/api/v1", HeaderClientKeyName: "Xds-Agent-Sid", CsrfDisable: true, LogOut: Log.Out, LogPrefix: "XDSAGENT: ", - LogLevel: common.HTTPLogLevelWarning, + LogLevel: lvl, } - HTTPCli, err = common.HTTPNewClient(baseURL, conf) + HTTPCli, err = common.HTTPNewClient(agentURL, conf) if err != nil { errmsg := err.Error() - if m, err := regexp.MatchString("Get http.?://", errmsg); m && err == nil { + m, err := regexp.MatchString("Get http.?://", errmsg) + if (m && err == nil) || strings.Contains(errmsg, "Failed to get device ID") { i := strings.LastIndex(errmsg, ":") - errmsg = "Cannot connection to " + baseURL + errmsg[i:] + newErr := "Cannot connection to " + agentURL + if i > 0 { + newErr += " (" + strings.TrimSpace(errmsg[i+1:]) + ")" + } else { + newErr += " (" + strings.TrimSpace(errmsg) + ")" + } + errmsg = newErr } return cli.NewExitError(errmsg, 1) } HTTPCli.SetLogLevel(ctx.String("loglevel")) + Log.Infoln("HTTP session ID : ", HTTPCli.GetClientID()) // Create io Websocket client - Log.Debugln("Connecting IO.socket client on ", baseURL) - - opts := &socketio_client.Options{ - Transport: "websocket", - Header: make(map[string][]string), - } - opts.Header["XDS-AGENT-SID"] = []string{HTTPCli.GetClientID()} + Log.Debugln("Connecting IO.socket client on ", agentURL) - IOsk, err = socketio_client.NewClient(baseURL, opts) + IOSkClient, err = NewIoSocketClient(agentURL, HTTPCli.GetClientID()) if err != nil { - return cli.NewExitError("IO.socket connection error: "+err.Error(), 1) + return cli.NewExitError(err.Error(), 1) } - IOsk.On("error", func(err error) { + IOSkClient.On("error", func(err error) { fmt.Println("ERROR Websocket: ", err.Error()) }) ctx.App.Metadata["httpCli"] = HTTPCli - ctx.App.Metadata["ioskCli"] = IOsk + ctx.App.Metadata["ioskCli"] = IOSkClient + + // Display version in logs (debug helpers) + ver := xaapiv1.XDSVersion{} + if err := XdsVersionGet(&ver); err != nil { + return cli.NewExitError("ERROR while retrieving XDS version: "+err.Error(), 1) + } + Log.Infof("XDS Agent/Server version: %v", ver) + + // Get current config and update connection to server when needed + xdsConf := xaapiv1.APIConfig{} + if err := XdsConfigGet(&xdsConf); err != nil { + return cli.NewExitError("ERROR while getting XDS config: "+err.Error(), 1) + } + if len(xdsConf.Servers) < 1 { + return cli.NewExitError("No XDS Server connected", 1) + } + svrCfg := xdsConf.Servers[XdsServerIndexGet()] + if (serverURL != "" && svrCfg.URL != serverURL) || !svrCfg.Connected { + Log.Infof("Update XDS Server config: serverURL=%v, svrCfg=%v", serverURL, svrCfg) + if serverURL != "" { + svrCfg.URL = serverURL + } + svrCfg.ConnRetry = 10 + if err := XdsConfigSet(xdsConf); err != nil { + return cli.NewExitError("ERROR while updating XDS server URL: "+err.Error(), 1) + } + } return nil }