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 }