github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/platform/runtime/runtime.go (about) 1 package runtime 2 3 import ( 4 "errors" 5 "os" 6 "path/filepath" 7 "strings" 8 9 "github.com/ActiveState/cli/pkg/buildplan" 10 bpResp "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response" 11 bpModel "github.com/ActiveState/cli/pkg/platform/model/buildplanner" 12 "golang.org/x/net/context" 13 14 "github.com/ActiveState/cli/internal/analytics" 15 anaConsts "github.com/ActiveState/cli/internal/analytics/constants" 16 "github.com/ActiveState/cli/internal/analytics/dimensions" 17 "github.com/ActiveState/cli/internal/constants" 18 "github.com/ActiveState/cli/internal/errs" 19 "github.com/ActiveState/cli/internal/fileutils" 20 "github.com/ActiveState/cli/internal/installation/storage" 21 "github.com/ActiveState/cli/internal/instanceid" 22 "github.com/ActiveState/cli/internal/locale" 23 "github.com/ActiveState/cli/internal/logging" 24 "github.com/ActiveState/cli/internal/multilog" 25 "github.com/ActiveState/cli/internal/osutils" 26 "github.com/ActiveState/cli/internal/output" 27 "github.com/ActiveState/cli/internal/rtutils/ptr" 28 "github.com/ActiveState/cli/pkg/platform/authentication" 29 "github.com/ActiveState/cli/pkg/platform/model" 30 "github.com/ActiveState/cli/pkg/platform/runtime/buildscript" 31 "github.com/ActiveState/cli/pkg/platform/runtime/envdef" 32 "github.com/ActiveState/cli/pkg/platform/runtime/setup" 33 "github.com/ActiveState/cli/pkg/platform/runtime/setup/buildlog" 34 "github.com/ActiveState/cli/pkg/platform/runtime/setup/events" 35 "github.com/ActiveState/cli/pkg/platform/runtime/store" 36 "github.com/ActiveState/cli/pkg/project" 37 ) 38 39 type Configurable interface { 40 GetString(key string) string 41 GetBool(key string) bool 42 } 43 44 type Runtime struct { 45 disabled bool 46 target setup.Targeter 47 store *store.Store 48 analytics analytics.Dispatcher 49 svcm *model.SvcModel 50 auth *authentication.Auth 51 completed bool 52 cfg Configurable 53 out output.Outputer 54 } 55 56 // NeedsCommitError is an error returned when the local runtime's build script has changes that need 57 // staging. This is not a fatal error. A runtime can still be used, but a warning should be emitted. 58 var NeedsCommitError = errors.New("runtime needs commit") 59 60 // NeedsBuildscriptResetError is an error returned when the runtime is improperly referenced in the project (eg. missing buildscript) 61 var NeedsBuildscriptResetError = errors.New("needs runtime reset") 62 63 func newRuntime(target setup.Targeter, an analytics.Dispatcher, svcModel *model.SvcModel, auth *authentication.Auth, cfg Configurable, out output.Outputer) (*Runtime, error) { 64 rt := &Runtime{ 65 target: target, 66 store: store.New(target.Dir()), 67 analytics: an, 68 svcm: svcModel, 69 auth: auth, 70 cfg: cfg, 71 out: out, 72 } 73 74 err := rt.validateCache() 75 if err != nil { 76 return rt, err 77 } 78 79 return rt, nil 80 } 81 82 // New attempts to create a new runtime from local storage. 83 func New(target setup.Targeter, an analytics.Dispatcher, svcm *model.SvcModel, auth *authentication.Auth, cfg Configurable, out output.Outputer) (*Runtime, error) { 84 logging.Debug("Initializing runtime for: %s/%s@%s", target.Owner(), target.Name(), target.CommitUUID()) 85 86 if strings.ToLower(os.Getenv(constants.DisableRuntime)) == "true" { 87 out.Notice(locale.T("notice_runtime_disabled")) 88 return &Runtime{disabled: true, target: target, analytics: an}, nil 89 } 90 recordAttempt(an, target) 91 an.Event(anaConsts.CatRuntimeDebug, anaConsts.ActRuntimeStart, &dimensions.Values{ 92 Trigger: ptr.To(target.Trigger().String()), 93 CommitID: ptr.To(target.CommitUUID().String()), 94 ProjectNameSpace: ptr.To(project.NewNamespace(target.Owner(), target.Name(), target.CommitUUID().String()).String()), 95 InstanceID: ptr.To(instanceid.ID()), 96 }) 97 98 r, err := newRuntime(target, an, svcm, auth, cfg, out) 99 if err == nil { 100 an.Event(anaConsts.CatRuntimeDebug, anaConsts.ActRuntimeCache, &dimensions.Values{ 101 CommitID: ptr.To(target.CommitUUID().String()), 102 }) 103 } 104 105 return r, err 106 } 107 108 func (r *Runtime) NeedsUpdate() bool { 109 if strings.ToLower(os.Getenv(constants.DisableRuntime)) == "true" { 110 return false 111 } 112 if !r.store.MarkerIsValid(r.target.CommitUUID()) { 113 if r.target.ReadOnly() { 114 logging.Debug("Using forced cache") 115 } else { 116 return true 117 } 118 } 119 return false 120 } 121 122 func (r *Runtime) validateCache() error { 123 if r.target.ProjectDir() == "" { 124 return nil 125 } 126 127 err := r.validateBuildScript() 128 if err != nil { 129 return errs.Wrap(err, "Error validating build script") 130 } 131 132 return nil 133 } 134 135 // validateBuildScript asserts the local build script does not have changes that should be committed. 136 func (r *Runtime) validateBuildScript() error { 137 logging.Debug("Checking to see if local build script has changes that should be committed") 138 if !r.cfg.GetBool(constants.OptinBuildscriptsConfig) { 139 logging.Debug("Not opted into buildscripts") 140 return nil 141 } 142 143 script, err := buildscript.ScriptFromProject(r.target) 144 if err != nil { 145 if errors.Is(err, buildscript.ErrBuildscriptNotExist) { 146 return errs.Pack(err, NeedsBuildscriptResetError) 147 } 148 return errs.Wrap(err, "Could not get buildscript from project") 149 } 150 151 cachedCommitID, err := r.store.CommitID() 152 if err != nil { 153 logging.Debug("No commit ID to read; refresh needed") 154 return nil 155 } 156 157 if cachedCommitID != r.target.CommitUUID().String() { 158 logging.Debug("Runtime commit ID does not match project commit ID; refresh needed") 159 return nil 160 } 161 162 cachedScript, err := r.store.BuildScript() 163 if err != nil { 164 if errors.Is(err, store.ErrNoBuildScriptFile) { 165 logging.Warning("No buildscript file exists in store, unable to check if buildscript is dirty. This can happen if you cleared your cache.") 166 } else { 167 return errs.Wrap(err, "Could not retrieve buildscript from store") 168 } 169 } 170 171 if cachedScript != nil { 172 if script != nil && !script.Equals(cachedScript) { 173 return NeedsCommitError 174 } 175 } 176 177 return nil 178 } 179 180 func (r *Runtime) Disabled() bool { 181 return r.disabled 182 } 183 184 func (r *Runtime) Target() setup.Targeter { 185 return r.target 186 } 187 188 func (r *Runtime) Setup(eventHandler events.Handler) *setup.Setup { 189 return setup.New(r.target, eventHandler, r.auth, r.analytics, r.cfg, r.out, r.svcm) 190 } 191 192 func (r *Runtime) Update(setup *setup.Setup, commit *bpModel.Commit) (rerr error) { 193 if r.disabled { 194 logging.Debug("Skipping update as it is disabled") 195 return nil // nothing to do 196 } 197 198 logging.Debug("Updating %s#%s @ %s", r.target.Name(), r.target.CommitUUID(), r.target.Dir()) 199 200 defer func() { 201 r.recordCompletion(rerr) 202 }() 203 204 if err := setup.Update(commit); err != nil { 205 return errs.Wrap(err, "Update failed") 206 } 207 208 // Reinitialize 209 rt, err := newRuntime(r.target, r.analytics, r.svcm, r.auth, r.cfg, r.out) 210 if err != nil { 211 return errs.Wrap(err, "Could not reinitialize runtime after update") 212 } 213 *r = *rt 214 215 return nil 216 } 217 218 // SolveAndUpdate updates the runtime by downloading all necessary artifacts from the Platform and installing them locally. 219 func (r *Runtime) SolveAndUpdate(eventHandler events.Handler) error { 220 if r.disabled { 221 logging.Debug("Skipping update as it is disabled") 222 return nil // nothing to do 223 } 224 225 setup := r.Setup(eventHandler) 226 commit, err := setup.Solve() 227 if err != nil { 228 return errs.Wrap(err, "Could not solve") 229 } 230 231 if err := r.Update(setup, commit); err != nil { 232 return errs.Wrap(err, "Could not update") 233 } 234 235 return nil 236 } 237 238 // HasCache tells us whether this runtime has any cached files. Note this does NOT tell you whether the cache is valid. 239 func (r *Runtime) HasCache() bool { 240 return fileutils.DirExists(r.target.Dir()) 241 } 242 243 // Env returns a key-value map of the environment variables that need to be set for this runtime 244 // It's different from envDef in that it merges in the current active environment and points the PATH variable to the 245 // Executors directory if requested 246 func (r *Runtime) Env(inherit bool, useExecutors bool) (map[string]string, error) { 247 logging.Debug("Getting runtime env, inherit: %v, useExec: %v", inherit, useExecutors) 248 249 envDef, err := r.envDef() 250 r.recordCompletion(err) 251 if err != nil { 252 return nil, errs.Wrap(err, "Could not grab environment definitions") 253 } 254 255 env := envDef.GetEnv(inherit) 256 257 execDir := filepath.Clean(setup.ExecDir(r.target.Dir())) 258 if useExecutors { 259 // Override PATH entry with exec path 260 pathEntries := []string{execDir} 261 if inherit { 262 pathEntries = append(pathEntries, os.Getenv("PATH")) 263 } 264 env["PATH"] = strings.Join(pathEntries, string(os.PathListSeparator)) 265 } else { 266 // Ensure we aren't inheriting the executor paths from something like an activated state 267 envdef.FilterPATH(env, execDir, storage.GlobalBinDir()) 268 } 269 270 return env, nil 271 } 272 273 func (r *Runtime) recordCompletion(err error) { 274 if r.completed { 275 logging.Debug("Not recording runtime completion as it was already recorded for this invocation") 276 return 277 } 278 r.completed = true 279 logging.Debug("Recording runtime completion, error: %v", err == nil) 280 281 var action string 282 if err != nil { 283 action = anaConsts.ActRuntimeFailure 284 } else { 285 action = anaConsts.ActRuntimeSuccess 286 r.recordUsage() 287 } 288 289 ns := project.Namespaced{ 290 Owner: r.target.Owner(), 291 Project: r.target.Name(), 292 } 293 294 errorType := "unknown" 295 switch { 296 // IsInputError should always be first because it is technically possible for something like a 297 // download error to be cause by an input error. 298 case locale.IsInputError(err): 299 errorType = "input" 300 case errs.Matches(err, &setup.BuildError{}), errs.Matches(err, &buildlog.BuildError{}): 301 errorType = "build" 302 case errs.Matches(err, &bpResp.BuildPlannerError{}): 303 errorType = "buildplan" 304 case errs.Matches(err, &setup.ArtifactSetupErrors{}): 305 if setupErrors := (&setup.ArtifactSetupErrors{}); errors.As(err, &setupErrors) { 306 // Label the loop so we can break out of it when we find the first download 307 // or build error. 308 Loop: 309 for _, err := range setupErrors.Errors() { 310 switch { 311 case errs.Matches(err, &setup.ArtifactDownloadError{}): 312 errorType = "download" 313 break Loop // it only takes one download failure to report the runtime failure as due to download error 314 case errs.Matches(err, &setup.ArtifactInstallError{}): 315 errorType = "install" 316 // Note: do not break because there could be download errors, and those take precedence 317 case errs.Matches(err, &setup.BuildError{}), errs.Matches(err, &buildlog.BuildError{}): 318 errorType = "build" 319 break Loop // it only takes one build failure to report the runtime failure as due to build error 320 } 321 } 322 } 323 // Progress/event handler errors should come last because they can wrap one of the above errors, 324 // and those errors actually caused the failure, not these. 325 case errs.Matches(err, &setup.ProgressReportError{}) || errs.Matches(err, &buildlog.EventHandlerError{}): 326 errorType = "progress" 327 case errs.Matches(err, &setup.ExecutorSetupError{}): 328 errorType = "postprocess" 329 } 330 331 var message string 332 if err != nil { 333 message = errs.JoinMessage(err) 334 } 335 336 r.analytics.Event(anaConsts.CatRuntimeDebug, action, &dimensions.Values{ 337 CommitID: ptr.To(r.target.CommitUUID().String()), 338 // Note: ProjectID is set by state-svc since ProjectNameSpace is specified. 339 ProjectNameSpace: ptr.To(ns.String()), 340 Error: ptr.To(errorType), 341 Message: &message, 342 }) 343 } 344 345 func (r *Runtime) recordUsage() { 346 if !r.target.Trigger().IndicatesUsage() { 347 logging.Debug("Not recording usage as %s is not a usage trigger", r.target.Trigger().String()) 348 return 349 } 350 351 // Fire initial runtime usage event right away, subsequent events will be fired via the service so long as the process is running 352 dims := usageDims(r.target) 353 dimsJson, err := dims.Marshal() 354 if err != nil { 355 multilog.Critical("Could not marshal dimensions for runtime-usage: %s", errs.JoinMessage(err)) 356 } 357 if r.svcm != nil { 358 if err := r.svcm.ReportRuntimeUsage(context.Background(), os.Getpid(), osutils.Executable(), anaConsts.SrcStateTool, dimsJson); err != nil { 359 multilog.Critical("Could not report runtime usage: %s", errs.JoinMessage(err)) 360 } 361 } 362 } 363 364 func recordAttempt(an analytics.Dispatcher, target setup.Targeter) { 365 if !target.Trigger().IndicatesUsage() { 366 logging.Debug("Not recording usage attempt as %s is not a usage trigger", target.Trigger().String()) 367 return 368 } 369 370 an.Event(anaConsts.CatRuntimeUsage, anaConsts.ActRuntimeAttempt, usageDims(target)) 371 } 372 373 func usageDims(target setup.Targeter) *dimensions.Values { 374 return &dimensions.Values{ 375 Trigger: ptr.To(target.Trigger().String()), 376 CommitID: ptr.To(target.CommitUUID().String()), 377 ProjectNameSpace: ptr.To(project.NewNamespace(target.Owner(), target.Name(), target.CommitUUID().String()).String()), 378 InstanceID: ptr.To(instanceid.ID()), 379 } 380 } 381 382 func (r *Runtime) envDef() (*envdef.EnvironmentDefinition, error) { 383 if r.disabled { 384 return nil, errs.New("Called envDef() on a disabled runtime.") 385 } 386 env, err := r.store.EnvDef() 387 if err != nil { 388 return nil, errs.Wrap(err, "store.EnvDef failed") 389 } 390 return env, nil 391 } 392 393 func (r *Runtime) ExecutablePaths() (envdef.ExecutablePaths, error) { 394 env, err := r.envDef() 395 if err != nil { 396 return nil, errs.Wrap(err, "Could not retrieve environment info") 397 } 398 return env.ExecutablePaths() 399 } 400 401 func (r *Runtime) ExecutableDirs() (envdef.ExecutablePaths, error) { 402 env, err := r.envDef() 403 if err != nil { 404 return nil, errs.Wrap(err, "Could not retrieve environment info") 405 } 406 return env.ExecutableDirs() 407 } 408 409 func IsRuntimeDir(dir string) bool { 410 return store.New(dir).HasMarker() 411 } 412 413 func (r *Runtime) BuildPlan() (*buildplan.BuildPlan, error) { 414 runtimeStore := r.store 415 if runtimeStore == nil { 416 runtimeStore = store.New(r.target.Dir()) 417 } 418 plan, err := runtimeStore.BuildPlan() 419 if err != nil { 420 return nil, errs.Wrap(err, "Unable to fetch build plan") 421 } 422 return plan, nil 423 }