Merge changes I555ad21a,I9d46761e,I917bdeee,I5bc5476a,I59bbdae4, ...
am: 6ebe07def7
Change-Id: I266e90807eada08ec39531d9cf2b85361ae389f1
This commit is contained in:
@@ -156,10 +156,12 @@ type mpContext struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
writer := terminal.NewWriter(terminal.StdioImpl{})
|
stdio := terminal.StdioImpl{}
|
||||||
defer writer.Finish()
|
|
||||||
|
|
||||||
log := logger.New(writer)
|
output := terminal.NewStatusOutput(stdio.Stdout(), "",
|
||||||
|
build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD"))
|
||||||
|
|
||||||
|
log := logger.New(output)
|
||||||
defer log.Cleanup()
|
defer log.Cleanup()
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@@ -172,8 +174,7 @@ func main() {
|
|||||||
|
|
||||||
stat := &status.Status{}
|
stat := &status.Status{}
|
||||||
defer stat.Finish()
|
defer stat.Finish()
|
||||||
stat.AddOutput(terminal.NewStatusOutput(writer, "",
|
stat.AddOutput(output)
|
||||||
build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD")))
|
|
||||||
|
|
||||||
var failures failureCount
|
var failures failureCount
|
||||||
stat.AddOutput(&failures)
|
stat.AddOutput(&failures)
|
||||||
@@ -188,7 +189,7 @@ func main() {
|
|||||||
Context: ctx,
|
Context: ctx,
|
||||||
Logger: log,
|
Logger: log,
|
||||||
Tracer: trace,
|
Tracer: trace,
|
||||||
Writer: writer,
|
Writer: output,
|
||||||
Status: stat,
|
Status: stat,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
@@ -341,7 +342,7 @@ func main() {
|
|||||||
} else if failures > 1 {
|
} else if failures > 1 {
|
||||||
log.Fatalf("%d failures", failures)
|
log.Fatalf("%d failures", failures)
|
||||||
} else {
|
} else {
|
||||||
writer.Print("Success")
|
fmt.Fprintln(output, "Success")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,7 +387,7 @@ func buildProduct(mpctx *mpContext, product string) {
|
|||||||
Context: mpctx.Context,
|
Context: mpctx.Context,
|
||||||
Logger: log,
|
Logger: log,
|
||||||
Tracer: mpctx.Tracer,
|
Tracer: mpctx.Tracer,
|
||||||
Writer: terminal.NewWriter(terminal.NewCustomStdio(nil, f, f)),
|
Writer: f,
|
||||||
Thread: mpctx.Tracer.NewThread(product),
|
Thread: mpctx.Tracer.NewThread(product),
|
||||||
Status: &status.Status{},
|
Status: &status.Status{},
|
||||||
}}
|
}}
|
||||||
@@ -466,3 +467,8 @@ func (f *failureCount) Message(level status.MsgLevel, message string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *failureCount) Flush() {}
|
func (f *failureCount) Flush() {}
|
||||||
|
|
||||||
|
func (f *failureCount) Write(p []byte) (int, error) {
|
||||||
|
// discard writes
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
@@ -109,10 +109,10 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
writer := terminal.NewWriter(c.stdio())
|
output := terminal.NewStatusOutput(c.stdio().Stdout(), os.Getenv("NINJA_STATUS"),
|
||||||
defer writer.Finish()
|
build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD"))
|
||||||
|
|
||||||
log := logger.New(writer)
|
log := logger.New(output)
|
||||||
defer log.Cleanup()
|
defer log.Cleanup()
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
@@ -125,8 +125,7 @@ func main() {
|
|||||||
|
|
||||||
stat := &status.Status{}
|
stat := &status.Status{}
|
||||||
defer stat.Finish()
|
defer stat.Finish()
|
||||||
stat.AddOutput(terminal.NewStatusOutput(writer, os.Getenv("NINJA_STATUS"),
|
stat.AddOutput(output)
|
||||||
build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD")))
|
|
||||||
stat.AddOutput(trace.StatusTracer())
|
stat.AddOutput(trace.StatusTracer())
|
||||||
|
|
||||||
build.SetupSignals(log, cancel, func() {
|
build.SetupSignals(log, cancel, func() {
|
||||||
@@ -140,7 +139,7 @@ func main() {
|
|||||||
Logger: log,
|
Logger: log,
|
||||||
Metrics: met,
|
Metrics: met,
|
||||||
Tracer: trace,
|
Tracer: trace,
|
||||||
Writer: writer,
|
Writer: output,
|
||||||
Status: stat,
|
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) {
|
func make(ctx build.Context, config build.Config, _ []string, logsDir string) {
|
||||||
if config.IsVerbose() {
|
if config.IsVerbose() {
|
||||||
writer := ctx.Writer
|
writer := ctx.Writer
|
||||||
writer.Print("! The argument `showcommands` is no longer supported.")
|
fmt.Fprintln(writer, "! The argument `showcommands` is no longer supported.")
|
||||||
writer.Print("! Instead, the verbose log is always written to a compressed file in the output dir:")
|
fmt.Fprintln(writer, "! Instead, the verbose log is always written to a compressed file in the output dir:")
|
||||||
writer.Print("!")
|
fmt.Fprintln(writer, "!")
|
||||||
writer.Print(fmt.Sprintf("! gzip -cd %s/verbose.log.gz | less -R", logsDir))
|
fmt.Fprintf(writer, "! gzip -cd %s/verbose.log.gz | less -R\n", logsDir)
|
||||||
writer.Print("!")
|
fmt.Fprintln(writer, "!")
|
||||||
writer.Print("! Older versions are saved in verbose.log.#.gz files")
|
fmt.Fprintln(writer, "! Older versions are saved in verbose.log.#.gz files")
|
||||||
writer.Print("")
|
fmt.Fprintln(writer, "")
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -22,14 +22,13 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"android/soong/ui/logger"
|
"android/soong/ui/logger"
|
||||||
"android/soong/ui/terminal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func testContext() Context {
|
func testContext() Context {
|
||||||
return Context{&ContextImpl{
|
return Context{&ContextImpl{
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
Logger: logger.New(&bytes.Buffer{}),
|
Logger: logger.New(&bytes.Buffer{}),
|
||||||
Writer: terminal.NewWriter(terminal.NewCustomStdio(&bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{})),
|
Writer: &bytes.Buffer{},
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -16,12 +16,12 @@ package build
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
|
|
||||||
"android/soong/ui/logger"
|
"android/soong/ui/logger"
|
||||||
"android/soong/ui/metrics"
|
"android/soong/ui/metrics"
|
||||||
"android/soong/ui/metrics/metrics_proto"
|
"android/soong/ui/metrics/metrics_proto"
|
||||||
"android/soong/ui/status"
|
"android/soong/ui/status"
|
||||||
"android/soong/ui/terminal"
|
|
||||||
"android/soong/ui/tracer"
|
"android/soong/ui/tracer"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ type ContextImpl struct {
|
|||||||
|
|
||||||
Metrics *metrics.Metrics
|
Metrics *metrics.Metrics
|
||||||
|
|
||||||
Writer terminal.Writer
|
Writer io.Writer
|
||||||
Status *status.Status
|
Status *status.Status
|
||||||
|
|
||||||
Thread tracer.Thread
|
Thread tracer.Thread
|
||||||
|
@@ -249,7 +249,7 @@ func runMakeProductConfig(ctx Context, config Config) {
|
|||||||
env := config.Environment()
|
env := config.Environment()
|
||||||
// Print the banner like make does
|
// Print the banner like make does
|
||||||
if !env.IsEnvTrue("ANDROID_QUIET_BUILD") {
|
if !env.IsEnvTrue("ANDROID_QUIET_BUILD") {
|
||||||
ctx.Writer.Print(Banner(make_vars))
|
fmt.Fprintln(ctx.Writer, Banner(make_vars))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate the environment
|
// Populate the environment
|
||||||
|
@@ -71,6 +71,11 @@ func (v *verboseLog) Message(level MsgLevel, message string) {
|
|||||||
fmt.Fprintf(v.w, "%s%s\n", level.Prefix(), message)
|
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 {
|
type errorLog struct {
|
||||||
w io.WriteCloser
|
w io.WriteCloser
|
||||||
|
|
||||||
@@ -134,3 +139,8 @@ func (e *errorLog) Message(level MsgLevel, message string) {
|
|||||||
|
|
||||||
fmt.Fprintf(e.w, "error: %s\n", message)
|
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
|
||||||
|
}
|
||||||
|
@@ -173,6 +173,9 @@ type StatusOutput interface {
|
|||||||
// Flush is called when your outputs should be flushed / closed. No
|
// Flush is called when your outputs should be flushed / closed. No
|
||||||
// output is expected after this call.
|
// output is expected after this call.
|
||||||
Flush()
|
Flush()
|
||||||
|
|
||||||
|
// Write lets StatusOutput implement io.Writer
|
||||||
|
Write(p []byte) (n int, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status is the multiplexer / accumulator between ToolStatus instances (via
|
// Status is the multiplexer / accumulator between ToolStatus instances (via
|
||||||
|
@@ -27,6 +27,11 @@ func (c *counterOutput) FinishAction(result ActionResult, counts Counts) {
|
|||||||
func (c counterOutput) Message(level MsgLevel, msg string) {}
|
func (c counterOutput) Message(level MsgLevel, msg string) {}
|
||||||
func (c counterOutput) Flush() {}
|
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) {
|
func (c counterOutput) Expect(t *testing.T, counts Counts) {
|
||||||
if Counts(c) == counts {
|
if Counts(c) == counts {
|
||||||
return
|
return
|
||||||
|
@@ -17,11 +17,15 @@ bootstrap_go_package {
|
|||||||
pkgPath: "android/soong/ui/terminal",
|
pkgPath: "android/soong/ui/terminal",
|
||||||
deps: ["soong-ui-status"],
|
deps: ["soong-ui-status"],
|
||||||
srcs: [
|
srcs: [
|
||||||
|
"dumb_status.go",
|
||||||
|
"format.go",
|
||||||
|
"smart_status.go",
|
||||||
"status.go",
|
"status.go",
|
||||||
"writer.go",
|
"stdio.go",
|
||||||
"util.go",
|
"util.go",
|
||||||
],
|
],
|
||||||
testSrcs: [
|
testSrcs: [
|
||||||
|
"status_test.go",
|
||||||
"util_test.go",
|
"util_test.go",
|
||||||
],
|
],
|
||||||
darwin: {
|
darwin: {
|
||||||
|
71
ui/terminal/dumb_status.go
Normal file
71
ui/terminal/dumb_status.go
Normal 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
123
ui/terminal/format.go
Normal 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
198
ui/terminal/smart_status.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@@ -15,131 +15,23 @@
|
|||||||
package terminal
|
package terminal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"io"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"android/soong/ui/status"
|
"android/soong/ui/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
type statusOutput struct {
|
|
||||||
writer Writer
|
|
||||||
format string
|
|
||||||
|
|
||||||
start time.Time
|
|
||||||
quiet bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStatusOutput returns a StatusOutput that represents the
|
// NewStatusOutput returns a StatusOutput that represents the
|
||||||
// current build status similarly to Ninja's built-in terminal
|
// current build status similarly to Ninja's built-in terminal
|
||||||
// output.
|
// output.
|
||||||
//
|
//
|
||||||
// statusFormat takes nearly all the same options as NINJA_STATUS.
|
// statusFormat takes nearly all the same options as NINJA_STATUS.
|
||||||
// %c is currently unsupported.
|
// %c is currently unsupported.
|
||||||
func NewStatusOutput(w Writer, statusFormat string, quietBuild bool) status.StatusOutput {
|
func NewStatusOutput(w io.Writer, statusFormat string, quietBuild bool) status.StatusOutput {
|
||||||
return &statusOutput{
|
formatter := newFormatter(statusFormat, quietBuild)
|
||||||
writer: w,
|
|
||||||
format: statusFormat,
|
|
||||||
|
|
||||||
start: time.Now(),
|
if isSmartTerminal(w) {
|
||||||
quiet: quietBuild,
|
return NewSmartStatusOutput(w, formatter)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
} else {
|
} 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
275
ui/terminal/status_test.go
Normal 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
55
ui/terminal/stdio.go
Normal 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{}
|
@@ -22,13 +22,15 @@ import (
|
|||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isTerminal(w io.Writer) bool {
|
func isSmartTerminal(w io.Writer) bool {
|
||||||
if f, ok := w.(*os.File); ok {
|
if f, ok := w.(*os.File); ok {
|
||||||
var termios syscall.Termios
|
var termios syscall.Termios
|
||||||
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, f.Fd(),
|
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, f.Fd(),
|
||||||
ioctlGetTermios, uintptr(unsafe.Pointer(&termios)),
|
ioctlGetTermios, uintptr(unsafe.Pointer(&termios)),
|
||||||
0, 0, 0)
|
0, 0, 0)
|
||||||
return err == 0
|
return err == 0
|
||||||
|
} else if _, ok := w.(*fakeSmartTerminal); ok {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -43,6 +45,8 @@ func termWidth(w io.Writer) (int, bool) {
|
|||||||
syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&winsize)),
|
syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&winsize)),
|
||||||
0, 0, 0)
|
0, 0, 0)
|
||||||
return int(winsize.ws_column), err == 0
|
return int(winsize.ws_column), err == 0
|
||||||
|
} else if f, ok := w.(*fakeSmartTerminal); ok {
|
||||||
|
return f.termWidth, true
|
||||||
}
|
}
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
@@ -99,3 +103,8 @@ func stripAnsiEscapes(input []byte) []byte {
|
|||||||
|
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fakeSmartTerminal struct {
|
||||||
|
bytes.Buffer
|
||||||
|
termWidth int
|
||||||
|
}
|
||||||
|
@@ -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{}
|
|
@@ -85,3 +85,8 @@ func (s *statusOutput) FinishAction(result status.ActionResult, counts status.Co
|
|||||||
|
|
||||||
func (s *statusOutput) Flush() {}
|
func (s *statusOutput) Flush() {}
|
||||||
func (s *statusOutput) Message(level status.MsgLevel, message string) {}
|
func (s *statusOutput) Message(level status.MsgLevel, message string) {}
|
||||||
|
|
||||||
|
func (s *statusOutput) Write(p []byte) (int, error) {
|
||||||
|
// Discard writes
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user