go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/system/prober/probe.go (about) 1 // Copyright 2017 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package prober exports Probe, which implements logic to identify a wrapper's 16 // wrapped target. In addition to basic PATH/filename lookup, Prober contains 17 // logic to ensure that the wrapper is not the same software as the current 18 // running instance, and enables optional hard-coded wrap target paths and 19 // runtime checks. 20 package prober 21 22 import ( 23 "context" 24 "os" 25 "path/filepath" 26 "runtime" 27 "strings" 28 29 "go.chromium.org/luci/common/errors" 30 "go.chromium.org/luci/common/logging" 31 "go.chromium.org/luci/common/system/environ" 32 "go.chromium.org/luci/common/system/filesystem" 33 ) 34 35 // CheckWrapperFunc is an optional function that can be implemented for a 36 // Prober to check if a candidate path is a wrapper. 37 type CheckWrapperFunc func(ctx context.Context, path string, env environ.Env) (isWrapper bool, err error) 38 39 // Probe can Locate a Target executable by probing the local system PATH. 40 // 41 // Target should be an executable name resolvable by exec.LookPath. On 42 // Windows systems, this may omit the executable extension (e.g., "bat", "exe") 43 // since that is augmented via the PATHEXT environment variable (see 44 // "probe_windows.go"). 45 type Probe struct { 46 // Target is the name of the target (as seen by exec.LookPath) that we are 47 // searching for. 48 Target string 49 50 // RelativePathOverride is a series of forward-slash-delimited paths to 51 // directories relative to the wrapper executable that will be checked 52 // prior to checking PATH. This allows bundles (e.g., CIPD) that include both 53 // the wrapper and a real implementation, to force the wrapper to use 54 // the bundled implementation. 55 RelativePathOverride []string 56 57 // CheckWrapper, if not nil, is a function called on a candidate wrapper to 58 // determine whether or not that candidate is valid. 59 // 60 // On success, it will return isWrapper, which will be true if path is a 61 // wrapper instance and false if it is not. If an error occurred during 62 // checking, the error should be returned and isWrapper will be ignored. If 63 // a candidate is a wrapper, or if an error occurred during check, the 64 // candidate will be discarded and the probe will continue. 65 // 66 // CheckWrapper should be lightweight and fast, as it may be called multiple 67 // times. 68 CheckWrapper CheckWrapperFunc 69 70 // Self and Selfstat are resolved contain the path and FileInfo of the 71 // currently running executable, respectively. They can both be resolved via 72 // ResolveSelf, and may be empty if resolution has not been performed, or if 73 // the current executable could not be resolved. They may also be set 74 // explicitly, bypassing the need to perform resolution. 75 Self string 76 SelfStat os.FileInfo 77 78 // PathDirs, if not zero, contains the list of directories to search. If 79 // zero, the os.PathListSeparator-delimited PATH environment variable will 80 // be used. 81 PathDirs []string 82 } 83 84 // ResolveSelf attempts to identify the current process. If successful, p's 85 // Self will be set to an absolute path reference to Self, and its SelfStat 86 // field will be set to the os.FileInfo for that path. 87 // 88 // If this process was invoked via symlink, the fully resolved path will be 89 // returned, except on Windows, where no guarantee is made. 90 func (p *Probe) ResolveSelf() error { 91 if p.Self != "" { 92 return nil 93 } 94 95 // Get the authoritative executable from the system. 96 exec, err := os.Executable() 97 if err != nil { 98 return errors.Annotate(err, "failed to get executable").Err() 99 } 100 101 // Make sure the path has all symlinks resolved. 102 // Skip EvalSymlinks for windows because it is broken: 103 // https://github.com/golang/go/issues/40180 104 if runtime.GOOS != "windows" { 105 if exec, err = filepath.EvalSymlinks(exec); err != nil { 106 return errors.Annotate(err, "failed to get real path for executable: %s", exec).Err() 107 } 108 } 109 110 execStat, err := os.Stat(exec) 111 if err != nil { 112 return errors.Annotate(err, "failed to stat executable: %s", exec).Err() 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(ctx context.Context, cached string, env environ.Env) (string, 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, cachedStat) { 137 logging.Debugf(ctx, "Using cached value: %s", cached) 138 return cached, nil 139 } 140 logging.Debugf(ctx, "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 look 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(ctx, "Failed to stat cached [%s]: %s", cached, err) 149 } 150 } 151 152 // Get stats on our parent directory. This may fail; if so, we'll skip the 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(ctx, "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.Get("PATH"), string(os.PathListSeparator)) 170 } 171 172 // Build our list of directories to check for Target. 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, filepath.FromSlash(rpo))) 177 } 178 } 179 checkDirs = append(checkDirs, pathDirs...) 180 181 // Iterate through each check directory and look for a Target candidate within 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(ctx, dir, selfDirStat, env) 191 if path != "" { 192 return path, nil 193 } 194 } 195 196 return "", errors.Reason("could not find target in system"). 197 InternalReason("target(%s)/dirs(%v)", p.Target, pathDirs).Err() 198 } 199 200 // checkDir checks "checkDir" for our Target executable. It ignores 201 // executables whose target is the same file or shares the same parent directory 202 // as "self". 203 func (p *Probe) checkDir(ctx context.Context, dir string, selfDir os.FileInfo, env environ.Env) string { 204 // If we have a self directory defined, ensure that "dir" isn't the same 205 // directory. If it is, we will ignore this option, since we are looking for 206 // something outside of the wrapper directory. 207 if selfDir != nil { 208 switch checkDirStat, err := os.Stat(dir); { 209 case err == nil: 210 // "dir" exists; if it is the same as "selfDir", we can ignore it. 211 if os.SameFile(selfDir, checkDirStat) { 212 logging.Debugf(ctx, "Candidate shares wrapper directory [%s]; skipping...", dir) 213 return "" 214 } 215 216 case os.IsNotExist(err): 217 logging.Debugf(ctx, "Candidate directory does not exist [%s]; skipping...", dir) 218 return "" 219 220 default: 221 logging.Debugf(ctx, "Failed to stat candidate directory [%s]: %s", dir, err) 222 return "" 223 } 224 } 225 226 t, err := findInDir(p.Target, dir, env) 227 if err != nil { 228 return "" 229 } 230 231 // Make sure this file isn't the same as "self", if available. 232 if p.SelfStat != nil { 233 switch st, err := os.Stat(t); { 234 case err == nil: 235 if os.SameFile(p.SelfStat, st) { 236 logging.Debugf(ctx, "Candidate [%s] is same file as wrapper; skipping...", t) 237 return "" 238 } 239 240 case os.IsNotExist(err): 241 // "t" no longer exists, so we can't use it. 242 return "" 243 244 default: 245 logging.Debugf(ctx, "Failed to stat candidate path [%s]: %s", t, err) 246 return "" 247 } 248 } 249 250 if err := filesystem.AbsPath(&t); err != nil { 251 logging.Debugf(ctx, "Failed to normalize candidate path [%s]: %s", t, err) 252 return "" 253 } 254 255 // Try running the candidate command and confirm that it is not a wrapper. 256 if p.CheckWrapper != nil { 257 switch isWrapper, err := p.CheckWrapper(ctx, t, env); { 258 case err != nil: 259 logging.Debugf(ctx, "Failed to check if [%s] is a wrapper: %s", t, err) 260 return "" 261 262 case isWrapper: 263 logging.Debugf(ctx, "Candidate is a wrapper: %s", t) 264 return "" 265 } 266 } 267 268 return t 269 }