| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2017 The LUCI Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 // Package prober exports Probe, which implements logic to identify a wrapper's |
| 6 // wrapped target. In addition to basic PATH/filename lookup, Prober contains |
| 7 // logic to ensure that the wrapper is not the same software as the current |
| 8 // running instance, and enables optional hard-coded wrap target paths and |
| 9 // runtime checks. |
| 10 package prober |
| 11 |
| 12 import ( |
| 13 "os" |
| 14 "path/filepath" |
| 15 "strings" |
| 16 |
| 17 "golang.org/x/net/context" |
| 18 |
| 19 "github.com/luci/luci-go/common/errors" |
| 20 "github.com/luci/luci-go/common/logging" |
| 21 "github.com/luci/luci-go/common/system/environ" |
| 22 "github.com/luci/luci-go/common/system/filesystem" |
| 23 ) |
| 24 |
| 25 // CheckWrapperFunc is an optional function that can be implemented for a |
| 26 // Prober to check if a candidate path is a wrapper. |
| 27 type CheckWrapperFunc func(c context.Context, path string, env environ.Env) (isW
rapper bool, err error) |
| 28 |
| 29 // Probe can Locate a Target executable by probing the local system PATH. |
| 30 // |
| 31 // Target should be an executable name resolvable by exec.LookPath. On |
| 32 // Windows systems, this may omit the executable extension (e.g., "bat", "exe") |
| 33 // since that is augmented via the PATHEXT environment variable (see |
| 34 // "probe_windows.go"). |
| 35 type Probe struct { |
| 36 // Target is the name of the target (as seen by exec.LookPath) that we a
re |
| 37 // searching for. |
| 38 Target string |
| 39 |
| 40 // RelativePathOverride is a series of forward-slash-delimited paths to |
| 41 // directories relative to the wrapper executable that will be checked |
| 42 // prior to checking PATH. This allows bundles (e.g., CIPD) that include
both |
| 43 // the wrapper and a real implementation, to force the wrapper to use |
| 44 // the bundled implementation. |
| 45 RelativePathOverride []string |
| 46 |
| 47 // CheckWrapper, if not nil, is a function called on a candidate wrapper
to |
| 48 // determine whether or not that candidate is valid. |
| 49 // |
| 50 // On success, it will return isWrapper, which will be true if path is a |
| 51 // wrapper instance and false if it is not. If an error occurred during |
| 52 // checking, the error should be returned and isWrapper will be ignored.
If |
| 53 // a candidate is a wrapper, or if an error occurred during check, the |
| 54 // candidate will be discarded and the probe will continue. |
| 55 // |
| 56 // CheckWrapper should be lightweight and fast, as it may be called mult
iple |
| 57 // times. |
| 58 CheckWrapper CheckWrapperFunc |
| 59 |
| 60 // Self and Selfstat are resolved contain the path and FileInfo of the |
| 61 // currently running executable, respectively. They can both be resolved
via |
| 62 // ResolveSelf, and may be empty if resolution has not been performed, o
r if |
| 63 // the current executable could not be resolved. They may also be set |
| 64 // explicitly, bypassing the need to perform resolution. |
| 65 Self string |
| 66 SelfStat os.FileInfo |
| 67 |
| 68 // PathDirs, if not zero, contains the list of directories to search. If |
| 69 // zero, the os.PathListSeparator-delimited PATH environment variable wi
ll |
| 70 // be used. |
| 71 PathDirs []string |
| 72 } |
| 73 |
| 74 // ResolveSelf attempts to identify the current process. If successful, p's |
| 75 // Self will be set to an absolute path reference to Self, and its SelfStat |
| 76 // field will be set to the os.FileInfo for that path. |
| 77 // |
| 78 // If this process was invoked via symlink, the path to the symlink will be |
| 79 // returned if possible. |
| 80 func (p *Probe) ResolveSelf(argv0 string) error { |
| 81 if p.Self != "" { |
| 82 return nil |
| 83 } |
| 84 |
| 85 // Get the authoritative executable from the system. |
| 86 exec, err := os.Executable() |
| 87 if err != nil { |
| 88 return errors.Annotate(err).Reason("failed to get executable").E
rr() |
| 89 } |
| 90 |
| 91 execStat, err := os.Stat(exec) |
| 92 if err != nil { |
| 93 return errors.Annotate(err).Reason("failed to stat executable: %
(path)s"). |
| 94 D("path", exec). |
| 95 Err() |
| 96 } |
| 97 |
| 98 // Before using "os.Executable" result, which is known to resolve symlin
ks on |
| 99 // Linux, try and identify via argv0. |
| 100 if argv0 != "" && filesystem.AbsPath(&argv0) == nil { |
| 101 if st, err := os.Stat(argv0); err == nil && os.SameFile(execStat
, st) { |
| 102 // argv[0] is the same file as our executable, but may b
e an unresolved |
| 103 // symlink. Prefer it. |
| 104 p.Self, p.SelfStat = argv0, st |
| 105 return nil |
| 106 } |
| 107 } |
| 108 |
| 109 p.Self, p.SelfStat = exec, execStat |
| 110 return nil |
| 111 } |
| 112 |
| 113 // Locate attempts to locate the system's Target by traversing the available |
| 114 // PATH. |
| 115 // |
| 116 // cached is the cached path, passed from wrapper to wrapper through the a |
| 117 // State struct in the environment. This may be empty, if there was no cached |
| 118 // path or if the cached path was invalid. |
| 119 // |
| 120 // env is the environment to operate with, and will not be modified during |
| 121 // execution. |
| 122 func (p *Probe) Locate(c context.Context, cached string, env environ.Env) (strin
g, error) { |
| 123 // If we have a cached path, check that it exists and is executable and
use it |
| 124 // if it is. |
| 125 if cached != "" { |
| 126 switch cachedStat, err := os.Stat(cached); { |
| 127 case err == nil: |
| 128 // Use the cached path. First, pass it through a sanity
check to ensure |
| 129 // that it is not self. |
| 130 if p.SelfStat == nil || !os.SameFile(p.SelfStat, cachedS
tat) { |
| 131 logging.Debugf(c, "Using cached value: %s", cach
ed) |
| 132 return cached, nil |
| 133 } |
| 134 logging.Debugf(c, "Cached value [%s] is this wrapper [%s
]; ignoring.", cached, p.Self) |
| 135 |
| 136 case os.IsNotExist(err): |
| 137 // Our cached path doesn't exist, so we will have to loo
k for a new one. |
| 138 |
| 139 case err != nil: |
| 140 // We couldn't check our cached path, so we will have to
look for a new |
| 141 // one. This is an unexpected error, though, so emit it. |
| 142 logging.Debugf(c, "Failed to stat cached [%s]: %s", cach
ed, err) |
| 143 } |
| 144 } |
| 145 |
| 146 // Get stats on our parent directory. This may fail; if so, we'll skip t
he |
| 147 // SameFile check. |
| 148 var selfDir string |
| 149 var selfDirStat os.FileInfo |
| 150 if p.Self != "" { |
| 151 selfDir = filepath.Dir(p.Self) |
| 152 |
| 153 var err error |
| 154 if selfDirStat, err = os.Stat(selfDir); err != nil { |
| 155 logging.Debugf(c, "Failed to stat self directory [%s]: %
s", selfDir, err) |
| 156 } |
| 157 } |
| 158 |
| 159 // Walk through PATH. Our goal is to find the first program named Target
that |
| 160 // isn't self and doesn't identify as a wrapper. |
| 161 pathDirs := p.PathDirs |
| 162 if pathDirs == nil { |
| 163 pathDirs = strings.Split(env.GetEmpty("PATH"), string(os.PathLis
tSeparator)) |
| 164 } |
| 165 |
| 166 // Build our list of directories to check for Git. |
| 167 checkDirs := make([]string, 0, len(pathDirs)+len(p.RelativePathOverride)
) |
| 168 if selfDir != "" { |
| 169 for _, rpo := range p.RelativePathOverride { |
| 170 checkDirs = append(checkDirs, filepath.Join(selfDir, fil
epath.FromSlash(rpo))) |
| 171 } |
| 172 } |
| 173 checkDirs = append(checkDirs, pathDirs...) |
| 174 |
| 175 // Iterate through each check directory and look for a Git candidate wit
hin |
| 176 // it. |
| 177 checked := make(map[string]struct{}, len(checkDirs)) |
| 178 for _, dir := range checkDirs { |
| 179 if _, ok := checked[dir]; ok { |
| 180 continue |
| 181 } |
| 182 checked[dir] = struct{}{} |
| 183 |
| 184 path := p.checkDir(c, dir, selfDirStat, env) |
| 185 if path != "" { |
| 186 return path, nil |
| 187 } |
| 188 } |
| 189 |
| 190 return "", errors.Reason("could not find target in system"). |
| 191 D("target", p.Target). |
| 192 D("dirs", pathDirs). |
| 193 Err() |
| 194 } |
| 195 |
| 196 // checkDir checks "checkDir" for our Target executable. It ignores |
| 197 // executables whose target is the same file or shares the same parent directory |
| 198 // as "self". |
| 199 func (p *Probe) checkDir(c context.Context, dir string, selfDir os.FileInfo, env
environ.Env) string { |
| 200 // If we have a self directory defined, ensure that "dir" isn't the same |
| 201 // directory. If it is, we will ignore this option, since we are looking
for |
| 202 // something outside of the wrapper directory. |
| 203 if selfDir != nil { |
| 204 switch checkDirStat, err := os.Stat(dir); { |
| 205 case err == nil: |
| 206 // "dir" exists; if it is the same as "selfDir", we can
ignore it. |
| 207 if os.SameFile(selfDir, checkDirStat) { |
| 208 logging.Debugf(c, "Candidate shares wrapper dire
ctory [%s]; skipping...", dir) |
| 209 return "" |
| 210 } |
| 211 |
| 212 case os.IsNotExist(err): |
| 213 logging.Debugf(c, "Candidate directory does not exist [%
s]; skipping...", dir) |
| 214 return "" |
| 215 |
| 216 default: |
| 217 logging.Debugf(c, "Failed to stat candidate directory [%
s]: %s", dir, err) |
| 218 return "" |
| 219 } |
| 220 } |
| 221 |
| 222 t, err := findInDir(p.Target, dir, env) |
| 223 if err != nil { |
| 224 return "" |
| 225 } |
| 226 |
| 227 // Make sure this file isn't the same as "self", if available. |
| 228 if p.SelfStat != nil { |
| 229 switch st, err := os.Stat(t); { |
| 230 case err == nil: |
| 231 if os.SameFile(p.SelfStat, st) { |
| 232 logging.Debugf(c, "Candidate [%s] is same file a
s wrapper; skipping...", t) |
| 233 return "" |
| 234 } |
| 235 |
| 236 case os.IsNotExist(err): |
| 237 // "t" no longer exists, so we can't use it. |
| 238 return "" |
| 239 |
| 240 default: |
| 241 logging.Debugf(c, "Failed to stat candidate path [%s]: %
s", t, err) |
| 242 return "" |
| 243 } |
| 244 } |
| 245 |
| 246 if err := filesystem.AbsPath(&t); err != nil { |
| 247 logging.Debugf(c, "Failed to normalize candidate path [%s]: %s",
t, err) |
| 248 return "" |
| 249 } |
| 250 |
| 251 // Try running the candidate command and confirm that it is not a wrappe
r. |
| 252 if p.CheckWrapper != nil { |
| 253 switch isWrapper, err := p.CheckWrapper(c, t, env); { |
| 254 case err != nil: |
| 255 logging.Debugf(c, "Failed to check if [%s] is a wrapper:
%s", t, err) |
| 256 return "" |
| 257 |
| 258 case isWrapper: |
| 259 logging.Debugf(c, "Candidate is a wrapper: %s", t) |
| 260 return "" |
| 261 } |
| 262 } |
| 263 |
| 264 return t |
| 265 } |
| OLD | NEW |