@@ -2,25 +2,36 @@ package subprocess
22
33import (
44 "bufio"
5+ "context"
56 "fmt"
67 "io"
78 "os"
89 "os/exec"
910 "sync"
1011)
1112
13+ // LogFormatter is a function type that formats a single line of output
14+ type LogFormatter func (line string ) string
15+
1216type Subprocess struct {
13- Command string
14- Args []string
15- LogPrefix string
16- Stdin io.Reader
17- Stdout io.Writer
18- Stderr io.Writer
19- Dir string
20- Env []string
17+ Command string
18+ Args []string
19+ StdoutFormatter LogFormatter // Function to format stdout lines
20+ StderrFormatter LogFormatter // Function to format stderr lines
21+ Stdin io.Reader
22+ Stdout io.Writer
23+ Stderr io.Writer
24+ Dir string
25+ Env []string
2126}
2227
28+ // Run executes the subprocess without context (for backward compatibility)
2329func Run (sb * Subprocess ) error {
30+ return RunWithContext (context .Background (), sb )
31+ }
32+
33+ // RunWithContext executes the subprocess with the provided context
34+ func RunWithContext (ctx context.Context , sb * Subprocess ) error {
2435 if sb .Stdin == nil {
2536 sb .Stdin = os .Stdin
2637 }
@@ -31,26 +42,35 @@ func Run(sb *Subprocess) error {
3142 sb .Stderr = os .Stderr
3243 }
3344
34- cmd := exec .Command (sb .Command , sb .Args ... )
45+ // Use Background context if ctx is nil
46+ if ctx == nil {
47+ ctx = context .Background ()
48+ }
49+
50+ // Use exec.CommandContext to enable context control
51+ cmd := exec .CommandContext (ctx , sb .Command , sb .Args ... )
3552 cmd .Dir = sb .Dir
3653 cmd .Env = sb .Env
3754 cmd .Stdin = sb .Stdin
3855
3956 m := new (sync.Mutex )
4057 wg := & sync.WaitGroup {}
4158
42- // use pipes to add a prefix to each line.
59+ // Variables to collect errors from goroutines
60+ var stdoutErr , stderrErr error
61+
62+ // Use pipes to add a prefix to each line
4363 stdout , err := cmd .StdoutPipe ()
4464 if err != nil {
4565 return fmt .Errorf ("failed to get stdout pipe: %w" , err )
4666 }
4767
4868 wg .Add (1 )
4969 go func () {
50- if err := scanLines (stdout , sb .Stdout , sb .LogPrefix , m ); err != nil {
51- _ , _ = fmt .Fprintf (sb .Stderr , "failed to scan stdout: %v" , err )
70+ defer wg .Done ()
71+ if err := scanLines (stdout , sb .Stdout , sb .StdoutFormatter , m ); err != nil {
72+ stdoutErr = fmt .Errorf ("failed to scan stdout: %w" , err )
5273 }
53- wg .Done ()
5474 }()
5575
5676 stderr , err := cmd .StderrPipe ()
@@ -59,31 +79,66 @@ func Run(sb *Subprocess) error {
5979 }
6080 wg .Add (1 )
6181 go func () {
62- if err := scanLines (stderr , sb .Stderr , sb .LogPrefix , m ); err != nil {
63- _ , _ = fmt .Fprintf (sb .Stderr , "failed to scan stderr: %v" , err )
82+ defer wg .Done ()
83+ if err := scanLines (stderr , sb .Stderr , sb .StderrFormatter , m ); err != nil {
84+ stderrErr = fmt .Errorf ("failed to scan stderr: %w" , err )
6485 }
65- wg .Done ()
6686 }()
6787
68- err = cmd .Run ()
88+ // Start the process
89+ err = cmd .Start ()
6990 if err != nil {
70- return err
91+ return fmt . Errorf ( "failed to start command: %w" , err )
7192 }
93+
94+ // Wait for process completion (ensures process is fully terminated)
95+ processErr := cmd .Wait ()
96+
97+ // Wait for stdout/stderr processing completion
7298 wg .Wait ()
7399
100+ // Return process error first, then scan errors
101+ if processErr != nil {
102+ return processErr
103+ }
104+
105+ if stdoutErr != nil {
106+ return stdoutErr
107+ }
108+ if stderrErr != nil {
109+ return stderrErr
110+ }
111+
74112 return nil
75113}
76114
77- func scanLines (src io.ReadCloser , dest io.Writer , prefix string , m * sync.Mutex ) error {
115+ func scanLines (src io.ReadCloser , dest io.Writer , formatter LogFormatter , m * sync.Mutex ) error {
116+ defer src .Close ()
78117 scanner := bufio .NewScanner (src )
79118 for scanner .Scan () {
80- // prevent mixing data in a line.
119+ line := scanner .Text ()
120+
121+ // Apply formatter if provided
122+ if formatter != nil {
123+ line = formatter (line )
124+ }
125+
126+ // Prevent mixing data in a line
81127 m .Lock ()
82- _ , _ = fmt .Fprintf (dest , "%s%s \n " , prefix , scanner . Text () )
128+ _ , _ = fmt .Fprintf (dest , "%s\n " , line )
83129 m .Unlock ()
84130 }
85131 if err := scanner .Err (); err != nil {
86132 return fmt .Errorf ("failed to scan lines: %w" , err )
87133 }
88134 return nil
89135}
136+
137+ // Built-in formatter functions
138+
139+ // PrefixFormatter creates a formatter that adds a prefix to each line
140+ func PrefixFormatter (prefix string ) LogFormatter {
141+ return func (line string ) string {
142+ return prefix + line
143+ }
144+ }
0 commit comments