Skip to content

Commit e1380c0

Browse files
authored
Bitrise logging optimisation v2 (#295)
* Separate closing functions * Move raw xcbuild logs after filter to ensure waiting for them * Flush sinks after closing filter * Swap order of closing the filter and the outputs * Wait for filter done when closing sinks * Use buffer pointer for xcoderaw * Wait for filtering and formatter to close before using the raw output * Add comments explaining the sync closes without defer * Separate filter closing and flushing from closing the rest of the piping * Wrap logging and error checking on filter close * Fix copy paste in xcpretty logs
1 parent b806599 commit e1380c0

File tree

7 files changed

+92
-43
lines changed

7 files changed

+92
-43
lines changed

logio/pipe_wiring.go

Lines changed: 37 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,89 +3,87 @@ package logio
33
import (
44
"bytes"
55
"errors"
6-
"fmt"
76
"io"
87
"os"
98
"regexp"
9+
"sync"
1010
)
1111

1212
// PipeWiring is a helper struct to define the setup and binding of tools and
1313
// xcbuild with a filter and stdout. It is purely boilerplate reduction and it is the
1414
// users responsibility to choose between this and manual hooking of the in/outputs.
1515
// It also provides a convenient Close() method that only closes things that can/should be closed.
1616
type PipeWiring struct {
17-
XcbuildRawout bytes.Buffer
17+
XcbuildRawout *bytes.Buffer
1818
XcbuildStdout io.Writer
1919
XcbuildStderr io.Writer
2020
ToolStdin io.ReadCloser
2121
ToolStdout io.WriteCloser
2222
ToolStderr io.WriteCloser
2323

24-
closer func() error
24+
toolPipeW *io.PipeWriter
25+
bufferedStdout *Sink
26+
toolInSink *Sink
27+
filter *PrefixFilter
28+
29+
closeFilterOnce sync.Once
30+
}
31+
32+
// CloseFilter closes the filter and waits for it to finish
33+
func (p *PipeWiring) CloseFilter() error {
34+
err := error(nil)
35+
p.closeFilterOnce.Do(func() {
36+
err = p.filter.Close()
37+
<-p.filter.Done()
38+
39+
})
40+
return err
2541
}
2642

27-
// Close closes the PipeWiring instances that needs to be closing as part of this instance.
28-
//
29-
// In reality it can only close the filter and the tool input as everything else is
30-
// managed by a command or the os.
31-
func (i *PipeWiring) Close() error {
32-
return i.closer()
43+
// Close ...
44+
func (p *PipeWiring) Close() error {
45+
filterErr := p.CloseFilter()
46+
toolSinkErr := p.toolInSink.Close()
47+
pipeWErr := p.toolPipeW.Close()
48+
bufferedStdoutErr := p.bufferedStdout.Close()
49+
50+
return errors.Join(filterErr, toolSinkErr, pipeWErr, bufferedStdoutErr)
3351
}
3452

3553
// SetupPipeWiring creates a new PipeWiring instance that contains the usual
3654
// input/outputs that an xcodebuild command and a logging tool needs when we are also
3755
// using a logging filter.
3856
func SetupPipeWiring(filter *regexp.Regexp) *PipeWiring {
3957
// Create a buffer to store raw xcbuild output
40-
var rawXcbuild bytes.Buffer
58+
rawXcbuild := bytes.NewBuffer(nil)
4159
// Pipe filtered logs to tool
4260
toolPipeR, toolPipeW := io.Pipe()
4361

4462
// Add a buffer before stdout
4563
bufferedStdout := NewSink(os.Stdout)
4664
// Add a buffer before tool input
47-
xcbuildLogs := NewSink(toolPipeW)
65+
toolInSink := NewSink(toolPipeW)
66+
xcbuildLogs := io.MultiWriter(rawXcbuild, toolInSink)
4867
// Create a filter for [Bitrise ...] prefixes
4968
bitrisePrefixFilter := NewPrefixFilter(
5069
filter,
5170
bufferedStdout,
5271
xcbuildLogs,
5372
)
5473

55-
// Send raw xcbuild out to raw out and filter
56-
rawInputDuplication := io.MultiWriter(&rawXcbuild, bitrisePrefixFilter)
57-
5874
return &PipeWiring{
5975
XcbuildRawout: rawXcbuild,
60-
XcbuildStdout: rawInputDuplication,
61-
XcbuildStderr: rawInputDuplication,
76+
XcbuildStdout: bitrisePrefixFilter,
77+
XcbuildStderr: bitrisePrefixFilter,
6278
ToolStdin: toolPipeR,
6379
ToolStdout: os.Stdout,
6480
ToolStderr: os.Stderr,
65-
closer: func() error {
66-
// XcbuildRawout - no need to close
67-
// XcbuildStdout - Multiwriter, meaning we need to close the subwriters
68-
// XcbuildStderr - Multiwriter, meaning we need to close the subwriters
69-
// ToolStdout - We are not closing stdout
70-
// ToolSterr - We are not closing stderr
71-
72-
var errStr string
73-
74-
if err := bitrisePrefixFilter.Close(); err != nil {
75-
errStr += fmt.Sprintf("failed to close log filter, error: %s", err.Error())
76-
}
77-
if err := toolPipeW.Close(); err != nil {
78-
if len(errStr) > 0 {
79-
errStr += ", "
80-
}
81-
errStr += fmt.Sprintf("failed to close xcodebuild-xcpretty pipe, error: %s", err.Error())
82-
}
8381

84-
if len(errStr) > 0 {
85-
return errors.New(errStr)
86-
}
82+
toolPipeW: toolPipeW,
83+
bufferedStdout: bufferedStdout,
84+
toolInSink: toolInSink,
85+
filter: bitrisePrefixFilter,
8786

88-
return nil
89-
},
87+
closeFilterOnce: sync.Once{},
9088
}
9189
}

logio/pipe_wiring_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package logio_test
2+
3+
import (
4+
"io"
5+
"regexp"
6+
"testing"
7+
8+
"github.com/bitrise-io/go-xcode/v2/logio"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestPipeWiring(t *testing.T) {
13+
sut := logio.SetupPipeWiring(regexp.MustCompile(`^\[Bitrise.*\].*`))
14+
15+
out := NewChanWriterCloser()
16+
go func() {
17+
_, _ = io.Copy(out, sut.ToolStdin)
18+
_ = out.Close()
19+
}()
20+
21+
_, _ = sut.XcbuildStdout.Write([]byte(msg1))
22+
_, _ = sut.XcbuildStdout.Write([]byte(msg2))
23+
_, _ = sut.XcbuildStdout.Write([]byte(msg3))
24+
_, _ = sut.XcbuildStdout.Write([]byte(msg4))
25+
26+
_ = sut.Close()
27+
28+
assert.Equal(t, msg1+msg4, sut.XcbuildRawout.String())
29+
assert.Equal(t, msg1+msg4, out.Messages())
30+
}

logio/prefix_filter.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type PrefixFilter struct {
2020
pipeW *io.PipeWriter
2121

2222
Matching *Sink
23-
Filtered *Sink
23+
Filtered io.Writer
2424

2525
// closing
2626
closeOnce sync.Once
@@ -48,7 +48,7 @@ func (p *PrefixFilter) ScannerError() <-chan error { return p.scannerError }
4848
// NewPrefixFilter returns a new PrefixFilter. Writes are based on line prefix.
4949
//
5050
// Note: Callers are responsible for closing intercepted and target writers that implement io.Closer
51-
func NewPrefixFilter(prefixRegexp *regexp.Regexp, matching, filtered *Sink) *PrefixFilter {
51+
func NewPrefixFilter(prefixRegexp *regexp.Regexp, matching *Sink, filtered io.Writer) *PrefixFilter {
5252
// This is the backing field of the bufio.ReadWriter
5353
pipeR, pipeW := io.Pipe()
5454
messageLost := make(chan error, 1)

logio/prefix_filter_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212

1313
const (
1414
msg1 = "Log message without prefix\n"
15-
msg2 = "[Bitrise Analytics] Log message with prefixs\n"
15+
msg2 = "[Bitrise Analytics] Log message with prefix\n"
1616
msg3 = "[Bitrise Build Cache] Log message with prefix\n"
1717
msg4 = "Stuff [Bitrise Build Cache] Log message without prefix\n"
1818
)

xcodecommand/xcbeautify.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ func (c *XcbeautifyRunner) Run(workDir string, xcodebuildArgs []string, xcbeauti
8282
}
8383
}
8484

85+
// Closing the filter to ensure all output is flushed and processed
86+
if err := loggingIO.CloseFilter(); err != nil {
87+
c.logger.Warnf("logging IO failure, error: %s", err)
88+
}
89+
8590
return Output{
8691
RawOut: loggingIO.XcbuildRawout.Bytes(),
8792
ExitCode: exitCode,

xcodecommand/xcpretty.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ func (c *XcprettyCommandRunner) Run(workDir string, xcodebuildArgs []string, xcp
8888
}
8989
}
9090

91+
// Closing the filter to ensure all output is flushed and processed
92+
if err := loggingIO.CloseFilter(); err != nil {
93+
c.logger.Warnf("logging IO failure, error: %s", err)
94+
}
95+
9196
return Output{
9297
RawOut: loggingIO.XcbuildRawout.Bytes(),
9398
ExitCode: exitCode,

xcpretty/xcpretty.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func (c CommandModel) Run() (string, error) {
6565
Stderr: loggingIO.ToolStderr,
6666
})
6767

68-
// Always close xcpretty outputs
68+
// Wait for the filtering to finish
6969
defer func() {
7070
if err := loggingIO.Close(); err != nil {
7171
fmt.Printf("logging IO failure, error: %s", err)
@@ -76,21 +76,32 @@ func (c CommandModel) Run() (string, error) {
7676
}
7777
}()
7878

79+
closeAndFlushFilter := func() {
80+
// Closing the filter to ensure all output is flushed and processed
81+
if err := loggingIO.CloseFilter(); err != nil {
82+
fmt.Printf("logging IO failure, error: %s", err)
83+
}
84+
}
85+
7986
// Run
8087
if err := xcodebuildCmd.Start(); err != nil {
88+
closeAndFlushFilter()
8189
out := loggingIO.XcbuildRawout.String()
8290
return out, err
8391
}
8492
if err := prettyCmd.Start(); err != nil {
93+
closeAndFlushFilter()
8594
out := loggingIO.XcbuildRawout.String()
8695
return out, err
8796
}
8897

8998
if err := xcodebuildCmd.Wait(); err != nil {
99+
closeAndFlushFilter()
90100
out := loggingIO.XcbuildRawout.String()
91101
return out, err
92102
}
93103

104+
closeAndFlushFilter()
94105
return loggingIO.XcbuildRawout.String(), nil
95106
}
96107

0 commit comments

Comments
 (0)