aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-08-27 16:07:05 +0900
committerOphestra <cat@gensokyo.uk>2026-08-27 16:07:05 +0900
commitccb004e2174ca9fedba7bc95b036fc62dbdc28b1 (patch)
treed8b0603060d3cec0ee28f4cd55ac88c2560c1ce0 /internal
parentebb1144efc6c09dff8a732c5baf11db5b8f515b2 (diff)
internal/pkg: treat bare carriage return as end-of-line marker
This avoids having progress indicators mess up logging. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal')
-rw-r--r--internal/pkg/exec.go32
-rw-r--r--internal/pkg/exec_test.go93
2 files changed, 124 insertions, 1 deletions
diff --git a/internal/pkg/exec.go b/internal/pkg/exec.go
index 7c4133eb..e499ecf1 100644
--- a/internal/pkg/exec.go
+++ b/internal/pkg/exec.go
@@ -2,6 +2,7 @@ package pkg
import (
"bufio"
+ "bytes"
"context"
"errors"
"fmt"
@@ -439,6 +440,34 @@ const (
execWaitDelay = time.Nanosecond
)
+// scanLinesCR is like [bufio.ScanLines], but also treats a bare \r as an
+// end-of-line marker.
+func scanLinesCR(data []byte, atEOF bool) (advance int, token []byte, err error) {
+ if atEOF && len(data) == 0 {
+ return 0, nil, nil
+ }
+ ri, ni := bytes.IndexByte(data, '\r'), bytes.IndexByte(data, '\n')
+
+ if ri >= 0 && (ni < 0 || ri < ni) {
+ if ri+1 == ni {
+ // We have a full \r\n-terminated line.
+ return ri + 2, data[:ri], nil
+ }
+ // We have a bare \r, probably some kind of progress indicator.
+ return ri + 1, data[:ri], nil
+ }
+ if ni >= 0 && (ri < 0 || ni < ri) {
+ // We have a full newline-terminated line.
+ return ni + 1, data[:ni], nil
+ }
+ // If we're at EOF, we have a final, non-terminated line. Return it.
+ if atEOF {
+ return len(data), data, nil
+ }
+ // Request more data.
+ return 0, nil, nil
+}
+
// scanVerbose prefixes program output for a verbose [message.Msg].
func scanVerbose(
msg message.Msg,
@@ -449,6 +478,7 @@ func scanVerbose(
) {
defer close(done)
s := bufio.NewScanner(r)
+ s.Split(scanLinesCR)
s.Buffer(
make([]byte, bufio.MaxScanTokenSize),
bufio.MaxScanTokenSize<<12,
@@ -458,7 +488,7 @@ func scanVerbose(
}
if err := s.Err(); err != nil && !errors.Is(err, os.ErrClosed) {
cancel()
- msg.Verbose("*"+prefix, err)
+ msg.Verbose("*"+prefix, err.Error()+suffix)
}
}
diff --git a/internal/pkg/exec_test.go b/internal/pkg/exec_test.go
index 222ab790..408d6257 100644
--- a/internal/pkg/exec_test.go
+++ b/internal/pkg/exec_test.go
@@ -1,6 +1,7 @@
package pkg_test
import (
+ "bufio"
"bytes"
_ "embed"
"encoding/gob"
@@ -11,7 +12,9 @@ import (
"os/exec"
"path/filepath"
"slices"
+ "strings"
"testing"
+ _ "unsafe" // for go:linkname
"hakurei.app/check"
"hakurei.app/container"
@@ -41,6 +44,96 @@ func init() {
})
}
+// scanLinesCR is like [bufio.ScanLines], but also treats a bare \r as an
+// end-of-line marker.
+//
+//go:linkname scanLinesCR hakurei.app/internal/pkg.scanLinesCR
+func scanLinesCR(data []byte, atEOF bool) (advance int, token []byte, err error)
+
+func TestScan(t *testing.T) {
+ t.Parallel()
+
+ testCases := []struct {
+ name string
+ data string
+ want []string
+ }{
+ {"progress indicator", "Updating files: 76% (8655/11256)\r" +
+ "Updating files: 77% (8668/11256)\r" +
+ "Updating files: 78% (8780/11256)\r" +
+ "Updating files: 79% (8893/11256)\r" +
+ "Updating files: 80% (9005/11256)\r" +
+ "Updating files: 81% (9118/11256)\r" +
+ "Updating files: 82% (9230/11256)\r" +
+ "Updating files: 83% (9343/11256)\r" +
+ "Updating files: 84% (9456/11256)\r" +
+ "Updating files: 85% (9568/11256)\r" +
+ "Updating files: 86% (9681/11256)\r" +
+ "Updating files: 87% (9793/11256)\r" +
+ "Updating files: 88% (9906/11256)\r" +
+ "Updating files: 89% (10018/11256)\r" +
+ "Updating files: 90% (10131/11256)\r" +
+ "Updating files: 91% (10243/11256)\r" +
+ "Updating files: 92% (10356/11256)\r" +
+ "Updating files: 93% (10469/11256)\r" +
+ "Updating files: 94% (10581/11256)\r" +
+ "Updating files: 95% (10694/11256)\r" +
+ "Updating files: 96% (10806/11256)\r" +
+ "Updating files: 97% (10919/11256)\r" +
+ "Updating files: 98% (11031/11256)\r" +
+ "Updating files: 99% (11144/11256)\r" +
+ "Updating files: 100% (11256/11256)\r" +
+ "Updating files: 100% (11256/11256), done.\n", []string{
+ "Updating files: 76% (8655/11256)",
+ "Updating files: 77% (8668/11256)",
+ "Updating files: 78% (8780/11256)",
+ "Updating files: 79% (8893/11256)",
+ "Updating files: 80% (9005/11256)",
+ "Updating files: 81% (9118/11256)",
+ "Updating files: 82% (9230/11256)",
+ "Updating files: 83% (9343/11256)",
+ "Updating files: 84% (9456/11256)",
+ "Updating files: 85% (9568/11256)",
+ "Updating files: 86% (9681/11256)",
+ "Updating files: 87% (9793/11256)",
+ "Updating files: 88% (9906/11256)",
+ "Updating files: 89% (10018/11256)",
+ "Updating files: 90% (10131/11256)",
+ "Updating files: 91% (10243/11256)",
+ "Updating files: 92% (10356/11256)",
+ "Updating files: 93% (10469/11256)",
+ "Updating files: 94% (10581/11256)",
+ "Updating files: 95% (10694/11256)",
+ "Updating files: 96% (10806/11256)",
+ "Updating files: 97% (10919/11256)",
+ "Updating files: 98% (11031/11256)",
+ "Updating files: 99% (11144/11256)",
+ "Updating files: 100% (11256/11256)",
+ "Updating files: 100% (11256/11256), done.",
+ }},
+
+ {"crlf", "0\r1\n2\n3\r\n4\n5\r6\r\n7\n", []string{
+ "0", "1", "2", "3", "4", "5", "6", "7",
+ }},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ s := bufio.NewScanner(strings.NewReader(tc.data))
+ s.Split(scanLinesCR)
+ got := make([]string, 0, len(tc.want))
+ for s.Scan() {
+ got = append(got, s.Text())
+ }
+ if err := s.Err(); err != nil {
+ t.Fatal(err)
+ }
+ if !slices.Equal(got, tc.want) {
+ t.Fatalf("Scan: %q, want %q", got, tc.want)
+ }
+ })
+ }
+}
+
func TestExec(t *testing.T) {
t.Parallel()