github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/runners/exec/exec.go (about)

     1  package exec
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"strconv"
     8  	"strings"
     9  
    10  	rtrunbit "github.com/ActiveState/cli/internal/runbits/runtime"
    11  	"github.com/shirou/gopsutil/v3/process"
    12  
    13  	"github.com/ActiveState/cli/internal/analytics"
    14  	"github.com/ActiveState/cli/internal/constants"
    15  	"github.com/ActiveState/cli/internal/errs"
    16  	"github.com/ActiveState/cli/internal/fileutils"
    17  	"github.com/ActiveState/cli/internal/hash"
    18  	"github.com/ActiveState/cli/internal/language"
    19  	"github.com/ActiveState/cli/internal/locale"
    20  	"github.com/ActiveState/cli/internal/logging"
    21  	"github.com/ActiveState/cli/internal/multilog"
    22  	"github.com/ActiveState/cli/internal/osutils"
    23  	"github.com/ActiveState/cli/internal/output"
    24  	"github.com/ActiveState/cli/internal/primer"
    25  	"github.com/ActiveState/cli/internal/runbits/rationalize"
    26  	"github.com/ActiveState/cli/internal/scriptfile"
    27  	"github.com/ActiveState/cli/internal/subshell"
    28  	"github.com/ActiveState/cli/internal/virtualenvironment"
    29  	"github.com/ActiveState/cli/pkg/platform/authentication"
    30  	"github.com/ActiveState/cli/pkg/platform/model"
    31  	"github.com/ActiveState/cli/pkg/platform/runtime"
    32  	"github.com/ActiveState/cli/pkg/platform/runtime/executors"
    33  	"github.com/ActiveState/cli/pkg/platform/runtime/target"
    34  	"github.com/ActiveState/cli/pkg/project"
    35  	"github.com/ActiveState/cli/pkg/projectfile"
    36  )
    37  
    38  type Configurable interface {
    39  	projectfile.ConfigGetter
    40  	GetBool(key string) bool
    41  }
    42  
    43  type Exec struct {
    44  	subshell  subshell.SubShell
    45  	proj      *project.Project
    46  	auth      *authentication.Auth
    47  	out       output.Outputer
    48  	cfg       Configurable
    49  	analytics analytics.Dispatcher
    50  	svcModel  *model.SvcModel
    51  }
    52  
    53  type primeable interface {
    54  	primer.Auther
    55  	primer.Outputer
    56  	primer.Subsheller
    57  	primer.Projecter
    58  	primer.Configurer
    59  	primer.Analyticer
    60  	primer.SvcModeler
    61  }
    62  
    63  type Params struct {
    64  	Path string
    65  }
    66  
    67  func New(prime primeable) *Exec {
    68  	return &Exec{
    69  		prime.Subshell(),
    70  		prime.Project(),
    71  		prime.Auth(),
    72  		prime.Output(),
    73  		prime.Config(),
    74  		prime.Analytics(),
    75  		prime.SvcModel(),
    76  	}
    77  }
    78  
    79  func NewParams() *Params {
    80  	return &Params{}
    81  }
    82  
    83  func (s *Exec) Run(params *Params, args ...string) (rerr error) {
    84  	var projectDir string
    85  	var projectNamespace string
    86  
    87  	if len(args) == 0 {
    88  		return nil
    89  	}
    90  
    91  	trigger := target.NewExecTrigger(args[0])
    92  
    93  	// Detect target and project dir
    94  	// If the path passed resolves to a runtime dir (ie. has a runtime marker) then the project is not used
    95  	var proj *project.Project
    96  	var err error
    97  	if params.Path != "" && runtime.IsRuntimeDir(params.Path) {
    98  		projectDir = projectFromRuntimeDir(s.cfg, params.Path)
    99  		proj, err = project.FromPath(projectDir)
   100  		if err != nil {
   101  			return locale.WrapInputError(err, "exec_no_project_at_path", "Could not find project file at {{.V0}}", projectDir)
   102  		}
   103  		projectNamespace = proj.NamespaceString()
   104  	} else {
   105  		proj = s.proj
   106  		if params.Path != "" {
   107  			var err error
   108  			proj, err = project.FromPath(params.Path)
   109  			if err != nil {
   110  				return locale.WrapInputError(err, "exec_no_project_at_path", "Could not find project file at {{.V0}}", params.Path)
   111  			}
   112  		}
   113  		if proj == nil {
   114  			return rationalize.ErrNoProject
   115  		}
   116  		projectDir = filepath.Dir(proj.Source().Path())
   117  		projectNamespace = proj.NamespaceString()
   118  	}
   119  
   120  	s.out.Notice(locale.Tr("operating_message", projectNamespace, projectDir))
   121  
   122  	rt, err := rtrunbit.SolveAndUpdate(s.auth, s.out, s.analytics, proj, nil, trigger, s.svcModel, s.cfg, rtrunbit.OptMinimalUI)
   123  	if err != nil {
   124  		return locale.WrapError(err, "err_activate_runtime", "Could not initialize a runtime for this project.")
   125  	}
   126  
   127  	venv := virtualenvironment.New(rt)
   128  
   129  	env, err := venv.GetEnv(true, false, projectDir, projectNamespace)
   130  	if err != nil {
   131  		return locale.WrapError(err, "err_exec_env", "Could not retrieve environment information for your runtime")
   132  	}
   133  	logging.Debug("Trying to exec %s on PATH=%s", args[0], env["PATH"])
   134  
   135  	if err := handleRecursion(env, args); err != nil {
   136  		return errs.Wrap(err, "Could not handle recursion")
   137  	}
   138  
   139  	exeTarget := args[0]
   140  	if !fileutils.TargetExists(exeTarget) {
   141  		rtDirs, err := rt.ExecutableDirs()
   142  		if err != nil {
   143  			return errs.Wrap(err, "Could not detect runtime executable paths")
   144  		}
   145  
   146  		RTPATH := strings.Join(rtDirs, string(os.PathListSeparator))
   147  
   148  		// Report recursive execution of executor: The path for the executable should be different from the default bin dir
   149  		exesOnPath := osutils.FilterExesOnPATH(args[0], RTPATH, func(exe string) bool {
   150  			v, err := executors.IsExecutor(exe)
   151  			if err != nil {
   152  				logging.Error("Could not find out if executable is an executor: %s", errs.JoinMessage(err))
   153  				return true // This usually means there's a permission issue, which means we likely don't own it
   154  			}
   155  			return !v
   156  		})
   157  
   158  		if len(exesOnPath) > 0 {
   159  			exeTarget = exesOnPath[0]
   160  		}
   161  	}
   162  
   163  	// Guard against invoking the executor from PATH (ie. by name alone)
   164  	if os.Getenv(constants.ExecRecursionAllowEnvVarName) != "true" && filepath.Base(exeTarget) == exeTarget { // not a full path
   165  		exe := osutils.FindExeInside(exeTarget, env["PATH"])
   166  		if exe != exeTarget { // Found the exe name on our PATH
   167  			isExec, err := executors.IsExecutor(exe)
   168  			if err != nil {
   169  				logging.Error("Could not find out if executable is an executor: %s", errs.JoinMessage(err))
   170  			} else if isExec {
   171  				// If the exe we resolve to is an executor then we have ourselves a recursive loop
   172  				return locale.NewError("err_exec_recursion", "", constants.ForumsURL, constants.ExecRecursionAllowEnvVarName)
   173  			}
   174  		}
   175  	}
   176  
   177  	err = s.subshell.SetEnv(env)
   178  	if err != nil {
   179  		return locale.WrapError(err, "err_subshell_setenv")
   180  	}
   181  
   182  	lang := language.Bash
   183  	scriptArgs := fmt.Sprintf(`%q "$@"`, exeTarget)
   184  	if strings.Contains(s.subshell.Binary(), "cmd") {
   185  		lang = language.PowerShell
   186  		scriptArgs = fmt.Sprintf("& %q @args\nexit $LASTEXITCODE", exeTarget)
   187  	}
   188  
   189  	sf, err := scriptfile.New(lang, "state-exec", scriptArgs)
   190  	if err != nil {
   191  		return locale.WrapError(err, "err_exec_create_scriptfile", "Could not generate script")
   192  	}
   193  	defer sf.Clean()
   194  
   195  	return s.subshell.Run(sf.Filename(), args[1:]...)
   196  }
   197  
   198  func projectFromRuntimeDir(cfg projectfile.ConfigGetter, runtimeDir string) string {
   199  	projects := projectfile.GetProjectMapping(cfg)
   200  	for _, paths := range projects {
   201  		for _, p := range paths {
   202  			targetBase := hash.ShortHash(p)
   203  			if filepath.Base(runtimeDir) == targetBase {
   204  				return p
   205  			}
   206  		}
   207  	}
   208  
   209  	return ""
   210  }
   211  
   212  func handleRecursion(env map[string]string, args []string) error {
   213  	recursionReadable := []string{}
   214  	recursionReadableFull := os.Getenv(constants.ExecRecursionEnvVarName)
   215  	if recursionReadableFull == "" {
   216  		recursionReadable = append(recursionReadable, getParentProcessArgs())
   217  	} else {
   218  		recursionReadable = strings.Split(recursionReadableFull, "\n")
   219  	}
   220  	recursionReadable = append(recursionReadable, filepath.Base(os.Args[0])+" "+strings.Join(os.Args[1:], " "))
   221  	var recursionLvl int64
   222  	lastLvl, err := strconv.ParseInt(os.Getenv(constants.ExecRecursionLevelEnvVarName), 10, 32)
   223  	if err == nil {
   224  		recursionLvl = lastLvl + 1
   225  	}
   226  	maxLevel, err := strconv.ParseInt(os.Getenv(constants.ExecRecursionMaxLevelEnvVarName), 10, 32)
   227  	if err == nil || maxLevel == 0 {
   228  		maxLevel = 10
   229  	}
   230  	if recursionLvl == 2 || recursionLvl == 10 || recursionLvl == 50 {
   231  		multilog.Error("executor recursion detected: parent %s (%d): %s (lvl=%d)", getParentProcessArgs(), os.Getppid(), strings.Join(args, " "), recursionLvl)
   232  	}
   233  	if recursionLvl >= maxLevel {
   234  		return locale.NewError("err_recursion_limit", "", strings.Join(recursionReadable, "\n"), constants.ExecRecursionMaxLevelEnvVarName)
   235  	}
   236  
   237  	env[constants.ExecRecursionLevelEnvVarName] = fmt.Sprintf("%d", recursionLvl)
   238  	env[constants.ExecRecursionMaxLevelEnvVarName] = fmt.Sprintf("%d", maxLevel)
   239  	env[constants.ExecRecursionEnvVarName] = strings.Join(recursionReadable, "\n")
   240  	return nil
   241  }
   242  
   243  func getParentProcessArgs() string {
   244  	p, err := process.NewProcess(int32(os.Getppid()))
   245  	if err != nil {
   246  		logging.Debug("Could not find parent process of executor: %v", err)
   247  		return "unknown"
   248  	}
   249  
   250  	args, err := p.CmdlineSlice()
   251  	if err != nil {
   252  		logging.Debug("Could not retrieve command line arguments of executor's calling process: %v", err)
   253  		return "unknown"
   254  	}
   255  
   256  	return strings.Join(args, " ")
   257  }