Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright 2017 The LUCI Authors. All rights reserved. | |
| 2 // Use of this source code is governed under the Apache License, Version 2.0 | |
| 3 // that can be found in the LICENSE file. | |
| 4 | |
| 5 package venv | |
| 6 | |
| 7 import ( | |
| 8 "os" | |
| 9 "path/filepath" | |
| 10 "sync" | |
| 11 "time" | |
| 12 | |
| 13 "github.com/luci/luci-go/vpython/api/vpython" | |
| 14 "github.com/luci/luci-go/vpython/filesystem" | |
| 15 "github.com/luci/luci-go/vpython/python" | |
| 16 "github.com/luci/luci-go/vpython/spec" | |
| 17 "github.com/luci/luci-go/vpython/wheel" | |
| 18 | |
| 19 "github.com/luci/luci-go/common/clock" | |
| 20 "github.com/luci/luci-go/common/errors" | |
| 21 "github.com/luci/luci-go/common/logging" | |
| 22 | |
| 23 "github.com/danjacques/gofslock/fslock" | |
| 24 "golang.org/x/net/context" | |
| 25 ) | |
| 26 | |
| 27 const ( | |
| 28 lockHeldDelay = 5 * time.Second | |
| 29 defaultKeepAliveInterval = 1 * time.Hour | |
| 30 ) | |
| 31 | |
| 32 // blocker is an fslock.Blocker implementation that sleeps lockHeldDelay in | |
| 33 // between attempts. | |
| 34 func blocker(c context.Context) fslock.Blocker { | |
| 35 return func() error { | |
| 36 logging.Debugf(c, "Lock is currently held. Sleeping %v and retry ing...", lockHeldDelay) | |
| 37 clock.Sleep(c, lockHeldDelay) | |
| 38 return nil | |
| 39 } | |
| 40 } | |
| 41 | |
| 42 // EnvRootFromSpecPath calculates the environment root from an exported | |
| 43 // environment specification file path. | |
| 44 // | |
| 45 // The specification path is: <EnvRoot>/<SpecHash>/<spec>, so our EnvRoot | |
| 46 // is two directories up. | |
| 47 // | |
| 48 // We export EnvSpecPath as an asbolute path. However, since someone else | |
| 49 // could have overridden it or exported their own, let's make sure. | |
| 50 func EnvRootFromSpecPath(path string) (string, error) { | |
| 51 if err := filesystem.AbsPath(&path); err != nil { | |
| 52 return "", errors.Annotate(err). | |
| 53 Reason("failed to get absolute path for specification fi le path: %(path)s"). | |
| 54 Err() | |
| 55 } | |
| 56 return filepath.Dir(filepath.Dir(path)), nil | |
| 57 } | |
| 58 | |
| 59 // With creates a new Env and executes "fn" with assumed ownership of that Env. | |
| 60 // | |
| 61 // The Context passed to "fn" will be cancelled if we lose perceived ownership | |
| 62 // of the configured environment. This is not an expected scenario, and should | |
| 63 // be considered an error condition. The Env passed to "fn" is valid only for | |
| 64 // the duration of the callback. | |
| 65 // | |
| 66 // It will lock around the VirtualEnv to ensure that multiple processes do not | |
| 67 // conflict with each other. If a VirtualEnv for this specification already | |
| 68 // exists, it will be used directly without any additional setup. | |
| 69 // | |
| 70 // If another process holds the lock, With will return an error (if blocking is | |
| 71 // false) or try again until it obtains the lock (if blocking is true). | |
| 72 func With(c context.Context, cfg Config, blocking bool, fn func(context.Context, *Env) error) error { | |
| 73 e, err := cfg.setupEnv(c) | |
| 74 if err != nil { | |
| 75 return errors.Annotate(err).Reason("failed to resolve VirtualEnv ").Err() | |
| 76 } | |
| 77 | |
| 78 // Setup the environment. | |
| 79 if err := e.setupImpl(c, blocking); err != nil { | |
| 80 return errors.Annotate(err).Err() | |
| 81 } | |
| 82 | |
| 83 // Perform a pruning round. Failure is non-fatal. | |
| 84 if perr := prune(c, e.Config, e.name); perr != nil { | |
| 85 logging.WithError(perr).Warningf(c, "Failed to perform pruning r ound after initialization.") | |
| 86 } | |
| 87 | |
| 88 // Set-up our environment Context. | |
| 89 var monitorWG sync.WaitGroup | |
| 90 c, cancelFunc := context.WithCancel(c) | |
| 91 defer func() { | |
| 92 // Cancel our Context and reap our monitor goroutine(s). | |
| 93 cancelFunc() | |
| 94 monitorWG.Wait() | |
| 95 }() | |
| 96 | |
| 97 // If we have a prune threshold, touch the "complete flag" periodically. | |
| 98 // | |
| 99 // Our refresh interval must be at least 1/4 the prune threshold, but id eally | |
| 100 // would be higher. | |
| 101 if interval := e.Config.PruneThreshold / 4; interval > 0 { | |
| 102 if interval > defaultKeepAliveInterval { | |
| 103 interval = defaultKeepAliveInterval | |
| 104 } | |
| 105 | |
| 106 monitorWG.Add(1) | |
| 107 go func() { | |
| 108 defer monitorWG.Done() | |
| 109 e.keepAliveMonitor(c, interval, cancelFunc) | |
| 110 }() | |
| 111 } | |
| 112 | |
| 113 return fn(c, e) | |
| 114 } | |
| 115 | |
| 116 // Env is a fully set-up Python virtual environment. It is configured | |
| 117 // based on the contents of an vpython.Spec file by Setup. | |
| 118 // | |
| 119 // Env should not be instantiated directly; it must be created by calling | |
| 120 // Config.Env. | |
| 121 // | |
| 122 // All paths in Env are absolute. | |
| 123 type Env struct { | |
| 124 // Config is this Env's Config, fully-resolved. | |
| 125 Config *Config | |
| 126 | |
| 127 // Root is the Env container's root directory path. | |
| 128 Root string | |
| 129 | |
| 130 // Python is the path to the Env Python interpreter. | |
| 131 Python string | |
| 132 | |
| 133 // SepcPath is the path to the specification file that was used to const ruct | |
| 134 // this environment. It will be in text protobuf format, and, therefore, | |
| 135 // suitable for input to other "vpython" invocations. | |
| 136 SpecPath string | |
| 137 | |
| 138 // name is the hash of the specification file for this Env. | |
| 139 name string | |
| 140 // lockPath is the path to this Env-specific lock file. It will be at: | |
| 141 // "<baseDir>/.<name>.lock". | |
| 142 lockPath string | |
| 143 // completeFlagPath is the path to this Env's complete flag. | |
| 144 // It will be at "<Root>/complete.flag". | |
| 145 completeFlagPath string | |
| 146 } | |
| 147 | |
| 148 // InterpreterCommand returns a Python interpreter Command pointing to the | |
| 149 // VirtualEnv's Python installation. | |
| 150 func (e *Env) InterpreterCommand() *python.Command { | |
| 151 i := python.Interpreter{ | |
| 152 Python: e.Python, | |
| 153 } | |
| 154 cmd := i.Command() | |
| 155 cmd.Isolated = true | |
| 156 return cmd | |
| 157 } | |
| 158 | |
| 159 func (e *Env) setupImpl(c context.Context, blocking bool) error { | |
| 160 // Repeatedly try and create our Env. We do this so that if we | |
| 161 // encounter a lock, we will let the other process finish and try and le verage | |
| 162 // its success. | |
| 163 for { | |
| 164 // We will be creating the Env. We will lock around a file for t his Env hash | |
| 165 // so that any other processes that may be trying to simultaneou sly | |
| 166 // manipulate Env will be forced to wait. | |
| 167 err := fslock.With(e.lockPath, func() error { | |
| 168 // Check for completion flag. | |
| 169 if _, err := os.Stat(e.completeFlagPath); err != nil { | |
| 170 // No complete flag. Create a new VirtualEnv her e. | |
| 171 if err := e.createLocked(c); err != nil { | |
| 172 return errors.Annotate(err).Reason("fail ed to create new VirtualEnv").Err() | |
| 173 } | |
| 174 | |
| 175 // Successfully created the environment! Mark th is with a completion | |
| 176 // flag. | |
| 177 if err := e.touchCompleteFlagLocked(); err != ni l { | |
| 178 return errors.Annotate(err).Reason("fail ed to create complete flag").Err() | |
| 179 } | |
| 180 } else { | |
| 181 // Fast path: if our complete flag is present, a ssume that the | |
| 182 // environment is setup and complete. No locking or additional work necessary. | |
| 183 logging.Debugf(c, "Completion flag found! Enviro nment is set-up: %s", e.completeFlagPath) | |
|
iannucci
2017/03/11 00:37:34
we don't touch the file at all any more in this ca
dnj
2017/03/11 01:13:46
We touch it repeatedly in the keep-alive monitor g
| |
| 184 } | |
| 185 | |
| 186 return nil | |
| 187 }) | |
| 188 switch err { | |
| 189 case nil: | |
| 190 return nil | |
| 191 | |
| 192 case fslock.ErrLockHeld: | |
| 193 if !blocking { | |
| 194 return errors.Annotate(err).Reason("VirtualEnv l ock is currently held (non-blocking)").Err() | |
| 195 } | |
| 196 | |
| 197 // Some other process holds the lock. Sleep a little and retry. | |
| 198 logging.Warningf(c, "VirtualEnv lock is currently held. Retrying after delay (%s)...", lockHeldDelay) | |
| 199 if tr := clock.Sleep(c, lockHeldDelay); tr.Incomplete() { | |
| 200 return tr.Err | |
| 201 } | |
| 202 continue | |
| 203 | |
| 204 default: | |
| 205 return errors.Annotate(err).Reason("failed to create Vir tualEnv").Err() | |
| 206 } | |
| 207 } | |
| 208 } | |
| 209 | |
| 210 func (e *Env) withLockBlocking(c context.Context, fn func() error) error { | |
| 211 return fslock.WithBlocking(e.lockPath, blocker(c), fn) | |
| 212 } | |
| 213 | |
| 214 // Delete deletes this environment, if it exists. | |
| 215 func (e *Env) Delete(c context.Context) error { | |
| 216 err := e.withLockBlocking(c, func() error { | |
| 217 if err := e.deleteLocked(c); err != nil { | |
| 218 return errors.Annotate(err).Err() | |
| 219 } | |
| 220 return nil | |
| 221 }) | |
| 222 if err != nil { | |
| 223 errors.Annotate(err).Reason("failed to delete environment").Err( ) | |
| 224 } | |
| 225 return nil | |
| 226 } | |
| 227 | |
| 228 func (e *Env) createLocked(c context.Context) error { | |
| 229 // If our root directory already exists, delete it. | |
| 230 if _, err := os.Stat(e.Root); err == nil { | |
| 231 logging.Warningf(c, "Deleting existing VirtualEnv: %s", e.Root) | |
| 232 if err := filesystem.RemoveAll(e.Root); err != nil { | |
| 233 return errors.Reason("failed to remove existing root").E rr() | |
| 234 } | |
| 235 } | |
| 236 | |
| 237 // Make sure our environment's base directory exists. | |
| 238 if err := filesystem.MakeDirs(e.Root); err != nil { | |
| 239 return errors.Annotate(err).Reason("failed to create environment root").Err() | |
| 240 } | |
| 241 logging.Infof(c, "Using virtual environment root: %s", e.Root) | |
| 242 | |
| 243 // Build our package list. Always install our base VirtualEnv package. | |
| 244 packages := make([]*vpython.Spec_Package, 1, 1+len(e.Config.Spec.Wheel)) | |
| 245 packages[0] = e.Config.Spec.Virtualenv | |
| 246 packages = append(packages, e.Config.Spec.Wheel...) | |
| 247 | |
| 248 bootstrapDir := filepath.Join(e.Root, ".vpython_bootstrap") | |
| 249 pkgDir := filepath.Join(bootstrapDir, "packages") | |
| 250 if err := filesystem.MakeDirs(pkgDir); err != nil { | |
| 251 return errors.Annotate(err).Reason("could not create bootstrap p ackages directory").Err() | |
| 252 } | |
| 253 | |
| 254 if err := e.downloadPackages(c, pkgDir, packages); err != nil { | |
| 255 return errors.Annotate(err).Reason("failed to download packages" ).Err() | |
| 256 } | |
| 257 | |
| 258 // Installing base VirtualEnv. | |
| 259 if err := e.installVirtualEnv(c, pkgDir); err != nil { | |
| 260 return errors.Annotate(err).Reason("failed to install VirtualEnv ").Err() | |
| 261 } | |
| 262 | |
| 263 // Install our wheel files. | |
| 264 if len(e.Config.Spec.Wheel) > 0 { | |
| 265 // Install wheels into our VirtualEnv. | |
| 266 if err := e.installWheels(c, bootstrapDir, pkgDir); err != nil { | |
| 267 return errors.Annotate(err).Reason("failed to install wh eels").Err() | |
| 268 } | |
| 269 } | |
| 270 | |
| 271 // Write our specification file. | |
| 272 if err := spec.Write(e.Config.Spec, e.SpecPath); err != nil { | |
| 273 return errors.Annotate(err).Reason("failed to write spec file to : %(path)s"). | |
| 274 D("path", e.SpecPath). | |
| 275 Err() | |
| 276 } | |
| 277 logging.Debugf(c, "Wrote specification file to: %s", e.SpecPath) | |
| 278 | |
| 279 // Finalize our VirtualEnv for bootstrap execution. | |
| 280 if err := e.finalize(c, bootstrapDir); err != nil { | |
| 281 return errors.Annotate(err).Reason("failed to prepare VirtualEnv ").Err() | |
| 282 } | |
| 283 | |
| 284 return nil | |
| 285 } | |
| 286 | |
| 287 func (e *Env) downloadPackages(c context.Context, dst string, packages []*vpytho n.Spec_Package) error { | |
| 288 // Create a wheel sub-directory underneath of root. | |
| 289 logging.Debugf(c, "Loading %d package(s) into: %s", len(packages), dst) | |
| 290 if err := e.Config.Loader.Ensure(c, dst, packages); err != nil { | |
| 291 return errors.Annotate(err).Reason("failed to download packages" ).Err() | |
| 292 } | |
| 293 return nil | |
| 294 } | |
| 295 | |
| 296 func (e *Env) installVirtualEnv(c context.Context, pkgDir string) error { | |
| 297 // Create our VirtualEnv package staging sub-directory underneath of roo t. | |
| 298 bsDir := filepath.Join(e.Root, ".virtualenv") | |
| 299 if err := filesystem.MakeDirs(bsDir); err != nil { | |
| 300 return errors.Annotate(err).Reason("failed to create VirtualEnv bootstrap directory"). | |
| 301 D("path", bsDir). | |
| 302 Err() | |
| 303 } | |
| 304 | |
| 305 // Identify the virtualenv directory: will have "virtualenv-" prefix. | |
| 306 matches, err := filepath.Glob(filepath.Join(pkgDir, "virtualenv-*")) | |
| 307 if err != nil { | |
| 308 return errors.Annotate(err).Reason("failed to glob for 'virtuale nv-' directory").Err() | |
| 309 } | |
| 310 if len(matches) == 0 { | |
| 311 return errors.Reason("no 'virtualenv-' directory provided by pac kage").Err() | |
| 312 } | |
| 313 | |
| 314 logging.Debugf(c, "Creating VirtualEnv at: %s", e.Root) | |
| 315 i := e.Config.systemInterpreter() | |
| 316 i.WorkDir = matches[0] | |
| 317 err = i.Run(c, | |
| 318 "virtualenv.py", | |
| 319 "--no-download", | |
| 320 e.Root) | |
| 321 if err != nil { | |
| 322 return errors.Annotate(err).Reason("failed to create VirtualEnv" ).Err() | |
| 323 } | |
| 324 | |
| 325 return nil | |
| 326 } | |
| 327 | |
| 328 func (e *Env) installWheels(c context.Context, bootstrapDir, pkgDir string) erro r { | |
| 329 // Identify all downloaded wheels and parse them. | |
| 330 wheels, err := wheel.ScanDir(pkgDir) | |
| 331 if err != nil { | |
| 332 return errors.Annotate(err).Reason("failed to load wheels").Err( ) | |
| 333 } | |
| 334 | |
| 335 // Build a "wheel" requirements file. | |
| 336 reqPath := filepath.Join(bootstrapDir, "requirements.txt") | |
| 337 logging.Debugf(c, "Rendering requirements file to: %s", reqPath) | |
| 338 if err := wheel.WriteRequirementsFile(reqPath, wheels); err != nil { | |
| 339 return errors.Annotate(err).Reason("failed to render requirement s file").Err() | |
| 340 } | |
| 341 | |
| 342 cmd := e.venvInterpreter() | |
| 343 err = cmd.Run(c, | |
| 344 "-m", "pip", | |
| 345 "install", | |
| 346 "--use-wheel", | |
| 347 "--compile", | |
| 348 "--no-index", | |
| 349 "--find-links", pkgDir, | |
| 350 "--requirement", reqPath) | |
| 351 if err != nil { | |
| 352 return errors.Annotate(err).Reason("failed to install wheels").E rr() | |
| 353 } | |
| 354 return nil | |
| 355 } | |
| 356 | |
| 357 func (e *Env) finalize(c context.Context, bootstrapDir string) error { | |
| 358 // Uninstall "pip" and "wheel", preventing (easy) augmentation of the | |
| 359 // environment. | |
| 360 if !e.Config.testPreserveInstallationCapability { | |
| 361 cmd := e.venvInterpreter() | |
| 362 err := cmd.Run(c, | |
| 363 "-m", "pip", | |
| 364 "uninstall", | |
| 365 "--quiet", | |
| 366 "--yes", | |
| 367 "pip", "wheel") | |
| 368 if err != nil { | |
| 369 return errors.Annotate(err).Reason("failed to install wh eels").Err() | |
| 370 } | |
| 371 } | |
| 372 | |
| 373 // Delete our bootstrap directory (non-fatal). | |
| 374 if err := filesystem.RemoveAll(bootstrapDir); err != nil { | |
| 375 logging.WithError(err).Warningf(c, "Failed to delete bootstrap d irectory: %s", bootstrapDir) | |
| 376 } | |
| 377 | |
| 378 // Change all files to read-only, except: | |
| 379 // - Our root directory, which must be writable in order to update our | |
| 380 // completion flag. | |
| 381 // - Our completion flag, which must be trivially re-writable. | |
| 382 if !e.Config.testLeaveReadWrite { | |
| 383 err := filesystem.MakeReadOnly(e.Root, func(path string) bool { | |
| 384 switch path { | |
| 385 case e.Root, e.completeFlagPath: | |
| 386 return false | |
| 387 default: | |
| 388 return true | |
| 389 } | |
| 390 }) | |
| 391 if err != nil { | |
| 392 return errors.Annotate(err).Reason("failed to mark envir onment read-only").Err() | |
| 393 } | |
| 394 } | |
| 395 return nil | |
| 396 } | |
| 397 | |
| 398 func (e *Env) venvInterpreter() *python.Command { | |
| 399 cmd := e.InterpreterCommand() | |
| 400 cmd.WorkDir = e.Root | |
| 401 return cmd | |
| 402 } | |
| 403 | |
| 404 // touchCompleteFlag touches the complete flag, creating it and/or | |
| 405 // updating its timestamp. | |
| 406 func (e *Env) touchCompleteFlag(c context.Context) error { | |
| 407 err := e.withLockBlocking(c, e.touchCompleteFlagLocked) | |
| 408 if err != nil { | |
| 409 return errors.Annotate(err).Reason("failed to touch complete fla g").Err() | |
| 410 } | |
| 411 return err | |
| 412 } | |
| 413 | |
| 414 func (e *Env) touchCompleteFlagLocked() error { | |
| 415 if err := filesystem.Touch(e.completeFlagPath, time.Time{}, 0644); err ! = nil { | |
| 416 return errors.Annotate(err).Err() | |
| 417 } | |
| 418 return nil | |
| 419 } | |
| 420 | |
| 421 func (e *Env) deleteLocked(c context.Context) error { | |
| 422 // Delete our environment directory. | |
| 423 if err := filesystem.RemoveAll(e.Root); err != nil { | |
| 424 return errors.Annotate(err).Reason("failed to delete environment root").Err() | |
| 425 } | |
| 426 | |
| 427 // Delete our lock path. | |
| 428 if err := os.Remove(e.lockPath); err != nil { | |
| 429 return errors.Annotate(err).Reason("failed to delete lock").Err( ) | |
| 430 } | |
| 431 return nil | |
| 432 } | |
| 433 | |
| 434 // keepAliveMonitor periodically refreshes the environment's "completed" flag | |
| 435 // timestamp. | |
| 436 // | |
| 437 // It runs in its own goroutine. | |
| 438 func (e *Env) keepAliveMonitor(c context.Context, interval time.Duration, cancel Func context.CancelFunc) { | |
| 439 timer := clock.NewTimer(c) | |
| 440 defer timer.Stop() | |
| 441 | |
| 442 for { | |
| 443 logging.Debugf(c, "Keep-alive: Sleeping %s...", interval) | |
| 444 timer.Reset(interval) | |
| 445 select { | |
| 446 case <-c.Done(): | |
| 447 logging.WithError(c.Err()).Debugf(c, "Keep-alive: monito r's Context was cancelled.") | |
| 448 return | |
| 449 | |
| 450 case tr := <-timer.GetC(): | |
| 451 if tr.Err != nil { | |
| 452 logging.WithError(tr.Err).Debugf(c, "Keep-alive: monitor's timer finished.") | |
| 453 return | |
| 454 } | |
| 455 } | |
| 456 | |
| 457 logging.Debugf(c, "Keep-alive: Updating completed flag timestamp .") | |
| 458 if err := e.touchCompleteFlag(c); err != nil { | |
| 459 errors.Log(c, errors.Annotate(err).Reason("failed to ref resh timestamp").Err()) | |
| 460 cancelFunc() | |
| 461 return | |
| 462 } | |
| 463 } | |
| 464 } | |
| OLD | NEW |