Files
dokku/plugins/common/exec.go
2025-11-09 20:21:10 -05:00

203 lines
4.8 KiB
Go

package common
import (
"errors"
"fmt"
"io"
"os"
"os/signal"
"strings"
"syscall"
"context"
execute "github.com/alexellis/go-execute/v2"
"github.com/fatih/color"
)
// ExecCommandInput is the input for the ExecCommand function
type ExecCommandInput struct {
// Command is the command to execute
Command string
// Args are the arguments to pass to the command
Args []string
// DisableStdioBuffer disables the stdio buffer
DisableStdioBuffer bool
// Env is the environment variables to pass to the command
Env map[string]string
// PrintCommand prints the command before executing
PrintCommand bool
// Stdin is the stdin of the command
Stdin io.Reader
// StreamStdio prints stdout and stderr directly to os.Stdout/err as
// the command runs
StreamStdio bool
// StreamStdout prints stdout directly to os.Stdout as the command runs.
StreamStdout bool
// StreamStderr prints stderr directly to os.Stderr as the command runs.
StreamStderr bool
// StdoutWriter is the writer to write stdout to
StdoutWriter io.Writer
// StderrWriter is the writer to write stderr to
StderrWriter io.Writer
// Sudo runs the command with sudo -n -u root
Sudo bool
// WorkingDirectory is the working directory to run the command in
WorkingDirectory string
}
// ExecCommandResponse is the response for the ExecCommand function
type ExecCommandResponse struct {
// Stdout is the stdout of the command
Stdout string
// Stderr is the stderr of the command
Stderr string
// ExitCode is the exit code of the command
ExitCode int
// Cancelled is whether the command was cancelled
Cancelled bool
}
// StdoutContents returns the trimmed stdout of the command
func (ecr ExecCommandResponse) StdoutContents() string {
return strings.TrimSpace(ecr.Stdout)
}
// StderrContents returns the trimmed stderr of the command
func (ecr ExecCommandResponse) StderrContents() string {
return strings.TrimSpace(ecr.Stderr)
}
// StderrBytes returns the trimmed stderr of the command as bytes
func (ecr ExecCommandResponse) StderrBytes() []byte {
return []byte(ecr.StderrContents())
}
// StdoutBytes returns the trimmed stdout of the command as bytes
func (ecr ExecCommandResponse) StdoutBytes() []byte {
return []byte(ecr.StdoutContents())
}
// CallExecCommand executes a command on the local host
func CallExecCommand(input ExecCommandInput) (ExecCommandResponse, error) {
ctx := context.Background()
return CallExecCommandWithContext(ctx, input)
}
// CallExecCommandWithContext executes a command on the local host with the given context
func CallExecCommandWithContext(ctx context.Context, input ExecCommandInput) (ExecCommandResponse, error) {
signals := make(chan os.Signal, 1)
signal.Notify(signals, os.Interrupt, syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGQUIT,
syscall.SIGTERM)
ctx, cancel := context.WithCancel(ctx)
go func() {
<-signals
cancel()
}()
// hack: colors do not work natively with io.MultiWriter
// as it isn't detected as a tty. If the output isn't
// being captured, then color output can be forced.
isatty := !color.NoColor
env := os.Environ()
if isatty && input.DisableStdioBuffer {
env = append(env, "FORCE_TTY=1")
}
if input.Env != nil {
for k, v := range input.Env {
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
}
command := input.Command
commandArgs := input.Args
if input.Sudo {
commandArgs = append([]string{"-n", "-u", "root", command}, commandArgs...)
command = "sudo"
}
cmd := execute.ExecTask{
Command: command,
Args: commandArgs,
Env: env,
DisableStdioBuffer: input.DisableStdioBuffer,
}
if input.WorkingDirectory != "" {
cmd.Cwd = input.WorkingDirectory
}
if input.PrintCommand || os.Getenv("DOKKU_TRACE") == "1" {
argsSt := ""
if len(cmd.Args) > 0 {
argsSt = strings.Join(cmd.Args, " ")
}
LogWarn(fmt.Sprintf("exec: %s %s", cmd.Command, argsSt))
}
if input.Stdin != nil {
cmd.Stdin = input.Stdin
} else if isatty {
cmd.Stdin = os.Stdin
}
if input.StreamStdio {
cmd.StreamStdio = true
}
if input.StreamStdout {
cmd.StdOutWriter = os.Stdout
}
if input.StreamStderr {
cmd.StdErrWriter = os.Stderr
}
if input.StdoutWriter != nil {
cmd.StdOutWriter = input.StdoutWriter
}
if input.StderrWriter != nil {
cmd.StdErrWriter = input.StderrWriter
}
res, err := cmd.Execute(ctx)
if err != nil {
return ExecCommandResponse{
Stdout: res.Stdout,
Stderr: res.Stderr,
ExitCode: res.ExitCode,
Cancelled: res.Cancelled,
}, err
}
if res.ExitCode != 0 {
return ExecCommandResponse{
Stdout: res.Stdout,
Stderr: res.Stderr,
ExitCode: res.ExitCode,
Cancelled: res.Cancelled,
}, errors.New(res.Stderr)
}
return ExecCommandResponse{
Stdout: res.Stdout,
Stderr: res.Stderr,
ExitCode: res.ExitCode,
Cancelled: res.Cancelled,
}, nil
}