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  }