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  }