shell.go
1 package executor 2 3 import ( 4 "bufio" 5 "bytes" 6 "context" 7 "io" 8 "log/slog" 9 "os/exec" 10 "sync" 11 ) 12 13 type LogCallback func(line string) 14 15 type Executor struct{} 16 17 func New() *Executor { 18 return &Executor{} 19 } 20 21 func (e *Executor) Execute(ctx context.Context, script string) (output string, exitCode int, err error) { 22 slog.Info("executing script", "length", len(script)) 23 24 cmd := exec.CommandContext(ctx, "/bin/sh", "-c", script) 25 var stdout, stderr bytes.Buffer 26 cmd.Stdout = &stdout 27 cmd.Stderr = &stderr 28 29 err = cmd.Run() 30 if err != nil { 31 if exitErr, ok := err.(*exec.ExitError); ok { 32 return stdout.String() + stderr.String(), exitErr.ExitCode(), nil 33 } 34 return "", -1, err 35 } 36 37 return stdout.String() + stderr.String(), 0, nil 38 } 39 40 func (e *Executor) ExecuteWithCallback(ctx context.Context, script string, onLog LogCallback) (exitCode int, err error) { 41 slog.Info("executing script with callback", "length", len(script)) 42 43 cmd := exec.CommandContext(ctx, "/bin/sh", "-c", script) 44 45 stdoutPipe, err := cmd.StdoutPipe() 46 if err != nil { 47 return -1, err 48 } 49 stderrPipe, err := cmd.StderrPipe() 50 if err != nil { 51 return -1, err 52 } 53 54 if err := cmd.Start(); err != nil { 55 return -1, err 56 } 57 58 var wg sync.WaitGroup 59 wg.Add(2) 60 61 streamLines := func(r io.Reader) { 62 defer wg.Done() 63 scanner := bufio.NewScanner(r) 64 scanner.Buffer(make([]byte, 64*1024), 1024*1024) 65 for scanner.Scan() { 66 if onLog != nil { 67 onLog(scanner.Text()) 68 } 69 } 70 } 71 72 go streamLines(stdoutPipe) 73 go streamLines(stderrPipe) 74 75 wg.Wait() 76 77 err = cmd.Wait() 78 if err != nil { 79 if exitErr, ok := err.(*exec.ExitError); ok { 80 return exitErr.ExitCode(), nil 81 } 82 return -1, err 83 } 84 85 return 0, nil 86 }