Skip to content

Commit 34062ad

Browse files
authored
capture go build / go mod tidy output to outputcapture (#2540)
1 parent f12ae1e commit 34062ad

File tree

4 files changed

+180
-12
lines changed

4 files changed

+180
-12
lines changed

.golangci.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ version: 2
33
linters:
44
disable:
55
- unused
6-
- unusedfunc
7-
- unusedparams
6+
87
issues:
98
exclude-rules:
109
- linters:

pkg/util/utilfn/streamtolines.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,32 @@ func StreamToLinesChan(input io.Reader) chan LineOutput {
8383
}()
8484
return ch
8585
}
86+
87+
// LineWriter is an io.Writer that processes data line-by-line via a callback.
88+
// Lines do not include the trailing newline. Lines longer than maxLineLength are dropped.
89+
type LineWriter struct {
90+
lineBuf lineBuf
91+
lineFn func([]byte)
92+
}
93+
94+
// NewLineWriter creates a new LineWriter with the given callback function.
95+
func NewLineWriter(lineFn func([]byte)) *LineWriter {
96+
return &LineWriter{
97+
lineFn: lineFn,
98+
}
99+
}
100+
101+
// Write implements io.Writer, processing the data and calling the callback for each complete line.
102+
func (lw *LineWriter) Write(p []byte) (n int, err error) {
103+
streamToLines_processBuf(&lw.lineBuf, p, lw.lineFn)
104+
return len(p), nil
105+
}
106+
107+
// Flush outputs any remaining buffered data as a final line.
108+
// Should be called when the input stream is complete (e.g., at EOF).
109+
func (lw *LineWriter) Flush() {
110+
if len(lw.lineBuf.buf) > 0 && !lw.lineBuf.inLongLine {
111+
lw.lineFn(lw.lineBuf.buf)
112+
lw.lineBuf.buf = nil
113+
}
114+
}

tsunami/build/build.go

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,42 @@ const MinSupportedGoMinorVersion = 22
3030
const TsunamiUIImportPath = "github.com/wavetermdev/waveterm/tsunami/ui"
3131

3232
type OutputCapture struct {
33-
lock sync.Mutex
34-
lines []string
33+
lock sync.Mutex
34+
lines []string
35+
lineWriter *util.LineWriter
3536
}
3637

