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 python | |
| 6 | |
| 7 import ( | |
| 8 "os" | |
| 9 "os/exec" | |
| 10 "strings" | |
| 11 "sync" | |
| 12 | |
| 13 "github.com/luci/luci-go/common/errors" | |
| 14 "github.com/luci/luci-go/common/logging" | |
| 15 "github.com/luci/luci-go/common/system/exitcode" | |
| 16 | |
| 17 "golang.org/x/net/context" | |
| 18 ) | |
| 19 | |
| 20 type runnerFunc func(cmd *exec.Cmd, capture bool) (string, error) | |
| 21 | |
| 22 // Interpreter is a configured Python interpreter. | |
|
iannucci
2017/02/21 10:08:16
maybe
Interpreter is a runnable Python interprete
dnj
2017/02/21 23:21:35
Done.
| |
| 23 type Interpreter struct { | |
| 24 // Python is the path to the Python interpreter. | |
|
iannucci
2017/02/21 10:08:16
absolute?
dnj
2017/02/21 23:21:35
Done.
| |
| 25 Python string | |
| 26 | |
| 27 // WorkDir is the working directory to use. | |
|
iannucci
2017/02/21 10:08:16
use for what?
dnj
2017/02/21 23:21:35
Done.
| |
| 28 WorkDir string | |
| 29 | |
| 30 // ConnectSTDIN, if true, says that STDIN should also be connected. | |
|
iannucci
2017/02/21 10:08:16
ConnectSTDIN will cause stdin to be passed through
dnj
2017/02/21 23:21:35
Done.
| |
| 31 ConnectSTDIN bool | |
| 32 | |
| 33 // Isolated means that the Python invocation should include flags to iso late | |
| 34 // it from local system modification. | |
|
iannucci
2017/02/21 10:08:16
Like what? "Local system modification"? From the c
dnj
2017/02/21 23:21:35
Done.
| |
| 35 Isolated bool | |
| 36 | |
| 37 // Env, if not nil, is the environment to supply. | |
| 38 Env []string | |
| 39 | |
| 40 // runner is the runner function to use to run an exec.Cmd. This can be | |
| 41 // swapped out for testing. | |
| 42 runner runnerFunc | |
|
iannucci
2017/02/21 10:08:16
lets prefix all testing members with 'testing'
dnj
2017/02/21 23:21:35
Done.
| |
| 43 | |
| 44 // cachedVersion is the cached Version for this interpreter. It is popul ated | |
| 45 // on the first GetVersion call. | |
| 46 cachedVersion *Version | |
| 47 cachedVersionMu sync.Mutex | |
|
iannucci
2017/02/21 10:08:16
sync.Once?
dnj
2017/02/21 23:21:35
Because there can be an error return code, I would
| |
| 48 } | |
| 49 | |
| 50 func (i *Interpreter) getRunnerFunc() runnerFunc { | |
| 51 if i.runner != nil { | |
| 52 return i.runner | |
| 53 } | |
| 54 | |
| 55 // Return default runner (actually run the process). | |
| 56 return func(cmd *exec.Cmd, capture bool) (string, error) { | |
| 57 // If we're capturing output, combine STDOUT and STDERR (see Get Version | |
| 58 // for details). | |
| 59 if capture { | |
| 60 out, err := cmd.CombinedOutput() | |
| 61 if err != nil { | |
| 62 return "", errors.Annotate(err).Err() | |
| 63 } | |
| 64 return string(out), nil | |
| 65 } | |
| 66 | |
| 67 // Non-capturing run. | |
| 68 if err := cmd.Run(); err != nil { | |
| 69 return "", errors.Annotate(err).Err() | |
| 70 } | |
| 71 return "", nil | |
| 72 } | |
| 73 } | |
| 74 | |
| 75 // Run runs the configured Interpreter with the supplied arguments. | |
|
iannucci
2017/02/21 10:08:16
Run executes the python interpreter with the suppl
dnj
2017/02/21 23:21:35
I split this off into "Command()" (returns a bound
| |
| 76 // | |
| 77 // Run returns wrapped errors. Use errors.Unwrap to get the main cause, if | |
| 78 // needed. If an error occurs during setup or invocation, it will be returned | |
| 79 // directly. If the interpreter runs and returns zero, nil will be returned. If | |
| 80 // the interpreter runs and returns non-zero, an Error instance will be returned | |
| 81 // containing that return code. | |
| 82 func (i *Interpreter) Run(c context.Context, args ...string) error { | |
| 83 if i.Python == "" { | |
| 84 return errors.New("a Python interpreter must be supplied") | |
|
iannucci
2017/02/21 10:08:16
please avoid passive voice everywhere
dnj
2017/02/21 23:21:35
Done.
| |
| 85 } | |
| 86 | |
| 87 if i.Isolated { | |
| 88 args = append([]string{ | |
| 89 "-B", // Don't compile "pyo" binaries. | |
| 90 "-E", // Don't use PYTHON* enviornment variables. | |
| 91 "-s", // Don't use user 'site.py'. | |
| 92 }, args...) | |
| 93 } | |
| 94 | |
| 95 cmd := exec.CommandContext(c, i.Python, args...) | |
| 96 cmd.Stdout = os.Stdout | |
| 97 cmd.Stderr = os.Stderr | |
| 98 if i.ConnectSTDIN { | |
| 99 cmd.Stdin = os.Stdin | |
| 100 } | |
| 101 cmd.Dir = i.WorkDir | |
| 102 cmd.Env = i.Env | |
| 103 | |
| 104 if logging.IsLogging(c, logging.Debug) { | |
| 105 logging.Debugf(c, "Running Python command (cwd=%s): %s", | |
| 106 cmd.Dir, strings.Join(cmd.Args, " ")) | |
| 107 } | |
| 108 | |
| 109 // Allow testing to supply an alternative runner function. | |
| 110 rf := i.getRunnerFunc() | |
| 111 if _, err := rf(cmd, false); err != nil { | |
| 112 // If the process failed because of a non-zero return value, ret urn that | |
| 113 // as our error. | |
| 114 if rc, has := exitcode.Get(err); has { | |
| 115 return errors.Annotate(Error(rc)).Reason("Python bootstr ap returned non-zero").Err() | |
| 116 } | |
| 117 | |
| 118 return errors.Annotate(err).Reason("failed to run Python command "). | |
| 119 D("python", i.Python). | |
| 120 D("args", args). | |
| 121 Err() | |
| 122 } | |
| 123 return nil | |
| 124 } | |
| 125 | |
| 126 // GetVersion runs the specified Python interpreter with the "--version" | |
| 127 // flag and maps it to a known specification verison. | |
| 128 func (i *Interpreter) GetVersion(c context.Context) (v Version, err error) { | |
| 129 if i.Python == "" { | |
| 130 err = errors.New("a Python interpreter must be supplied") | |
| 131 return | |
| 132 } | |
| 133 | |
| 134 i.cachedVersionMu.Lock() | |
| 135 defer i.cachedVersionMu.Unlock() | |
| 136 | |
| 137 // Check again, under write-lock. | |
| 138 if i.cachedVersion != nil { | |
| 139 v = *i.cachedVersion | |
| 140 return | |
| 141 } | |
| 142 | |
| 143 // We use CombinedOutput here becuase Python2 writes the version to STDE RR, | |
| 144 // while Python3+ writes it to STDOUT. | |
| 145 cmd := exec.CommandContext(c, i.Python, "--version") | |
| 146 | |
| 147 rf := i.getRunnerFunc() | |
|
iannucci
2017/02/21 10:08:16
why not `i.run(cmd, capture)`? I think that might
dnj
2017/02/21 23:21:35
Changed to a Command struct.
| |
| 148 out, err := rf(cmd, true) | |
| 149 if err != nil { | |
| 150 err = errors.Annotate(err).Reason("failed to get Python version" ).Err() | |
| 151 return | |
| 152 } | |
| 153 if v, err = parseVersionOutput(strings.TrimSpace(out)); err != nil { | |
| 154 err = errors.Annotate(err).Err() | |
| 155 return | |
| 156 } | |
| 157 | |
| 158 i.cachedVersion = &v | |
| 159 return | |
| 160 } | |
| 161 | |
| 162 func parseVersionOutput(output string) (Version, error) { | |
| 163 // Expected output: | |
| 164 // Python X.Y.Z | |
| 165 parts := strings.SplitN(output, " ", 2) | |
| 166 if len(parts) != 2 || parts[0] != "Python" { | |
| 167 return Version{}, errors.Reason("unknown version output"). | |
| 168 D("output", output). | |
| 169 Err() | |
| 170 } | |
| 171 | |
| 172 v, err := ParseVersion(parts[1]) | |
| 173 if err != nil { | |
| 174 err = errors.Annotate(err).Reason("failed to parse version from: %(value)q"). | |
| 175 D("value", parts[1]). | |
| 176 Err() | |
| 177 return v, err | |
| 178 } | |
| 179 return v, nil | |
| 180 } | |
| OLD | NEW |