1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
|
package exec
import (
"bytes"
"fmt"
"io/ioutil"
"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 := ioutil.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.Write([]byte(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 string, 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
cmd := exec.Command(os.Args[0], args...)
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = &stdout
if err := cmd.Start(); nil != err {
return nil, errors.Wrapf(err, "error starting: %s %s", os.Args[0], strings.Join(args, " "))
}
if err := cmd.Wait(); nil != err {
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 {
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:
os.Exit(code)
}
}
}
|