github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/runners/clean/run_win.go (about) 1 //go:build windows 2 // +build windows 3 4 package clean 5 6 import ( 7 "errors" 8 "fmt" 9 "os" 10 "path/filepath" 11 12 svcApp "github.com/ActiveState/cli/cmd/state-svc/app" 13 "github.com/ActiveState/cli/internal/assets" 14 "github.com/ActiveState/cli/internal/config" 15 "github.com/ActiveState/cli/internal/errs" 16 "github.com/ActiveState/cli/internal/installation" 17 "github.com/ActiveState/cli/internal/installation/storage" 18 "github.com/ActiveState/cli/internal/language" 19 "github.com/ActiveState/cli/internal/legacytray" 20 "github.com/ActiveState/cli/internal/locale" 21 "github.com/ActiveState/cli/internal/logging" 22 "github.com/ActiveState/cli/internal/osutils" 23 "github.com/ActiveState/cli/internal/output" 24 "github.com/ActiveState/cli/internal/rtutils/ptr" 25 "github.com/ActiveState/cli/internal/scriptfile" 26 ) 27 28 func (u *Uninstall) runUninstall(params *UninstallParams) error { 29 // we aggregate installation errors, such that we can display all installation problems in the end 30 // TODO: This behavior should be replaced with a proper rollback mechanism https://www.pivotaltracker.com/story/show/178134918 31 var aggErr error 32 logFile, err := os.CreateTemp("", "state-clean-uninstall") 33 if err != nil { 34 logging.Error("Could not create temporary log file: %s", errs.JoinMessage(err)) 35 aggErr = locale.WrapError(aggErr, "err_clean_logfile", "Could not create temporary log file") 36 } 37 38 stateExec, err := installation.StateExec() 39 if err != nil { 40 logging.Debug("Could not get State Tool executable: %s", errs.JoinMessage(err)) 41 aggErr = locale.WrapError(aggErr, "err_state_exec") 42 } 43 44 err = removeInstall(logFile.Name(), params, u.cfg) 45 if err != nil { 46 logging.Debug("Could not remove installation: %s", errs.JoinMessage(err)) 47 aggErr = locale.WrapError(aggErr, "uninstall_remove_executables_err", "Failed to remove all State Tool files in installation directory {{.V0}}", filepath.Dir(stateExec)) 48 } 49 50 err = removeApp() 51 if err != nil { 52 logging.Debug("Could not remove app: %s", errs.JoinMessage(err)) 53 aggErr = locale.WrapError(aggErr, "uninstall_remove_app_err", "Failed to remove service application") 54 } 55 56 if params.All { 57 err = removeCache(storage.CachePath()) 58 if err != nil { 59 logging.Debug("Could not remove cache at %s: %s", storage.CachePath(), errs.JoinMessage(err)) 60 aggErr = locale.WrapError(aggErr, "uninstall_remove_cache_err", "Failed to remove cache directory {{.V0}}.", storage.CachePath()) 61 } 62 } 63 64 err = undoPrepare() 65 if err != nil { 66 logging.Debug("Could not undo prepare: %s", errs.JoinMessage(err)) 67 aggErr = locale.WrapError(aggErr, "uninstall_prepare_err", "Failed to undo some installation steps.") 68 } 69 70 err = removeEnvPaths(u.cfg) 71 if err != nil { 72 logging.Debug("Could not remove environment paths: %s", errs.JoinMessage(err)) 73 aggErr = locale.WrapError(aggErr, "uninstall_remove_paths_err", "Failed to remove PATH entries from environment") 74 } 75 76 if aggErr != nil { 77 return aggErr 78 } 79 80 u.out.Notice(locale.Tr("clean_message_windows", logFile.Name())) 81 if params.Prompt { 82 u.out.Print(locale.Tl("clean_uninstall_confirm_exit", "Press enter to exit.")) 83 fmt.Scanln(ptr.To("")) // Wait for input from user 84 } 85 return nil 86 } 87 88 func removeConfig(configPath string, out output.Outputer) error { 89 logFile, err := os.CreateTemp("", "state-clean-config") 90 if err != nil { 91 return locale.WrapError(err, "err_clean_logfile", "Could not create temporary log file") 92 } 93 94 out.Notice(locale.Tr("clean_config_message_windows", logFile.Name())) 95 return removePaths(logFile.Name(), configPath) 96 } 97 98 func removeInstall(logFile string, params *UninstallParams, cfg *config.Instance) error { 99 svcExec, err := installation.ServiceExec() 100 if err != nil { 101 return locale.WrapError(err, "err_service_exec") 102 } 103 104 err = legacytray.DetectAndRemove(filepath.Dir(svcExec), cfg) 105 if err != nil { 106 return locale.WrapError(err, "err_remove_legacy_tray", "Could not remove legacy tray application") 107 } 108 109 err = os.Remove(svcExec) 110 if err != nil && !errors.Is(err, os.ErrNotExist) { 111 return locale.WrapError(err, "uninstall_rm_exec", "Could not remove executable: {{.V0}}. Error: {{.V1}}.", svcExec, err.Error()) 112 } 113 114 stateExec, err := installation.StateExec() 115 if err != nil { 116 return locale.WrapError(err, "err_state_exec") 117 } 118 119 // Schedule removal of the entire install directory. 120 // This is because Windows often thinks the installation.InstallDirMarker and 121 // constants.StateInstallerCmd files are still in use. 122 installDir, err := installation.InstallPathFromExecPath() 123 if err != nil { 124 return errs.Wrap(err, "Could not get installation path") 125 } 126 paths := []string{stateExec, installDir} 127 if params.All { 128 paths = append(paths, cfg.ConfigPath()) // also remove the config directory 129 } 130 // If the transitional state tool path is known, we remove it. This is done in the background, because the transitional State Tool can be the initiator of the uninstall request 131 if transitionalStateTool := cfg.GetString(installation.CfgTransitionalStateToolPath); transitionalStateTool != "" { 132 paths = append(paths, transitionalStateTool) 133 } 134 135 return removePaths(logFile, paths...) 136 } 137 138 func removePaths(logFile string, paths ...string) error { 139 logging.Debug("Removing paths: %v", paths) 140 scriptName := "removePaths" 141 scriptBlock, err := assets.ReadFileBytes(fmt.Sprintf("scripts/%s.bat", scriptName)) 142 if err != nil { 143 return err 144 } 145 sf, err := scriptfile.New(language.Batch, scriptName, string(scriptBlock)) 146 if err != nil { 147 return locale.WrapError(err, "err_clean_script", "Could not create new scriptfile") 148 } 149 150 exe, err := os.Executable() 151 if err != nil { 152 return locale.WrapError(err, "err_clean_executable", "Could not get executable name") 153 } 154 155 args := []string{"/C", sf.Filename(), logFile, fmt.Sprintf("%d", os.Getpid()), filepath.Base(exe)} 156 args = append(args, paths...) 157 158 _, err = osutils.ExecuteAndForget("cmd.exe", args) 159 if err != nil { 160 return locale.WrapError(err, "err_clean_start", "Could not start remove direcotry script") 161 } 162 163 return nil 164 } 165 166 func removeApp() error { 167 svcApp, err := svcApp.New() 168 if err != nil { 169 return locale.WrapError(err, "err_autostart_app") 170 } 171 172 err = svcApp.Uninstall() 173 if err != nil { 174 return locale.WrapError(err, "err_uninstall_app", "Could not uninstall the State Tool service app.") 175 } 176 177 return nil 178 } 179 180 // verifyInstallation ensures that the State Tool was installed in a way 181 // that will allow us to properly uninstall 182 func verifyInstallation() error { 183 installationContext, err := installation.GetContext() 184 if err != nil { 185 return errs.Wrap(err, "Could not check if initial installation was run as admin") 186 } 187 188 isAdmin, err := osutils.IsAdmin() 189 if err != nil { 190 return errs.Wrap(err, "Could not check if current user is an administrator") 191 } 192 193 if installationContext.InstalledAsAdmin && !isAdmin { 194 return locale.NewInputError("err_uninstall_privilege_mismatch") 195 } 196 197 return nil 198 }