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 "time" | |
| 11 | |
| 12 "github.com/luci/luci-go/vpython/api/env" | |
| 13 "github.com/luci/luci-go/vpython/filesystem" | |
| 14 "github.com/luci/luci-go/vpython/python" | |
| 15 "github.com/luci/luci-go/vpython/spec" | |
| 16 "github.com/luci/luci-go/vpython/wheel" | |
| 17 | |
| 18 "github.com/luci/luci-go/common/clock" | |
| 19 "github.com/luci/luci-go/common/errors" | |
| 20 "github.com/luci/luci-go/common/logging" | |
| 21 | |
| 22 "github.com/danjacques/gofslock/fslock" | |
| 23 "golang.org/x/net/context" | |
| 24 ) | |
| 25 | |
| 26 const lockHeldDelay = 5 * time.Second | |
| 27 | |
| 28 // blocker is an fslock.Blocker implementation that sleeps lockHeldDelay in | |
| 29 // between attempts. | |
| 30 func blocker(c context.Context) fslock.Blocker { | |
| 31 return func() error { | |
| 32 logging.Debugf(c, "Lock is currently held. Sleeping %v and retry ing...", lockHeldDelay) | |
| 33 clock.Sleep(c, lockHeldDelay) | |
| 34 return nil | |
| 35 } | |
| 36 } | |
| 37 | |
| 38 // EnvRootFromSpecPath calculates the environment root from an exported | |
| 39 // environment specification file path. | |
| 40 // | |
| 41 // The specification path is: <EnvRoot>/<SpecHash>/<spec>, so our EnvRoot | |
| 42 // is two directories up. | |
| 43 // | |
| 44 // We export EnvSpecPath as an asbolute path. However, since someone else | |
| 45 // could have overridden it or exported their own, let's make sure. | |
| 46 func EnvRootFromSpecPath(path string) (string, error) { | |
| 47 if err := filesystem.AbsPath(&path); err != nil { | |
| 48 return "", errors.Annotate(err). | |
| 49 Reason("failed to get absolute path for specification fi le path: %(path)s"). | |
| 50 Err() | |
| 51 } | |
| 52 return filepath.Dir(filepath.Dir(path)), nil | |
| 53 } | |
| 54 | |
| 55 // Env is a fully set-up Python virtual enviornment. It is configured | |
| 56 // based on the contents of an env.Spec file by Setup. | |
| 57 // | |
| 58 // Env should not be instantiated directly; it must be created by calling | |
| 59 // Config.Env. | |
| 60 // | |
| 61 // All paths in Env are absolute. | |
| 62 type Env struct { | |
| 63 // Config is this Env's Config, fully-resolved. | |
| 64 Config *Config | |
| 65 | |
| 66 // Root is the Env container's root directory path. | |
| 67 Root string | |
| 68 | |
| 69 // Python is the path to the Env Python interpreter. | |
| 70 Python string | |
| 71 | |
| 72 // SepcPath is the path to the specification file that was used to const ruct | |
| 73 // this enviornment. It will be in text protobuf format, and, therefore, | |
| 74 // suitable for input to other "vpython" invocations. | |
| 75 SpecPath string | |
| 76 | |
| 77 // name is the hash of the specification file for this Env. | |
| 78 name string | |
| 79 // lockPath is the path to this Env-specific lock file. It will be at: | |
| 80 // "<baseDir>/.<name>.lock". | |
| 81 lockPath string | |
| 82 // completeFlagPath is the path to this Env's complete flag. | |
| 83 // It will be at "<Root>/complete.flag". | |
| 84 completeFlagPath string | |
| 85 } | |
| 86 | |
| 87 // Setup creates a new Env. | |
| 88 // | |
| 89 // It will lock around the Env to ensure that multiple processes do not | |
| 90 // conflict with each other. If a Env for this specification already | |
| 91 // exists, it will be used directly without any additional setup. | |
| 92 // | |
| 93 // If another process holds the lock and blocking is true, we will wait for our | |
| 94 // turn at the lock. Otherwise, we will return immediately with a locking error. | |
| 95 func (e *Env) Setup(c context.Context, blocking bool) error { | |
| 96 if err := e.setupImpl(c, blocking); err != nil { | |
| 97 return errors.Annotate(err).Err() | |
| 98 } | |
| 99 | |
| 100 // Perform a pruning round. Failure is non-fatal. | |
| 101 if perr := prune(c, e.Config, e.name); perr != nil { | |
| 102 logging.WithError(perr).Warningf(c, "Failed to perform pruning r ound after initialization.") | |
| 103 } | |
| 104 return nil | |
| 105 } | |
| 106 | |
| 107 func (e *Env) setupImpl(c context.Context, blocking bool) error { | |
| 108 // Repeatedly try and create our Env. We do this so that if we | |
| 109 // encounter a lock, we will let the other process finish and try and le verage | |
| 110 // its success. | |
| 111 for { | |
| 112 // Fast path: if our complete flag is present, assume that the e nvironment | |
| 113 // is setup and complete. No locking or additional work necessar y. | |
| 114 if _, err := os.Stat(e.completeFlagPath); err == nil { | |
| 115 logging.Debugf(c, "Completion flag found! Environment is set-up: %s", e.completeFlagPath) | |
| 116 | |
| 117 // Update the complete flag so the timestamp reflects ou r usage of it. | |
| 118 // This is non-fatal if it fails. | |
|
iannucci
2017/02/23 00:54:21
couldn't this mean that we return and this env mig
dnj
2017/02/23 20:38:50
Hm, yeah that sounds reasonable.
| |
| 119 if err := e.touchCompleteFlag(); err != nil { | |
| 120 logging.WithError(err).Warningf(c, "Failed to up date complete flag.") | |
| 121 } | |
| 122 | |
| 123 return nil | |
| 124 } | |
| 125 | |
| 126 // We will be creating the Env. We will to lock around a file fo r this | |
|
iannucci
2017/02/23 00:54:21
'will to lock'
dnj
2017/02/23 20:38:49
Done.
| |
| 127 // Env hash so that any other processes that may be trying to | |
| 128 // simultaneously create a Env will be forced to wait. | |
| 129 err := fslock.With(e.lockPath, func() error { | |
| 130 // Mark that we hit some lock contention. If we did, we will try again | |
| 131 // from scratch. | |
| 132 if err := e.createLocked(c); err != nil { | |
| 133 return errors.Annotate(err).Reason("failed to cr eate new VirtualEnv").Err() | |
| 134 } | |
| 135 return nil | |
| 136 }) | |
| 137 switch err { | |
| 138 case nil: | |
| 139 // Successfully created the environment! Mark this with a completion flag. | |
| 140 if err := e.touchCompleteFlag(); err != nil { | |
| 141 return errors.Annotate(err).Reason("failed to cr eate complete flag").Err() | |
|
iannucci
2017/02/23 00:54:21
shouldn't we loop? at least up to some limit?
dnj
2017/02/23 20:38:49
I don't think so. Touching a file should be pretty
| |
| 142 } | |
| 143 return nil | |
| 144 | |
| 145 case fslock.ErrLockHeld: | |
| 146 if !blocking { | |
| 147 return errors.Annotate(err).Reason("VirtualEnv l ock is currently held (non-blocking)").Err() | |
| 148 } | |
| 149 | |
| 150 // Some other process holds the lock. Sleep a little and retry. | |
| 151 logging.Warningf(c, "VirtualEnv lock is currently held. Retrying after delay (%s)...", | |
| 152 lockHeldDelay) | |
| 153 if tr := clock.Sleep(c, lockHeldDelay); tr.Incomplete() { | |
| 154 return tr.Err | |
| 155 } | |
| 156 continue | |
| 157 | |
| 158 default: | |
| 159 return errors.Annotate(err).Reason("failed to create Vir tualEnv").Err() | |
| 160 } | |
| 161 } | |
| 162 } | |
| 163 | |
| 164 // Delete deletes this enviornment, if it exists. | |
| 165 func (e *Env) Delete(c context.Context) error { | |
| 166 err := fslock.WithBlocking(e.lockPath, blocker(c), func() error { | |
| 167 if err := e.deleteLocked(c); err != nil { | |
| 168 return errors.Annotate(err).Err() | |
| 169 } | |
| 170 return nil | |
| 171 }) | |
| 172 if err != nil { | |
| 173 errors.Annotate(err).Reason("failed to delete enviornment").Err( ) | |
| 174 } | |
| 175 return nil | |
| 176 } | |
| 177 | |
| 178 func (e *Env) createLocked(c context.Context) error { | |
| 179 // If our root directory already exists, delete it. | |
| 180 if _, err := os.Stat(e.Root); err == nil { | |
| 181 logging.Warningf(c, "Deleting existing VirtualEnv: %s", e.Root) | |
| 182 if err := filesystem.RemoveAll(e.Root); err != nil { | |
| 183 return errors.Reason("failed to remove existing root").E rr() | |
| 184 } | |
| 185 } | |
| 186 | |
| 187 // Make sure our environment's base directory exists. | |
| 188 if err := filesystem.MakeDirs(e.Root); err != nil { | |
| 189 return errors.Annotate(err).Reason("failed to create environment root").Err() | |
| 190 } | |
| 191 logging.Infof(c, "Using virtual environment root: %s", e.Root) | |
| 192 | |
| 193 // Build our package list. Always install our base VirtualEnv package. | |
| 194 // For resolution purposes, our VirtualEnv package will be index 0. | |
|
iannucci
2017/02/23 00:54:21
why? why not just sort them?
The Spec_Package are
dnj
2017/02/23 20:38:49
Doesn't really matter. The comment isn't too usefu
| |
| 195 packages := make([]*env.Spec_Package, 1, 1+len(e.Config.Spec.Wheel)) | |
| 196 packages[0] = e.Config.Spec.Virtualenv | |
| 197 packages = append(packages, e.Config.Spec.Wheel...) | |
| 198 | |
| 199 bootstrapDir := filepath.Join(e.Root, ".vpython_bootstrap") | |
| 200 pkgDir := filepath.Join(bootstrapDir, "packages") | |
|
iannucci
2017/02/23 00:54:21
we should make sure to delete this directory after
dnj
2017/02/23 20:38:50
We do, yeah. See "finalize".
| |
| 201 if err := filesystem.MakeDirs(pkgDir); err != nil { | |
| 202 return errors.Annotate(err).Reason("could not create bootstrap p ackages directory").Err() | |
| 203 } | |
| 204 | |
| 205 if err := e.downloadPackages(c, pkgDir, packages); err != nil { | |
| 206 return errors.Annotate(err).Reason("failed to download packages" ).Err() | |
| 207 } | |
| 208 | |
| 209 // Installing base VirtualEnv. | |
| 210 if err := e.installVirtualEnv(c, pkgDir); err != nil { | |
| 211 return errors.Annotate(err).Reason("failed to install VirtualEnv ").Err() | |
| 212 } | |
| 213 | |
| 214 // Download our wheel files. | |
|
iannucci
2017/02/23 00:54:21
Install?
dnj
2017/02/23 20:38:50
Done.
| |
| 215 if len(e.Config.Spec.Wheel) > 0 { | |
| 216 // Install wheels into our VirtualEnv. | |
| 217 if err := e.installWheels(c, bootstrapDir, pkgDir); err != nil { | |
| 218 return errors.Annotate(err).Reason("failed to install wh eels").Err() | |
| 219 } | |
| 220 } | |
| 221 | |
| 222 // Write our specification file. | |
| 223 if err := spec.Write(e.Config.Spec, e.SpecPath); err != nil { | |
| 224 return errors.Annotate(err).Reason("failed to write spec file to : %(path)s"). | |
| 225 D("path", e.SpecPath). | |
| 226 Err() | |
| 227 } | |
| 228 logging.Debugf(c, "Wrote specification file to: %s", e.SpecPath) | |
| 229 | |
| 230 // Finalize our VirtualEnv for bootstrap execution. | |
| 231 if err := e.finalize(c, bootstrapDir); err != nil { | |
| 232 return errors.Annotate(err).Reason("failed to prepare VirtualEnv ").Err() | |
| 233 } | |
| 234 | |
| 235 return nil | |
| 236 } | |
| 237 | |
| 238 func (e *Env) downloadPackages(c context.Context, dst string, packages []*env.Sp ec_Package) error { | |
| 239 // Create a wheel sub-directory underneath of root. | |
| 240 logging.Debugf(c, "Loading %d package(s) into: %s", len(packages), dst) | |
| 241 if err := e.Config.Loader.Ensure(c, dst, packages); err != nil { | |
| 242 return errors.Annotate(err).Reason("failed to download packages" ).Err() | |
|
iannucci
2017/02/23 00:54:21
pro tip: errors.Annotate(nil).Reason(...).D(...).E
dnj
2017/02/23 20:38:50
For me, this is too much on a single line :)
| |
| 243 } | |
| 244 return nil | |
| 245 } | |
| 246 | |
| 247 func (e *Env) installVirtualEnv(c context.Context, pkgDir string) error { | |
| 248 // Create our VirtualEnv package staging sub-directory underneath of roo t. | |
| 249 bsDir := filepath.Join(e.Root, ".virtualenv") | |
| 250 if err := filesystem.MakeDirs(bsDir); err != nil { | |
| 251 return errors.Annotate(err).Reason("failed to create VirtualEnv bootstrap directory"). | |
| 252 D("path", bsDir). | |
| 253 Err() | |
| 254 } | |
| 255 | |
| 256 // Identify the virtualenv directory: will have "virtualenv-" prefix. | |
| 257 matches, err := filepath.Glob(filepath.Join(pkgDir, "virtualenv-*")) | |
| 258 if err != nil { | |
| 259 return errors.Annotate(err).Reason("failed to glob for 'virtuale nv-' directory").Err() | |
| 260 } | |
| 261 if len(matches) == 0 { | |
| 262 return errors.Reason("no 'virtualenv-' directory provided by pac kage").Err() | |
| 263 } | |
| 264 | |
| 265 logging.Debugf(c, "Creating VirtualEnv at: %s", e.Root) | |
| 266 i := e.Config.systemInterpreter() | |
| 267 i.WorkDir = matches[0] | |
| 268 err = i.Run(c, | |
| 269 "virtualenv.py", | |
| 270 "--no-download", | |
| 271 e.Root) | |
| 272 if err != nil { | |
| 273 return errors.Annotate(err).Reason("failed to create VirtualEnv" ).Err() | |
| 274 } | |
| 275 | |
| 276 return nil | |
| 277 } | |
| 278 | |
| 279 func (e *Env) installWheels(c context.Context, bootstrapDir, pkgDir string) erro r { | |
| 280 // Identify all downloaded wheels and parse them. | |
| 281 wheels, err := wheel.ScanDir(pkgDir) | |
| 282 if err != nil { | |
| 283 return errors.Annotate(err).Reason("failed to load wheels").Err( ) | |
| 284 } | |
| 285 | |
| 286 // Build a "wheel" requirements file. | |
| 287 reqPath := filepath.Join(bootstrapDir, "requirements.txt") | |
| 288 logging.Debugf(c, "Rendering requirements file to: %s", reqPath) | |
| 289 if err := wheel.WriteRequirementsFile(reqPath, wheels); err != nil { | |
| 290 return errors.Annotate(err).Reason("failed to render requirement s file").Err() | |
| 291 } | |
| 292 | |
| 293 cmd := e.venvInterpreter() | |
| 294 err = cmd.Run(c, | |
| 295 "-m", "pip", | |
| 296 "install", | |
| 297 "--use-wheel", | |
| 298 "--compile", | |
| 299 "--no-index", | |
| 300 "--find-links", pkgDir, | |
| 301 "--requirement", reqPath) | |
| 302 if err != nil { | |
| 303 return errors.Annotate(err).Reason("failed to install wheels").E rr() | |
| 304 } | |
| 305 return nil | |
| 306 } | |
| 307 | |
| 308 func (e *Env) finalize(c context.Context, bootstrapDir string) error { | |
| 309 // Uninstall "pip" and "wheel", preventing (easy) augmentation of the | |
| 310 // enviornment. | |
| 311 cmd := e.venvInterpreter() | |
| 312 err := cmd.Run(c, | |
| 313 "-m", "pip", | |
| 314 "uninstall", | |
| 315 "--quiet", | |
| 316 "--yes", | |
| 317 "pip", "wheel") | |
| 318 if err != nil { | |
| 319 return errors.Annotate(err).Reason("failed to install wheels").E rr() | |
| 320 } | |
| 321 | |
| 322 // Delete our bootstrap directory (non-fatal). | |
|
iannucci
2017/02/23 00:54:21
ah, this is the place we delete it
dnj
2017/02/23 20:38:50
Acknowledged.
| |
| 323 if err := filesystem.RemoveAll(bootstrapDir); err != nil { | |
| 324 logging.WithError(err).Warningf(c, "Failed to delete bootstrap d irectory: %s", bootstrapDir) | |
| 325 } | |
| 326 | |
| 327 // Change all files to read-only, except: | |
| 328 // - Our root directory, which must be writable in order to update our | |
| 329 // completion flag. | |
| 330 // - Our completion flag, which must be trivially re-writable. | |
| 331 err = filesystem.MakeReadOnly(e.Root, func(path string) bool { | |
| 332 switch path { | |
| 333 case e.Root, e.completeFlagPath: | |
| 334 return false | |
| 335 default: | |
| 336 return true | |
| 337 } | |
| 338 }) | |
| 339 if err != nil { | |
| 340 return errors.Annotate(err).Reason("failed to mark environment r ead-only").Err() | |
| 341 } | |
| 342 return nil | |
| 343 } | |
| 344 | |
| 345 func (e *Env) venvInterpreter() *python.Command { | |
| 346 cmd := e.InterpreterCommand() | |
| 347 cmd.WorkDir = e.Root | |
| 348 return cmd | |
| 349 } | |
| 350 | |
| 351 // InterpreterCommand returns a Python interpreter Command pointing to the | |
| 352 // VirtualEnv's Python installation. | |
| 353 func (e *Env) InterpreterCommand() *python.Command { | |
| 354 i := python.Interpreter{ | |
| 355 Python: e.Python, | |
| 356 } | |
| 357 cmd := i.Command() | |
| 358 cmd.Isolated = true | |
| 359 return cmd | |
| 360 } | |
| 361 | |
| 362 // touchCompleteFlag touches the complete flag, creating it and/or updating its | |
| 363 // timestamp. | |
| 364 // | |
| 365 // This is safe to call without the lock held, since worst-case an update is | |
| 366 // overwritten if contested. | |
| 367 func (e *Env) touchCompleteFlag() error { | |
| 368 if err := filesystem.Touch(e.completeFlagPath, 0644); err != nil { | |
| 369 return errors.Annotate(err).Err() | |
| 370 } | |
| 371 return nil | |
| 372 } | |
| 373 | |
| 374 func (e *Env) deleteLocked(c context.Context) error { | |
| 375 // Delete our environment directory. | |
| 376 if err := filesystem.RemoveAll(e.Root); err != nil { | |
| 377 return errors.Annotate(err).Reason("failed to delete environment root").Err() | |
| 378 } | |
| 379 | |
| 380 // Delete our lock path. | |
| 381 if err := os.Remove(e.lockPath); err != nil { | |
| 382 return errors.Annotate(err).Reason("failed to delete lock").Err( ) | |
| 383 } | |
| 384 return nil | |
| 385 } | |
| OLD | NEW |