github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/cmd/state-remote-installer/main.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"regexp"
     8  	"runtime/debug"
     9  	"syscall"
    10  	"time"
    11  
    12  	"github.com/ActiveState/cli/internal/analytics"
    13  	"github.com/ActiveState/cli/internal/analytics/client/sync"
    14  	anaConst "github.com/ActiveState/cli/internal/analytics/constants"
    15  	"github.com/ActiveState/cli/internal/captain"
    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/events"
    20  	"github.com/ActiveState/cli/internal/locale"
    21  	"github.com/ActiveState/cli/internal/logging"
    22  	"github.com/ActiveState/cli/internal/multilog"
    23  	"github.com/ActiveState/cli/internal/osutils"
    24  	"github.com/ActiveState/cli/internal/output"
    25  	"github.com/ActiveState/cli/internal/primer"
    26  	"github.com/ActiveState/cli/internal/prompt"
    27  	"github.com/ActiveState/cli/internal/rollbar"
    28  	"github.com/ActiveState/cli/internal/rtutils/ptr"
    29  	"github.com/ActiveState/cli/internal/runbits/errors"
    30  	"github.com/ActiveState/cli/internal/runbits/panics"
    31  	"github.com/ActiveState/cli/internal/updater"
    32  )
    33  
    34  type Params struct {
    35  	channel        string
    36  	force          bool
    37  	version        string
    38  	nonInteractive bool
    39  }
    40  
    41  func newParams() *Params {
    42  	return &Params{}
    43  }
    44  
    45  var filenameRe = regexp.MustCompile(`(?P<name>[^/\\]+?)_(?P<webclientId>[^/\\_.]+)(\.(?P<ext>[^.]+))?$`)
    46  
    47  func main() {
    48  	var exitCode int
    49  
    50  	var an analytics.Dispatcher
    51  
    52  	var cfg *config.Instance
    53  
    54  	// Handle things like panics, exit codes and the closing of globals
    55  	defer func() {
    56  		if panics.HandlePanics(recover(), debug.Stack()) {
    57  			exitCode = 1
    58  		}
    59  
    60  		if err := cfg.Close(); err != nil {
    61  			logging.Error("Failed to close config: %w", err)
    62  		}
    63  
    64  		if err := events.WaitForEvents(5*time.Second, rollbar.Wait, an.Wait, logging.Close); err != nil {
    65  			logging.Warning("state-remote-installer failed to wait for events: %v", err)
    66  		}
    67  		os.Exit(exitCode)
    68  	}()
    69  
    70  	// Set up verbose logging
    71  	logging.CurrentHandler().SetVerbose(os.Getenv("VERBOSE") != "")
    72  	// Set up rollbar reporting
    73  	rollbar.SetupRollbar(constants.StateInstallerRollbarToken)
    74  
    75  	// Allow starting the installer via a double click
    76  	captain.DisableMousetrap()
    77  
    78  	// Set up configuration handler
    79  	cfg, err := config.New()
    80  	if err != nil {
    81  		logging.Error("Could not set up configuration handler: " + errs.JoinMessage(err))
    82  		fmt.Fprintln(os.Stderr, err.Error())
    83  		exitCode = 1
    84  	}
    85  
    86  	rollbar.SetConfig(cfg)
    87  
    88  	// Set up output handler
    89  	out, err := output.New("plain", &output.Config{
    90  		OutWriter:   os.Stdout,
    91  		ErrWriter:   os.Stderr,
    92  		Colored:     true,
    93  		Interactive: false,
    94  	})
    95  	if err != nil {
    96  		logging.Error("Could not set up output handler: " + errs.JoinMessage(err))
    97  		fmt.Fprintln(os.Stderr, err.Error())
    98  		exitCode = 1
    99  		return
   100  	}
   101  
   102  	// Store sessionToken to config
   103  	webclientId := "remote_" + constants.RemoteInstallerVersion
   104  	if matches := filenameRe.FindStringSubmatch(os.Args[0]); matches != nil {
   105  		if index := filenameRe.SubexpIndex("webclientId"); index != -1 {
   106  			webclientId = matches[index]
   107  		} else {
   108  			multilog.Error("Invalid subexpression ID for webclient ID")
   109  		}
   110  	}
   111  	err = cfg.Set(anaConst.CfgSessionToken, webclientId)
   112  	if err != nil {
   113  		logging.Error("Unable to set session token: " + errs.JoinMessage(err))
   114  	}
   115  
   116  	an = sync.New(anaConst.SrcStateRemoteInstaller, cfg, nil, out)
   117  
   118  	// Set up prompter
   119  	prompter := prompt.New(true, an)
   120  
   121  	params := newParams()
   122  	cmd := captain.NewCommand(
   123  		"state-installer",
   124  		"",
   125  		"Installs or updates the State Tool",
   126  		primer.New(nil, out, nil, nil, nil, nil, cfg, nil, nil, an),
   127  		[]*captain.Flag{ // The naming of these flags is slightly inconsistent due to backwards compatibility requirements
   128  			{
   129  				Name:        "channel",
   130  				Description: "Defaults to 'release'.  Specify an alternative channel to install from (eg. beta)",
   131  				Value:       &params.channel,
   132  			},
   133  			{
   134  				Shorthand: "b", // backwards compatibility
   135  				Hidden:    true,
   136  				Value:     &params.channel,
   137  			},
   138  			{
   139  				Name:        "version",
   140  				Shorthand:   "v",
   141  				Description: "The version of the State Tool to install",
   142  				Value:       &params.version,
   143  			},
   144  			{
   145  				Name:        "force",
   146  				Shorthand:   "f",
   147  				Description: "Force the installation, overwriting any version of the State Tool already installed",
   148  				Value:       &params.force,
   149  			},
   150  			{
   151  				Name:      "non-interactive",
   152  				Shorthand: "n",
   153  				Hidden:    true,
   154  				Value:     &params.nonInteractive,
   155  			},
   156  		},
   157  		[]*captain.Argument{},
   158  		func(ccmd *captain.Command, args []string) error {
   159  			return execute(out, prompter, cfg, an, args, params)
   160  		},
   161  	)
   162  
   163  	err = cmd.Execute(os.Args[1:])
   164  	if err != nil {
   165  		errors.ReportError(err, cmd, an)
   166  		if locale.IsInputError(err) {
   167  			logging.Error("Installer input error: " + errs.JoinMessage(err))
   168  		} else if errs.IsExternalError(err) {
   169  			logging.Error("Installer external error: " + errs.JoinMessage(err))
   170  		} else {
   171  			multilog.Critical("Installer error: " + errs.JoinMessage(err))
   172  		}
   173  
   174  		exitCode, err = errors.ParseUserFacing(err)
   175  		if err != nil {
   176  			out.Error(err)
   177  		}
   178  		return
   179  	}
   180  }
   181  
   182  func execute(out output.Outputer, prompt prompt.Prompter, cfg *config.Instance, an analytics.Dispatcher, args []string, params *Params) error {
   183  	msg := locale.Tr("tos_disclaimer", constants.TermsOfServiceURLLatest)
   184  	msg += locale.Tr("tos_disclaimer_prompt", constants.TermsOfServiceURLLatest)
   185  	cont, err := prompt.Confirm(locale.Tr("install_remote_title"), msg, ptr.To(true))
   186  	if err != nil {
   187  		return errs.Wrap(err, "Could not prompt for confirmation")
   188  	}
   189  
   190  	if !cont {
   191  		return locale.NewInputError("install_cancel", "Installation cancelled")
   192  	}
   193  
   194  	channel := params.channel
   195  	if channel == "" {
   196  		channel = constants.ReleaseChannel
   197  	}
   198  
   199  	// Fetch payload
   200  	checker := updater.NewDefaultChecker(cfg, an)
   201  	checker.InvocationSource = updater.InvocationSourceInstall // Installing from a remote source is only ever encountered via the install flow
   202  	availableUpdate, err := checker.CheckFor(channel, params.version)
   203  	if err != nil {
   204  		return errs.Wrap(err, "Could not retrieve install package information")
   205  	}
   206  
   207  	version := availableUpdate.Version
   208  	if params.channel != "" {
   209  		version = fmt.Sprintf("%s (%s)", version, channel)
   210  	}
   211  
   212  	update := updater.NewUpdateInstaller(an, availableUpdate)
   213  	out.Fprint(os.Stdout, locale.Tl("remote_install_downloading", "• Downloading State Tool version [NOTICE]{{.V0}}[/RESET]... ", version))
   214  	tmpDir, err := update.DownloadAndUnpack()
   215  	if err != nil {
   216  		out.Print(locale.Tl("remote_install_status_fail", "[ERROR]x Failed[/RESET]"))
   217  		return errs.Wrap(err, "Could not download and unpack")
   218  	}
   219  	out.Print(locale.Tl("remote_install_status_done", "[SUCCESS]✔ Done[/RESET]"))
   220  
   221  	if params.nonInteractive {
   222  		args = append(args, "-n") // forward to installer
   223  	}
   224  	env := []string{
   225  		constants.InstallerNoSubshell + "=true",
   226  	}
   227  	_, cmd, err := osutils.ExecuteAndPipeStd(filepath.Join(tmpDir, constants.StateInstallerCmd+osutils.ExeExtension), args, env)
   228  	if err != nil {
   229  		if cmd != nil && cmd.ProcessState.Sys().(syscall.WaitStatus).Exited() {
   230  			// The issue happened while running the command itself, meaning the responsibility for conveying the error
   231  			// is on the command, rather than us.
   232  			return errs.Silence(errs.Wrap(err, "Installer failed"))
   233  		}
   234  		return errs.Wrap(err, "Could not run installer")
   235  	}
   236  
   237  	out.Print(locale.Tl("remote_install_exit_prompt", "Press ENTER to exit."))
   238  	fmt.Scanln(ptr.To("")) // Wait for input from user
   239  
   240  	return nil
   241  }