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  }