github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/subshell/sscommon/sscommon.go (about) 1 package sscommon 2 3 import ( 4 "os" 5 "os/exec" 6 "path/filepath" 7 "strings" 8 9 "github.com/ActiveState/cli/internal/errs" 10 "github.com/ActiveState/cli/internal/locale" 11 "github.com/ActiveState/cli/internal/logging" 12 "github.com/ActiveState/cli/internal/osutils" 13 "github.com/ActiveState/cli/internal/rtutils" 14 "github.com/ActiveState/cli/internal/sighandler" 15 ) 16 17 func NewCommand(command string, args []string, env []string) *exec.Cmd { 18 cmd := exec.Command(command, args...) 19 if env != nil { 20 cmd.Env = append(os.Environ(), env...) 21 } 22 return cmd 23 } 24 25 // Start wires stdin/stdout/stderr into the provided command, starts it, and 26 // returns a channel to monitor errors on. 27 func Start(cmd *exec.Cmd) chan error { 28 logging.Debug("Starting subshell with cmd: %s", cmd.String()) 29 30 cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr 31 32 errors := make(chan error, 1) 33 34 err := cmd.Start() 35 if err != nil { 36 errors <- errs.Wrap(err, "Failed to start command: %s", cmd.String()) 37 close(errors) 38 return errors 39 } 40 41 go func() { 42 defer close(errors) 43 44 if err := cmd.Wait(); err != nil { 45 if eerr, ok := err.(*exec.ExitError); ok { 46 code := eerr.ExitCode() 47 valid := eerr.Exited() 48 // code 130 is returned when a process halts 49 // due to SIGTERM after receiving a SIGINT 50 // code -1 is returned when a process halts 51 // due to SIGTERM without any interference. 52 if code == 130 || (valid && code == -1) { 53 logging.Debug("exit - valid: %t, code: %d", valid, code) 54 return 55 } 56 logging.Debug("exit - with non-zero: %s, %s", eerr, cmd.String()) 57 58 errors <- errs.Silence(errs.WrapExitCode(eerr, code)) 59 return 60 } 61 62 err = errs.AddTips(errs.Wrap(err, "Command Failed: %s", cmd.String()), 63 "Checking environment vars like SHELL may help resolve this.", 64 ) 65 errors <- err 66 67 return 68 } 69 }() 70 71 return errors 72 } 73 74 // Stop signals the provided command to terminate. 75 func Stop(cmd *exec.Cmd) error { 76 return stop(cmd) 77 } 78 79 // RunFunc ... 80 type RunFunc func(env []string, name string, args ...string) error 81 82 func RunFuncByBinary(binary string) RunFunc { 83 bin := strings.ToLower(binary) 84 switch { 85 case strings.Contains(bin, "bash"): 86 return runWithBash 87 case strings.Contains(bin, "cmd"): 88 return runWithCmd 89 default: 90 return runDirect 91 } 92 } 93 94 func runWithBash(env []string, name string, args ...string) error { 95 filePath, err := osutils.BashifyPath(name) 96 if err != nil { 97 return err 98 } 99 100 esc := osutils.NewBashEscaper() 101 102 quotedArgs := filePath 103 for _, arg := range args { 104 quotedArgs += " " + esc.Quote(arg) 105 } 106 107 return runDirect(env, "bash", "-c", quotedArgs) 108 } 109 110 func runWithCmd(env []string, name string, args ...string) error { 111 ext := filepath.Ext(name) 112 switch ext { 113 case ".py": 114 args = append([]string{name}, args...) 115 pythonPath, err := binaryPathCmd(env, "python") 116 if err != nil { 117 return err 118 } 119 name = pythonPath 120 case ".pl": 121 args = append([]string{name}, args...) 122 perlPath, err := binaryPathCmd(env, "perl") 123 if err != nil { 124 return err 125 } 126 name = perlPath 127 case ".bat": 128 // No action required 129 case ".ps1": 130 args = append([]string{"-executionpolicy", "bypass", "-file", name}, args...) 131 name = "powershell" 132 case ".sh": 133 bashPath, err := osutils.BashifyPath(name) 134 if err != nil { 135 return locale.WrapError( 136 err, "err_sscommon_cannot_translate_path", 137 "Cannot translate Windows path ({{.V0}}) to bash path.", name, 138 ) 139 } 140 args = append([]string{bashPath}, args...) 141 name = "bash" 142 default: 143 return locale.NewInputError("err_sscommon_unsupported_language", "", ext) 144 } 145 146 return runDirect(env, name, args...) 147 } 148 149 func binaryPathCmd(env []string, name string) (string, error) { 150 cmd := exec.Command("where", name) 151 cmd.Env = env 152 153 out, err := cmd.Output() 154 if err != nil { 155 return "", errs.Wrap(err, "Failed to get output of %s", strings.Join(cmd.Args, " ")) 156 } 157 158 split := strings.Split(string(out), "\r\n") 159 if len(split) == 0 { 160 return "", locale.NewExternalError("err_sscommon_binary_path", name) 161 } 162 163 return split[0], nil 164 } 165 166 func runDirect(env []string, name string, args ...string) (rerr error) { 167 logging.Debug("Running command: %s %s", name, strings.Join(args, " ")) 168 169 runCmd := exec.Command(name, args...) 170 runCmd.Stdin, runCmd.Stdout, runCmd.Stderr = os.Stdin, os.Stdout, os.Stderr 171 runCmd.Env = env 172 173 // CTRL+C interrupts are sent to all processes in a terminal at the same 174 // time (with some extra control through process groups). 175 // Here is what can happen *without* the next line: 176 // - `state run` gets interrupted and exits, returning to the parent shell. 177 // - child processes started by state run ignores or handles interrupt, and stays alive. 178 // - the parent shell and the child process read from stdin simultaneously. 179 // This behavior has been reported in 180 // - https://www.pivotaltracker.com/story/show/169509213 and 181 // - https://www.pivotaltracker.com/story/show/167523128 182 bs := sighandler.NewBackgroundSignalHandler(func(_ os.Signal) {}, os.Interrupt) 183 sighandler.Push(bs) 184 defer rtutils.Closer(sighandler.Pop, &rerr) 185 186 err := runCmd.Run() 187 // silence exit code errors 188 if eerr, ok := err.(*exec.ExitError); ok { 189 return errs.Silence(errs.WrapExitCode(eerr, eerr.ExitCode())) 190 } 191 return err 192 }