Skip to content

Commit 0be00bc

Browse files
Merge pull request #8 from kohkimakimoto/dev
update subprocess package
2 parents a73b6e0 + b49a297 commit 0be00bc

File tree

3 files changed

+128
-30
lines changed

3 files changed

+128
-30
lines changed

subprocess/subprocess.go

Lines changed: 76 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,36 @@ package subprocess
22

33
import (
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+
1216
type 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)
2329
func 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+
}

subprocess/subprocess_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package subprocess
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"strings"
7+
"testing"
8+
"time"
9+
)
10+
11+
func TestRunWithContext(t *testing.T) {
12+
var stdout bytes.Buffer
13+
err := RunWithContext(context.Background(), &Subprocess{
14+
Command: "echo",
15+
Args: []string{"hello world"},
16+
Stdout: &stdout,
17+
})
18+
if err != nil {
19+
t.Fatalf("Failed: %v", err)
20+
}
21+
22+
if !strings.Contains(stdout.String(), "hello world") {
23+
t.Errorf("Expected 'hello world', got: %s", stdout.String())
24+
}
25+
}
26+
27+
func TestRunWithContext_Timeout(t *testing.T) {
28+
var stdout bytes.Buffer
29+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
30+
defer cancel()
31+
err := RunWithContext(ctx, &Subprocess{
32+
Command: "sleep",
33+
Args: []string{"10"},
34+
Stdout: &stdout,
35+
})
36+
if err == nil {
37+
t.Fatal("Expected timeout error")
38+
}
39+
t.Logf("Expected timeout error, got: %v", err)
40+
}

viewkit.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,13 @@ type ViewKit struct {
8282
// ViteDevServerStdout is a writer for the Vite dev server stdout.
8383
// The default value is os.Stdout.
8484
ViteDevServerStdout io.Writer
85+
// ViteDevServerStdoutFormatter is a formatter for the Vite dev server stdout.
86+
ViteDevServerStdoutFormatter subprocess.LogFormatter
8587
// ViteDevServerStderr is a writer for the Vite dev server stderr.
8688
// The default value is os.Stderr.
8789
ViteDevServerStderr io.Writer
88-
// ViteDevServerLogPrefix is a prefix for the Vite dev server log.
89-
// The default value is "[vite] ".
90-
ViteDevServerLogPrefix string
90+
// ViteDevServerStderrFormatter is a formatter for the Vite dev server stderr.
91+
ViteDevServerStderrFormatter subprocess.LogFormatter
9192
// ViteManifest is a Vite manifest.
9293
// This is needed to resolve the asset paths in production environment.
9394
ViteManifest ViteManifest
@@ -122,8 +123,9 @@ func New() *ViewKit {
122123
ViteDevServerURL: "http://localhost:5173",
123124
ViteDevServerCommand: []string{"npx", "vite", "--clearScreen=false"},
124125
ViteDevServerStdout: os.Stdout,
126+
ViteDevServerStdoutFormatter: subprocess.PrefixFormatter("[echo-viewkit:vite] "),
125127
ViteDevServerStderr: os.Stderr,
126-
ViteDevServerLogPrefix: "[echo-viewkit:vite] ",
128+
ViteDevServerStderrFormatter: subprocess.PrefixFormatter("[echo-viewkit:vite] "),
127129
ViteManifest: nil,
128130
ViteBasePath: "",
129131
}
@@ -272,11 +274,12 @@ func (v *ViewKit) StartViteDevServer() error {
272274
}
273275

274276
return subprocess.Run(&subprocess.Subprocess{
275-
Command: v.ViteDevServerCommand[0],
276-
Args: v.ViteDevServerCommand[1:],
277-
Stdout: v.ViteDevServerStdout,
278-
Stderr: v.ViteDevServerStderr,
279-
LogPrefix: v.ViteDevServerLogPrefix,
277+
Command: v.ViteDevServerCommand[0],
278+
Args: v.ViteDevServerCommand[1:],
279+
Stdout: v.ViteDevServerStdout,
280+
Stderr: v.ViteDevServerStderr,
281+
StdoutFormatter: v.ViteDevServerStdoutFormatter,
282+
StderrFormatter: v.ViteDevServerStderrFormatter,
280283
})
281284
}
282285

0 commit comments

Comments
 (0)