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  }