Skip to content

Commit 157a86d

Browse files
authored
Print step output into a single buffer (#619)
This is a side-quest to EM2 https://github.com/sourcegraph/sourcegraph/issues/24421 because we realised that we'll make things harder for ourselves if we separate stdout/stderr into separate buffers. We'd have to zip them up together in the UI but that's hard without having additional timing information. But timing information is also a bit overkill (at least for now) so we thought we'd use a single buffer. That's what this PR here contains. Stdout and stderr are logged into a single buffer, each line prefixed with `stdout: ` and `stderr: ` respectively. That works because the `process.PipeOutput` function only writes lines (not chunks) to the passed in writers. So it works even if commands print half a line on stdout, then half a line on stderr, and only then the rest.
1 parent 6bffe0f commit 157a86d

File tree

7 files changed

+98
-83
lines changed

7 files changed

+98
-83
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ All notable changes to `src-cli` are documented in this file.
1717

1818
### Changed
1919

20+
- For internal use only: when `src batch [preview|apply|exec]` are executed in `-text-only` mode, command output on stdout/stderr will be logged in the same message, with each line prefixed accordingly. [#619](https://github.com/sourcegraph/src-cli/pull/619)
21+
2022
### Fixed
2123

2224
### Removed

internal/batches/executor/run_steps.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -315,16 +315,14 @@ func executeSingleStep(
315315

316316
writerCtx, writerCancel := context.WithCancel(ctx)
317317
defer writerCancel()
318-
uiStdoutWriter := opts.ui.StepStdoutWriter(writerCtx, opts.task, i)
319-
uiStderrWriter := opts.ui.StepStderrWriter(writerCtx, opts.task, i)
318+
outputWriter := opts.ui.StepOutputWriter(writerCtx, opts.task, i)
320319
defer func() {
321-
uiStdoutWriter.Close()
322-
uiStderrWriter.Close()
320+
outputWriter.Close()
323321
}()
324322

325323
var stdoutBuffer, stderrBuffer bytes.Buffer
326-
stdout := io.MultiWriter(&stdoutBuffer, uiStdoutWriter, opts.logger.PrefixWriter("stdout"))
327-
stderr := io.MultiWriter(&stderrBuffer, uiStderrWriter, opts.logger.PrefixWriter("stderr"))
324+
stdout := io.MultiWriter(&stdoutBuffer, outputWriter.StdoutWriter(), opts.logger.PrefixWriter("stdout"))
325+
stderr := io.MultiWriter(&stderrBuffer, outputWriter.StderrWriter(), opts.logger.PrefixWriter("stderr"))
328326

329327
// Setup readers that pipe the output into the given buffers
330328
wg, err := process.PipeOutput(ctx, cmd, stdout, stderr)

internal/batches/executor/ui.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ type TaskExecutionUI interface {
2020
StepsExecutionUI(*Task) StepsExecutionUI
2121
}
2222

23+
type StepOutputWriter interface {
24+
StdoutWriter() io.Writer
25+
StderrWriter() io.Writer
26+
Close() error
27+
}
28+
2329
type StepsExecutionUI interface {
2430
ArchiveDownloadStarted()
2531
ArchiveDownloadFinished()
@@ -34,8 +40,7 @@ type StepsExecutionUI interface {
3440
StepPreparing(int)
3541
StepStarted(int, string)
3642

37-
StepStdoutWriter(context.Context, *Task, int) io.WriteCloser
38-
StepStderrWriter(context.Context, *Task, int) io.WriteCloser
43+
StepOutputWriter(context.Context, *Task, int) StepOutputWriter
3944

4045
CalculatingDiffStarted()
4146
CalculatingDiffFinished()
@@ -54,19 +59,16 @@ func (noop NoopStepsExecUI) SkippingStepsUpto(startStep int) {}
5459
func (noop NoopStepsExecUI) StepSkipped(step int) {}
5560
func (noop NoopStepsExecUI) StepPreparing(step int) {}
5661
func (noop NoopStepsExecUI) StepStarted(step int, runScript string) {}
57-
func (noop NoopStepsExecUI) StepStdoutWriter(ctx context.Context, task *Task, step int) io.WriteCloser {
58-
return discardCloser{io.Discard}
59-
}
60-
func (noop NoopStepsExecUI) StepStderrWriter(ctx context.Context, task *Task, step int) io.WriteCloser {
61-
return discardCloser{io.Discard}
62+
func (noop NoopStepsExecUI) StepOutputWriter(ctx context.Context, task *Task, step int) StepOutputWriter {
63+
return NoopStepOutputWriter{}
6264
}
6365
func (noop NoopStepsExecUI) CalculatingDiffStarted() {}
6466
func (noop NoopStepsExecUI) CalculatingDiffFinished() {}
6567
func (noop NoopStepsExecUI) StepFinished(idx int, diff []byte, changes *git.Changes, outputs map[string]interface{}) {
6668
}
6769

68-
type discardCloser struct {
69-
io.Writer
70-
}
70+
type NoopStepOutputWriter struct{}
7171

72-
func (discardCloser) Close() error { return nil }
72+
func (noop NoopStepOutputWriter) StdoutWriter() io.Writer { return io.Discard }
73+
func (noop NoopStepOutputWriter) StderrWriter() io.Writer { return io.Discard }
74+
func (noop NoopStepOutputWriter) Close() error { return nil }

internal/batches/ui/interval_writer.go

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ package ui
33
import (
44
"bytes"
55
"context"
6+
"io"
67
"time"
78

89
"github.com/derision-test/glock"
910
)
1011

11-
// IntervalWriter is a io.Writer that flushes to the given sink on the given
12-
// interval.
13-
type IntervalWriter struct {
12+
// IntervalProcessWriter accepts stdout/stderr writes from processes, prefixed
13+
// them accordingly, and flushes to the given sink on the given interval.
14+
type IntervalProcessWriter struct {
1415
sink func(string)
1516

1617
ticker glock.Ticker
@@ -25,8 +26,8 @@ type IntervalWriter struct {
2526
done chan struct{}
2627
}
2728

28-
func newIntervalWriter(ctx context.Context, ticker glock.Ticker, sink func(string)) *IntervalWriter {
29-
l := &IntervalWriter{
29+
func newIntervalProcessWriter(ctx context.Context, ticker glock.Ticker, sink func(string)) *IntervalProcessWriter {
30+
l := &IntervalProcessWriter{
3031
sink: sink,
3132
ticker: ticker,
3233

@@ -44,37 +45,42 @@ func newIntervalWriter(ctx context.Context, ticker glock.Ticker, sink func(strin
4445
return l
4546
}
4647

47-
// NewLogger returns a new Logger instance and spawns a goroutine in the
48-
// background that regularily flushed the logged output to the given sink.
48+
// NewIntervalProcessWriter returns a new IntervalProcessWriter instance and
49+
// spawns a goroutine in the background that regularily flushed the logged
50+
// output to the given sink.
4951
//
5052
// If the passed in ctx is canceled the goroutine will exit.
51-
func NewIntervalWriter(ctx context.Context, interval time.Duration, sink func(string)) *IntervalWriter {
52-
return newIntervalWriter(ctx, glock.NewRealTicker(interval), sink)
53+
func NewIntervalProcessWriter(ctx context.Context, interval time.Duration, sink func(string)) *IntervalProcessWriter {
54+
return newIntervalProcessWriter(ctx, glock.NewRealTicker(interval), sink)
5355
}
5456

55-
func (l *IntervalWriter) flush() {
56-
if l.buf.Len() == 0 {
57-
return
58-
}
59-
l.sink(l.buf.String())
60-
l.buf.Reset()
57+
// StdoutWriter returns an io.Writer that prefixes every line with "stdout: "
58+
func (l *IntervalProcessWriter) StdoutWriter() io.Writer {
59+
return &prefixedWriter{writes: l.writes, writesDone: l.writesDone, prefix: "stdout: "}
6160
}
6261

63-
// Close flushes the
64-
func (l *IntervalWriter) Close() error {
62+
// SterrWriter returns an io.Writer that prefixes every line with "stderr: "
63+
func (l *IntervalProcessWriter) StderrWriter() io.Writer {
64+
return &prefixedWriter{writes: l.writes, writesDone: l.writesDone, prefix: "stderr: "}
65+
}
66+
67+
// Close blocks until all pending writes have been flushed to the buffer. It
68+
// then causes the underlying goroutine to exit.
69+
func (l *IntervalProcessWriter) Close() error {
6570
l.closed <- struct{}{}
6671
<-l.done
6772
return nil
6873
}
6974

70-
// Write handler of IntervalWriter.
71-
func (l *IntervalWriter) Write(p []byte) (int, error) {
72-
l.writes <- p
73-
<-l.writesDone
74-
return len(p), nil
75+
func (l *IntervalProcessWriter) flush() {
76+
if l.buf.Len() == 0 {
77+
return
78+
}
79+
l.sink(l.buf.String())
80+
l.buf.Reset()
7581
}
7682

77-
func (l *IntervalWriter) writeLines(ctx context.Context) {
83+
func (l *IntervalProcessWriter) writeLines(ctx context.Context) {
7884
defer func() {
7985
l.flush()
8086
l.ticker.Stop()
@@ -103,3 +109,22 @@ func (l *IntervalWriter) writeLines(ctx context.Context) {
103109
}
104110
}
105111
}
112+
113+
type prefixedWriter struct {
114+
writes chan []byte
115+
writesDone chan struct{}
116+
prefix string
117+
}
118+
119+
func (w *prefixedWriter) Write(p []byte) (int, error) {
120+
var prefixedLines []byte
121+
for _, line := range bytes.Split(p, []byte("\n")) {
122+
prefixedLine := append([]byte(w.prefix), line...)
123+
prefixedLine = append(prefixedLine, []byte("\n")...)
124+
125+
prefixedLines = append(prefixedLines, prefixedLine...)
126+
}
127+
w.writes <- prefixedLines
128+
<-w.writesDone
129+
return len(p), nil
130+
}

internal/batches/ui/interval_writer_test.go

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ func TestIntervalWriter(t *testing.T) {
1818
}
1919

2020
ticker := glock.NewMockTicker(1 * time.Second)
21-
writer := newIntervalWriter(ctx, ticker, sink)
21+
writer := newIntervalProcessWriter(ctx, ticker, sink)
2222

23-
writer.Write([]byte("1"))
23+
stdoutWriter := writer.StdoutWriter()
24+
stderrWriter := writer.StderrWriter()
25+
stdoutWriter.Write([]byte("1"))
26+
stderrWriter.Write([]byte("1"))
2427
select {
2528
case <-ch:
2629
t.Fatalf("ch has data")
@@ -31,17 +34,22 @@ func TestIntervalWriter(t *testing.T) {
3134

3235
select {
3336
case d := <-ch:
34-
if d != "1" {
35-
t.Fatalf("wrong data in sink")
37+
want := "stdout: 1\nstderr: 1\n"
38+
if d != want {
39+
t.Fatalf("wrong data in sink. want=%q, have=%q", want, d)
3640
}
3741
case <-time.After(1 * time.Second):
3842
t.Fatalf("ch has NO data")
3943
}
4044

41-
writer.Write([]byte("2"))
42-
writer.Write([]byte("3"))
43-
writer.Write([]byte("4"))
44-
writer.Write([]byte("5"))
45+
stdoutWriter.Write([]byte("2"))
46+
stderrWriter.Write([]byte("2"))
47+
stdoutWriter.Write([]byte("3"))
48+
stderrWriter.Write([]byte("3"))
49+
stdoutWriter.Write([]byte("4"))
50+
stderrWriter.Write([]byte("4"))
51+
stdoutWriter.Write([]byte("5"))
52+
stderrWriter.Write([]byte("5"))
4553

4654
select {
4755
case <-ch:
@@ -54,8 +62,13 @@ func TestIntervalWriter(t *testing.T) {
5462

5563
select {
5664
case d := <-ch:
57-
if d != "2345" {
58-
t.Fatalf("wrong data in sink")
65+
want := "stdout: 2\nstderr: 2\n" +
66+
"stdout: 3\nstderr: 3\n" +
67+
"stdout: 4\nstderr: 4\n" +
68+
"stdout: 5\nstderr: 5\n"
69+
70+
if d != want {
71+
t.Fatalf("wrong data in sink. want")
5972
}
6073
case <-time.After(1 * time.Second):
6174
t.Fatalf("ch has NO data")

internal/batches/ui/json_lines.go

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7-
"io"
87
"math/rand"
98
"os"
109
"strconv"
@@ -325,35 +324,18 @@ func (ui *stepsExecutionJSONLines) StepStarted(step int, runScript string) {
325324
logOperationStart(batcheslib.LogEventOperationTaskStep, map[string]interface{}{"taskID": ui.linesTask.ID, "step": step, "runScript": runScript})
326325
}
327326

328-
func (ui *stepsExecutionJSONLines) StepStdoutWriter(ctx context.Context, task *executor.Task, step int) io.WriteCloser {
327+
func (ui *stepsExecutionJSONLines) StepOutputWriter(ctx context.Context, task *executor.Task, step int) executor.StepOutputWriter {
329328
sink := func(data string) {
330329
logOperationProgress(
331330
batcheslib.LogEventOperationTaskStep,
332331
map[string]interface{}{
333-
"taskID": ui.linesTask.ID,
334-
"step": step,
335-
"out": data,
336-
"output_type": "stdout",
332+
"taskID": ui.linesTask.ID,
333+
"step": step,
334+
"out": data,
337335
},
338336
)
339337
}
340-
return NewIntervalWriter(ctx, stepFlushDuration, sink)
341-
}
342-
343-
func (ui *stepsExecutionJSONLines) StepStderrWriter(ctx context.Context, task *executor.Task, step int) io.WriteCloser {
344-
sink := func(data string) {
345-
logOperationProgress(
346-
batcheslib.LogEventOperationTaskStep,
347-
map[string]interface{}{
348-
"taskID": ui.linesTask.ID,
349-
"step": step,
350-
"out": data,
351-
"output_type": "stderr",
352-
},
353-
)
354-
}
355-
356-
return NewIntervalWriter(ctx, stepFlushDuration, sink)
338+
return NewIntervalProcessWriter(ctx, stepFlushDuration, sink)
357339
}
358340

359341
func (ui *stepsExecutionJSONLines) StepFinished(step int, diff []byte, changes *git.Changes, outputs map[string]interface{}) {

internal/batches/ui/task_exec_tui.go

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package ui
33
import (
44
"context"
55
"fmt"
6-
"io"
76
"sort"
87
"strings"
98
"sync"
@@ -458,12 +457,10 @@ func (ui stepsExecTUI) StepStarted(step int, runScript string) {
458457
ui.updateStatusBar(runScript)
459458
}
460459

461-
func (ui stepsExecTUI) StepStdoutWriter(ctx context.Context, task *executor.Task, step int) io.WriteCloser {
462-
return discardCloser{io.Discard}
463-
}
464-
func (ui stepsExecTUI) StepStderrWriter(ctx context.Context, task *executor.Task, step int) io.WriteCloser {
465-
return discardCloser{io.Discard}
460+
func (ui stepsExecTUI) StepOutputWriter(ctx context.Context, task *executor.Task, step int) executor.StepOutputWriter {
461+
return executor.NoopStepOutputWriter{}
466462
}
463+
467464
func (ui stepsExecTUI) CalculatingDiffStarted() {
468465
ui.updateStatusBar("Calculating diff")
469466
}
@@ -473,7 +470,3 @@ func (ui stepsExecTUI) CalculatingDiffFinished() {
473470
func (ui stepsExecTUI) StepFinished(idx int, diff []byte, changes *git.Changes, outputs map[string]interface{}) {
474471
// noop right now
475472
}
476-
477-
type discardCloser struct{ io.Writer }
478-
479-
func (discardCloser) Close() error { return nil }

0 commit comments

Comments
 (0)