chore: import upstream snapshot with attribution
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
package shellenv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type shellOutputPipe struct {
|
||||
reader *os.File
|
||||
writer *os.File
|
||||
dst io.Writer
|
||||
}
|
||||
|
||||
// RunShellCommand starts cmd with StartShellCommand, waits for it, and
|
||||
// terminates any surviving command-group descendants.
|
||||
//
|
||||
// Use this instead of cmd.Run after ConfigureShellCommand so clean exits and
|
||||
// ordinary errors get the same process-tree cleanup as context cancellation.
|
||||
func RunShellCommand(cmd *exec.Cmd) error {
|
||||
pipes, err := prepareShellOutputPipes(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(pipes) > 0 {
|
||||
return runShellCommandWithOutputPipes(cmd, pipes)
|
||||
}
|
||||
if err := StartShellCommand(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
defer TerminateShellCommandGroup(cmd)
|
||||
return cmd.Wait()
|
||||
}
|
||||
|
||||
// OutputShellCommand is the process-tree-cleaning counterpart to cmd.Output.
|
||||
// It requires cmd.Stdout to be unset, captures stdout, and delegates lifecycle
|
||||
// cleanup to RunShellCommand.
|
||||
func OutputShellCommand(cmd *exec.Cmd) ([]byte, error) {
|
||||
if cmd.Stdout != nil {
|
||||
return nil, errors.New("exec: Stdout already set")
|
||||
}
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
err := RunShellCommand(cmd)
|
||||
return stdout.Bytes(), err
|
||||
}
|
||||
|
||||
// CombinedOutputShellCommand is the process-tree-cleaning counterpart to
|
||||
// cmd.CombinedOutput. It requires cmd.Stdout and cmd.Stderr to be unset,
|
||||
// captures both streams, and delegates lifecycle cleanup to RunShellCommand.
|
||||
func CombinedOutputShellCommand(cmd *exec.Cmd) ([]byte, error) {
|
||||
if cmd.Stdout != nil {
|
||||
return nil, errors.New("exec: Stdout already set")
|
||||
}
|
||||
if cmd.Stderr != nil {
|
||||
return nil, errors.New("exec: Stderr already set")
|
||||
}
|
||||
var output bytes.Buffer
|
||||
cmd.Stdout = &output
|
||||
cmd.Stderr = &output
|
||||
err := RunShellCommand(cmd)
|
||||
return output.Bytes(), err
|
||||
}
|
||||
|
||||
func prepareShellOutputPipes(cmd *exec.Cmd) ([]shellOutputPipe, error) {
|
||||
var pipes []shellOutputPipe
|
||||
stdout := cmd.Stdout
|
||||
stderr := cmd.Stderr
|
||||
|
||||
if stdout != nil && !isShellOutputFile(stdout) {
|
||||
pipe, err := newShellOutputPipe(stdout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd.Stdout = pipe.writer
|
||||
pipes = append(pipes, pipe)
|
||||
if sameShellOutputWriter(stdout, stderr) {
|
||||
cmd.Stderr = pipe.writer
|
||||
return pipes, nil
|
||||
}
|
||||
}
|
||||
|
||||
if stderr != nil && !isShellOutputFile(stderr) {
|
||||
pipe, err := newShellOutputPipe(stderr)
|
||||
if err != nil {
|
||||
closeShellOutputPipes(pipes)
|
||||
return nil, err
|
||||
}
|
||||
cmd.Stderr = pipe.writer
|
||||
pipes = append(pipes, pipe)
|
||||
}
|
||||
|
||||
return pipes, nil
|
||||
}
|
||||
|
||||
func newShellOutputPipe(dst io.Writer) (shellOutputPipe, error) {
|
||||
reader, writer, err := os.Pipe()
|
||||
if err != nil {
|
||||
return shellOutputPipe{}, err
|
||||
}
|
||||
return shellOutputPipe{reader: reader, writer: writer, dst: dst}, nil
|
||||
}
|
||||
|
||||
func runShellCommandWithOutputPipes(cmd *exec.Cmd, pipes []shellOutputPipe) error {
|
||||
if err := StartShellCommand(cmd); err != nil {
|
||||
closeShellOutputPipes(pipes)
|
||||
return err
|
||||
}
|
||||
for _, pipe := range pipes {
|
||||
_ = pipe.writer.Close()
|
||||
}
|
||||
|
||||
copyCh := make(chan error, len(pipes))
|
||||
for _, pipe := range pipes {
|
||||
go func(pipe shellOutputPipe) {
|
||||
_, err := io.Copy(pipe.dst, pipe.reader)
|
||||
_ = pipe.reader.Close()
|
||||
copyCh <- err
|
||||
}(pipe)
|
||||
}
|
||||
|
||||
waitCh := make(chan error, 1)
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
TerminateShellCommandGroup(cmd)
|
||||
waitCh <- err
|
||||
}()
|
||||
|
||||
var waitErr error
|
||||
waitDone := false
|
||||
remainingCopies := len(pipes)
|
||||
var copyErr error
|
||||
var waitDelay <-chan time.Time
|
||||
var waitDelayTimer *time.Timer
|
||||
waitDelayExpired := false
|
||||
for !waitDone || remainingCopies > 0 {
|
||||
select {
|
||||
case err := <-waitCh:
|
||||
waitErr = err
|
||||
waitDone = true
|
||||
if remainingCopies > 0 && cmd.WaitDelay > 0 {
|
||||
waitDelayTimer = time.NewTimer(cmd.WaitDelay)
|
||||
waitDelay = waitDelayTimer.C
|
||||
}
|
||||
case err := <-copyCh:
|
||||
remainingCopies--
|
||||
if err != nil && copyErr == nil {
|
||||
copyErr = err
|
||||
TerminateShellCommandGroup(cmd)
|
||||
}
|
||||
case <-waitDelay:
|
||||
waitDelayExpired = true
|
||||
waitDelay = nil
|
||||
closeShellOutputPipeReaders(pipes)
|
||||
}
|
||||
}
|
||||
if waitDelayTimer != nil {
|
||||
waitDelayTimer.Stop()
|
||||
}
|
||||
if waitErr != nil {
|
||||
return waitErr
|
||||
}
|
||||
if waitDelayExpired {
|
||||
return exec.ErrWaitDelay
|
||||
}
|
||||
return copyErr
|
||||
}
|
||||
|
||||
func closeShellOutputPipes(pipes []shellOutputPipe) {
|
||||
for _, pipe := range pipes {
|
||||
_ = pipe.reader.Close()
|
||||
_ = pipe.writer.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func closeShellOutputPipeReaders(pipes []shellOutputPipe) {
|
||||
for _, pipe := range pipes {
|
||||
_ = pipe.reader.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func isShellOutputFile(w io.Writer) bool {
|
||||
_, ok := w.(*os.File)
|
||||
return ok
|
||||
}
|
||||
|
||||
func sameShellOutputWriter(a, b io.Writer) bool {
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
av := reflect.ValueOf(a)
|
||||
bv := reflect.ValueOf(b)
|
||||
if av.Type() != bv.Type() || !av.Type().Comparable() {
|
||||
return false
|
||||
}
|
||||
return av.Equal(bv)
|
||||
}
|
||||
Reference in New Issue
Block a user