From 1f61b73f368df24b71f291596bbb640b9d9413b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bala=CC=81zs=20Hajagos?= Date: Tue, 2 Sep 2025 15:56:59 +0200 Subject: [PATCH 01/13] Add a log interceptor Singles out [Bitrise ...] logs from the rest and forwards them to stdout --- loginterceptor/loginterceptor.go | 79 ++++++++++++++++++++++++++++++++ xcpretty/xcpretty.go | 11 +++-- 2 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 loginterceptor/loginterceptor.go diff --git a/loginterceptor/loginterceptor.go b/loginterceptor/loginterceptor.go new file mode 100644 index 00000000..40aa9788 --- /dev/null +++ b/loginterceptor/loginterceptor.go @@ -0,0 +1,79 @@ +package loginterceptor + +import ( + "bufio" + "fmt" + "io" + "regexp" + "sync" +) + +// PrefixInterceptor intercept writes: if a line begins with prefix, it will be written to +// both writers. Partial writes without newline are buffered until a newline. +type PrefixInterceptor struct { + re *regexp.Regexp + intercepted io.Writer + original io.Writer + + // internal pipe and goroutine to scan and route + pr *io.PipeReader + pw *io.PipeWriter + + // close once + closeOnce sync.Once + closeErr error +} + +// NewPrefixInterceptor returns an io.WriteCloser. Writes are based on line prefix. +func NewPrefixInterceptor(re *regexp.Regexp, intercepted, original io.Writer) *PrefixInterceptor { + pr, pw := io.Pipe() + interceptor := &PrefixInterceptor{ + re: re, + intercepted: intercepted, + original: original, + pr: pr, + pw: pw, + } + go interceptor.run() + return interceptor +} + +// Write implements io.Writer. It writes into an internal pipe which the interceptor goroutine consumes. +func (i *PrefixInterceptor) Write(p []byte) (int, error) { + // write into pipe; pipe writer preserves order + return i.pw.Write(p) +} + +// Close stops the interceptor and closes the pipe. +func (i *PrefixInterceptor) Close() error { + i.closeOnce.Do(func() { + // close the writer side which causes reader side to EOF + i.closeErr = i.pw.Close() + // ensure reader is drained (run goroutine will finish) + _ = i.pr.Close() + }) + return i.closeErr +} + +// run reads lines (and partial final chunk) and writes them. +func (i *PrefixInterceptor) run() { + // Use a scanner but with a large buffer to handle long lines. + scanner := bufio.NewScanner(i.pr) + const maxTokenSize = 10 * 1024 * 1024 + buf := make([]byte, 0, 64*1024) + scanner.Buffer(buf, maxTokenSize) + + for scanner.Scan() { + line := scanner.Text() // note: newline removed + // re-append newline to preserve same output format + outLine := line + "\n" + if i.re.MatchString(line) { + _, _ = io.WriteString(i.intercepted, outLine) + } + _, _ = io.WriteString(i.original, outLine) + } + // handle any scanner error + if err := scanner.Err(); err != nil && err != io.EOF { + _, _ = fmt.Fprintf(i.original, "router scanner error: %v\n", err) + } +} diff --git a/xcpretty/xcpretty.go b/xcpretty/xcpretty.go index d1f53502..2ca173d1 100644 --- a/xcpretty/xcpretty.go +++ b/xcpretty/xcpretty.go @@ -5,18 +5,21 @@ import ( "fmt" "io" "os" + "regexp" "github.com/bitrise-io/go-steputils/v2/ruby" loggerV1 "github.com/bitrise-io/go-utils/log" "github.com/bitrise-io/go-utils/v2/command" "github.com/bitrise-io/go-utils/v2/env" "github.com/bitrise-io/go-utils/v2/log" + "github.com/bitrise-io/go-xcode/v2/loginterceptor" "github.com/bitrise-io/go-xcode/v2/xcodebuild" "github.com/hashicorp/go-version" ) const ( toolName = "xcpretty" + prefixed = `^[Bitrise.*].*` ) // CommandModel ... @@ -53,17 +56,19 @@ func (c CommandModel) PrintableCmd() string { // Run ... func (c CommandModel) Run() (string, error) { - // Configure cmd in- and outputs pipeReader, pipeWriter := io.Pipe() var outBuffer bytes.Buffer outWriter := io.MultiWriter(&outBuffer, pipeWriter) + re := regexp.MustCompile(prefixed) + interceptor := loginterceptor.NewPrefixInterceptor(re, os.Stdout, outWriter) + xcodebuildCmd := c.xcodebuildCommand.Command(&command.Opts{ Stdin: nil, - Stdout: outWriter, - Stderr: outWriter, + Stdout: interceptor, + Stderr: interceptor, }) prettyCmd := c.Command(&command.Opts{ From e7f0a9d6e0c8f7ef62200b1bc36bb00f2c9c3f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bala=CC=81zs=20Hajagos?= Date: Tue, 2 Sep 2025 16:03:53 +0200 Subject: [PATCH 02/13] Close intercepted writer --- loginterceptor/loginterceptor.go | 9 +++++++-- xcpretty/xcpretty.go | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/loginterceptor/loginterceptor.go b/loginterceptor/loginterceptor.go index 40aa9788..da09faae 100644 --- a/loginterceptor/loginterceptor.go +++ b/loginterceptor/loginterceptor.go @@ -68,9 +68,14 @@ func (i *PrefixInterceptor) run() { // re-append newline to preserve same output format outLine := line + "\n" if i.re.MatchString(line) { - _, _ = io.WriteString(i.intercepted, outLine) + if _, err := io.WriteString(i.intercepted, outLine); err != nil { + _, _ = fmt.Fprintf(i.original, "intercepted writer error: %v\n", err) + } + } + if _, err := io.WriteString(i.original, outLine); err != nil { + // Log error but continue processing + _, _ = fmt.Fprintf(i.original, "original writer error: %v\n", err) } - _, _ = io.WriteString(i.original, outLine) } // handle any scanner error if err := scanner.Err(); err != nil && err != io.EOF { diff --git a/xcpretty/xcpretty.go b/xcpretty/xcpretty.go index 2ca173d1..27651bc7 100644 --- a/xcpretty/xcpretty.go +++ b/xcpretty/xcpretty.go @@ -64,6 +64,11 @@ func (c CommandModel) Run() (string, error) { re := regexp.MustCompile(prefixed) interceptor := loginterceptor.NewPrefixInterceptor(re, os.Stdout, outWriter) + defer func() { + if err := interceptor.Close(); err != nil { + loggerV1.Warnf("Failed to close log interceptor, error: %s", err) + } + }() xcodebuildCmd := c.xcodebuildCommand.Command(&command.Opts{ Stdin: nil, From ec9cdc389e4ec11e58b87206aefcd86281f76ea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bala=CC=81zs=20Hajagos?= Date: Tue, 2 Sep 2025 16:07:35 +0200 Subject: [PATCH 03/13] Log writer and scanner errors to stderr --- loginterceptor/loginterceptor.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/loginterceptor/loginterceptor.go b/loginterceptor/loginterceptor.go index da09faae..0f98c26d 100644 --- a/loginterceptor/loginterceptor.go +++ b/loginterceptor/loginterceptor.go @@ -4,6 +4,7 @@ import ( "bufio" "fmt" "io" + "os" "regexp" "sync" ) @@ -69,16 +70,16 @@ func (i *PrefixInterceptor) run() { outLine := line + "\n" if i.re.MatchString(line) { if _, err := io.WriteString(i.intercepted, outLine); err != nil { - _, _ = fmt.Fprintf(i.original, "intercepted writer error: %v\n", err) + _, _ = fmt.Fprintf(os.Stderr, "intercepted writer error: %v\n", err) } } if _, err := io.WriteString(i.original, outLine); err != nil { // Log error but continue processing - _, _ = fmt.Fprintf(i.original, "original writer error: %v\n", err) + _, _ = fmt.Fprintf(os.Stderr, "original writer error: %v\n", err) } } // handle any scanner error if err := scanner.Err(); err != nil && err != io.EOF { - _, _ = fmt.Fprintf(i.original, "router scanner error: %v\n", err) + _, _ = fmt.Fprintf(os.Stderr, "router scanner error: %v\n", err) } } From 19d4f3a443a84e7daedf2f31a19dcb6729f0e867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Hajagos?= Date: Tue, 2 Sep 2025 16:41:42 +0200 Subject: [PATCH 04/13] Update loginterceptor/loginterceptor.go Co-authored-by: bitrise-ip-bot <95076763+bitrise-ip-bot@users.noreply.github.com> --- loginterceptor/loginterceptor.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/loginterceptor/loginterceptor.go b/loginterceptor/loginterceptor.go index 0f98c26d..79bacfbb 100644 --- a/loginterceptor/loginterceptor.go +++ b/loginterceptor/loginterceptor.go @@ -41,6 +41,15 @@ func NewPrefixInterceptor(re *regexp.Regexp, intercepted, original io.Writer) *P // Write implements io.Writer. It writes into an internal pipe which the interceptor goroutine consumes. func (i *PrefixInterceptor) Write(p []byte) (int, error) { + // Check if already closed to prevent writing to closed pipe + select { + case <-i.pr.Done(): // Add a done channel to track closure + return 0, io.ErrClosedPipe + default: + // write into pipe; pipe writer preserves order + return i.pw.Write(p) + } +} // write into pipe; pipe writer preserves order return i.pw.Write(p) } From 878fd89452c4667f6d14b1aa73485b1ebbe7037e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Hajagos?= Date: Tue, 2 Sep 2025 16:42:39 +0200 Subject: [PATCH 05/13] Fix regex Co-authored-by: bitrise-ip-bot <95076763+bitrise-ip-bot@users.noreply.github.com> --- xcpretty/xcpretty.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xcpretty/xcpretty.go b/xcpretty/xcpretty.go index 27651bc7..9f3bcd17 100644 --- a/xcpretty/xcpretty.go +++ b/xcpretty/xcpretty.go @@ -19,7 +19,7 @@ import ( const ( toolName = "xcpretty" - prefixed = `^[Bitrise.*].*` + prefixed = `^\\[Bitrise.*\\].*` ) // CommandModel ... From a915afc19dccb6c5f901b24e0ea2174a659172d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Hajagos?= Date: Tue, 2 Sep 2025 16:44:20 +0200 Subject: [PATCH 06/13] Close pipe reader on coroutine end Co-authored-by: bitrise-ip-bot <95076763+bitrise-ip-bot@users.noreply.github.com> --- loginterceptor/loginterceptor.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/loginterceptor/loginterceptor.go b/loginterceptor/loginterceptor.go index 79bacfbb..0f306005 100644 --- a/loginterceptor/loginterceptor.go +++ b/loginterceptor/loginterceptor.go @@ -67,6 +67,11 @@ func (i *PrefixInterceptor) Close() error { // run reads lines (and partial final chunk) and writes them. func (i *PrefixInterceptor) run() { + defer func() { + // Ensure pipe reader is closed when goroutine exits + _ = i.pr.Close() + }() + // Use a scanner but with a large buffer to handle long lines. scanner := bufio.NewScanner(i.pr) const maxTokenSize = 10 * 1024 * 1024 From 9e64079e26ba39f97a43aaa1ee33bfec07897412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bala=CC=81zs=20Hajagos?= Date: Tue, 2 Sep 2025 16:46:51 +0200 Subject: [PATCH 07/13] Fix AI review suggestions --- loginterceptor/loginterceptor.go | 12 +----------- xcpretty/xcpretty.go | 2 +- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/loginterceptor/loginterceptor.go b/loginterceptor/loginterceptor.go index 0f306005..e615d40f 100644 --- a/loginterceptor/loginterceptor.go +++ b/loginterceptor/loginterceptor.go @@ -41,16 +41,6 @@ func NewPrefixInterceptor(re *regexp.Regexp, intercepted, original io.Writer) *P // Write implements io.Writer. It writes into an internal pipe which the interceptor goroutine consumes. func (i *PrefixInterceptor) Write(p []byte) (int, error) { - // Check if already closed to prevent writing to closed pipe - select { - case <-i.pr.Done(): // Add a done channel to track closure - return 0, io.ErrClosedPipe - default: - // write into pipe; pipe writer preserves order - return i.pw.Write(p) - } -} - // write into pipe; pipe writer preserves order return i.pw.Write(p) } @@ -71,7 +61,7 @@ func (i *PrefixInterceptor) run() { // Ensure pipe reader is closed when goroutine exits _ = i.pr.Close() }() - + // Use a scanner but with a large buffer to handle long lines. scanner := bufio.NewScanner(i.pr) const maxTokenSize = 10 * 1024 * 1024 diff --git a/xcpretty/xcpretty.go b/xcpretty/xcpretty.go index 9f3bcd17..b2d47f63 100644 --- a/xcpretty/xcpretty.go +++ b/xcpretty/xcpretty.go @@ -19,7 +19,7 @@ import ( const ( toolName = "xcpretty" - prefixed = `^\\[Bitrise.*\\].*` + prefixed = `^\[Bitrise.*\].*` ) // CommandModel ... From 62bff66db068d9e265822aa8e69aea0c46f21cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bala=CC=81zs=20Hajagos?= Date: Tue, 2 Sep 2025 16:51:50 +0200 Subject: [PATCH 08/13] Remove pr close on run, Close() handles it --- loginterceptor/loginterceptor.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/loginterceptor/loginterceptor.go b/loginterceptor/loginterceptor.go index e615d40f..bdfa7a4f 100644 --- a/loginterceptor/loginterceptor.go +++ b/loginterceptor/loginterceptor.go @@ -57,11 +57,6 @@ func (i *PrefixInterceptor) Close() error { // run reads lines (and partial final chunk) and writes them. func (i *PrefixInterceptor) run() { - defer func() { - // Ensure pipe reader is closed when goroutine exits - _ = i.pr.Close() - }() - // Use a scanner but with a large buffer to handle long lines. scanner := bufio.NewScanner(i.pr) const maxTokenSize = 10 * 1024 * 1024 From 0b980e1479ce6d33226e2c3619dc13c4fbbc459b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bala=CC=81zs=20Hajagos?= Date: Wed, 3 Sep 2025 11:18:54 +0200 Subject: [PATCH 09/13] Add test for interceptor and close chained writers on interceptor close --- loginterceptor/logininterceptor_test.go | 70 +++++++++++++++++++++++++ loginterceptor/loginterceptor.go | 8 +-- 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 loginterceptor/logininterceptor_test.go diff --git a/loginterceptor/logininterceptor_test.go b/loginterceptor/logininterceptor_test.go new file mode 100644 index 00000000..d1a2e0d7 --- /dev/null +++ b/loginterceptor/logininterceptor_test.go @@ -0,0 +1,70 @@ +package loginterceptor_test + +import ( + "bytes" + "io" + "regexp" + "sync" + "testing" + + "github.com/bitrise-io/go-xcode/v2/loginterceptor" + "github.com/stretchr/testify/assert" +) + +func TestPrefixInterceptor(t *testing.T) { + ir, iw := io.Pipe() + tr, tw := io.Pipe() + re := regexp.MustCompile(`^\[Bitrise.*\].*`) + + sut := loginterceptor.NewPrefixInterceptor(re, iw, tw) + + msg1 := "Log message without prefix\n" + msg2 := "[Bitrise Analytics] Log message with prefix\n" + msg3 := "[Bitrise Build Cache] Log message with prefix\n" + msg4 := "Stuff [Bitrise Build Cache] Log message without prefix\n" + + go func() { + defer sut.Close() + + sut.Write([]byte(msg1)) + sut.Write([]byte(msg2)) + sut.Write([]byte(msg3)) + sut.Write([]byte(msg4)) + }() + + intercepted, target, err := readTwo(ir, tr) + assert.NoError(t, err) + assert.Equal(t, msg2+msg3, string(intercepted)) + assert.Equal(t, msg1+msg2+msg3+msg4, string(target)) +} + +func readTwo(r1, r2 io.Reader) (out1, out2 []byte, err error) { + var ( + wg sync.WaitGroup + e1, e2 error + ) + wg.Add(2) + + var b1, b2 bytes.Buffer + + go func() { + defer wg.Done() + _, e1 = io.Copy(&b1, r1) + }() + + go func() { + defer wg.Done() + _, e2 = io.Copy(&b2, r2) + }() + + wg.Wait() + + // prefer to return the first non-nil error + if e1 != nil { + return b1.Bytes(), b2.Bytes(), e1 + } + if e2 != nil { + return b1.Bytes(), b2.Bytes(), e2 + } + return b1.Bytes(), b2.Bytes(), nil +} diff --git a/loginterceptor/loginterceptor.go b/loginterceptor/loginterceptor.go index bdfa7a4f..bc9fe57a 100644 --- a/loginterceptor/loginterceptor.go +++ b/loginterceptor/loginterceptor.go @@ -13,8 +13,8 @@ import ( // both writers. Partial writes without newline are buffered until a newline. type PrefixInterceptor struct { re *regexp.Regexp - intercepted io.Writer - original io.Writer + intercepted io.WriteCloser + original io.WriteCloser // internal pipe and goroutine to scan and route pr *io.PipeReader @@ -26,7 +26,7 @@ type PrefixInterceptor struct { } // NewPrefixInterceptor returns an io.WriteCloser. Writes are based on line prefix. -func NewPrefixInterceptor(re *regexp.Regexp, intercepted, original io.Writer) *PrefixInterceptor { +func NewPrefixInterceptor(re *regexp.Regexp, intercepted, original io.WriteCloser) *PrefixInterceptor { pr, pw := io.Pipe() interceptor := &PrefixInterceptor{ re: re, @@ -57,6 +57,8 @@ func (i *PrefixInterceptor) Close() error { // run reads lines (and partial final chunk) and writes them. func (i *PrefixInterceptor) run() { + defer i.intercepted.Close() + defer i.original.Close() // Use a scanner but with a large buffer to handle long lines. scanner := bufio.NewScanner(i.pr) const maxTokenSize = 10 * 1024 * 1024 From 368c1e68f0efe7290dd6c1e76703e6cdfa32f807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bala=CC=81zs=20Hajagos?= Date: Wed, 3 Sep 2025 11:42:10 +0200 Subject: [PATCH 10/13] Make closing optional for interceptor writers --- loginterceptor/logininterceptor_test.go | 9 +++++---- loginterceptor/loginterceptor.go | 16 +++++++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/loginterceptor/logininterceptor_test.go b/loginterceptor/logininterceptor_test.go index d1a2e0d7..984bb9f2 100644 --- a/loginterceptor/logininterceptor_test.go +++ b/loginterceptor/logininterceptor_test.go @@ -24,12 +24,13 @@ func TestPrefixInterceptor(t *testing.T) { msg4 := "Stuff [Bitrise Build Cache] Log message without prefix\n" go func() { + //nolint:errCheck defer sut.Close() - sut.Write([]byte(msg1)) - sut.Write([]byte(msg2)) - sut.Write([]byte(msg3)) - sut.Write([]byte(msg4)) + _, _ = sut.Write([]byte(msg1)) + _, _ = sut.Write([]byte(msg2)) + _, _ = sut.Write([]byte(msg3)) + _, _ = sut.Write([]byte(msg4)) }() intercepted, target, err := readTwo(ir, tr) diff --git a/loginterceptor/loginterceptor.go b/loginterceptor/loginterceptor.go index bc9fe57a..35432b1a 100644 --- a/loginterceptor/loginterceptor.go +++ b/loginterceptor/loginterceptor.go @@ -13,8 +13,8 @@ import ( // both writers. Partial writes without newline are buffered until a newline. type PrefixInterceptor struct { re *regexp.Regexp - intercepted io.WriteCloser - original io.WriteCloser + intercepted io.Writer + original io.Writer // internal pipe and goroutine to scan and route pr *io.PipeReader @@ -26,7 +26,7 @@ type PrefixInterceptor struct { } // NewPrefixInterceptor returns an io.WriteCloser. Writes are based on line prefix. -func NewPrefixInterceptor(re *regexp.Regexp, intercepted, original io.WriteCloser) *PrefixInterceptor { +func NewPrefixInterceptor(re *regexp.Regexp, intercepted, original io.Writer) *PrefixInterceptor { pr, pw := io.Pipe() interceptor := &PrefixInterceptor{ re: re, @@ -57,8 +57,14 @@ func (i *PrefixInterceptor) Close() error { // run reads lines (and partial final chunk) and writes them. func (i *PrefixInterceptor) run() { - defer i.intercepted.Close() - defer i.original.Close() + if interceptedCloser, ok := i.intercepted.(io.Closer); ok { + //nolint:errCheck + defer interceptedCloser.Close() + } + if originalCloser, ok := i.original.(io.Closer); ok { + //nolint:errCheck + defer originalCloser.Close() + } // Use a scanner but with a large buffer to handle long lines. scanner := bufio.NewScanner(i.pr) const maxTokenSize = 10 * 1024 * 1024 From 115dc4099d5ebed03c432e319783515a0b70fee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bala=CC=81zs=20Hajagos?= Date: Wed, 3 Sep 2025 16:08:09 +0200 Subject: [PATCH 11/13] Update variable names, introduce logger, update closing mechanism --- loginterceptor/loginterceptor.go | 61 ++++++++++--------- ...rceptor_test.go => loginterceptor_test.go} | 9 +-- xcpretty/xcpretty.go | 5 +- 3 files changed, 41 insertions(+), 34 deletions(-) rename loginterceptor/{logininterceptor_test.go => loginterceptor_test.go} (81%) diff --git a/loginterceptor/loginterceptor.go b/loginterceptor/loginterceptor.go index 35432b1a..cd33e911 100644 --- a/loginterceptor/loginterceptor.go +++ b/loginterceptor/loginterceptor.go @@ -2,23 +2,24 @@ package loginterceptor import ( "bufio" - "fmt" "io" - "os" "regexp" "sync" + + "github.com/bitrise-io/go-utils/v2/log" ) // PrefixInterceptor intercept writes: if a line begins with prefix, it will be written to // both writers. Partial writes without newline are buffered until a newline. type PrefixInterceptor struct { - re *regexp.Regexp - intercepted io.Writer - original io.Writer + prefixRegexp *regexp.Regexp + intercepted io.Writer + target io.Writer + logger log.Logger // internal pipe and goroutine to scan and route - pr *io.PipeReader - pw *io.PipeWriter + internalReader *io.PipeReader + internalWriter *io.PipeWriter // close once closeOnce sync.Once @@ -26,14 +27,15 @@ type PrefixInterceptor struct { } // NewPrefixInterceptor returns an io.WriteCloser. Writes are based on line prefix. -func NewPrefixInterceptor(re *regexp.Regexp, intercepted, original io.Writer) *PrefixInterceptor { - pr, pw := io.Pipe() +func NewPrefixInterceptor(prefixRegexp *regexp.Regexp, intercepted, target io.Writer, logger log.Logger) *PrefixInterceptor { + pipeReader, pipeWriter := io.Pipe() interceptor := &PrefixInterceptor{ - re: re, - intercepted: intercepted, - original: original, - pr: pr, - pw: pw, + prefixRegexp: prefixRegexp, + intercepted: intercepted, + target: target, + logger: logger, + internalReader: pipeReader, + internalWriter: pipeWriter, } go interceptor.run() return interceptor @@ -41,32 +43,32 @@ func NewPrefixInterceptor(re *regexp.Regexp, intercepted, original io.Writer) *P // Write implements io.Writer. It writes into an internal pipe which the interceptor goroutine consumes. func (i *PrefixInterceptor) Write(p []byte) (int, error) { - return i.pw.Write(p) + return i.internalWriter.Write(p) } // Close stops the interceptor and closes the pipe. func (i *PrefixInterceptor) Close() error { i.closeOnce.Do(func() { - // close the writer side which causes reader side to EOF - i.closeErr = i.pw.Close() - // ensure reader is drained (run goroutine will finish) - _ = i.pr.Close() + i.closeErr = i.internalWriter.Close() }) return i.closeErr } // run reads lines (and partial final chunk) and writes them. func (i *PrefixInterceptor) run() { + // Close writers if able if interceptedCloser, ok := i.intercepted.(io.Closer); ok { //nolint:errCheck defer interceptedCloser.Close() } - if originalCloser, ok := i.original.(io.Closer); ok { + if originalCloser, ok := i.target.(io.Closer); ok { //nolint:errCheck defer originalCloser.Close() } + defer i.internalWriter.Close() + // Use a scanner but with a large buffer to handle long lines. - scanner := bufio.NewScanner(i.pr) + scanner := bufio.NewScanner(i.internalReader) const maxTokenSize = 10 * 1024 * 1024 buf := make([]byte, 0, 64*1024) scanner.Buffer(buf, maxTokenSize) @@ -75,18 +77,21 @@ func (i *PrefixInterceptor) run() { line := scanner.Text() // note: newline removed // re-append newline to preserve same output format outLine := line + "\n" - if i.re.MatchString(line) { + + // Write to intercepted channel if matching regexp + if i.prefixRegexp.MatchString(line) { if _, err := io.WriteString(i.intercepted, outLine); err != nil { - _, _ = fmt.Fprintf(os.Stderr, "intercepted writer error: %v\n", err) + i.logger.Errorf("intercept writer error: %v", err) } } - if _, err := io.WriteString(i.original, outLine); err != nil { - // Log error but continue processing - _, _ = fmt.Fprintf(os.Stderr, "original writer error: %v\n", err) + // Always write to target channel + if _, err := io.WriteString(i.target, outLine); err != nil { + i.logger.Errorf("writer error: %v", err) } } + // handle any scanner error - if err := scanner.Err(); err != nil && err != io.EOF { - _, _ = fmt.Fprintf(os.Stderr, "router scanner error: %v\n", err) + if err := scanner.Err(); err != nil { + i.logger.Errorf("router scanner error: %v\n", err) } } diff --git a/loginterceptor/logininterceptor_test.go b/loginterceptor/loginterceptor_test.go similarity index 81% rename from loginterceptor/logininterceptor_test.go rename to loginterceptor/loginterceptor_test.go index 984bb9f2..d37c5082 100644 --- a/loginterceptor/logininterceptor_test.go +++ b/loginterceptor/loginterceptor_test.go @@ -7,16 +7,17 @@ import ( "sync" "testing" + "github.com/bitrise-io/go-utils/v2/log" "github.com/bitrise-io/go-xcode/v2/loginterceptor" "github.com/stretchr/testify/assert" ) func TestPrefixInterceptor(t *testing.T) { - ir, iw := io.Pipe() - tr, tw := io.Pipe() + interceptReader, interceptWriter := io.Pipe() + targetReader, targetWriter := io.Pipe() re := regexp.MustCompile(`^\[Bitrise.*\].*`) - sut := loginterceptor.NewPrefixInterceptor(re, iw, tw) + sut := loginterceptor.NewPrefixInterceptor(re, interceptWriter, targetWriter, log.NewLogger()) msg1 := "Log message without prefix\n" msg2 := "[Bitrise Analytics] Log message with prefix\n" @@ -33,7 +34,7 @@ func TestPrefixInterceptor(t *testing.T) { _, _ = sut.Write([]byte(msg4)) }() - intercepted, target, err := readTwo(ir, tr) + intercepted, target, err := readTwo(interceptReader, targetReader) assert.NoError(t, err) assert.Equal(t, msg2+msg3, string(intercepted)) assert.Equal(t, msg1+msg2+msg3+msg4, string(target)) diff --git a/xcpretty/xcpretty.go b/xcpretty/xcpretty.go index b2d47f63..38a9331a 100644 --- a/xcpretty/xcpretty.go +++ b/xcpretty/xcpretty.go @@ -62,11 +62,12 @@ func (c CommandModel) Run() (string, error) { var outBuffer bytes.Buffer outWriter := io.MultiWriter(&outBuffer, pipeWriter) + logger := log.NewLogger() re := regexp.MustCompile(prefixed) - interceptor := loginterceptor.NewPrefixInterceptor(re, os.Stdout, outWriter) + interceptor := loginterceptor.NewPrefixInterceptor(re, os.Stdout, outWriter, logger) defer func() { if err := interceptor.Close(); err != nil { - loggerV1.Warnf("Failed to close log interceptor, error: %s", err) + logger.Warnf("Failed to close log interceptor, error: %s", err) } }() From 126f22c88378a36b5056607c9c70d6e397ea6644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bala=CC=81zs=20Hajagos?= Date: Wed, 3 Sep 2025 16:21:24 +0200 Subject: [PATCH 12/13] Close reader too, restructure after goroutine closing --- loginterceptor/loginterceptor.go | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/loginterceptor/loginterceptor.go b/loginterceptor/loginterceptor.go index cd33e911..73e51866 100644 --- a/loginterceptor/loginterceptor.go +++ b/loginterceptor/loginterceptor.go @@ -54,18 +54,32 @@ func (i *PrefixInterceptor) Close() error { return i.closeErr } -// run reads lines (and partial final chunk) and writes them. -func (i *PrefixInterceptor) run() { +func (i *PrefixInterceptor) closeAfterRun() { // Close writers if able if interceptedCloser, ok := i.intercepted.(io.Closer); ok { - //nolint:errCheck - defer interceptedCloser.Close() + if err := interceptedCloser.Close(); err != nil { + i.logger.Errorf("closing intercepted writer: %v", err) + } } if originalCloser, ok := i.target.(io.Closer); ok { - //nolint:errCheck - defer originalCloser.Close() + if err := originalCloser.Close(); err != nil { + i.logger.Errorf("closing original writer: %v", err) + } + } + + // Close internals (writer might be closed already by the Close() above) + if err := i.internalWriter.Close(); err != nil { + i.logger.Errorf("internal writer: %v", err) } - defer i.internalWriter.Close() + + if err := i.internalReader.Close(); err != nil { + i.logger.Errorf("internal reader: %v", err) + } +} + +// run reads lines (and partial final chunk) and writes them. +func (i *PrefixInterceptor) run() { + defer i.closeAfterRun() // Use a scanner but with a large buffer to handle long lines. scanner := bufio.NewScanner(i.internalReader) From 5922f82c3bbac18a44c756961c66d55bb0271315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bala=CC=81zs=20Hajagos?= Date: Wed, 3 Sep 2025 16:35:15 +0200 Subject: [PATCH 13/13] Remove superfuous writer close --- loginterceptor/loginterceptor.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/loginterceptor/loginterceptor.go b/loginterceptor/loginterceptor.go index 73e51866..d4eb3bf6 100644 --- a/loginterceptor/loginterceptor.go +++ b/loginterceptor/loginterceptor.go @@ -67,11 +67,6 @@ func (i *PrefixInterceptor) closeAfterRun() { } } - // Close internals (writer might be closed already by the Close() above) - if err := i.internalWriter.Close(); err != nil { - i.logger.Errorf("internal writer: %v", err) - } - if err := i.internalReader.Close(); err != nil { i.logger.Errorf("internal reader: %v", err) }