1
0
mirror of https://github.com/smallstep/cli.git synced 2026-10-24 11:19:19 +03:00
Files
step-ca-cli/exec/exec.go
max furman a62a7fa71c nolinlint
2022-08-18 19:22:46 -07:00

217 lines
5.6 KiB
Go

package exec
import (
"bytes"
"fmt"
"os"
"os/exec"
"os/signal"
"path"
"runtime"
"strconv"
"strings"
"syscall"
"github.com/pkg/errors"
"github.com/smallstep/cli/utils/sysutils"
)
// LookPath is an alias for exec.LookPath. It searches for an executable named
// file in the directories named by the PATH environment variable. If file
// contains a slash, it is tried directly and the PATH is not consulted. The
// result may be an absolute path or a path relative to the current directory.
func LookPath(file string) (string, error) {
return exec.LookPath(file)
}
// IsWSL returns true if Windows Subsystem for Linux is detected.
//
// "Official" way of detecting WSL
// https://github.com/Microsoft/WSL/issues/423#issuecomment-221627364
func IsWSL() bool {
b, err := os.ReadFile("/proc/sys/kernel/osrelease")
if err != nil {
return false
}
return strings.Contains(strings.ToLower(string(b)), "microsoft") || strings.Contains(strings.ToLower(string(b)), "wsl")
}
// Exec is wrapper over syscall.Exec, invokes the execve(2) system call. On
// windows it executes Run with the same arguments.
func Exec(name string, arg ...string) {
if runtime.GOOS == "windows" {
Run(name, arg...)
return
}
args := append([]string{name}, arg...)
if err := sysutils.Exec(name, args, os.Environ()); err != nil {
errorAndExit(name, err)
}
}
// Run is a wrapper over os/exec Cmd.Run that configures Stderr/Stdin/Stdout
// to the current ones and wait until the process finishes, exiting with the
// same code. Run will also forward all the signals sent to step to the
// command.
func Run(name string, arg ...string) {
cmd, exitCh, err := run(name, arg...)
if err != nil {
errorAndExit(name, err)
}
if err = cmd.Wait(); err != nil {
errorf(name, err)
}
// exit and wait until os.Exit
exitCh <- getExitStatus(cmd)
exitCh <- 0
}
// RunWithPid calls Run and writes the process ID in pidFile.
func RunWithPid(pidFile, name string, arg ...string) {
f, err := os.OpenFile(pidFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
errorAndExit(name, err)
}
// Run process
cmd, exitCh, err := run(name, arg...)
if err != nil {
f.Close()
os.Remove(f.Name())
errorAndExit(name, err)
}
// Write pid
f.WriteString(strconv.Itoa(cmd.Process.Pid))
f.Close()
// Wait until it finishes
if err = cmd.Wait(); err != nil {
errorf(name, err)
}
// clean, exit and wait until os.Exit
os.Remove(f.Name())
exitCh <- getExitStatus(cmd)
exitCh <- 0
}
// OpenInBrowser opens the given url on a web browser
func OpenInBrowser(url, browser string) error {
var cmd *exec.Cmd
switch runtime.GOOS {
case "darwin":
if browser == "" {
cmd = exec.Command("open", url)
} else {
cmd = exec.Command("open", "-a", browser, url)
}
case "linux":
if IsWSL() {
cmd = exec.Command("rundll32.exe", "url.dll,FileProtocolHandler", url)
} else {
cmd = exec.Command("xdg-open", url)
}
case "windows":
cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
default:
return errors.Errorf("unsupported platform '%s'", runtime.GOOS)
}
return errors.WithStack(cmd.Start())
}
// Step executes step with the given commands and returns the standard output.
func Step(args ...string) ([]byte, error) {
var stdout bytes.Buffer
//nolint:gosec // arguments controlled by step.
cmd := exec.Command(os.Args[0], args...)
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = &stdout
if err := cmd.Start(); err != nil {
return nil, errors.Wrapf(err, "error starting: %s %s", os.Args[0], strings.Join(args, " "))
}
if err := cmd.Wait(); err != nil {
return nil, errors.Wrapf(err, "error running: %s %s", os.Args[0], strings.Join(args, " "))
}
return stdout.Bytes(), nil
}
// Command executes the given command with it's arguments and returns the
// standard output.
func Command(name string, args ...string) ([]byte, error) {
var stderr bytes.Buffer
cmd := exec.Command(name, args...)
cmd.Stderr = &stderr
out, err := cmd.Output()
if err != nil {
return nil, errors.Wrapf(err, "error running %s %s:\n%s", name, strings.Join(args, " "), stderr.String())
}
return out, nil
}
func run(name string, arg ...string) (*exec.Cmd, chan int, error) {
cmd := exec.Command(name, arg...)
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
// Start process
if err := cmd.Start(); err != nil {
return nil, nil, err
}
// Forward signals
exitCh := make(chan int)
go signalHandler(cmd, exitCh)
return cmd, exitCh, nil
}
func getExitStatus(cmd *exec.Cmd) int {
if cmd.ProcessState != nil {
//nolint:gocritic // ignore single conditional in switch warning
switch sys := cmd.ProcessState.Sys().(type) {
case syscall.WaitStatus:
return sys.ExitStatus()
}
}
return 1
}
func errorf(name string, err error) {
fmt.Fprintf(os.Stderr, "%s: %s\n", path.Base(name), err.Error())
}
func errorAndExit(name string, err error) {
fmt.Fprintf(os.Stderr, "%s: %s\n", path.Base(name), err.Error())
os.Exit(-1)
}
// signalHandler forwards all the signals to the cmd.
func signalHandler(cmd *exec.Cmd, exitCh chan int) {
// signal.Notify prefers a buffered channel. From documentation: "For a
// channel used for notification of just one signal value, a buffer of size
// 1 is sufficient." As we do not know how many signal values the cmd is
// expecting we select 1 as a sane default. In the future maybe we can make
// this value configurable.
signals := make(chan os.Signal, 1)
signal.Notify(signals)
defer signal.Stop(signals)
for {
select {
case sig := <-signals:
cmd.Process.Signal(sig)
case code := <-exitCh:
//nolint:gocritic // ignore exitAfterDefer error - defer is required for recovery.
os.Exit(code)
}
}
}