1//nolint:errcheck
2package system
3
4import (
5 "bufio"
6 "fmt"
7 "os"
8 "os/exec"
9 "strings"
10 "sync"
11)
12
13// CommandResult holds the result of a command execution
14type CommandResult struct {
15 ExitCode int
16 Output string
17 Error error
18}
19
20// RunConfig configures how a command should be executed
21type RunConfig struct {
22 Command string
23 Args []string
24 StreamToLog bool
25 LogPath string
26 WorkingDir string
27 Env []string
28}
29
30// Run executes a command with the given configuration
31func Run(cfg RunConfig) CommandResult {
32 cmd := exec.Command(cfg.Command, cfg.Args...)
33
34 if cfg.WorkingDir != "" {
35 cmd.Dir = cfg.WorkingDir
36 }
37
38 if len(cfg.Env) > 0 {
39 cmd.Env = append(os.Environ(), cfg.Env...)
40 }
41
42 // Create pipes for stdout and stderr
43 stdout, err := cmd.StdoutPipe()
44 if err != nil {
45 return CommandResult{
46 ExitCode: -1,
47 Error: fmt.Errorf("failed to create stdout pipe: %w", err),
48 }
49 }
50
51 stderr, err := cmd.StderrPipe()
52 if err != nil {
53 return CommandResult{
54 ExitCode: -1,
55 Error: fmt.Errorf("failed to create stderr pipe: %w", err),
56 }
57 }
58
59 // Start the command
60 if err := cmd.Start(); err != nil {
61 return CommandResult{
62 ExitCode: -1,
63 Error: fmt.Errorf("failed to start command: %w", err),
64 }
65 }
66
67 var wg sync.WaitGroup
68 var output strings.Builder
69 var logFile *os.File
70
71 // Open log file if streaming is enabled
72 if cfg.StreamToLog && cfg.LogPath != "" {
73 logFile, err = os.OpenFile(cfg.LogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
74 if err != nil {
75 // Continue without logging if we can't open the file
76 fmt.Fprintf(os.Stderr, "Warning: failed to open log file: %v\n", err)
77 } else {
78 defer logFile.Close()
79 }
80 }
81
82 // Process stdout
83 wg.Add(1)
84 go func() {
85 defer wg.Done()
86 scanner := bufio.NewScanner(stdout)
87 for scanner.Scan() {
88 line := scanner.Text()
89 output.WriteString(line + "\n")
90 if logFile != nil {
91 if _, err := fmt.Fprintln(logFile, line); err != nil {
92 fmt.Fprintf(os.Stderr, "failed to write log file: %v\n", err)
93 }
94 if err := logFile.Sync(); err != nil {
95 fmt.Fprintf(os.Stderr, "failed to sync log file: %v\n", err)
96 } // Flush to disk immediately
97 }
98 }
99 }()
100
101 // Process stderr
102 wg.Add(1)
103 go func() {
104 defer wg.Done()
105 scanner := bufio.NewScanner(stderr)
106 for scanner.Scan() {
107 line := scanner.Text()
108 output.WriteString(line + "\n")
109 if logFile != nil {
110 if _, err := fmt.Fprintln(logFile, line); err != nil {
111 fmt.Fprintf(os.Stderr, "failed to write log file: %v\n", err)
112 }
113 if err := logFile.Sync(); err != nil {
114 fmt.Fprintf(os.Stderr, "failed to sync log file: %v\n", err)
115 } // Flush to disk immediately
116 }
117 }
118 }()
119
120 // Wait for output processing to complete
121 wg.Wait()
122
123 // Wait for command to finish
124 err = cmd.Wait()
125
126 exitCode := 0
127 if err != nil {
128 if exitError, ok := err.(*exec.ExitError); ok {
129 exitCode = exitError.ExitCode()
130 } else {
131 exitCode = -1
132 }
133 }
134
135 return CommandResult{
136 ExitCode: exitCode,
137 Output: output.String(),
138 Error: err,
139 }
140}
141
142// RunSimple is a convenience function for simple command execution
143func RunSimple(command string, args ...string) CommandResult {
144 return Run(RunConfig{
145 Command: command,
146 Args: args,
147 })
148}
149
150// RunLogged runs a command and streams output to the log file
151func RunLogged(logPath, command string, args ...string) CommandResult {
152 return Run(RunConfig{
153 Command: command,
154 Args: args,
155 StreamToLog: true,
156 LogPath: logPath,
157 })
158}