Chromium Code Reviews| 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 both | |
|
iannucci
2017/06/07 17:01:12
"both" windows systems?
dnj
2017/06/07 17:09:12
Done.
| |
| 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 is the absolute path to the current executable, resolved via | |
| 61 // ResolveSelf. It may be empty if the resolution has not been performed , or | |
| 62 // if the current executable could not be resolved. | |
| 63 // | |
| 64 // If Self is set, SelfStat must also be non-nil. | |
| 65 // | |
| 66 // Self may be set explicitly, or resolved via ResolveSelf. | |
| 67 Self string | |
| 68 // SelfStat is the FileInfo for self. If self is not empty, SelfStat wil l not | |
|
iannucci
2017/06/07 17:01:12
newline.
If you want to group these, I would do
dnj
2017/06/07 17:09:12
Done.
| |
| 69 // be nil. | |
| 70 // | |
| 71 // SelfStat may be set explicitly, or resolved via ResolveSelf. | |
| 72 SelfStat os.FileInfo | |
| 73 | |
| 74 // PathDirs, if not zero, contains the list of directories to search. If | |
| 75 // zero, the os.PathListSeparator-delimited PATH environment variable wi ll | |
| 76 // be used. | |
| 77 PathDirs []string | |
| 78 } | |
| 79 | |
| 80 // ResolveSelf attempts to identify the current process. If successful, p's | |
| 81 // Self will be set to an absolute path reference to Self, and its SelfStat | |
| 82 // field will be set to the os.FileInfo for that path. | |
| 83 // | |
| 84 // If this process was invoked via symlink, the path to the symlink will be | |
| 85 // returned if possible. | |
| 86 func (p *Probe) ResolveSelf(argv0 string) error { | |
| 87 if p.Self != "" { | |
| 88 return nil | |
| 89 } | |
| 90 | |
| 91 // Get the authoritative executable from the system. | |
| 92 exec, err := os.Executable() | |
| 93 if err != nil { | |
| 94 return errors.Annotate(err).Reason("failed to get executable").E rr() | |
| 95 } | |
| 96 | |
| 97 execStat, err := os.Stat(exec) | |
| 98 if err != nil { | |
| 99 return errors.Annotate(err).Reason("failed to stat executable: % (path)s"). | |
| 100 D("path", exec). | |
| 101 Err() | |
| 102 } | |
| 103 | |
| 104 // Before using "os.Executable" result, which is known to resolve symlin ks on | |
| 105 // Linux, try and identify via argv0. | |
| 106 if argv0 != "" && filesystem.AbsPath(&argv0) == nil { | |
| 107 if st, err := os.Stat(argv0); err == nil && os.SameFile(execStat , st) { | |
| 108 // argv[0] is the same file as our executable, but may b e an unresolved | |
| 109 // symlink. Prefer it. | |
| 110 p.Self, p.SelfStat = argv0, st | |
| 111 return nil | |
| 112 } | |
| 113 } | |
| 114 | |
| 115 p.Self, p.SelfStat = exec, execStat | |
| 116 return nil | |
| 117 } | |
| 118 | |
| 119 // Locate attempts to locate the system's Target by traversing the available | |
| 120 // PATH. | |
| 121 // | |
| 122 // cached is the cached path, passed from wrapper to wrapper through the a | |
| 123 // State struct in the environment. This may be empty, if there was no cached | |
| 124 // path or if the cached path was invalid. | |
| 125 // | |
| 126 // env is the environment to operate with, and will not be modified during | |
| 127 // execution. | |
| 128 func (p *Probe) Locate(c context.Context, cached string, env environ.Env) (strin g, error) { | |
| 129 // If we have a cached path, check that it exists and is executable and use it | |
| 130 // if it is. | |
| 131 if cached != "" { | |
| 132 switch cachedStat, err := os.Stat(cached); { | |
| 133 case err == nil: | |
| 134 // Use the cached path. First, pass it through a sanity check to ensure | |
| 135 // that it is not self. | |
| 136 if p.SelfStat == nil || !os.SameFile(p.SelfStat, cachedS tat) { | |
| 137 logging.Debugf(c, "Using cached value: %s", cach ed) | |
| 138 return cached, nil | |
| 139 } | |
| 140 logging.Debugf(c, "Cached value [%s] is this wrapper [%s ]; ignoring.", cached, p.Self) | |
| 141 | |
| 142 case os.IsNotExist(err): | |
| 143 // Our cached path doesn't exist, so we will have to loo k for a new one. | |
| 144 | |
| 145 case err != nil: | |
| 146 // We couldn't check our cached path, so we will have to look for a new | |
| 147 // one. This is an unexpected error, though, so emit it. | |
| 148 logging.Debugf(c, "Failed to stat cached [%s]: %s", cach ed, err) | |
| 149 } | |
| 150 } | |
| 151 | |
| 152 // Get stats on our parent directory. This may fail; if so, we'll skip t he | |
| 153 // SameFile check. | |
| 154 var selfDir string | |
| 155 var selfDirStat os.FileInfo | |
| 156 if p.Self != "" { | |
| 157 selfDir = filepath.Dir(p.Self) | |
| 158 | |
| 159 var err error | |
| 160 if selfDirStat, err = os.Stat(selfDir); err != nil { | |
| 161 logging.Debugf(c, "Failed to stat self directory [%s]: % s", selfDir, err) | |
| 162 } | |
| 163 } | |
| 164 | |
| 165 // Walk through PATH. Our goal is to find the first program named Target that | |
| 166 // isn't self and doesn't identify as a wrapper. | |
| 167 pathDirs := p.PathDirs | |
| 168 if pathDirs == nil { | |
| 169 pathDirs = strings.Split(env.GetEmpty("PATH"), string(os.PathLis tSeparator)) | |
| 170 } | |
| 171 | |
| 172 // Build our list of directories to check for Git. | |
| 173 checkDirs := make([]string, 0, len(pathDirs)+len(p.RelativePathOverride) ) | |
| 174 if selfDir != "" { | |
| 175 for _, rpo := range p.RelativePathOverride { | |
| 176 checkDirs = append(checkDirs, filepath.Join(selfDir, fil epath.FromSlash(rpo))) | |
| 177 } | |
| 178 } | |
| 179 checkDirs = append(checkDirs, pathDirs...) | |
| 180 | |
| 181 // Iterate through each check directory and look for a Git candidate wit hin | |
| 182 // it. | |
| 183 checked := make(map[string]struct{}, len(checkDirs)) | |
| 184 for _, dir := range checkDirs { | |
| 185 if _, ok := checked[dir]; ok { | |
| 186 continue | |
| 187 } | |
| 188 checked[dir] = struct{}{} | |
| 189 | |
| 190 path := p.checkDir(c, dir, selfDirStat, env) | |
| 191 if path != "" { | |
| 192 return path, nil | |
| 193 } | |
| 194 } | |
| 195 | |
| 196 return "", errors.Reason("could not find target in system"). | |
| 197 D("target", p.Target). | |
| 198 D("dirs", pathDirs). | |
| 199 Err() | |
| 200 } | |
| 201 | |
| 202 // checkDir checks "checkDir" for our Target executable. It ignores | |
| 203 // executables whose target is the same file or shares the same parent directory | |
| 204 // as "self". | |
| 205 func (p *Probe) checkDir(c context.Context, dir string, selfDir os.FileInfo, env environ.Env) string { | |
| 206 // If we have a self directory defined, ensure that "dir" isn't the same | |
| 207 // directory. If it is, we will ignore this option, since we are looking for | |
| 208 // something outside of the wrapper directory. | |
| 209 if selfDir != nil { | |
| 210 switch checkDirStat, err := os.Stat(dir); { | |
| 211 case err == nil: | |
| 212 // "dir" exists; if it is the same as "selfDir", we can ignore it. | |
| 213 if os.SameFile(selfDir, checkDirStat) { | |
| 214 logging.Debugf(c, "Candidate shares wrapper dire ctory [%s]; skipping...", dir) | |
| 215 return "" | |
| 216 } | |
| 217 | |
| 218 case os.IsNotExist(err): | |
| 219 logging.Debugf(c, "Candidate directory does not exist [% s]; skipping...", dir) | |
| 220 return "" | |
| 221 | |
| 222 default: | |
| 223 logging.Debugf(c, "Failed to stat candidate directory [% s]: %s", dir, err) | |
| 224 return "" | |
| 225 } | |
| 226 } | |
| 227 | |
| 228 t, err := findInDir(p.Target, dir, env) | |
| 229 if err != nil { | |
| 230 return "" | |
| 231 } | |
| 232 | |
| 233 // Make sure this file isn't the same as "self", if available. | |
| 234 if p.SelfStat != nil { | |
| 235 switch st, err := os.Stat(t); { | |
| 236 case err == nil: | |
| 237 if os.SameFile(p.SelfStat, st) { | |
| 238 logging.Debugf(c, "Candidate [%s] is same file a s wrapper; skipping...", t) | |
| 239 return "" | |
| 240 } | |
| 241 | |
| 242 case os.IsNotExist(err): | |
| 243 // "t" no longer exists, so we can't use it. | |
| 244 return "" | |
| 245 | |
| 246 default: | |
| 247 logging.Debugf(c, "Failed to stat candidate path [%s]: % s", t, err) | |
| 248 return "" | |
| 249 } | |
| 250 } | |
| 251 | |
| 252 if err := filesystem.AbsPath(&t); err != nil { | |
| 253 logging.Debugf(c, "Failed to normalize candidate path [%s]: %s", t, err) | |
| 254 return "" | |
| 255 } | |
| 256 | |
| 257 // Try running the candidate command and confirm that it is not a wrappe r. | |
| 258 if p.CheckWrapper != nil { | |
| 259 switch isWrapper, err := p.CheckWrapper(c, t, env); { | |
| 260 case err != nil: | |
| 261 logging.Debugf(c, "Failed to check if [%s] is a wrapper: %s", t, err) | |
| 262 return "" | |
| 263 | |
| 264 case isWrapper: | |
| 265 logging.Debugf(c, "Candidate is a wrapper: %s", t) | |
| 266 return "" | |
| 267 } | |
| 268 } | |
| 269 | |
| 270 return t | |
| 271 } | |
| OLD | NEW |