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  }