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 }