github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/cmd/state-installer/installer.go (about) 1 package main 2 3 import ( 4 "errors" 5 "os" 6 "path/filepath" 7 "strings" 8 9 svcApp "github.com/ActiveState/cli/cmd/state-svc/app" 10 svcAutostart "github.com/ActiveState/cli/cmd/state-svc/autostart" 11 "github.com/ActiveState/cli/internal/analytics" 12 "github.com/ActiveState/cli/internal/config" 13 "github.com/ActiveState/cli/internal/constants" 14 "github.com/ActiveState/cli/internal/errs" 15 "github.com/ActiveState/cli/internal/fileutils" 16 "github.com/ActiveState/cli/internal/installation" 17 "github.com/ActiveState/cli/internal/installmgr" 18 "github.com/ActiveState/cli/internal/legacytray" 19 "github.com/ActiveState/cli/internal/locale" 20 "github.com/ActiveState/cli/internal/logging" 21 "github.com/ActiveState/cli/internal/multilog" 22 "github.com/ActiveState/cli/internal/osutils" 23 "github.com/ActiveState/cli/internal/osutils/autostart" 24 "github.com/ActiveState/cli/internal/output" 25 "github.com/ActiveState/cli/internal/prompt" 26 "github.com/ActiveState/cli/internal/rtutils/ptr" 27 "github.com/ActiveState/cli/internal/subshell" 28 "github.com/ActiveState/cli/internal/subshell/sscommon" 29 "github.com/ActiveState/cli/internal/updater" 30 ) 31 32 type Installer struct { 33 out output.Outputer 34 cfg *config.Instance 35 an analytics.Dispatcher 36 payloadPath string 37 *Params 38 } 39 40 func NewInstaller(cfg *config.Instance, out output.Outputer, an analytics.Dispatcher, payloadPath string, params *Params) (*Installer, error) { 41 i := &Installer{cfg: cfg, out: out, an: an, payloadPath: payloadPath, Params: params} 42 if err := i.sanitizeInput(); err != nil { 43 return nil, errs.Wrap(err, "Could not sanitize input") 44 } 45 46 logging.Debug("Instantiated installer with source dir: %s, target dir: %s", i.payloadPath, i.path) 47 48 return i, nil 49 } 50 51 func (i *Installer) Install() (rerr error) { 52 isAdmin, err := osutils.IsAdmin() 53 if err != nil { 54 return errs.Wrap(err, "Could not determine if running as Windows administrator") 55 } 56 if isAdmin && !i.Params.force && !i.Params.isUpdate && !i.Params.nonInteractive { 57 prompter := prompt.New(true, i.an) 58 confirm, err := prompter.Confirm("", locale.T("installer_prompt_is_admin"), ptr.To(false)) 59 if err != nil { 60 return errs.Wrap(err, "Unable to confirm") 61 } 62 if !confirm { 63 return locale.NewInputError("installer_aborted", "Installation aborted by the user") 64 } 65 } 66 67 // Store update tag 68 if i.updateTag != "" { 69 if err := i.cfg.Set(updater.CfgUpdateTag, i.updateTag); err != nil { 70 return errs.Wrap(err, "Failed to set update tag") 71 } 72 } 73 74 // Stop any running processes that might interfere 75 if err := installmgr.StopRunning(i.path); err != nil { 76 return errs.Wrap(err, "Failed to stop running services") 77 } 78 79 // Detect if existing installation needs to be cleaned 80 err = detectCorruptedInstallDir(i.path) 81 if errors.Is(err, errCorruptedInstall) { 82 err = i.sanitizeInstallPath() 83 if err != nil { 84 return locale.WrapError(err, "err_update_corrupt_install") 85 } 86 } else if err != nil { 87 return locale.WrapInputError(err, "err_update_corrupt_install", constants.DocumentationURL) 88 } 89 90 err = legacytray.DetectAndRemove(i.path, i.cfg) 91 if err != nil { 92 multilog.Error("Unable to detect and/or remove legacy tray. Will try again next update. Error: %v", err) 93 } 94 95 // Create target dir 96 if err := fileutils.MkdirUnlessExists(i.path); err != nil { 97 return errs.Wrap(err, "Could not create target directory: %s", i.path) 98 } 99 100 // Prepare bin targets is an OS specific method that will ensure we don't run into conflicts while installing 101 if err := i.PrepareBinTargets(); err != nil { 102 return errs.Wrap(err, "Could not prepare for installation") 103 } 104 105 // Copy all the files except for the current executable 106 if err := fileutils.CopyAndRenameFiles(i.payloadPath, i.path, filepath.Base(osutils.Executable())); err != nil { 107 if osutils.IsAccessDeniedError(err) { 108 // If we got to this point, we could not copy and rename over existing files. 109 // This is a permission issue. (We have an installer test for copying and renaming over a file 110 // in use, which does not raise an error.) 111 return locale.WrapExternalError(err, "err_update_access_denied", "", errs.JoinMessage(err)) 112 } 113 return errs.Wrap(err, "Failed to copy installation files to dir %s. Error received: %s", i.path, errs.JoinMessage(err)) 114 } 115 116 // Set up the environment 117 binDir := filepath.Join(i.path, installation.BinDirName) 118 119 // Install the state service as an app if necessary 120 if err := i.installSvcApp(binDir); err != nil { 121 return errs.Wrap(err, "Installation of service app failed.") 122 } 123 124 // Configure available shells 125 shell := subshell.New(i.cfg) 126 err = subshell.ConfigureAvailableShells(shell, i.cfg, map[string]string{"PATH": binDir}, sscommon.InstallID, !isAdmin) 127 if err != nil { 128 return errs.Wrap(err, "Could not configure available shells") 129 } 130 131 err = installation.SaveContext(&installation.Context{InstalledAsAdmin: isAdmin}) 132 if err != nil { 133 return errs.Wrap(err, "Failed to set current privilege level in config") 134 } 135 136 stateExec, err := installation.StateExecFromDir(binDir) 137 if err != nil { 138 return locale.WrapError(err, "err_state_exec") 139 } 140 141 // Run state _prepare after updates to facilitate anything the new version of the state tool might need to set up 142 // Yes this is awkward, followup story here: https://www.pivotaltracker.com/story/show/176507898 143 if stdout, stderr, err := osutils.ExecSimple(stateExec, []string{"_prepare"}, []string{}); err != nil { 144 multilog.Error("_prepare failed after update: %v\n\nstdout: %s\n\nstderr: %s", err, stdout, stderr) 145 } 146 147 logging.Debug("Installation was successful") 148 149 return nil 150 } 151 152 func (i *Installer) InstallPath() string { 153 return i.path 154 } 155 156 // sanitizeInput cleans up the input and inserts fallback values 157 func (i *Installer) sanitizeInput() error { 158 if tag, ok := os.LookupEnv(constants.UpdateTagEnvVarName); ok { 159 i.updateTag = tag 160 } 161 162 var err error 163 if i.path, err = resolveInstallPath(i.path); err != nil { 164 return errs.Wrap(err, "Could not resolve installation path") 165 } 166 167 return nil 168 } 169 170 func (i *Installer) installSvcApp(binDir string) error { 171 app, err := svcApp.NewFromDir(binDir) 172 if err != nil { 173 return errs.Wrap(err, "Could not create app") 174 } 175 176 err = app.Install() 177 if err != nil { 178 return errs.Wrap(err, "Could not install app") 179 } 180 181 if err = autostart.Upgrade(app.Path(), svcAutostart.Options); err != nil { 182 return errs.Wrap(err, "Failed to upgrade autostart for service app.") 183 } 184 185 if err = autostart.Enable(app.Path(), svcAutostart.Options); err != nil { 186 return errs.Wrap(err, "Failed to enable autostart for service app.") 187 } 188 189 return nil 190 } 191 192 var errCorruptedInstall = errs.New("Corrupted install") 193 194 // detectCorruptedInstallDir will return an error if it detects that the given install path is not a proper 195 // State Tool installation path. This mainly covers cases where we are working off of a legacy install of the State 196 // Tool or cases where the uninstall was not completed properly. 197 func detectCorruptedInstallDir(path string) error { 198 if !fileutils.TargetExists(path) { 199 return nil 200 } 201 202 isEmpty, err := fileutils.IsEmptyDir(path) 203 if err != nil { 204 return errs.Wrap(err, "Could not check if install dir is empty") 205 } 206 if isEmpty { 207 return nil 208 } 209 210 // Detect if the install dir has files in it 211 files, err := os.ReadDir(path) 212 if err != nil { 213 return errs.Wrap(err, "Could not read directory: %s", path) 214 } 215 216 // Executable files should be in bin dir, not root dir 217 for _, file := range files { 218 if isStateExecutable(strings.ToLower(file.Name())) { 219 return errs.Wrap(errCorruptedInstall, "Install directory should only contain dirs: %s", path) 220 } 221 } 222 223 return nil 224 } 225 226 func isStateExecutable(name string) bool { 227 if name == constants.StateCmd+osutils.ExeExtension || name == constants.StateSvcCmd+osutils.ExeExtension { 228 return true 229 } 230 return false 231 } 232 233 func installedOnPath(installRoot, channel string) (bool, string, error) { 234 if !fileutils.DirExists(installRoot) { 235 return false, "", nil 236 } 237 238 // This is not using appinfo on purpose because we want to deal with legacy installation formats, which appinfo does not 239 stateCmd := constants.StateCmd + osutils.ExeExtension 240 241 // Check for state.exe in channel, root and bin dir 242 // This is to handle older state tool versions that gave incompatible input paths 243 candidates := []string{ 244 filepath.Join(installRoot, channel, installation.BinDirName, stateCmd), 245 filepath.Join(installRoot, channel, stateCmd), 246 filepath.Join(installRoot, installation.BinDirName, stateCmd), 247 filepath.Join(installRoot, stateCmd), 248 } 249 for _, candidate := range candidates { 250 if fileutils.TargetExists(candidate) { 251 return true, installRoot, nil 252 } 253 } 254 255 return false, installRoot, nil 256 } 257 258 // installationIsOnPATH returns whether the installed State Tool root is on $PATH or %PATH%. 259 func installationIsOnPATH(installRoot string) bool { 260 // This is not using appinfo on purpose because we want to deal with legacy installation formats, which appinfo does not 261 stateCmd := constants.StateCmd + osutils.ExeExtension 262 263 exeOnPATH := osutils.FindExeOnPATH(stateCmd) 264 if exeOnPATH == "" { 265 return false 266 } 267 onPATH, err := fileutils.PathContainsParent(exeOnPATH, installRoot) 268 if err != nil { 269 multilog.Error("Unable to determine if state tool on PATH is in path to install to: %v", err) 270 } 271 return onPATH 272 }