github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/cmd/state/main.go (about) 1 package main 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "runtime/debug" 8 "strings" 9 "time" 10 11 "github.com/ActiveState/cli/cmd/state/internal/cmdtree" 12 "github.com/ActiveState/cli/cmd/state/internal/cmdtree/exechandlers/messenger" 13 anAsync "github.com/ActiveState/cli/internal/analytics/client/async" 14 anaConst "github.com/ActiveState/cli/internal/analytics/constants" 15 "github.com/ActiveState/cli/internal/captain" 16 "github.com/ActiveState/cli/internal/config" 17 "github.com/ActiveState/cli/internal/constants" 18 "github.com/ActiveState/cli/internal/constraints" 19 "github.com/ActiveState/cli/internal/errs" 20 "github.com/ActiveState/cli/internal/events" 21 "github.com/ActiveState/cli/internal/installation" 22 "github.com/ActiveState/cli/internal/installation/storage" 23 "github.com/ActiveState/cli/internal/locale" 24 "github.com/ActiveState/cli/internal/logging" 25 configMediator "github.com/ActiveState/cli/internal/mediators/config" 26 "github.com/ActiveState/cli/internal/migrator" 27 "github.com/ActiveState/cli/internal/multilog" 28 "github.com/ActiveState/cli/internal/output" 29 "github.com/ActiveState/cli/internal/primer" 30 "github.com/ActiveState/cli/internal/profile" 31 "github.com/ActiveState/cli/internal/prompt" 32 _ "github.com/ActiveState/cli/internal/prompt" // Sets up survey defaults 33 "github.com/ActiveState/cli/internal/rollbar" 34 "github.com/ActiveState/cli/internal/rtutils" 35 "github.com/ActiveState/cli/internal/runbits/errors" 36 "github.com/ActiveState/cli/internal/runbits/panics" 37 "github.com/ActiveState/cli/internal/subshell" 38 "github.com/ActiveState/cli/internal/svcctl" 39 secretsapi "github.com/ActiveState/cli/pkg/platform/api/secrets" 40 "github.com/ActiveState/cli/pkg/platform/authentication" 41 "github.com/ActiveState/cli/pkg/platform/model" 42 "github.com/ActiveState/cli/pkg/project" 43 "github.com/ActiveState/cli/pkg/projectfile" 44 ) 45 46 func main() { 47 startTime := time.Now() 48 49 var exitCode int 50 // Set up logging 51 rollbar.SetupRollbar(constants.StateToolRollbarToken) 52 53 // We have to disable mouse trap as without it the state:// protocol cannot work 54 captain.DisableMousetrap() 55 56 var cfg *config.Instance 57 defer func() { 58 // Handle panics gracefully, and ensure that we exit with non-zero code 59 if panics.HandlePanics(recover(), debug.Stack()) { 60 exitCode = 1 61 } 62 63 // ensure rollbar messages are called 64 if err := events.WaitForEvents(5*time.Second, rollbar.Wait, authentication.LegacyClose, logging.Close); err != nil { 65 logging.Warning("Failed waiting for events: %v", err) 66 } 67 68 if cfg != nil { 69 events.Close("config", cfg.Close) 70 } 71 72 profile.Measure("main", startTime) 73 74 // exit with exitCode 75 os.Exit(exitCode) 76 }() 77 78 var err error 79 cfg, err = config.New() 80 if err != nil { 81 multilog.Critical("Could not initialize config: %v", errs.JoinMessage(err)) 82 fmt.Fprintf(os.Stderr, "Could not load config, if this problem persists please reinstall the State Tool. Error: %s\n", errs.JoinMessage(err)) 83 exitCode = 1 84 return 85 } 86 rollbar.SetConfig(cfg) 87 88 // Configuration options 89 // This should only be used if the config option is not exclusive to one package. 90 configMediator.RegisterOption(constants.OptinBuildscriptsConfig, configMediator.Bool, false) 91 92 // Set up our output formatter/writer 93 outFlags := parseOutputFlags(os.Args) 94 shellName, _ := subshell.DetectShell(cfg) 95 out, err := initOutput(outFlags, "", shellName) 96 if err != nil { 97 multilog.Critical("Could not initialize outputer: %s", errs.JoinMessage(err)) 98 os.Stderr.WriteString(locale.Tr("err_main_outputer", err.Error())) 99 exitCode = 1 100 return 101 } 102 103 // Set up our legacy outputer 104 setPrinterColors(outFlags) 105 106 isInteractive := strings.ToLower(os.Getenv(constants.NonInteractiveEnvVarName)) != "true" && out.Config().Interactive 107 // Run our main command logic, which is logic that defers to the error handling logic below 108 err = run(os.Args, isInteractive, cfg, out) 109 if err != nil { 110 exitCode, err = errors.ParseUserFacing(err) 111 if err != nil { 112 out.Error(err) 113 } 114 } 115 } 116 117 func run(args []string, isInteractive bool, cfg *config.Instance, out output.Outputer) (rerr error) { 118 defer profile.Measure("main:run", time.Now()) 119 120 // Set up profiling 121 if os.Getenv(constants.CPUProfileEnvVarName) != "" { 122 cleanup, err := profile.CPU() 123 if err != nil { 124 return err 125 } 126 defer rtutils.Closer(cleanup, &rerr) 127 } 128 129 logging.CurrentHandler().SetVerbose(os.Getenv("VERBOSE") != "" || argsHaveVerbose(args)) 130 131 logging.Debug("ConfigPath: %s", cfg.ConfigPath()) 132 logging.Debug("CachePath: %s", storage.CachePath()) 133 134 svcExec, err := installation.ServiceExec() 135 if err != nil { 136 return errs.Wrap(err, "Could not get service info") 137 } 138 139 ipcClient := svcctl.NewDefaultIPCClient() 140 argText := strings.Join(args, " ") 141 svcPort, err := svcctl.EnsureExecStartedAndLocateHTTP(ipcClient, svcExec, argText, out) 142 if err != nil { 143 return locale.WrapError(err, "start_svc_failed", "Failed to start state-svc at state tool invocation") 144 } 145 146 svcmodel := model.NewSvcModel(svcPort) 147 148 // Amend Rollbar data to also send the state-svc log tail. This cannot be done inside the rollbar 149 // package itself because importing pkg/platform/model creates an import cycle. 150 rollbar.AddLogDataAmender(func(logData string) string { 151 ctx, cancel := context.WithTimeout(context.Background(), model.SvcTimeoutMinimal) 152 defer cancel() 153 svcLogData, err := svcmodel.FetchLogTail(ctx) 154 if err != nil { 155 svcLogData = fmt.Sprintf("Could not fetch state-svc log: %v", err) 156 } 157 logData += "\nstate-svc log:\n" 158 if len(svcLogData) == logging.TailSize { 159 logData += "<truncated>\n" 160 } 161 logData += svcLogData 162 return logData 163 }) 164 165 auth := authentication.New(cfg) 166 defer events.Close("auth", auth.Close) 167 168 if err := auth.Sync(); err != nil { 169 logging.Warning("Could not sync authenticated state: %s", errs.JoinMessage(err)) 170 } 171 172 projectfile.RegisterMigrator(migrator.NewMigrator(auth, cfg)) 173 174 // Retrieve project file 175 pjPath, err := projectfile.GetProjectFilePath() 176 if err != nil && errs.Matches(err, &projectfile.ErrorNoProjectFromEnv{}) { 177 // Fail if we are meant to inherit the projectfile from the environment, but the file doesn't exist 178 return err 179 } 180 181 // Set up project (if we have a valid path) 182 var pj *project.Project 183 if pjPath != "" { 184 pjf, err := projectfile.FromPath(pjPath) 185 if err != nil { 186 return err 187 } 188 pj, err = project.New(pjf, out) 189 if err != nil { 190 return err 191 } 192 } 193 194 pjNamespace := "" 195 if pj != nil { 196 pjNamespace = pj.Namespace().String() 197 } 198 199 an := anAsync.New(anaConst.SrcStateTool, svcmodel, cfg, auth, out, pjNamespace) 200 defer func() { 201 if err := events.WaitForEvents(time.Second, an.Wait); err != nil { 202 logging.Warning("Failed waiting for events: %v", err) 203 } 204 }() 205 206 // Set up prompter 207 prompter := prompt.New(isInteractive, an) 208 209 // Set up conditional, which accesses a lot of primer data 210 sshell := subshell.New(cfg) 211 212 conditional := constraints.NewPrimeConditional(auth, pj, sshell.Shell()) 213 project.RegisterConditional(conditional) 214 if err := project.RegisterExpander("mixin", project.NewMixin(auth).Expander); err != nil { 215 logging.Debug("Could not register mixin expander: %v", err) 216 } 217 218 if err := project.RegisterExpander("secrets", project.NewSecretPromptingExpander(secretsapi.Get(auth), prompter, cfg, auth)); err != nil { 219 logging.Debug("Could not register secrets expander: %v", err) 220 } 221 222 // Run the actual command 223 cmds := cmdtree.New(primer.New(pj, out, auth, prompter, sshell, conditional, cfg, ipcClient, svcmodel, an), args...) 224 225 childCmd, err := cmds.Command().Find(args[1:]) 226 if err != nil { 227 logging.Debug("Could not find child command, error: %v", err) 228 } 229 230 msger := messenger.New(out, svcmodel) 231 cmds.OnExecStart(msger.OnExecStart) 232 cmds.OnExecStop(msger.OnExecStop) 233 234 if childCmd != nil && !childCmd.SkipChecks() && !out.Type().IsStructured() { 235 // Auto update to latest state tool version 236 if updated, err := autoUpdate(svcmodel, args, cfg, an, out); err == nil && updated { 237 return nil // command will be run by updated exe 238 } else if err != nil { 239 multilog.Error("Failed to autoupdate: %v", err) 240 } 241 242 if childCmd.Name() != "update" && pj != nil && pj.IsLocked() { 243 if (pj.Version() != "" && pj.Version() != constants.Version) || 244 (pj.Channel() != "" && pj.Channel() != constants.ChannelName) { 245 return errs.AddTips( 246 locale.NewInputError("lock_version_mismatch", "", pj.Source().Lock, constants.ChannelName, constants.Version), 247 locale.Tr("lock_update_legacy_version", constants.DocumentationURLLocking), 248 locale.T("lock_update_lock"), 249 ) 250 } 251 } 252 } 253 254 err = cmds.Execute(args[1:]) 255 if err != nil && !errs.IsSilent(err) { 256 cmdName := "" 257 if childCmd != nil { 258 cmdName = childCmd.JoinedSubCommandNames() + " " 259 } 260 if !out.Type().IsStructured() { 261 err = errs.AddTips(err, locale.Tl("err_tip_run_help", "Run → '[ACTIONABLE]state {{.V0}}--help[/RESET]' for general help", cmdName)) 262 } 263 errors.ReportError(err, cmds.Command(), an) 264 } 265 266 return err 267 } 268 269 func argsHaveVerbose(args []string) bool { 270 var isRunOrExec bool 271 nextArg := 0 272 273 for i, arg := range args { 274 if arg == "run" || arg == "exec" { 275 isRunOrExec = true 276 nextArg = i + 1 277 } 278 279 // Skip looking for verbose args after --, eg. for `state shim -- perl -v` 280 if arg == "--" { 281 return false 282 } 283 if (arg == "--verbose" || arg == "-v") && (!isRunOrExec || i == nextArg) { 284 return true 285 } 286 } 287 return false 288 }