github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/cmd/state/autoupdate.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/thoas/go-funk"
    11  
    12  	"github.com/ActiveState/cli/internal/analytics"
    13  	anaConst "github.com/ActiveState/cli/internal/analytics/constants"
    14  	"github.com/ActiveState/cli/internal/analytics/dimensions"
    15  	"github.com/ActiveState/cli/internal/condition"
    16  	"github.com/ActiveState/cli/internal/config"
    17  	"github.com/ActiveState/cli/internal/constants"
    18  	"github.com/ActiveState/cli/internal/errs"
    19  	"github.com/ActiveState/cli/internal/installation"
    20  	"github.com/ActiveState/cli/internal/locale"
    21  	"github.com/ActiveState/cli/internal/logging"
    22  	configMediator "github.com/ActiveState/cli/internal/mediators/config"
    23  	"github.com/ActiveState/cli/internal/multilog"
    24  	"github.com/ActiveState/cli/internal/osutils"
    25  	"github.com/ActiveState/cli/internal/output"
    26  	"github.com/ActiveState/cli/internal/profile"
    27  	"github.com/ActiveState/cli/internal/rtutils/ptr"
    28  	"github.com/ActiveState/cli/internal/updater"
    29  	"github.com/ActiveState/cli/pkg/platform/model"
    30  )
    31  
    32  type ErrStateExe struct{ *locale.LocalizedError }
    33  
    34  type ErrExecuteRelaunch struct{ *errs.WrapperError }
    35  
    36  func init() {
    37  	configMediator.RegisterOption(constants.AutoUpdateConfigKey, configMediator.Bool, !condition.IsLTS())
    38  }
    39  
    40  func autoUpdate(svc *model.SvcModel, args []string, cfg *config.Instance, an analytics.Dispatcher, out output.Outputer) (bool, error) {
    41  	profile.Measure("autoUpdate", time.Now())
    42  
    43  	if !shouldRunAutoUpdate(args, cfg, an) {
    44  		return false, nil
    45  	}
    46  
    47  	// Check for available update
    48  	upd, err := svc.CheckUpdate(context.Background(), constants.ChannelName, "")
    49  	if err != nil {
    50  		return false, errs.Wrap(err, "Failed to check for update")
    51  	}
    52  
    53  	avUpdate := updater.NewAvailableUpdate(upd.Channel, upd.Version, upd.Platform, upd.Path, upd.Sha256, "")
    54  	up := updater.NewUpdateInstaller(an, avUpdate)
    55  	if !up.ShouldInstall() {
    56  		logging.Debug("Update is not needed")
    57  		return false, nil
    58  	}
    59  
    60  	if !isEnabled(cfg) {
    61  		logging.Debug("Not performing autoupdates because user turned off autoupdates.")
    62  		an.EventWithLabel(anaConst.CatUpdates, anaConst.ActShouldUpdate, anaConst.UpdateLabelDisabledConfig)
    63  		out.Notice(output.Title(locale.T("update_available_header")))
    64  		out.Notice(locale.Tr("update_available", constants.Version, avUpdate.Version))
    65  		return false, nil
    66  	}
    67  
    68  	out.Notice(output.Title(locale.Tl("auto_update_title", "Auto Update")))
    69  	out.Notice(locale.Tr("auto_update_to_version", constants.Version, avUpdate.Version))
    70  
    71  	logging.Debug("Auto updating to %s", avUpdate.Version)
    72  
    73  	err = up.InstallBlocking("")
    74  	if err != nil {
    75  		if errs.Matches(err, &updater.ErrorInProgress{}) {
    76  			return false, nil // ignore
    77  		}
    78  		if os.IsPermission(err) {
    79  			return false, locale.WrapExternalError(err, locale.Tr("auto_update_permission_err", constants.DocumentationURL, errs.JoinMessage(err)))
    80  		}
    81  		return false, locale.WrapError(err, locale.T("auto_update_failed"))
    82  	}
    83  
    84  	out.Notice(locale.Tr("auto_update_relaunch"))
    85  	out.Notice("") // Ensure output doesn't stick to our messaging
    86  
    87  	code, err := relaunch(args)
    88  	if err != nil {
    89  		var msg string
    90  		if errs.Matches(err, &ErrStateExe{}) {
    91  			msg = anaConst.UpdateErrorExecutable
    92  		} else if errs.Matches(err, &ErrExecuteRelaunch{}) {
    93  			msg = anaConst.UpdateErrorRelaunch
    94  		}
    95  		an.EventWithLabel(anaConst.CatUpdates, anaConst.ActUpdateRelaunch, anaConst.UpdateLabelFailed, &dimensions.Values{
    96  			TargetVersion: ptr.To(avUpdate.Version),
    97  			Error:         ptr.To(msg),
    98  		})
    99  		return true, errs.Silence(errs.WrapExitCode(err, code))
   100  	}
   101  
   102  	an.EventWithLabel(anaConst.CatUpdates, anaConst.ActUpdateRelaunch, anaConst.UpdateLabelSuccess, &dimensions.Values{
   103  		TargetVersion: ptr.To(avUpdate.Version),
   104  	})
   105  	return true, nil
   106  }
   107  
   108  func isEnabled(cfg *config.Instance) bool {
   109  	return cfg.GetBool(constants.AutoUpdateConfigKey)
   110  }
   111  
   112  func shouldRunAutoUpdate(args []string, cfg *config.Instance, an analytics.Dispatcher) bool {
   113  	shouldUpdate := true
   114  	label := anaConst.UpdateLabelTrue
   115  
   116  	switch {
   117  	// In a forward
   118  	case os.Getenv(constants.ForwardedStateEnvVarName) == "true":
   119  		logging.Debug("Not running auto updates because we're in a forward")
   120  		shouldUpdate = false
   121  		label = anaConst.UpdateLabelForward
   122  
   123  	// Forced enabled (breaks out of switch)
   124  	case os.Getenv(constants.TestAutoUpdateEnvVarName) == "true":
   125  		logging.Debug("Forcing auto update as it was forced by env var")
   126  		shouldUpdate = true
   127  		label = anaConst.UpdateLabelTrue
   128  
   129  	// In unit test
   130  	case condition.InUnitTest():
   131  		logging.Debug("Not running auto updates in unit tests")
   132  		shouldUpdate = false
   133  		label = anaConst.UpdateLabelUnitTest
   134  
   135  	// Running command that could conflict
   136  	case funk.Contains(args, "update") || funk.Contains(args, "export") || funk.Contains(args, "_prepare") || funk.Contains(args, "clean"):
   137  		logging.Debug("Not running auto updates because current command might conflict")
   138  		shouldUpdate = false
   139  		label = anaConst.UpdateLabelConflict
   140  
   141  	// Updates are disabled
   142  	case strings.ToLower(os.Getenv(constants.DisableUpdates)) == "true":
   143  		logging.Debug("Not running auto updates because updates are disabled by env var")
   144  		shouldUpdate = false
   145  		label = anaConst.UpdateLabelDisabledEnv
   146  
   147  	// We're on CI
   148  	case (condition.OnCI()) && strings.ToLower(os.Getenv(constants.DisableUpdates)) != "false":
   149  		logging.Debug("Not running auto updates because we're on CI")
   150  		shouldUpdate = false
   151  		label = anaConst.UpdateLabelCI
   152  
   153  	// Exe is not old enough
   154  	case isFreshInstall():
   155  		logging.Debug("Not running auto updates because we just freshly installed")
   156  		shouldUpdate = false
   157  		label = anaConst.UpdateLabelFreshInstall
   158  
   159  	case cfg.GetString(updater.CfgKeyInstallVersion) != "":
   160  		logging.Debug("Not running auto update because a specific version had been installed on purpose")
   161  		shouldUpdate = false
   162  		label = anaConst.UpdateLabelLocked
   163  	}
   164  
   165  	an.EventWithLabel(anaConst.CatUpdates, anaConst.ActShouldUpdate, label)
   166  	return shouldUpdate
   167  }
   168  
   169  // When an update was found and applied, re-launch the update with the current
   170  // arguments and wait for return before exitting.
   171  func relaunch(args []string) (int, error) {
   172  	exec, err := installation.StateExec()
   173  	if err != nil {
   174  		return -1, &ErrStateExe{locale.WrapError(err, "err_state_exec")}
   175  	}
   176  
   177  	code, _, err := osutils.ExecuteAndPipeStd(exec, args[1:], []string{fmt.Sprintf("%s=true", constants.ForwardedStateEnvVarName)})
   178  	if err != nil {
   179  		return code, &ErrExecuteRelaunch{errs.Wrap(err, "Forwarded command after auto-updating failed. Exit code: %d", code)}
   180  	}
   181  
   182  	return code, nil
   183  }
   184  
   185  func isFreshInstall() bool {
   186  	exe := osutils.Executable()
   187  	stat, err := os.Stat(exe)
   188  	if err != nil {
   189  		multilog.Error("Could not stat file: %s, error: %v", exe, err)
   190  		return true
   191  	}
   192  	diff := time.Since(stat.ModTime())
   193  	return diff < 24*time.Hour
   194  }