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: ¶ms.channel, 132 }, 133 { 134 Shorthand: "b", // backwards compatibility 135 Hidden: true, 136 Value: ¶ms.channel, 137 }, 138 { 139 Name: "version", 140 Shorthand: "v", 141 Description: "The version of the State Tool to install", 142 Value: ¶ms.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: ¶ms.force, 149 }, 150 { 151 Name: "non-interactive", 152 Shorthand: "n", 153 Hidden: true, 154 Value: ¶ms.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 }