github.com/TBD54566975/ftl@v0.219.0/internal/exec/exec.go (about)

     1  package exec
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"os/exec" //nolint:depguard
     7  	"syscall"
     8  
     9  	"github.com/kballard/go-shellquote"
    10  
    11  	"github.com/TBD54566975/ftl/internal/log"
    12  )
    13  
    14  type Cmd struct {
    15  	*exec.Cmd
    16  	level log.Level
    17  }
    18  
    19  func LookPath(exe string) (string, error) {
    20  	path, err := exec.LookPath(exe)
    21  	return path, err
    22  }
    23  
    24  func Capture(ctx context.Context, dir, exe string, args ...string) ([]byte, error) {
    25  	cmd := Command(ctx, log.Debug, dir, exe, args...)
    26  	cmd.Stdout = nil
    27  	cmd.Stderr = nil
    28  	out, err := cmd.CombinedOutput()
    29  	return out, err
    30  }
    31  
    32  func Command(ctx context.Context, level log.Level, dir, exe string, args ...string) *Cmd {
    33  	logger := log.FromContext(ctx)
    34  	pgid, err := syscall.Getpgid(0)
    35  	if err != nil {
    36  		panic(err)
    37  	}
    38  	logger.Tracef("exec: cd %s && %s %s", shellquote.Join(dir), exe, shellquote.Join(args...))
    39  	cmd := exec.CommandContext(ctx, exe, args...)
    40  	cmd.SysProcAttr = &syscall.SysProcAttr{
    41  		Pgid:    pgid,
    42  		Setpgid: true,
    43  	}
    44  	cmd.Dir = dir
    45  	output := logger.WriterAt(level)
    46  	cmd.Stdout = output
    47  	cmd.Stderr = output
    48  	cmd.Env = os.Environ()
    49  	return &Cmd{cmd, level}
    50  }
    51  
    52  // RunBuffered runs the command and captures the output. If the command fails, the output is logged.
    53  func (c *Cmd) RunBuffered(ctx context.Context) error {
    54  	outputBuffer := NewCircularBuffer(100)
    55  	output := outputBuffer.WriterAt(ctx, c.level)
    56  	c.Cmd.Stdout = output
    57  	c.Cmd.Stderr = output
    58  
    59  	err := c.Run()
    60  	if err != nil {
    61  		log.FromContext(ctx).Infof("%s", outputBuffer.Bytes())
    62  		return err
    63  	}
    64  
    65  	return nil
    66  }
    67  
    68  // Kill sends a signal to the process group of the command.
    69  func (c *Cmd) Kill(signal syscall.Signal) error {
    70  	if c.Process == nil {
    71  		return nil
    72  	}
    73  	return syscall.Kill(c.Process.Pid, signal)
    74  }