Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(79)

Side by Side Diff: vpython/venv/venv.go

Issue 2699063004: vpython: Add VirtualEnv creation package. (Closed)
Patch Set: only Python and VirtualEnv parts of the test may fail Created 3 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698