summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/multiproduct_kati/main.go22
-rw-r--r--cmd/soong_ui/main.go25
-rw-r--r--ui/build/config_test.go3
-rw-r--r--ui/build/context.go4
-rw-r--r--ui/build/dumpvars.go2
-rw-r--r--ui/status/log.go10
-rw-r--r--ui/status/status.go3
-rw-r--r--ui/status/status_test.go5
-rw-r--r--ui/terminal/Android.bp6
-rw-r--r--ui/terminal/dumb_status.go71
-rw-r--r--ui/terminal/format.go123
-rw-r--r--ui/terminal/smart_status.go198
-rw-r--r--ui/terminal/status.go120
-rw-r--r--ui/terminal/status_test.go275
-rw-r--r--ui/terminal/stdio.go55
-rw-r--r--ui/terminal/util.go11
-rw-r--r--ui/terminal/writer.go229
-rw-r--r--ui/tracer/status.go5
18 files changed, 796 insertions, 371 deletions
diff --git a/cmd/multiproduct_kati/main.go b/cmd/multiproduct_kati/main.go
index 330c5dd27..1171a6521 100644
--- a/cmd/multiproduct_kati/main.go
+++ b/cmd/multiproduct_kati/main.go
@@ -156,10 +156,12 @@ type mpContext struct {
}
func main() {
- writer := terminal.NewWriter(terminal.StdioImpl{})
- defer writer.Finish()
+ stdio := terminal.StdioImpl{}
- log := logger.New(writer)
+ output := terminal.NewStatusOutput(stdio.Stdout(), "",
+ build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD"))
+
+ log := logger.New(output)
defer log.Cleanup()
flag.Parse()
@@ -172,8 +174,7 @@ func main() {
stat := &status.Status{}
defer stat.Finish()
- stat.AddOutput(terminal.NewStatusOutput(writer, "",
- build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD")))
+ stat.AddOutput(output)
var failures failureCount
stat.AddOutput(&failures)
@@ -188,7 +189,7 @@ func main() {
Context: ctx,
Logger: log,
Tracer: trace,
- Writer: writer,
+ Writer: output,
Status: stat,
}}
@@ -341,7 +342,7 @@ func main() {
} else if failures > 1 {
log.Fatalf("%d failures", failures)
} else {
- writer.Print("Success")
+ fmt.Fprintln(output, "Success")
}
}
@@ -386,7 +387,7 @@ func buildProduct(mpctx *mpContext, product string) {
Context: mpctx.Context,
Logger: log,
Tracer: mpctx.Tracer,
- Writer: terminal.NewWriter(terminal.NewCustomStdio(nil, f, f)),
+ Writer: f,
Thread: mpctx.Tracer.NewThread(product),
Status: &status.Status{},
}}
@@ -466,3 +467,8 @@ func (f *failureCount) Message(level status.MsgLevel, message string) {
}
func (f *failureCount) Flush() {}
+
+func (f *failureCount) Write(p []byte) (int, error) {
+ // discard writes
+ return len(p), nil
+}
diff --git a/cmd/soong_ui/main.go b/cmd/soong_ui/main.go
index 5f9bd016e..f5276c335 100644
--- a/cmd/soong_ui/main.go
+++ b/cmd/soong_ui/main.go
@@ -109,10 +109,10 @@ func main() {
os.Exit(1)
}
- writer := terminal.NewWriter(c.stdio())
- defer writer.Finish()
+ output := terminal.NewStatusOutput(c.stdio().Stdout(), os.Getenv("NINJA_STATUS"),
+ build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD"))
- log := logger.New(writer)
+ log := logger.New(output)
defer log.Cleanup()
ctx, cancel := context.WithCancel(context.Background())
@@ -125,8 +125,7 @@ func main() {
stat := &status.Status{}
defer stat.Finish()
- stat.AddOutput(terminal.NewStatusOutput(writer, os.Getenv("NINJA_STATUS"),
- build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD")))
+ stat.AddOutput(output)
stat.AddOutput(trace.StatusTracer())
build.SetupSignals(log, cancel, func() {
@@ -140,7 +139,7 @@ func main() {
Logger: log,
Metrics: met,
Tracer: trace,
- Writer: writer,
+ Writer: output,
Status: stat,
}}
@@ -312,13 +311,13 @@ func dumpVarConfig(ctx build.Context, args ...string) build.Config {
func make(ctx build.Context, config build.Config, _ []string, logsDir string) {
if config.IsVerbose() {
writer := ctx.Writer
- writer.Print("! The argument `showcommands` is no longer supported.")
- writer.Print("! Instead, the verbose log is always written to a compressed file in the output dir:")
- writer.Print("!")
- writer.Print(fmt.Sprintf("! gzip -cd %s/verbose.log.gz | less -R", logsDir))
- writer.Print("!")
- writer.Print("! Older versions are saved in verbose.log.#.gz files")
- writer.Print("")
+ fmt.Fprintln(writer, "! The argument `showcommands` is no longer supported.")
+ fmt.Fprintln(writer, "! Instead, the verbose log is always written to a compressed file in the output dir:")
+ fmt.Fprintln(writer, "!")
+ fmt.Fprintf(writer, "! gzip -cd %s/verbose.log.gz | less -R\n", logsDir)
+ fmt.Fprintln(writer, "!")
+ fmt.Fprintln(writer, "! Older versions are saved in verbose.log.#.gz files")
+ fmt.Fprintln(writer, "")
time.Sleep(5 * time.Second)
}
diff --git a/ui/build/config_test.go b/ui/build/config_test.go
index 242e3afb0..1d23fec57 100644
--- a/ui/build/config_test.go
+++ b/ui/build/config_test.go
@@ -22,14 +22,13 @@ import (
"testing"
"android/soong/ui/logger"
- "android/soong/ui/terminal"
)
func testContext() Context {
return Context{&ContextImpl{
Context: context.Background(),
Logger: logger.New(&bytes.Buffer{}),
- Writer: terminal.NewWriter(terminal.NewCustomStdio(&bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{})),
+ Writer: &bytes.Buffer{},
}}
}
diff --git a/ui/build/context.go b/ui/build/context.go
index 249e89822..7ff98ef78 100644
--- a/ui/build/context.go
+++ b/ui/build/context.go
@@ -16,12 +16,12 @@ package build
import (
"context"
+ "io"
"android/soong/ui/logger"
"android/soong/ui/metrics"
"android/soong/ui/metrics/metrics_proto"
"android/soong/ui/status"
- "android/soong/ui/terminal"
"android/soong/ui/tracer"
)
@@ -35,7 +35,7 @@ type ContextImpl struct {
Metrics *metrics.Metrics
- Writer terminal.Writer
+ Writer io.Writer
Status *status.Status
Thread tracer.Thread
diff --git a/ui/build/dumpvars.go b/ui/build/dumpvars.go
index 4335667d1..266130f04 100644
--- a/ui/build/dumpvars.go
+++ b/ui/build/dumpvars.go
@@ -249,7 +249,7 @@ func runMakeProductConfig(ctx Context, config Config) {
env := config.Environment()
// Print the banner like make does
if !env.IsEnvTrue("ANDROID_QUIET_BUILD") {
- ctx.Writer.Print(Banner(make_vars))
+ fmt.Fprintln(ctx.Writer, Banner(make_vars))
}
// Populate the environment
diff --git a/ui/status/log.go b/ui/status/log.go
index 921aa4401..7badac73c 100644
--- a/ui/status/log.go
+++ b/ui/status/log.go
@@ -71,6 +71,11 @@ func (v *verboseLog) Message(level MsgLevel, message string) {
fmt.Fprintf(v.w, "%s%s\n", level.Prefix(), message)
}
+func (v *verboseLog) Write(p []byte) (int, error) {
+ fmt.Fprint(v.w, string(p))
+ return len(p), nil
+}
+
type errorLog struct {
w io.WriteCloser
@@ -134,3 +139,8 @@ func (e *errorLog) Message(level MsgLevel, message string) {
fmt.Fprintf(e.w, "error: %s\n", message)
}
+
+func (e *errorLog) Write(p []byte) (int, error) {
+ fmt.Fprint(e.w, string(p))
+ return len(p), nil
+}
diff --git a/ui/status/status.go b/ui/status/status.go
index 46ec72e80..3d8cd7a2c 100644
--- a/ui/status/status.go
+++ b/ui/status/status.go
@@ -173,6 +173,9 @@ type StatusOutput interface {
// Flush is called when your outputs should be flushed / closed. No
// output is expected after this call.
Flush()
+
+ // Write lets StatusOutput implement io.Writer
+ Write(p []byte) (n int, err error)
}
// Status is the multiplexer / accumulator between ToolStatus instances (via
diff --git a/ui/status/status_test.go b/ui/status/status_test.go
index e62785f43..949458222 100644
--- a/ui/status/status_test.go
+++ b/ui/status/status_test.go
@@ -27,6 +27,11 @@ func (c *counterOutput) FinishAction(result ActionResult, counts Counts) {
func (c counterOutput) Message(level MsgLevel, msg string) {}
func (c counterOutput) Flush() {}
+func (c counterOutput) Write(p []byte) (int, error) {
+ // Discard writes
+ return len(p), nil
+}
+
func (c counterOutput) Expect(t *testing.T, counts Counts) {
if Counts(c) == counts {
return
diff --git a/ui/terminal/Android.bp b/ui/terminal/Android.bp
index 7104a5047..b533b0d30 100644
--- a/ui/terminal/Android.bp
+++ b/ui/terminal/Android.bp
@@ -17,11 +17,15 @@ bootstrap_go_package {
pkgPath: "android/soong/ui/terminal",
deps: ["soong-ui-status"],
srcs: [
+ "dumb_status.go",
+ "format.go",
+ "smart_status.go",
"status.go",
- "writer.go",
+ "stdio.go",
"util.go",
],
testSrcs: [
+ "status_test.go",
"util_test.go",
],
darwin: {
diff --git a/ui/terminal/dumb_status.go b/ui/terminal/dumb_status.go
new file mode 100644
index 000000000..201770fac
--- /dev/null
+++ b/ui/terminal/dumb_status.go
@@ -0,0 +1,71 @@
+// Copyright 2019 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package terminal
+
+import (
+ "fmt"
+ "io"
+
+ "android/soong/ui/status"
+)
+
+type dumbStatusOutput struct {
+ writer io.Writer
+ formatter formatter
+}
+
+// NewDumbStatusOutput returns a StatusOutput that represents the
+// current build status similarly to Ninja's built-in terminal
+// output.
+func NewDumbStatusOutput(w io.Writer, formatter formatter) status.StatusOutput {
+ return &dumbStatusOutput{
+ writer: w,
+ formatter: formatter,
+ }
+}
+
+func (s *dumbStatusOutput) Message(level status.MsgLevel, message string) {
+ if level >= status.StatusLvl {
+ fmt.Fprintln(s.writer, s.formatter.message(level, message))
+ }
+}
+
+func (s *dumbStatusOutput) StartAction(action *status.Action, counts status.Counts) {
+}
+
+func (s *dumbStatusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
+ str := result.Description
+ if str == "" {
+ str = result.Command
+ }
+
+ progress := s.formatter.progress(counts) + str
+
+ output := s.formatter.result(result)
+ output = string(stripAnsiEscapes([]byte(output)))
+
+ if output != "" {
+ fmt.Fprint(s.writer, progress, "\n", output)
+ } else {
+ fmt.Fprintln(s.writer, progress)
+ }
+}
+
+func (s *dumbStatusOutput) Flush() {}
+
+func (s *dumbStatusOutput) Write(p []byte) (int, error) {
+ fmt.Fprint(s.writer, string(p))
+ return len(p), nil
+}
diff --git a/ui/terminal/format.go b/ui/terminal/format.go
new file mode 100644
index 000000000..4205bdc22
--- /dev/null
+++ b/ui/terminal/format.go
@@ -0,0 +1,123 @@
+// Copyright 2019 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package terminal
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "android/soong/ui/status"
+)
+
+type formatter struct {
+ format string
+ quiet bool
+ start time.Time
+}
+
+// newFormatter returns a formatter for formatting output to
+// the terminal in a format similar to Ninja.
+// format takes nearly all the same options as NINJA_STATUS.
+// %c is currently unsupported.
+func newFormatter(format string, quiet bool) formatter {
+ return formatter{
+ format: format,
+ quiet: quiet,
+ start: time.Now(),
+ }
+}
+
+func (s formatter) message(level status.MsgLevel, message string) string {
+ if level >= status.ErrorLvl {
+ return fmt.Sprintf("FAILED: %s", message)
+ } else if level > status.StatusLvl {
+ return fmt.Sprintf("%s%s", level.Prefix(), message)
+ } else if level == status.StatusLvl {
+ return message
+ }
+ return ""
+}
+
+func (s formatter) progress(counts status.Counts) string {
+ if s.format == "" {
+ return fmt.Sprintf("[%3d%% %d/%d] ", 100*counts.FinishedActions/counts.TotalActions, counts.FinishedActions, counts.TotalActions)
+ }
+
+ buf := &strings.Builder{}
+ for i := 0; i < len(s.format); i++ {
+ c := s.format[i]
+ if c != '%' {
+ buf.WriteByte(c)
+ continue
+ }
+
+ i = i + 1
+ if i == len(s.format) {
+ buf.WriteByte(c)
+ break
+ }
+
+ c = s.format[i]
+ switch c {
+ case '%':
+ buf.WriteByte(c)
+ case 's':
+ fmt.Fprintf(buf, "%d", counts.StartedActions)
+ case 't':
+ fmt.Fprintf(buf, "%d", counts.TotalActions)
+ case 'r':
+ fmt.Fprintf(buf, "%d", counts.RunningActions)
+ case 'u':
+ fmt.Fprintf(buf, "%d", counts.TotalActions-counts.StartedActions)
+ case 'f':
+ fmt.Fprintf(buf, "%d", counts.FinishedActions)
+ case 'o':
+ fmt.Fprintf(buf, "%.1f", float64(counts.FinishedActions)/time.Since(s.start).Seconds())
+ case 'c':
+ // TODO: implement?
+ buf.WriteRune('?')
+ case 'p':
+ fmt.Fprintf(buf, "%3d%%", 100*counts.FinishedActions/counts.TotalActions)
+ case 'e':
+ fmt.Fprintf(buf, "%.3f", time.Since(s.start).Seconds())
+ default:
+ buf.WriteString("unknown placeholder '")
+ buf.WriteByte(c)
+ buf.WriteString("'")
+ }
+ }
+ return buf.String()
+}
+
+func (s formatter) result(result status.ActionResult) string {
+ var ret string
+ if result.Error != nil {
+ targets := strings.Join(result.Outputs, " ")
+ if s.quiet || result.Command == "" {
+ ret = fmt.Sprintf("FAILED: %s\n%s", targets, result.Output)
+ } else {
+ ret = fmt.Sprintf("FAILED: %s\n%s\n%s", targets, result.Command, result.Output)
+ }
+ } else if result.Output != "" {
+ ret = result.Output
+ }
+
+ if len(ret) > 0 && ret[len(ret)-1] != '\n' {
+ ret += "\n"
+ }
+
+ return ret
+}
diff --git a/ui/terminal/smart_status.go b/ui/terminal/smart_status.go
new file mode 100644
index 000000000..999a2d0f1
--- /dev/null
+++ b/ui/terminal/smart_status.go
@@ -0,0 +1,198 @@
+// Copyright 2019 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package terminal
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "os/signal"
+ "strings"
+ "sync"
+ "syscall"
+
+ "android/soong/ui/status"
+)
+
+type smartStatusOutput struct {
+ writer io.Writer
+ formatter formatter
+
+ lock sync.Mutex
+
+ haveBlankLine bool
+
+ termWidth int
+ sigwinch chan os.Signal
+}
+
+// NewSmartStatusOutput returns a StatusOutput that represents the
+// current build status similarly to Ninja's built-in terminal
+// output.
+func NewSmartStatusOutput(w io.Writer, formatter formatter) status.StatusOutput {
+ s := &smartStatusOutput{
+ writer: w,
+ formatter: formatter,
+
+ haveBlankLine: true,
+
+ sigwinch: make(chan os.Signal),
+ }
+
+ s.updateTermSize()
+
+ s.startSigwinch()
+
+ return s
+}
+
+func (s *smartStatusOutput) Message(level status.MsgLevel, message string) {
+ if level < status.StatusLvl {
+ return
+ }
+
+ str := s.formatter.message(level, message)
+
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ if level > status.StatusLvl {
+ s.print(str)
+ } else {
+ s.statusLine(str)
+ }
+}
+
+func (s *smartStatusOutput) StartAction(action *status.Action, counts status.Counts) {
+ str := action.Description
+ if str == "" {
+ str = action.Command
+ }
+
+ progress := s.formatter.progress(counts)
+
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ s.statusLine(progress + str)
+}
+
+func (s *smartStatusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
+ str := result.Description
+ if str == "" {
+ str = result.Command
+ }
+
+ progress := s.formatter.progress(counts) + str
+
+ output := s.formatter.result(result)
+
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ if output != "" {
+ s.statusLine(progress)
+ s.requestLine()
+ s.print(output)
+ } else {
+ s.statusLine(progress)
+ }
+}
+
+func (s *smartStatusOutput) Flush() {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ s.stopSigwinch()
+
+ s.requestLine()
+}
+
+func (s *smartStatusOutput) Write(p []byte) (int, error) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+ s.print(string(p))
+ return len(p), nil
+}
+
+func (s *smartStatusOutput) requestLine() {
+ if !s.haveBlankLine {
+ fmt.Fprintln(s.writer)
+ s.haveBlankLine = true
+ }
+}
+
+func (s *smartStatusOutput) print(str string) {
+ if !s.haveBlankLine {
+ fmt.Fprint(s.writer, "\r", "\x1b[K")
+ s.haveBlankLine = true
+ }
+ fmt.Fprint(s.writer, str)
+ if len(str) == 0 || str[len(str)-1] != '\n' {
+ fmt.Fprint(s.writer, "\n")
+ }
+}
+
+func (s *smartStatusOutput) statusLine(str string) {
+ idx := strings.IndexRune(str, '\n')
+ if idx != -1 {
+ str = str[0:idx]
+ }
+
+ // Limit line width to the terminal width, otherwise we'll wrap onto
+ // another line and we won't delete the previous line.
+ if s.termWidth > 0 {
+ str = s.elide(str)
+ }
+
+ // Move to the beginning on the line, turn on bold, print the output,
+ // turn off bold, then clear the rest of the line.
+ start := "\r\x1b[1m"
+ end := "\x1b[0m\x1b[K"
+ fmt.Fprint(s.writer, start, str, end)
+ s.haveBlankLine = false
+}
+
+func (s *smartStatusOutput) elide(str string) string {
+ if len(str) > s.termWidth {
+ // TODO: Just do a max. Ninja elides the middle, but that's
+ // more complicated and these lines aren't that important.
+ str = str[:s.termWidth]
+ }
+
+ return str
+}
+
+func (s *smartStatusOutput) startSigwinch() {
+ signal.Notify(s.sigwinch, syscall.SIGWINCH)
+ go func() {
+ for _ = range s.sigwinch {
+ s.lock.Lock()
+ s.updateTermSize()
+ s.lock.Unlock()
+ }
+ }()
+}
+
+func (s *smartStatusOutput) stopSigwinch() {
+ signal.Stop(s.sigwinch)
+ close(s.sigwinch)
+}
+
+func (s *smartStatusOutput) updateTermSize() {
+ if w, ok := termWidth(s.writer); ok {
+ s.termWidth = w
+ }
+}
diff --git a/ui/terminal/status.go b/ui/terminal/status.go
index 2445c5b2c..69a2a0929 100644
--- a/ui/terminal/status.go
+++ b/ui/terminal/status.go
@@ -15,131 +15,23 @@
package terminal
import (
- "fmt"
- "strings"
- "time"
+ "io"
"android/soong/ui/status"
)
-type statusOutput struct {
- writer Writer
- format string
-
- start time.Time
- quiet bool
-}
-
// NewStatusOutput returns a StatusOutput that represents the
// current build status similarly to Ninja's built-in terminal
// output.
//
// statusFormat takes nearly all the same options as NINJA_STATUS.
// %c is currently unsupported.
-func NewStatusOutput(w Writer, statusFormat string, quietBuild bool) status.StatusOutput {
- return &statusOutput{
- writer: w,
- format: statusFormat,
-
- start: time.Now(),
- quiet: quietBuild,
- }
-}
-
-func (s *statusOutput) Message(level status.MsgLevel, message string) {
- if level >= status.ErrorLvl {
- s.writer.Print(fmt.Sprintf("FAILED: %s", message))
- } else if level > status.StatusLvl {
- s.writer.Print(fmt.Sprintf("%s%s", level.Prefix(), message))
- } else if level == status.StatusLvl {
- s.writer.StatusLine(message)
- }
-}
-
-func (s *statusOutput) StartAction(action *status.Action, counts status.Counts) {
- if !s.writer.isSmartTerminal() {
- return
- }
-
- str := action.Description
- if str == "" {
- str = action.Command
- }
+func NewStatusOutput(w io.Writer, statusFormat string, quietBuild bool) status.StatusOutput {
+ formatter := newFormatter(statusFormat, quietBuild)
- s.writer.StatusLine(s.progress(counts) + str)
-}
-
-func (s *statusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
- str := result.Description
- if str == "" {
- str = result.Command
- }
-
- progress := s.progress(counts) + str
-
- if result.Error != nil {
- targets := strings.Join(result.Outputs, " ")
- if s.quiet || result.Command == "" {
- s.writer.StatusAndMessage(progress, fmt.Sprintf("FAILED: %s\n%s", targets, result.Output))
- } else {
- s.writer.StatusAndMessage(progress, fmt.Sprintf("FAILED: %s\n%s\n%s", targets, result.Command, result.Output))
- }
- } else if result.Output != "" {
- s.writer.StatusAndMessage(progress, result.Output)
+ if isSmartTerminal(w) {
+ return NewSmartStatusOutput(w, formatter)
} else {
- s.writer.StatusLine(progress)
- }
-}
-
-func (s *statusOutput) Flush() {}
-
-func (s *statusOutput) progress(counts status.Counts) string {
- if s.format == "" {
- return fmt.Sprintf("[%3d%% %d/%d] ", 100*counts.FinishedActions/counts.TotalActions, counts.FinishedActions, counts.TotalActions)
- }
-
- buf := &strings.Builder{}
- for i := 0; i < len(s.format); i++ {
- c := s.format[i]
- if c != '%' {
- buf.WriteByte(c)
- continue
- }
-
- i = i + 1
- if i == len(s.format) {
- buf.WriteByte(c)
- break
- }
-
- c = s.format[i]
- switch c {
- case '%':
- buf.WriteByte(c)
- case 's':
- fmt.Fprintf(buf, "%d", counts.StartedActions)
- case 't':
- fmt.Fprintf(buf, "%d", counts.TotalActions)
- case 'r':
- fmt.Fprintf(buf, "%d", counts.RunningActions)
- case 'u':
- fmt.Fprintf(buf, "%d", counts.TotalActions-counts.StartedActions)
- case 'f':
- fmt.Fprintf(buf, "%d", counts.FinishedActions)
- case 'o':
- fmt.Fprintf(buf, "%.1f", float64(counts.FinishedActions)/time.Since(s.start).Seconds())
- case 'c':
- // TODO: implement?
- buf.WriteRune('?')
- case 'p':
- fmt.Fprintf(buf, "%3d%%", 100*counts.FinishedActions/counts.TotalActions)
- case 'e':
- fmt.Fprintf(buf, "%.3f", time.Since(s.start).Seconds())
- default:
- buf.WriteString("unknown placeholder '")
- buf.WriteByte(c)
- buf.WriteString("'")
- }
+ return NewDumbStatusOutput(w, formatter)
}
- return buf.String()
}
diff --git a/ui/terminal/status_test.go b/ui/terminal/status_test.go
new file mode 100644
index 000000000..fc9315b96
--- /dev/null
+++ b/ui/terminal/status_test.go
@@ -0,0 +1,275 @@
+// Copyright 2018 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package terminal
+
+import (
+ "bytes"
+ "fmt"
+ "syscall"
+ "testing"
+
+ "android/soong/ui/status"
+)
+
+func TestStatusOutput(t *testing.T) {
+ tests := []struct {
+ name string
+ calls func(stat status.StatusOutput)
+ smart string
+ dumb string
+ }{
+ {
+ name: "two actions",
+ calls: twoActions,
+ smart: "\r\x1b[1m[ 0% 0/2] action1\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action1\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action2\x1b[0m\x1b[K\r\x1b[1m[100% 2/2] action2\x1b[0m\x1b[K\n",
+ dumb: "[ 50% 1/2] action1\n[100% 2/2] action2\n",
+ },
+ {
+ name: "two parallel actions",
+ calls: twoParallelActions,
+ smart: "\r\x1b[1m[ 0% 0/2] action1\x1b[0m\x1b[K\r\x1b[1m[ 0% 0/2] action2\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action1\x1b[0m\x1b[K\r\x1b[1m[100% 2/2] action2\x1b[0m\x1b[K\n",
+ dumb: "[ 50% 1/2] action1\n[100% 2/2] action2\n",
+ },
+ {
+ name: "action with output",
+ calls: actionsWithOutput,
+ smart: "\r\x1b[1m[ 0% 0/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action2\x1b[0m\x1b[K\r\x1b[1m[ 66% 2/3] action2\x1b[0m\x1b[K\noutput1\noutput2\n\r\x1b[1m[ 66% 2/3] action3\x1b[0m\x1b[K\r\x1b[1m[100% 3/3] action3\x1b[0m\x1b[K\n",
+ dumb: "[ 33% 1/3] action1\n[ 66% 2/3] action2\noutput1\noutput2\n[100% 3/3] action3\n",
+ },
+ {
+ name: "action with output without newline",
+ calls: actionsWithOutputWithoutNewline,
+ smart: "\r\x1b[1m[ 0% 0/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action2\x1b[0m\x1b[K\r\x1b[1m[ 66% 2/3] action2\x1b[0m\x1b[K\noutput1\noutput2\n\r\x1b[1m[ 66% 2/3] action3\x1b[0m\x1b[K\r\x1b[1m[100% 3/3] action3\x1b[0m\x1b[K\n",
+ dumb: "[ 33% 1/3] action1\n[ 66% 2/3] action2\noutput1\noutput2\n[100% 3/3] action3\n",
+ },
+ {
+ name: "action with error",
+ calls: actionsWithError,
+ smart: "\r\x1b[1m[ 0% 0/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action2\x1b[0m\x1b[K\r\x1b[1m[ 66% 2/3] action2\x1b[0m\x1b[K\nFAILED: f1 f2\ntouch f1 f2\nerror1\nerror2\n\r\x1b[1m[ 66% 2/3] action3\x1b[0m\x1b[K\r\x1b[1m[100% 3/3] action3\x1b[0m\x1b[K\n",
+ dumb: "[ 33% 1/3] action1\n[ 66% 2/3] action2\nFAILED: f1 f2\ntouch f1 f2\nerror1\nerror2\n[100% 3/3] action3\n",
+ },
+ {
+ name: "action with empty description",
+ calls: actionWithEmptyDescription,
+ smart: "\r\x1b[1m[ 0% 0/1] command1\x1b[0m\x1b[K\r\x1b[1m[100% 1/1] command1\x1b[0m\x1b[K\n",
+ dumb: "[100% 1/1] command1\n",
+ },
+ {
+ name: "messages",
+ calls: actionsWithMessages,
+ smart: "\r\x1b[1m[ 0% 0/2] action1\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action1\x1b[0m\x1b[K\r\x1b[1mstatus\x1b[0m\x1b[K\r\x1b[Kprint\nFAILED: error\n\r\x1b[1m[ 50% 1/2] action2\x1b[0m\x1b[K\r\x1b[1m[100% 2/2] action2\x1b[0m\x1b[K\n",
+ dumb: "[ 50% 1/2] action1\nstatus\nprint\nFAILED: error\n[100% 2/2] action2\n",
+ },
+ {
+ name: "action with long description",
+ calls: actionWithLongDescription,
+ smart: "\r\x1b[1m[ 0% 0/2] action with very long descrip\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action with very long descrip\x1b[0m\x1b[K\n",
+ dumb: "[ 50% 1/2] action with very long description to test eliding\n",
+ },
+ {
+ name: "action with output with ansi codes",
+ calls: actionWithOuptutWithAnsiCodes,
+ smart: "\r\x1b[1m[ 0% 0/1] action1\x1b[0m\x1b[K\r\x1b[1m[100% 1/1] action1\x1b[0m\x1b[K\n\x1b[31mcolor\x1b[0m\n",
+ dumb: "[100% 1/1] action1\ncolor\n",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Run("smart", func(t *testing.T) {
+ smart := &fakeSmartTerminal{termWidth: 40}
+ stat := NewStatusOutput(smart, "", false)
+ tt.calls(stat)
+ stat.Flush()
+
+ if g, w := smart.String(), tt.smart; g != w {
+ t.Errorf("want:\n%q\ngot:\n%q", w, g)
+ }
+ })
+
+ t.Run("dumb", func(t *testing.T) {
+ dumb := &bytes.Buffer{}
+ stat := NewStatusOutput(dumb, "", false)
+ tt.calls(stat)
+ stat.Flush()
+
+ if g, w := dumb.String(), tt.dumb; g != w {
+ t.Errorf("want:\n%q\ngot:\n%q", w, g)
+ }
+ })
+ })
+ }
+}
+
+type runner struct {
+ counts status.Counts
+ stat status.StatusOutput
+}
+
+func newRunner(stat status.StatusOutput, totalActions int) *runner {
+ return &runner{
+ counts: status.Counts{TotalActions: totalActions},
+ stat: stat,
+ }
+}
+
+func (r *runner) startAction(action *status.Action) {
+ r.counts.StartedActions++
+ r.counts.RunningActions++
+ r.stat.StartAction(action, r.counts)
+}
+
+func (r *runner) finishAction(result status.ActionResult) {
+ r.counts.FinishedActions++
+ r.counts.RunningActions--
+ r.stat.FinishAction(result, r.counts)
+}
+
+func (r *runner) finishAndStartAction(result status.ActionResult, action *status.Action) {
+ r.counts.FinishedActions++
+ r.stat.FinishAction(result, r.counts)
+
+ r.counts.StartedActions++
+ r.stat.StartAction(action, r.counts)
+}
+
+var (
+ action1 = &status.Action{Description: "action1"}
+ result1 = status.ActionResult{Action: action1}
+ action2 = &status.Action{Description: "action2"}
+ result2 = status.ActionResult{Action: action2}
+ action3 = &status.Action{Description: "action3"}
+ result3 = status.ActionResult{Action: action3}
+)
+
+func twoActions(stat status.StatusOutput) {
+ runner := newRunner(stat, 2)
+ runner.startAction(action1)
+ runner.finishAction(result1)
+ runner.startAction(action2)
+ runner.finishAction(result2)
+}
+
+func twoParallelActions(stat status.StatusOutput) {
+ runner := newRunner(stat, 2)
+ runner.startAction(action1)
+ runner.startAction(action2)
+ runner.finishAction(result1)
+ runner.finishAction(result2)
+}
+
+func actionsWithOutput(stat status.StatusOutput) {
+ result2WithOutput := status.ActionResult{Action: action2, Output: "output1\noutput2\n"}
+
+ runner := newRunner(stat, 3)
+ runner.startAction(action1)
+ runner.finishAction(result1)
+ runner.startAction(action2)
+ runner.finishAction(result2WithOutput)
+ runner.startAction(action3)
+ runner.finishAction(result3)
+}
+
+func actionsWithOutputWithoutNewline(stat status.StatusOutput) {
+ result2WithOutputWithoutNewline := status.ActionResult{Action: action2, Output: "output1\noutput2"}
+
+ runner := newRunner(stat, 3)
+ runner.startAction(action1)
+ runner.finishAction(result1)
+ runner.startAction(action2)
+ runner.finishAction(result2WithOutputWithoutNewline)
+ runner.startAction(action3)
+ runner.finishAction(result3)
+}
+
+func actionsWithError(stat status.StatusOutput) {
+ action2WithError := &status.Action{Description: "action2", Outputs: []string{"f1", "f2"}, Command: "touch f1 f2"}
+ result2WithError := status.ActionResult{Action: action2WithError, Output: "error1\nerror2\n", Error: fmt.Errorf("error1")}
+
+ runner := newRunner(stat, 3)
+ runner.startAction(action1)
+ runner.finishAction(result1)
+ runner.startAction(action2WithError)
+ runner.finishAction(result2WithError)
+ runner.startAction(action3)
+ runner.finishAction(result3)
+}
+
+func actionWithEmptyDescription(stat status.StatusOutput) {
+ action1 := &status.Action{Command: "command1"}
+ result1 := status.ActionResult{Action: action1}
+
+ runner := newRunner(stat, 1)
+ runner.startAction(action1)
+ runner.finishAction(result1)
+}
+
+func actionsWithMessages(stat status.StatusOutput) {
+ runner := newRunner(stat, 2)
+
+ runner.startAction(action1)
+ runner.finishAction(result1)
+
+ stat.Message(status.VerboseLvl, "verbose")
+ stat.Message(status.StatusLvl, "status")
+ stat.Message(status.PrintLvl, "print")
+ stat.Message(status.ErrorLvl, "error")
+
+ runner.startAction(action2)
+ runner.finishAction(result2)
+}
+
+func actionWithLongDescription(stat status.StatusOutput) {
+ action1 := &status.Action{Description: "action with very long description to test eliding"}
+ result1 := status.ActionResult{Action: action1}
+
+ runner := newRunner(stat, 2)
+
+ runner.startAction(action1)
+
+ runner.finishAction(result1)
+}
+
+func actionWithOuptutWithAnsiCodes(stat status.StatusOutput) {
+ result1WithOutputWithAnsiCodes := status.ActionResult{Action: action1, Output: "\x1b[31mcolor\x1b[0m"}
+
+ runner := newRunner(stat, 1)
+ runner.startAction(action1)
+ runner.finishAction(result1WithOutputWithAnsiCodes)
+}
+
+func TestSmartStatusOutputWidthChange(t *testing.T) {
+ smart := &fakeSmartTerminal{termWidth: 40}
+ stat := NewStatusOutput(smart, "", false)
+
+ runner := newRunner(stat, 2)
+
+ action := &status.Action{Description: "action with very long description to test eliding"}
+ result := status.ActionResult{Action: action}
+
+ runner.startAction(action)
+ smart.termWidth = 30
+ // Fake a SIGWINCH
+ stat.(*smartStatusOutput).sigwinch <- syscall.SIGWINCH
+ runner.finishAction(result)
+
+ stat.Flush()
+
+ w := "\r\x1b[1m[ 0% 0/2] action with very long descrip\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action with very lo\x1b[0m\x1b[K\n"
+
+ if g := smart.String(); g != w {
+ t.Errorf("want:\n%q\ngot:\n%q", w, g)
+ }
+}
diff --git a/ui/terminal/stdio.go b/ui/terminal/stdio.go
new file mode 100644
index 000000000..dec296312
--- /dev/null
+++ b/ui/terminal/stdio.go
@@ -0,0 +1,55 @@
+// Copyright 2018 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package terminal provides a set of interfaces that can be used to interact
+// with the terminal (including falling back when the terminal is detected to
+// be a redirect or other dumb terminal)
+package terminal
+
+import (
+ "io"
+ "os"
+)
+
+// StdioInterface represents a set of stdin/stdout/stderr Reader/Writers
+type StdioInterface interface {
+ Stdin() io.Reader
+ Stdout() io.Writer
+ Stderr() io.Writer
+}
+
+// StdioImpl uses the OS stdin/stdout/stderr to implement StdioInterface
+type StdioImpl struct{}
+
+func (StdioImpl) Stdin() io.Reader { return os.Stdin }
+func (StdioImpl) Stdout() io.Writer { return os.Stdout }
+func (StdioImpl) Stderr() io.Writer { return os.Stderr }
+
+var _ StdioInterface = StdioImpl{}
+
+type customStdio struct {
+ stdin io.Reader
+ stdout io.Writer
+ stderr io.Writer
+}
+
+func NewCustomStdio(stdin io.Reader, stdout, stderr io.Writer) StdioInterface {
+ return customStdio{stdin, stdout, stderr}
+}
+
+func (c customStdio) Stdin() io.Reader { return c.stdin }
+func (c customStdio) Stdout() io.Writer { return c.stdout }
+func (c customStdio) Stderr() io.Writer { return c.stderr }
+
+var _ StdioInterface = customStdio{}
diff --git a/ui/terminal/util.go b/ui/terminal/util.go
index a85a517b9..3a11b79bb 100644
--- a/ui/terminal/util.go
+++ b/ui/terminal/util.go
@@ -22,13 +22,15 @@ import (
"unsafe"
)
-func isTerminal(w io.Writer) bool {
+func isSmartTerminal(w io.Writer) bool {
if f, ok := w.(*os.File); ok {
var termios syscall.Termios
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, f.Fd(),
ioctlGetTermios, uintptr(unsafe.Pointer(&termios)),
0, 0, 0)
return err == 0
+ } else if _, ok := w.(*fakeSmartTerminal); ok {
+ return true
}
return false
}
@@ -43,6 +45,8 @@ func termWidth(w io.Writer) (int, bool) {
syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&winsize)),
0, 0, 0)
return int(winsize.ws_column), err == 0
+ } else if f, ok := w.(*fakeSmartTerminal); ok {
+ return f.termWidth, true
}
return 0, false
}
@@ -99,3 +103,8 @@ func stripAnsiEscapes(input []byte) []byte {
return input
}
+
+type fakeSmartTerminal struct {
+ bytes.Buffer
+ termWidth int
+}
diff --git a/ui/terminal/writer.go b/ui/terminal/writer.go
deleted file mode 100644
index ebe4b2aad..000000000
--- a/ui/terminal/writer.go
+++ /dev/null
@@ -1,229 +0,0 @@
-// Copyright 2018 Google Inc. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-// Package terminal provides a set of interfaces that can be used to interact
-// with the terminal (including falling back when the terminal is detected to
-// be a redirect or other dumb terminal)
-package terminal
-
-import (
- "fmt"
- "io"
- "os"
- "strings"
- "sync"
-)
-
-// Writer provides an interface to write temporary and permanent messages to
-// the terminal.
-//
-// The terminal is considered to be a dumb terminal if TERM==dumb, or if a
-// terminal isn't detected on stdout/stderr (generally because it's a pipe or
-// file). Dumb terminals will strip out all ANSI escape sequences, including
-// colors.
-type Writer interface {
- // Print prints the string to the terminal, overwriting any current
- // status being displayed.
- //
- // On a dumb terminal, the status messages will be kept.
- Print(str string)
-
- // Status prints the first line of the string to the terminal,
- // overwriting any previous status line. Strings longer than the width
- // of the terminal will be cut off.
- //
- // On a dumb terminal, previous status messages will remain, and the
- // entire first line of the string will be printed.
- StatusLine(str string)
-
- // StatusAndMessage prints the first line of status to the terminal,
- // similarly to StatusLine(), then prints the full msg below that. The
- // status line is retained.
- //
- // There is guaranteed to be no other output in between the status and
- // message.
- StatusAndMessage(status, msg string)
-
- // Finish ensures that the output ends with a newline (preserving any
- // current status line that is current displayed).
- //
- // This does nothing on dumb terminals.
- Finish()
-
- // Write implements the io.Writer interface. This is primarily so that
- // the logger can use this interface to print to stderr without
- // breaking the other semantics of this interface.
- //
- // Try to use any of the other functions if possible.
- Write(p []byte) (n int, err error)
-
- isSmartTerminal() bool
-}
-
-// NewWriter creates a new Writer based on the stdio and the TERM
-// environment variable.
-func NewWriter(stdio StdioInterface) Writer {
- w := &writerImpl{
- stdio: stdio,
-
- haveBlankLine: true,
- }
-
- if term, ok := os.LookupEnv("TERM"); ok && term != "dumb" {
- w.smartTerminal = isTerminal(stdio.Stdout())
- }
- w.stripEscapes = !w.smartTerminal
-
- return w
-}
-
-type writerImpl struct {
- stdio StdioInterface
-
- haveBlankLine bool
-
- // Protecting the above, we assume that smartTerminal and stripEscapes
- // does not change after initial setup.
- lock sync.Mutex
-
- smartTerminal bool
- stripEscapes bool
-}
-
-func (w *writerImpl) isSmartTerminal() bool {
- return w.smartTerminal
-}
-
-func (w *writerImpl) requestLine() {
- if !w.haveBlankLine {
- fmt.Fprintln(w.stdio.Stdout())
- w.haveBlankLine = true
- }
-}
-
-func (w *writerImpl) Print(str string) {
- if w.stripEscapes {
- str = string(stripAnsiEscapes([]byte(str)))
- }
-
- w.lock.Lock()
- defer w.lock.Unlock()
- w.print(str)
-}
-
-func (w *writerImpl) print(str string) {
- if !w.haveBlankLine {
- fmt.Fprint(w.stdio.Stdout(), "\r", "\x1b[K")
- w.haveBlankLine = true
- }
- fmt.Fprint(w.stdio.Stdout(), str)
- if len(str) == 0 || str[len(str)-1] != '\n' {
- fmt.Fprint(w.stdio.Stdout(), "\n")
- }
-}
-
-func (w *writerImpl) StatusLine(str string) {
- w.lock.Lock()
- defer w.lock.Unlock()
-
- w.statusLine(str)
-}
-
-func (w *writerImpl) statusLine(str string) {
- if !w.smartTerminal {
- fmt.Fprintln(w.stdio.Stdout(), str)
- return
- }
-
- idx := strings.IndexRune(str, '\n')
- if idx != -1 {
- str = str[0:idx]
- }
-
- // Limit line width to the terminal width, otherwise we'll wrap onto
- // another line and we won't delete the previous line.
- //
- // Run this on every line in case the window has been resized while
- // we're printing. This could be optimized to only re-run when we get
- // SIGWINCH if it ever becomes too time consuming.
- if max, ok := termWidth(w.stdio.Stdout()); ok {
- if len(str) > max {
- // TODO: Just do a max. Ninja elides the middle, but that's
- // more complicated and these lines aren't that important.
- str = str[:max]
- }
- }
-
- // Move to the beginning on the line, print the output, then clear
- // the rest of the line.
- fmt.Fprint(w.stdio.Stdout(), "\r", str, "\x1b[K")
- w.haveBlankLine = false
-}
-
-func (w *writerImpl) StatusAndMessage(status, msg string) {
- if w.stripEscapes {
- msg = string(stripAnsiEscapes([]byte(msg)))
- }
-
- w.lock.Lock()
- defer w.lock.Unlock()
-
- w.statusLine(status)
- w.requestLine()
- w.print(msg)
-}
-
-func (w *writerImpl) Finish() {
- w.lock.Lock()
- defer w.lock.Unlock()
-
- w.requestLine()
-}
-
-func (w *writerImpl) Write(p []byte) (n int, err error) {
- w.Print(string(p))
- return len(p), nil
-}
-
-// StdioInterface represents a set of stdin/stdout/stderr Reader/Writers
-type StdioInterface interface {
- Stdin() io.Reader
- Stdout() io.Writer
- Stderr() io.Writer
-}
-
-// StdioImpl uses the OS stdin/stdout/stderr to implement StdioInterface
-type StdioImpl struct{}
-
-func (StdioImpl) Stdin() io.Reader { return os.Stdin }
-func (StdioImpl) Stdout() io.Writer { return os.Stdout }
-func (StdioImpl) Stderr() io.Writer { return os.Stderr }
-
-var _ StdioInterface = StdioImpl{}
-
-type customStdio struct {
- stdin io.Reader
- stdout io.Writer
- stderr io.Writer
-}
-
-func NewCustomStdio(stdin io.Reader, stdout, stderr io.Writer) StdioInterface {
- return customStdio{stdin, stdout, stderr}
-}
-
-func (c customStdio) Stdin() io.Reader { return c.stdin }
-func (c customStdio) Stdout() io.Writer { return c.stdout }
-func (c customStdio) Stderr() io.Writer { return c.stderr }
-
-var _ StdioInterface = customStdio{}
diff --git a/ui/tracer/status.go b/ui/tracer/status.go
index af50e2d4d..c83125514 100644
--- a/ui/tracer/status.go
+++ b/ui/tracer/status.go
@@ -85,3 +85,8 @@ func (s *statusOutput) FinishAction(result status.ActionResult, counts status.Co
func (s *statusOutput) Flush() {}
func (s *statusOutput) Message(level status.MsgLevel, message string) {}
+
+func (s *statusOutput) Write(p []byte) (int, error) {
+ // Discard writes
+ return len(p), nil
+}