Chromium Code Reviews| Index: common/wrapper/prober/probe.go |
| diff --git a/common/wrapper/prober/probe.go b/common/wrapper/prober/probe.go |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..f7e6e7aa9401bb336230f5b877dfce6db01a1223 |
| --- /dev/null |
| +++ b/common/wrapper/prober/probe.go |
| @@ -0,0 +1,271 @@ |
| +// Copyright 2017 The LUCI Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +// Package prober exports Probe, which implements logic to identify a wrapper's |
| +// wrapped target. In addition to basic PATH/filename lookup, Prober contains |
| +// logic to ensure that the wrapper is not the same software as the current |
| +// running instance, and enables optional hard-coded wrap target paths and |
| +// runtime checks. |
| +package prober |
| + |
| +import ( |
| + "os" |
| + "path/filepath" |
| + "strings" |
| + |
| + "golang.org/x/net/context" |
| + |
| + "github.com/luci/luci-go/common/errors" |
| + "github.com/luci/luci-go/common/logging" |
| + "github.com/luci/luci-go/common/system/environ" |
| + "github.com/luci/luci-go/common/system/filesystem" |
| +) |
| + |
| +// CheckWrapperFunc is an optional function that can be implemented for a |
| +// Prober to check if a candidate path is a wrapper. |
| +type CheckWrapperFunc func(c context.Context, path string, env environ.Env) (isWrapper bool, err error) |
| + |
| +// Probe can Locate a Target executable by probing the local system PATH. |
| +// |
| +// 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.
|
| +// Windows systems, this may omit the executable extension (e.g., "bat", "exe") |
| +// since that is augmented via the PATHEXT environment variable (see |
| +// "probe_windows.go"). |
| +type Probe struct { |
| + // Target is the name of the target (as seen by exec.LookPath) that we are |
| + // searching for. |
| + Target string |
| + |
| + // RelativePathOverride is a series of forward-slash-delimited paths to |
| + // directories relative to the wrapper executable that will be checked |
| + // prior to checking PATH. This allows bundles (e.g., CIPD) that include both |
| + // the wrapper and a real implementation, to force the wrapper to use |
| + // the bundled implementation. |
| + RelativePathOverride []string |
| + |
| + // CheckWrapper, if not nil, is a function called on a candidate wrapper to |
| + // determine whether or not that candidate is valid. |
| + // |
| + // On success, it will return isWrapper, which will be true if path is a |
| + // wrapper instance and false if it is not. If an error occurred during |
| + // checking, the error should be returned and isWrapper will be ignored. If |
| + // a candidate is a wrapper, or if an error occurred during check, the |
| + // candidate will be discarded and the probe will continue. |
| + // |
| + // CheckWrapper should be lightweight and fast, as it may be called multiple |
| + // times. |
| + CheckWrapper CheckWrapperFunc |
| + |
| + // Self is the absolute path to the current executable, resolved via |
| + // ResolveSelf. It may be empty if the resolution has not been performed, or |
| + // if the current executable could not be resolved. |
| + // |
| + // If Self is set, SelfStat must also be non-nil. |
| + // |
| + // Self may be set explicitly, or resolved via ResolveSelf. |
| + Self string |
| + // SelfStat is the FileInfo for self. If self is not empty, SelfStat will 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.
|
| + // be nil. |
| + // |
| + // SelfStat may be set explicitly, or resolved via ResolveSelf. |
| + SelfStat os.FileInfo |
| + |
| + // PathDirs, if not zero, contains the list of directories to search. If |
| + // zero, the os.PathListSeparator-delimited PATH environment variable will |
| + // be used. |
| + PathDirs []string |
| +} |
| + |
| +// ResolveSelf attempts to identify the current process. If successful, p's |
| +// Self will be set to an absolute path reference to Self, and its SelfStat |
| +// field will be set to the os.FileInfo for that path. |
| +// |
| +// If this process was invoked via symlink, the path to the symlink will be |
| +// returned if possible. |
| +func (p *Probe) ResolveSelf(argv0 string) error { |
| + if p.Self != "" { |
| + return nil |
| + } |
| + |
| + // Get the authoritative executable from the system. |
| + exec, err := os.Executable() |
| + if err != nil { |
| + return errors.Annotate(err).Reason("failed to get executable").Err() |
| + } |
| + |
| + execStat, err := os.Stat(exec) |
| + if err != nil { |
| + return errors.Annotate(err).Reason("failed to stat executable: %(path)s"). |
| + D("path", exec). |
| + Err() |
| + } |
| + |
| + // Before using "os.Executable" result, which is known to resolve symlinks on |
| + // Linux, try and identify via argv0. |
| + if argv0 != "" && filesystem.AbsPath(&argv0) == nil { |
| + if st, err := os.Stat(argv0); err == nil && os.SameFile(execStat, st) { |
| + // argv[0] is the same file as our executable, but may be an unresolved |
| + // symlink. Prefer it. |
| + p.Self, p.SelfStat = argv0, st |
| + return nil |
| + } |
| + } |
| + |
| + p.Self, p.SelfStat = exec, execStat |
| + return nil |
| +} |
| + |
| +// Locate attempts to locate the system's Target by traversing the available |
| +// PATH. |
| +// |
| +// cached is the cached path, passed from wrapper to wrapper through the a |
| +// State struct in the environment. This may be empty, if there was no cached |
| +// path or if the cached path was invalid. |
| +// |
| +// env is the environment to operate with, and will not be modified during |
| +// execution. |
| +func (p *Probe) Locate(c context.Context, cached string, env environ.Env) (string, error) { |
| + // If we have a cached path, check that it exists and is executable and use it |
| + // if it is. |
| + if cached != "" { |
| + switch cachedStat, err := os.Stat(cached); { |
| + case err == nil: |
| + // Use the cached path. First, pass it through a sanity check to ensure |
| + // that it is not self. |
| + if p.SelfStat == nil || !os.SameFile(p.SelfStat, cachedStat) { |
| + logging.Debugf(c, "Using cached value: %s", cached) |
| + return cached, nil |
| + } |
| + logging.Debugf(c, "Cached value [%s] is this wrapper [%s]; ignoring.", cached, p.Self) |
| + |
| + case os.IsNotExist(err): |
| + // Our cached path doesn't exist, so we will have to look for a new one. |
| + |
| + case err != nil: |
| + // We couldn't check our cached path, so we will have to look for a new |
| + // one. This is an unexpected error, though, so emit it. |
| + logging.Debugf(c, "Failed to stat cached [%s]: %s", cached, err) |
| + } |
| + } |
| + |
| + // Get stats on our parent directory. This may fail; if so, we'll skip the |
| + // SameFile check. |
| + var selfDir string |
| + var selfDirStat os.FileInfo |
| + if p.Self != "" { |
| + selfDir = filepath.Dir(p.Self) |
| + |
| + var err error |
| + if selfDirStat, err = os.Stat(selfDir); err != nil { |
| + logging.Debugf(c, "Failed to stat self directory [%s]: %s", selfDir, err) |
| + } |
| + } |
| + |
| + // Walk through PATH. Our goal is to find the first program named Target that |
| + // isn't self and doesn't identify as a wrapper. |
| + pathDirs := p.PathDirs |
| + if pathDirs == nil { |
| + pathDirs = strings.Split(env.GetEmpty("PATH"), string(os.PathListSeparator)) |
| + } |
| + |
| + // Build our list of directories to check for Git. |
| + checkDirs := make([]string, 0, len(pathDirs)+len(p.RelativePathOverride)) |
| + if selfDir != "" { |
| + for _, rpo := range p.RelativePathOverride { |
| + checkDirs = append(checkDirs, filepath.Join(selfDir, filepath.FromSlash(rpo))) |
| + } |
| + } |
| + checkDirs = append(checkDirs, pathDirs...) |
| + |
| + // Iterate through each check directory and look for a Git candidate within |
| + // it. |
| + checked := make(map[string]struct{}, len(checkDirs)) |
| + for _, dir := range checkDirs { |
| + if _, ok := checked[dir]; ok { |
| + continue |
| + } |
| + checked[dir] = struct{}{} |
| + |
| + path := p.checkDir(c, dir, selfDirStat, env) |
| + if path != "" { |
| + return path, nil |
| + } |
| + } |
| + |
| + return "", errors.Reason("could not find target in system"). |
| + D("target", p.Target). |
| + D("dirs", pathDirs). |
| + Err() |
| +} |
| + |
| +// checkDir checks "checkDir" for our Target executable. It ignores |
| +// executables whose target is the same file or shares the same parent directory |
| +// as "self". |
| +func (p *Probe) checkDir(c context.Context, dir string, selfDir os.FileInfo, env environ.Env) string { |
| + // If we have a self directory defined, ensure that "dir" isn't the same |
| + // directory. If it is, we will ignore this option, since we are looking for |
| + // something outside of the wrapper directory. |
| + if selfDir != nil { |
| + switch checkDirStat, err := os.Stat(dir); { |
| + case err == nil: |
| + // "dir" exists; if it is the same as "selfDir", we can ignore it. |
| + if os.SameFile(selfDir, checkDirStat) { |
| + logging.Debugf(c, "Candidate shares wrapper directory [%s]; skipping...", dir) |
| + return "" |
| + } |
| + |
| + case os.IsNotExist(err): |
| + logging.Debugf(c, "Candidate directory does not exist [%s]; skipping...", dir) |
| + return "" |
| + |
| + default: |
| + logging.Debugf(c, "Failed to stat candidate directory [%s]: %s", dir, err) |
| + return "" |
| + } |
| + } |
| + |
| + t, err := findInDir(p.Target, dir, env) |
| + if err != nil { |
| + return "" |
| + } |
| + |
| + // Make sure this file isn't the same as "self", if available. |
| + if p.SelfStat != nil { |
| + switch st, err := os.Stat(t); { |
| + case err == nil: |
| + if os.SameFile(p.SelfStat, st) { |
| + logging.Debugf(c, "Candidate [%s] is same file as wrapper; skipping...", t) |
| + return "" |
| + } |
| + |
| + case os.IsNotExist(err): |
| + // "t" no longer exists, so we can't use it. |
| + return "" |
| + |
| + default: |
| + logging.Debugf(c, "Failed to stat candidate path [%s]: %s", t, err) |
| + return "" |
| + } |
| + } |
| + |
| + if err := filesystem.AbsPath(&t); err != nil { |
| + logging.Debugf(c, "Failed to normalize candidate path [%s]: %s", t, err) |
| + return "" |
| + } |
| + |
| + // Try running the candidate command and confirm that it is not a wrapper. |
| + if p.CheckWrapper != nil { |
| + switch isWrapper, err := p.CheckWrapper(c, t, env); { |
| + case err != nil: |
| + logging.Debugf(c, "Failed to check if [%s] is a wrapper: %s", t, err) |
| + return "" |
| + |
| + case isWrapper: |
| + logging.Debugf(c, "Candidate is a wrapper: %s", t) |
| + return "" |
| + } |
| + } |
| + |
| + return t |
| +} |