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