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 }