-
Notifications
You must be signed in to change notification settings - Fork 13
/
installer.go
272 lines (228 loc) · 9.03 KB
/
installer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
package main
import (
"errors"
"os"
"path/filepath"
"strings"
svcApp "github.com/ActiveState/cli/cmd/state-svc/app"
svcAutostart "github.com/ActiveState/cli/cmd/state-svc/autostart"
"github.com/ActiveState/cli/internal/analytics"
"github.com/ActiveState/cli/internal/config"
"github.com/ActiveState/cli/internal/constants"
"github.com/ActiveState/cli/internal/errs"
"github.com/ActiveState/cli/internal/fileutils"
"github.com/ActiveState/cli/internal/installation"
"github.com/ActiveState/cli/internal/installmgr"
"github.com/ActiveState/cli/internal/legacytray"
"github.com/ActiveState/cli/internal/locale"
"github.com/ActiveState/cli/internal/logging"
"github.com/ActiveState/cli/internal/multilog"
"github.com/ActiveState/cli/internal/osutils"
"github.com/ActiveState/cli/internal/osutils/autostart"
"github.com/ActiveState/cli/internal/output"
"github.com/ActiveState/cli/internal/prompt"
"github.com/ActiveState/cli/internal/rtutils/ptr"
"github.com/ActiveState/cli/internal/subshell"
"github.com/ActiveState/cli/internal/subshell/sscommon"
"github.com/ActiveState/cli/internal/updater"
)
type Installer struct {
out output.Outputer
cfg *config.Instance
an analytics.Dispatcher
payloadPath string
*Params
}
func NewInstaller(cfg *config.Instance, out output.Outputer, an analytics.Dispatcher, payloadPath string, params *Params) (*Installer, error) {
i := &Installer{cfg: cfg, out: out, an: an, payloadPath: payloadPath, Params: params}
if err := i.sanitizeInput(); err != nil {
return nil, errs.Wrap(err, "Could not sanitize input")
}
logging.Debug("Instantiated installer with source dir: %s, target dir: %s", i.payloadPath, i.path)
return i, nil
}
func (i *Installer) Install() (rerr error) {
isAdmin, err := osutils.IsAdmin()
if err != nil {
return errs.Wrap(err, "Could not determine if running as Windows administrator")
}
if isAdmin && !i.Params.force && !i.Params.isUpdate && !i.Params.nonInteractive {
prompter := prompt.New(true, i.an)
confirm, err := prompter.Confirm("", locale.T("installer_prompt_is_admin"), ptr.To(false))
if err != nil {
return errs.Wrap(err, "Unable to confirm")
}
if !confirm {
return locale.NewInputError("installer_aborted", "Installation aborted by the user")
}
}
// Store update tag
if i.updateTag != "" {
if err := i.cfg.Set(updater.CfgUpdateTag, i.updateTag); err != nil {
return errs.Wrap(err, "Failed to set update tag")
}
}
// Stop any running processes that might interfere
if err := installmgr.StopRunning(i.path); err != nil {
return errs.Wrap(err, "Failed to stop running services")
}
// Detect if existing installation needs to be cleaned
err = detectCorruptedInstallDir(i.path)
if errors.Is(err, errCorruptedInstall) {
err = i.sanitizeInstallPath()
if err != nil {
return locale.WrapError(err, "err_update_corrupt_install")
}
} else if err != nil {
return locale.WrapInputError(err, "err_update_corrupt_install", constants.DocumentationURL)
}
err = legacytray.DetectAndRemove(i.path, i.cfg)
if err != nil {
multilog.Error("Unable to detect and/or remove legacy tray. Will try again next update. Error: %v", err)
}
// Create target dir
if err := fileutils.MkdirUnlessExists(i.path); err != nil {
return errs.Wrap(err, "Could not create target directory: %s", i.path)
}
// Prepare bin targets is an OS specific method that will ensure we don't run into conflicts while installing
if err := i.PrepareBinTargets(); err != nil {
return errs.Wrap(err, "Could not prepare for installation")
}
// Copy all the files except for the current executable
if err := fileutils.CopyAndRenameFiles(i.payloadPath, i.path, filepath.Base(osutils.Executable())); err != nil {
if osutils.IsAccessDeniedError(err) {
// If we got to this point, we could not copy and rename over existing files.
// This is a permission issue. (We have an installer test for copying and renaming over a file
// in use, which does not raise an error.)
return locale.WrapExternalError(err, "err_update_access_denied", "", errs.JoinMessage(err))
}
return errs.Wrap(err, "Failed to copy installation files to dir %s. Error received: %s", i.path, errs.JoinMessage(err))
}
// Set up the environment
binDir := filepath.Join(i.path, installation.BinDirName)
// Install the state service as an app if necessary
if err := i.installSvcApp(binDir); err != nil {
return errs.Wrap(err, "Installation of service app failed.")
}
// Configure available shells
shell := subshell.New(i.cfg)
err = subshell.ConfigureAvailableShells(shell, i.cfg, map[string]string{"PATH": binDir}, sscommon.InstallID, !isAdmin)
if err != nil {
return errs.Wrap(err, "Could not configure available shells")
}
err = installation.SaveContext(&installation.Context{InstalledAsAdmin: isAdmin})
if err != nil {
return errs.Wrap(err, "Failed to set current privilege level in config")
}
stateExec, err := installation.StateExecFromDir(binDir)
if err != nil {
return locale.WrapError(err, "err_state_exec")
}
// Run state _prepare after updates to facilitate anything the new version of the state tool might need to set up
// Yes this is awkward, followup story here: https://www.pivotaltracker.com/story/show/176507898
if stdout, stderr, err := osutils.ExecSimple(stateExec, []string{"_prepare"}, []string{}); err != nil {
multilog.Error("_prepare failed after update: %v\n\nstdout: %s\n\nstderr: %s", err, stdout, stderr)
}
logging.Debug("Installation was successful")
return nil
}
func (i *Installer) InstallPath() string {
return i.path
}
// sanitizeInput cleans up the input and inserts fallback values
func (i *Installer) sanitizeInput() error {
if tag, ok := os.LookupEnv(constants.UpdateTagEnvVarName); ok {
i.updateTag = tag
}
var err error
if i.path, err = resolveInstallPath(i.path); err != nil {
return errs.Wrap(err, "Could not resolve installation path")
}
return nil
}
func (i *Installer) installSvcApp(binDir string) error {
app, err := svcApp.NewFromDir(binDir)
if err != nil {
return errs.Wrap(err, "Could not create app")
}
err = app.Install()
if err != nil {
return errs.Wrap(err, "Could not install app")
}
if err = autostart.Upgrade(app.Path(), svcAutostart.Options); err != nil {
return errs.Wrap(err, "Failed to upgrade autostart for service app.")
}
if err = autostart.Enable(app.Path(), svcAutostart.Options); err != nil {
return errs.Wrap(err, "Failed to enable autostart for service app.")
}
return nil
}
var errCorruptedInstall = errs.New("Corrupted install")
// detectCorruptedInstallDir will return an error if it detects that the given install path is not a proper
// State Tool installation path. This mainly covers cases where we are working off of a legacy install of the State
// Tool or cases where the uninstall was not completed properly.
func detectCorruptedInstallDir(path string) error {
if !fileutils.TargetExists(path) {
return nil
}
isEmpty, err := fileutils.IsEmptyDir(path)
if err != nil {
return errs.Wrap(err, "Could not check if install dir is empty")
}
if isEmpty {
return nil
}
// Detect if the install dir has files in it
files, err := os.ReadDir(path)
if err != nil {
return errs.Wrap(err, "Could not read directory: %s", path)
}
// Executable files should be in bin dir, not root dir
for _, file := range files {
if isStateExecutable(strings.ToLower(file.Name())) {
return errs.Wrap(errCorruptedInstall, "Install directory should only contain dirs: %s", path)
}
}
return nil
}
func isStateExecutable(name string) bool {
if name == constants.StateCmd+osutils.ExeExtension || name == constants.StateSvcCmd+osutils.ExeExtension {
return true
}
return false
}
func installedOnPath(installRoot, channel string) (bool, string, error) {
if !fileutils.DirExists(installRoot) {
return false, "", nil
}
// This is not using appinfo on purpose because we want to deal with legacy installation formats, which appinfo does not
stateCmd := constants.StateCmd + osutils.ExeExtension
// Check for state.exe in channel, root and bin dir
// This is to handle older state tool versions that gave incompatible input paths
candidates := []string{
filepath.Join(installRoot, channel, installation.BinDirName, stateCmd),
filepath.Join(installRoot, channel, stateCmd),
filepath.Join(installRoot, installation.BinDirName, stateCmd),
filepath.Join(installRoot, stateCmd),
}
for _, candidate := range candidates {
if fileutils.TargetExists(candidate) {
return true, installRoot, nil
}
}
return false, installRoot, nil
}
// installationIsOnPATH returns whether the installed State Tool root is on $PATH or %PATH%.
func installationIsOnPATH(installRoot string) bool {
// This is not using appinfo on purpose because we want to deal with legacy installation formats, which appinfo does not
stateCmd := constants.StateCmd + osutils.ExeExtension
exeOnPATH := osutils.FindExeOnPATH(stateCmd)
if exeOnPATH == "" {
return false
}
onPATH, err := fileutils.PathContainsParent(exeOnPATH, installRoot)
if err != nil {
multilog.Error("Unable to determine if state tool on PATH is in path to install to: %v", err)
}
return onPATH
}