3738
func MakeOutputCapture() *OutputCapture {
38-
return &OutputCapture{
39+
oc := &OutputCapture{
3940
lines: make([]string, 0),
4041
}
42+
oc.lineWriter = util.NewLineWriter(func(line []byte) {
43+
// synchronized via the Write/Flush functions
44+
oc.lines = append(oc.lines, string(line))
45+
})
46+
return oc
4147
}
4248

43-
func (oc *OutputCapture) Printf(format string, args ...interface{}) {
49+
func (oc *OutputCapture) Write(p []byte) (n int, err error) {
4450
if oc == nil {
51+
return os.Stdout.Write(p)
52+
}
53+
oc.lock.Lock()
54+
defer oc.lock.Unlock()
55+
return oc.lineWriter.Write(p)
56+
}
57+
58+
func (oc *OutputCapture) Flush() {
59+
if oc == nil || oc.lineWriter == nil {
60+
return
61+
}
62+
oc.lock.Lock()
63+
defer oc.lock.Unlock()
64+
oc.lineWriter.Flush()
65+
}
66+
67+
func (oc *OutputCapture) Printf(format string, args ...interface{}) {
68+
if oc == nil || oc.lineWriter == nil {
4569
log.Printf(format, args...)
4670
return
4771
}
@@ -319,15 +343,16 @@ func createGoMod(tempDir, appName, goVersion string, opts BuildOpts, verbose boo
319343
tidyCmd := exec.Command("go", "mod", "tidy")
320344
tidyCmd.Dir = tempDir
321345

322-
if verbose {
346+
if oc != nil || verbose {
323347
oc.Printf("Running go mod tidy")
324-
tidyCmd.Stdout = os.Stdout
325-
tidyCmd.Stderr = os.Stderr
348+
tidyCmd.Stdout = oc
349+
tidyCmd.Stderr = oc
326350
}
327351

328352
if err := tidyCmd.Run(); err != nil {
329353
return fmt.Errorf("failed to run go mod tidy: %w", err)
330354
}
355+
oc.Flush()
331356

332357
if verbose {
333358
oc.Printf("Successfully ran go mod tidy")
@@ -651,15 +676,16 @@ func runGoBuild(tempDir string, opts BuildOpts) error {
651676
buildCmd := exec.Command("go", args...)
652677
buildCmd.Dir = tempDir
653678

654-
if opts.Verbose {
679+
if oc != nil || opts.Verbose {
655680
oc.Printf("Running: %s", strings.Join(buildCmd.Args, " "))
656-
buildCmd.Stdout = os.Stdout
657-
buildCmd.Stderr = os.Stderr
681+
buildCmd.Stdout = oc
682+
buildCmd.Stderr = oc
658683
}
659684

660685
if err := buildCmd.Run(); err != nil {
661686
return fmt.Errorf("failed to build application: %w", err)
662687
}
688+
oc.Flush()
663689

664690
if opts.Verbose {
665691
if opts.OutputFile != "" {

tsunami/util/streamtolines.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright 2025, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package util
5+
6+
import (
7+
"bytes"
8+
"context"
9+
"io"
10+
"time"
11+
)
12+
13+
type LineOutput struct {
14+
Line string
15+
Error error
16+
}
17+
18+
type lineBuf struct {
19+
buf []byte
20+
inLongLine bool
21+
}
22+
23+
const maxLineLength = 128 * 1024
24+
25+
func ReadLineWithTimeout(ch chan LineOutput, timeout time.Duration) (string, error) {
26+
select {
27+
case output := <-ch:
28+
if output.Error != nil {
29+
return "", output.Error
30+
}
31+
return output.Line, nil
32+
case <-time.After(timeout):
33+
return "", context.DeadlineExceeded
34+
}
35+
}
36+
37+
func streamToLines_processBuf(lineBuf *lineBuf, readBuf []byte, lineFn func([]byte)) {
38+
for len(readBuf) > 0 {
39+
nlIdx := bytes.IndexByte(readBuf, '\n')
40+
if nlIdx == -1 {
41+
if lineBuf.inLongLine || len(lineBuf.buf)+len(readBuf) > maxLineLength {
42+
lineBuf.buf = nil
43+
lineBuf.inLongLine = true
44+
return
45+
}
46+
lineBuf.buf = append(lineBuf.buf, readBuf...)
47+
return
48+
}
49+
if !lineBuf.inLongLine && len(lineBuf.buf)+nlIdx <= maxLineLength {
50+
line := append(lineBuf.buf, readBuf[:nlIdx]...)
51+
lineFn(line)
52+
}
53+
lineBuf.buf = nil
54+
lineBuf.inLongLine = false
55+
readBuf = readBuf[nlIdx+1:]
56+
}
57+
}
58+
59+
func StreamToLines(input io.Reader, lineFn func([]byte)) error {
60+
var lineBuf lineBuf
61+
readBuf := make([]byte, 64*1024)
62+
for {
63+
n, err := input.Read(readBuf)
64+
streamToLines_processBuf(&lineBuf, readBuf[:n], lineFn)
65+
if err != nil {
66+
return err
67+
}
68+
}
69+
}
70+
71+
// starts a goroutine to drive the channel
72+
// line output does not include the trailing newline
73+
func StreamToLinesChan(input io.Reader) chan LineOutput {
74+
ch := make(chan LineOutput)
75+
go func() {
76+
defer close(ch)
77+
err := StreamToLines(input, func(line []byte) {
78+
ch <- LineOutput{Line: string(line)}
79+
})
80+
if err != nil && err != io.EOF {
81+
ch <- LineOutput{Error: err}
82+
}
83+
}()
84+
return ch
85+
}
86+
87+
// LineWriter is an io.Writer that processes data line-by-line via a callback.
88+
// Lines do not include the trailing newline. Lines longer than maxLineLength are dropped.
89+
type LineWriter struct {
90+
lineBuf lineBuf
91+
lineFn func([]byte)
92+
}
93+
94+
// NewLineWriter creates a new LineWriter with the given callback function.
95+
func NewLineWriter(lineFn func([]byte)) *LineWriter {
96+
return &LineWriter{
97+
lineFn: lineFn,
98+
}
99+
}
100+
101+
// Write implements io.Writer, processing the data and calling the callback for each complete line.
102+
func (lw *LineWriter) Write(p []byte) (n int, err error) {
103+
streamToLines_processBuf(&lw.lineBuf, p, lw.lineFn)
104+
return len(p), nil
105+
}
106+
107+
// Flush outputs any remaining buffered data as a final line.
108+
// Should be called when the input stream is complete (e.g., at EOF).
109+
func (lw *LineWriter) Flush() {
110+
if len(lw.lineBuf.buf) > 0 && !lw.lineBuf.inLongLine {
111+
lw.lineFn(lw.lineBuf.buf)
112+
lw.lineBuf.buf = nil
113+
}
114+
}

0 commit comments

Comments
 (0)