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  }