Merge changes I555ad21a,I9d46761e,I917bdeee,I5bc5476a,I59bbdae4, ...

am: 6ebe07def7

Change-Id: I266e90807eada08ec39531d9cf2b85361ae389f1
This commit is contained in:
Colin Cross
2019-06-13 11:01:53 -07:00
committed by android-build-merger
18 changed files with 796 additions and 371 deletions

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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{},
}}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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: {

View File

@@ -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
}

123
ui/terminal/format.go Normal file
View File

@@ -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
}

198
ui/terminal/smart_status.go Normal file
View File

@@ -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
}
}

View File

@@ -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,
func NewStatusOutput(w io.Writer, statusFormat string, quietBuild bool) status.StatusOutput {
formatter := newFormatter(statusFormat, quietBuild)
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
}
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)
return NewDumbStatusOutput(w, formatter)
}
}
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 buf.String()
}

275
ui/terminal/status_test.go Normal file
View File

@@ -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)
}
}

55
ui/terminal/stdio.go Normal file
View File

@@ -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{}

View File

@@ -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
}

View File

@@ -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{}

View File

@@ -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
}