Merge "Add a unified status reporting UI"

This commit is contained in:
Treehugger Robot 2018-07-12 23:14:44 +00:00 committed by Gerrit Code Review
commit ecc71f8a49
39 changed files with 2624 additions and 550 deletions

View File

@ -17,6 +17,7 @@ blueprint_go_binary {
deps: [
"soong-ui-build",
"soong-ui-logger",
"soong-ui-terminal",
"soong-ui-tracer",
"soong-zip",
],

View File

@ -29,6 +29,8 @@ import (
"android/soong/ui/build"
"android/soong/ui/logger"
"android/soong/ui/status"
"android/soong/ui/terminal"
"android/soong/ui/tracer"
"android/soong/zip"
)
@ -66,98 +68,34 @@ type Product struct {
ctx build.Context
config build.Config
logFile string
action *status.Action
}
type Status struct {
cur int
total int
failed int
ctx build.Context
haveBlankLine bool
smartTerminal bool
lock sync.Mutex
}
func NewStatus(ctx build.Context) *Status {
return &Status{
ctx: ctx,
haveBlankLine: true,
smartTerminal: ctx.IsTerminal(),
}
}
func (s *Status) SetTotal(total int) {
s.total = total
}
func (s *Status) Fail(product string, err error, logFile string) {
s.Finish(product)
s.lock.Lock()
defer s.lock.Unlock()
if s.smartTerminal && !s.haveBlankLine {
fmt.Fprintln(s.ctx.Stdout())
s.haveBlankLine = true
func errMsgFromLog(filename string) string {
if filename == "" {
return ""
}
s.failed++
fmt.Fprintln(s.ctx.Stderr(), "FAILED:", product)
s.ctx.Verboseln("FAILED:", product)
if logFile != "" {
data, err := ioutil.ReadFile(logFile)
if err == nil {
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
if len(lines) > errorLeadingLines+errorTrailingLines+1 {
lines[errorLeadingLines] = fmt.Sprintf("... skipping %d lines ...",
len(lines)-errorLeadingLines-errorTrailingLines)
lines = append(lines[:errorLeadingLines+1],
lines[len(lines)-errorTrailingLines:]...)
}
for _, line := range lines {
fmt.Fprintln(s.ctx.Stderr(), "> ", line)
s.ctx.Verboseln(line)
}
}
data, err := ioutil.ReadFile(filename)
if err != nil {
return ""
}
s.ctx.Print(err)
}
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
if len(lines) > errorLeadingLines+errorTrailingLines+1 {
lines[errorLeadingLines] = fmt.Sprintf("... skipping %d lines ...",
len(lines)-errorLeadingLines-errorTrailingLines)
func (s *Status) Finish(product string) {
s.lock.Lock()
defer s.lock.Unlock()
s.cur++
line := fmt.Sprintf("[%d/%d] %s", s.cur, s.total, product)
if s.smartTerminal {
if max, ok := s.ctx.TermWidth(); ok {
if len(line) > max {
line = line[:max]
}
}
fmt.Fprint(s.ctx.Stdout(), "\r", line, "\x1b[K")
s.haveBlankLine = false
} else {
s.ctx.Println(line)
lines = append(lines[:errorLeadingLines+1],
lines[len(lines)-errorTrailingLines:]...)
}
}
func (s *Status) Finished() int {
s.lock.Lock()
defer s.lock.Unlock()
if !s.haveBlankLine {
fmt.Fprintln(s.ctx.Stdout())
s.haveBlankLine = true
var buf strings.Builder
for _, line := range lines {
buf.WriteString("> ")
buf.WriteString(line)
buf.WriteString("\n")
}
return s.failed
return buf.String()
}
// TODO(b/70370883): This tool uses a lot of open files -- over the default
@ -194,6 +132,9 @@ func inList(str string, list []string) bool {
}
func main() {
writer := terminal.NewWriter(terminal.StdioImpl{})
defer writer.Finish()
log := logger.New(os.Stderr)
defer log.Cleanup()
@ -205,20 +146,24 @@ func main() {
trace := tracer.New(log)
defer trace.Close()
stat := &status.Status{}
defer stat.Finish()
stat.AddOutput(terminal.NewStatusOutput(writer, ""))
build.SetupSignals(log, cancel, func() {
trace.Close()
log.Cleanup()
stat.Finish()
})
buildCtx := build.Context{&build.ContextImpl{
Context: ctx,
Logger: log,
Tracer: trace,
StdioInterface: build.StdioImpl{},
Context: ctx,
Logger: log,
Tracer: trace,
Writer: writer,
Status: stat,
}}
status := NewStatus(buildCtx)
config := build.NewConfig(buildCtx)
if *outDir == "" {
name := "multiproduct-" + time.Now().Format("20060102150405")
@ -303,7 +248,8 @@ func main() {
log.Verbose("Got product list: ", products)
status.SetTotal(len(products))
s := buildCtx.Status.StartTool()
s.SetTotalActions(len(products))
var wg sync.WaitGroup
productConfigs := make(chan Product, len(products))
@ -315,8 +261,18 @@ func main() {
var stdLog string
defer wg.Done()
action := &status.Action{
Description: product,
Outputs: []string{product},
}
s.StartAction(action)
defer logger.Recover(func(err error) {
status.Fail(product, err, stdLog)
s.FinishAction(status.ActionResult{
Action: action,
Error: err,
Output: errMsgFromLog(stdLog),
})
})
productOutDir := filepath.Join(config.OutDir(), product)
@ -339,12 +295,14 @@ func main() {
productLog.SetOutput(filepath.Join(productLogDir, "soong.log"))
productCtx := build.Context{&build.ContextImpl{
Context: ctx,
Logger: productLog,
Tracer: trace,
StdioInterface: build.NewCustomStdio(nil, f, f),
Thread: trace.NewThread(product),
Context: ctx,
Logger: productLog,
Tracer: trace,
Writer: terminal.NewWriter(terminal.NewCustomStdio(nil, f, f)),
Thread: trace.NewThread(product),
Status: &status.Status{},
}}
productCtx.Status.AddOutput(terminal.NewStatusOutput(productCtx.Writer, ""))
productConfig := build.NewConfig(productCtx)
productConfig.Environment().Set("OUT_DIR", productOutDir)
@ -352,7 +310,7 @@ func main() {
productConfig.Lunch(productCtx, product, *buildVariant)
build.Build(productCtx, productConfig, build.BuildProductConfig)
productConfigs <- Product{productCtx, productConfig, stdLog}
productConfigs <- Product{productCtx, productConfig, stdLog, action}
}(product)
}
go func() {
@ -369,7 +327,11 @@ func main() {
for product := range productConfigs {
func() {
defer logger.Recover(func(err error) {
status.Fail(product.config.TargetProduct(), err, product.logFile)
s.FinishAction(status.ActionResult{
Action: product.action,
Error: err,
Output: errMsgFromLog(product.logFile),
})
})
defer func() {
@ -400,7 +362,9 @@ func main() {
}
}
build.Build(product.ctx, product.config, buildWhat)
status.Finish(product.config.TargetProduct())
s.FinishAction(status.ActionResult{
Action: product.action,
})
}()
}
}()
@ -421,7 +385,5 @@ func main() {
}
}
if count := status.Finished(); count > 0 {
log.Fatalln(count, "products failed")
}
s.Finish()
}

View File

@ -17,6 +17,7 @@ blueprint_go_binary {
deps: [
"soong-ui-build",
"soong-ui-logger",
"soong-ui-terminal",
"soong-ui-tracer",
],
srcs: [

View File

@ -26,6 +26,8 @@ import (
"android/soong/ui/build"
"android/soong/ui/logger"
"android/soong/ui/status"
"android/soong/ui/terminal"
"android/soong/ui/tracer"
)
@ -44,7 +46,10 @@ func inList(s string, list []string) bool {
}
func main() {
log := logger.New(os.Stderr)
writer := terminal.NewWriter(terminal.StdioImpl{})
defer writer.Finish()
log := logger.New(writer)
defer log.Cleanup()
if len(os.Args) < 2 || !(inList("--make-mode", os.Args) ||
@ -60,16 +65,23 @@ func main() {
trace := tracer.New(log)
defer trace.Close()
stat := &status.Status{}
defer stat.Finish()
stat.AddOutput(terminal.NewStatusOutput(writer, os.Getenv("NINJA_STATUS")))
stat.AddOutput(trace.StatusTracer())
build.SetupSignals(log, cancel, func() {
trace.Close()
log.Cleanup()
stat.Finish()
})
buildCtx := build.Context{&build.ContextImpl{
Context: ctx,
Logger: log,
Tracer: trace,
StdioInterface: build.StdioImpl{},
Context: ctx,
Logger: log,
Tracer: trace,
Writer: writer,
Status: stat,
}}
var config build.Config
if os.Args[1] == "--dumpvars-mode" || os.Args[1] == "--dumpvar-mode" {
@ -78,19 +90,19 @@ func main() {
config = build.NewConfig(buildCtx, os.Args[1:]...)
}
log.SetVerbose(config.IsVerbose())
build.SetupOutDir(buildCtx, config)
logsDir := config.OutDir()
if config.Dist() {
logsDir := filepath.Join(config.DistDir(), "logs")
os.MkdirAll(logsDir, 0777)
log.SetOutput(filepath.Join(logsDir, "soong.log"))
trace.SetOutput(filepath.Join(logsDir, "build.trace"))
} else {
log.SetOutput(filepath.Join(config.OutDir(), "soong.log"))
trace.SetOutput(filepath.Join(config.OutDir(), "build.trace"))
logsDir = filepath.Join(config.DistDir(), "logs")
}
os.MkdirAll(logsDir, 0777)
log.SetOutput(filepath.Join(logsDir, "soong.log"))
trace.SetOutput(filepath.Join(logsDir, "build.trace"))
stat.AddOutput(status.NewVerboseLog(log, filepath.Join(logsDir, "verbose.log")))
stat.AddOutput(status.NewErrorLog(log, filepath.Join(logsDir, "error.log")))
if start, ok := os.LookupEnv("TRACE_BEGIN_SOONG"); ok {
if !strings.HasSuffix(start, "N") {
if start_time, err := strconv.ParseUint(start, 10, 64); err == nil {
@ -114,6 +126,17 @@ func main() {
} else if os.Args[1] == "--dumpvars-mode" {
dumpVars(buildCtx, config, os.Args[2:])
} else {
if config.IsVerbose() {
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("")
time.Sleep(5 * time.Second)
}
toBuild := build.BuildAll
if config.Checkbuild() {
toBuild |= build.RunBuildTests

View File

@ -59,7 +59,7 @@ function soong_build_go
BUILDDIR=$(getoutdir) \
SRCDIR=${TOP} \
BLUEPRINTDIR=${TOP}/build/blueprint \
EXTRA_ARGS="-pkg-path android/soong=${TOP}/build/soong" \
EXTRA_ARGS="-pkg-path android/soong=${TOP}/build/soong -pkg-path github.com/golang/protobuf=${TOP}/external/golang-protobuf" \
build_go $@
}

View File

@ -30,6 +30,8 @@ bootstrap_go_package {
deps: [
"soong-ui-build-paths",
"soong-ui-logger",
"soong-ui-status",
"soong-ui-terminal",
"soong-ui-tracer",
"soong-shared",
"soong-finder",
@ -62,13 +64,11 @@ bootstrap_go_package {
darwin: {
srcs: [
"sandbox_darwin.go",
"util_darwin.go"
],
},
linux: {
srcs: [
"sandbox_linux.go",
"util_linux.go"
],
},
}

View File

@ -105,9 +105,7 @@ func checkCaseSensitivity(ctx Context, config Config) {
func help(ctx Context, config Config, what int) {
cmd := Command(ctx, config, "help.sh", "build/make/help.sh")
cmd.Sandbox = dumpvarsSandbox
cmd.Stdout = ctx.Stdout()
cmd.Stderr = ctx.Stderr()
cmd.RunOrFatal()
cmd.RunAndPrintOrFatal()
}
// Build the tree. The 'what' argument can be used to chose which components of

View File

@ -22,13 +22,14 @@ import (
"testing"
"android/soong/ui/logger"
"android/soong/ui/terminal"
)
func testContext() Context {
return Context{&ContextImpl{
Context: context.Background(),
Logger: logger.New(&bytes.Buffer{}),
StdioInterface: NewCustomStdio(&bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{}),
Context: context.Background(),
Logger: logger.New(&bytes.Buffer{}),
Writer: terminal.NewWriter(terminal.NewCustomStdio(&bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{})),
}}
}

View File

@ -16,45 +16,14 @@ package build
import (
"context"
"io"
"os"
"time"
"android/soong/ui/logger"
"android/soong/ui/status"
"android/soong/ui/terminal"
"android/soong/ui/tracer"
)
type StdioInterface interface {
Stdin() io.Reader
Stdout() io.Writer
Stderr() io.Writer
}
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{}
// Context combines a context.Context, logger.Logger, and StdIO redirection.
// Context combines a context.Context, logger.Logger, and terminal.Writer.
// These all are agnostic of the current build, and may be used for multiple
// builds, while the Config objects contain per-build information.
type Context struct{ *ContextImpl }
@ -62,7 +31,8 @@ type ContextImpl struct {
context.Context
logger.Logger
StdioInterface
Writer terminal.Writer
Status *status.Status
Thread tracer.Thread
Tracer tracer.Tracer
@ -88,28 +58,3 @@ func (c ContextImpl) CompleteTrace(name string, begin, end uint64) {
c.Tracer.Complete(name, c.Thread, begin, end)
}
}
// ImportNinjaLog imports a .ninja_log file into the tracer.
func (c ContextImpl) ImportNinjaLog(filename string, startOffset time.Time) {
if c.Tracer != nil {
c.Tracer.ImportNinjaLog(c.Thread, filename, startOffset)
}
}
func (c ContextImpl) IsTerminal() bool {
if term, ok := os.LookupEnv("TERM"); ok {
return term != "dumb" && isTerminal(c.Stdout()) && isTerminal(c.Stderr())
}
return false
}
func (c ContextImpl) IsErrTerminal() bool {
if term, ok := os.LookupEnv("TERM"); ok {
return term != "dumb" && isTerminal(c.Stderr())
}
return false
}
func (c ContextImpl) TermWidth() (int, bool) {
return termWidth(c.Stdout())
}

View File

@ -18,6 +18,8 @@ import (
"bytes"
"fmt"
"strings"
"android/soong/ui/status"
)
// DumpMakeVars can be used to extract the values of Make variables after the
@ -60,7 +62,7 @@ func dumpMakeVars(ctx Context, config Config, goals, vars []string, write_soong_
}
cmd.StartOrFatal()
// TODO: error out when Stderr contains any content
katiRewriteOutput(ctx, pipe)
status.KatiReader(ctx.Status.StartTool(), pipe)
cmd.WaitOrFatal()
ret := make(map[string]string, len(vars))
@ -175,7 +177,7 @@ func runMakeProductConfig(ctx Context, config Config) {
}
// Print the banner like make does
fmt.Fprintln(ctx.Stdout(), Banner(make_vars))
ctx.Writer.Print(Banner(make_vars))
// Populate the environment
env := config.Environment()

View File

@ -122,3 +122,20 @@ func (c *Cmd) CombinedOutputOrFatal() []byte {
c.reportError(err)
return ret
}
// RunAndPrintOrFatal will run the command, then after finishing
// print any output, then handling any errors with a call to
// ctx.Fatal
func (c *Cmd) RunAndPrintOrFatal() {
ret, err := c.CombinedOutput()
st := c.ctx.Status.StartTool()
if len(ret) > 0 {
if err != nil {
st.Error(string(ret))
} else {
st.Print(string(ret))
}
}
st.Finish()
c.reportError(err)
}

View File

@ -15,15 +15,14 @@
package build
import (
"bufio"
"crypto/md5"
"fmt"
"io"
"io/ioutil"
"path/filepath"
"regexp"
"strconv"
"strings"
"android/soong/ui/status"
)
var spaceSlashReplacer = strings.NewReplacer("/", "_", " ", "_")
@ -117,77 +116,10 @@ func runKati(ctx Context, config Config) {
cmd.Stderr = cmd.Stdout
cmd.StartOrFatal()
katiRewriteOutput(ctx, pipe)
status.KatiReader(ctx.Status.StartTool(), pipe)
cmd.WaitOrFatal()
}
var katiIncludeRe = regexp.MustCompile(`^(\[\d+/\d+] )?including [^ ]+ ...$`)
var katiLogRe = regexp.MustCompile(`^\*kati\*: `)
func katiRewriteOutput(ctx Context, pipe io.ReadCloser) {
haveBlankLine := true
smartTerminal := ctx.IsTerminal()
errSmartTerminal := ctx.IsErrTerminal()
scanner := bufio.NewScanner(pipe)
for scanner.Scan() {
line := scanner.Text()
verbose := katiIncludeRe.MatchString(line)
// Only put kati debug/stat lines in our verbose log
if katiLogRe.MatchString(line) {
ctx.Verbose(line)
continue
}
// For verbose lines, write them on the current line without a newline,
// then overwrite them if the next thing we're printing is another
// verbose line.
if smartTerminal && verbose {
// 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(ctx.Stdout()); ok {
if len(line) > max {
// Just do a max. Ninja elides the middle, but that's
// more complicated and these lines aren't that important.
line = line[:max]
}
}
// Move to the beginning on the line, print the output, then clear
// the rest of the line.
fmt.Fprint(ctx.Stdout(), "\r", line, "\x1b[K")
haveBlankLine = false
continue
} else if smartTerminal && !haveBlankLine {
// If we've previously written a verbose message, send a newline to save
// that message instead of overwriting it.
fmt.Fprintln(ctx.Stdout())
haveBlankLine = true
} else if !errSmartTerminal {
// Most editors display these as garbage, so strip them out.
line = string(stripAnsiEscapes([]byte(line)))
}
// Assume that non-verbose lines are important enough for stderr
fmt.Fprintln(ctx.Stderr(), line)
}
// Save our last verbose line.
if !haveBlankLine {
fmt.Fprintln(ctx.Stdout())
}
if err := scanner.Err(); err != nil {
ctx.Println("Error from kati parser:", err)
io.Copy(ctx.Stderr(), pipe)
}
}
func runKatiCleanSpec(ctx Context, config Config) {
ctx.BeginTrace("kati cleanspec")
defer ctx.EndTrace()
@ -220,6 +152,6 @@ func runKatiCleanSpec(ctx Context, config Config) {
cmd.Stderr = cmd.Stdout
cmd.StartOrFatal()
katiRewriteOutput(ctx, pipe)
status.KatiReader(ctx.Status.StartTool(), pipe)
cmd.WaitOrFatal()
}

View File

@ -21,15 +21,21 @@ import (
"strconv"
"strings"
"time"
"android/soong/ui/status"
)
func runNinja(ctx Context, config Config) {
ctx.BeginTrace("ninja")
defer ctx.EndTrace()
fifo := filepath.Join(config.OutDir(), ".ninja_fifo")
status.NinjaReader(ctx, ctx.Status.StartTool(), fifo)
executable := config.PrebuiltBuildTool("ninja")
args := []string{
"-d", "keepdepfile",
fmt.Sprintf("--frontend=cat <&3 >%s", fifo),
}
args = append(args, config.NinjaArgs()...)
@ -47,9 +53,6 @@ func runNinja(ctx Context, config Config) {
args = append(args, "-f", config.CombinedNinjaFile())
if config.IsVerbose() {
args = append(args, "-v")
}
args = append(args, "-w", "dupbuild=err")
cmd := Command(ctx, config, "ninja", executable, args...)
@ -66,13 +69,6 @@ func runNinja(ctx Context, config Config) {
cmd.Args = append(cmd.Args, strings.Fields(extra)...)
}
if _, ok := cmd.Environment.Get("NINJA_STATUS"); !ok {
cmd.Environment.Set("NINJA_STATUS", "[%p %f/%t] ")
}
cmd.Stdin = ctx.Stdin()
cmd.Stdout = ctx.Stdout()
cmd.Stderr = ctx.Stderr()
logPath := filepath.Join(config.OutDir(), ".ninja_log")
ninjaHeartbeatDuration := time.Minute * 5
if overrideText, ok := cmd.Environment.Get("NINJA_HEARTBEAT_INTERVAL"); ok {
@ -99,10 +95,7 @@ func runNinja(ctx Context, config Config) {
}
}()
startTime := time.Now()
defer ctx.ImportNinjaLog(logPath, startTime)
cmd.RunOrFatal()
cmd.RunAndPrintOrFatal()
}
type statusChecker struct {

View File

@ -15,12 +15,15 @@
package build
import (
"fmt"
"os"
"path/filepath"
"strconv"
"time"
"strings"
"github.com/google/blueprint/microfactory"
"android/soong/ui/status"
)
func runSoong(ctx Context, config Config) {
@ -41,9 +44,8 @@ func runSoong(ctx Context, config Config) {
cmd.Environment.Set("SRCDIR", ".")
cmd.Environment.Set("TOPNAME", "Android.bp")
cmd.Sandbox = soongSandbox
cmd.Stdout = ctx.Stdout()
cmd.Stderr = ctx.Stderr()
cmd.RunOrFatal()
cmd.RunAndPrintOrFatal()
}()
func() {
@ -56,12 +58,18 @@ func runSoong(ctx Context, config Config) {
if _, err := os.Stat(envTool); err == nil {
cmd := Command(ctx, config, "soong_env", envTool, envFile)
cmd.Sandbox = soongSandbox
cmd.Stdout = ctx.Stdout()
cmd.Stderr = ctx.Stderr()
var buf strings.Builder
cmd.Stdout = &buf
cmd.Stderr = &buf
if err := cmd.Run(); err != nil {
ctx.Verboseln("soong_env failed, forcing manifest regeneration")
os.Remove(envFile)
}
if buf.Len() > 0 {
ctx.Verboseln(buf.String())
}
} else {
ctx.Verboseln("Missing soong_env tool, forcing manifest regeneration")
os.Remove(envFile)
@ -100,22 +108,18 @@ func runSoong(ctx Context, config Config) {
ctx.BeginTrace(name)
defer ctx.EndTrace()
fifo := filepath.Join(config.OutDir(), ".ninja_fifo")
status.NinjaReader(ctx, ctx.Status.StartTool(), fifo)
cmd := Command(ctx, config, "soong "+name,
config.PrebuiltBuildTool("ninja"),
"-d", "keepdepfile",
"-w", "dupbuild=err",
"-j", strconv.Itoa(config.Parallel()),
fmt.Sprintf("--frontend=cat <&3 >%s", fifo),
"-f", filepath.Join(config.SoongOutDir(), file))
if config.IsVerbose() {
cmd.Args = append(cmd.Args, "-v")
}
cmd.Sandbox = soongSandbox
cmd.Stdin = ctx.Stdin()
cmd.Stdout = ctx.Stdout()
cmd.Stderr = ctx.Stderr()
defer ctx.ImportNinjaLog(filepath.Join(config.OutDir(), ".ninja_log"), time.Now())
cmd.RunOrFatal()
cmd.RunAndPrintOrFatal()
}
ninja("minibootstrap", ".minibootstrap/build.ninja")

View File

@ -15,13 +15,9 @@
package build
import (
"bytes"
"io"
"os"
"path/filepath"
"strings"
"syscall"
"unsafe"
)
func absPath(ctx Context, p string) string {
@ -117,81 +113,3 @@ func decodeKeyValue(str string) (string, string, bool) {
}
return str[:idx], str[idx+1:], true
}
func isTerminal(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
}
return false
}
func termWidth(w io.Writer) (int, bool) {
if f, ok := w.(*os.File); ok {
var winsize struct {
ws_row, ws_column uint16
ws_xpixel, ws_ypixel uint16
}
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, f.Fd(),
syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&winsize)),
0, 0, 0)
return int(winsize.ws_column), err == 0
}
return 0, false
}
// stripAnsiEscapes strips ANSI control codes from a byte array in place.
func stripAnsiEscapes(input []byte) []byte {
// read represents the remaining part of input that needs to be processed.
read := input
// write represents where we should be writing in input.
// It will share the same backing store as input so that we make our modifications
// in place.
write := input
// advance will copy count bytes from read to write and advance those slices
advance := func(write, read []byte, count int) ([]byte, []byte) {
copy(write, read[:count])
return write[count:], read[count:]
}
for {
// Find the next escape sequence
i := bytes.IndexByte(read, 0x1b)
// If it isn't found, or if there isn't room for <ESC>[, finish
if i == -1 || i+1 >= len(read) {
copy(write, read)
break
}
// Not a CSI code, continue searching
if read[i+1] != '[' {
write, read = advance(write, read, i+1)
continue
}
// Found a CSI code, advance up to the <ESC>
write, read = advance(write, read, i)
// Find the end of the CSI code
i = bytes.IndexFunc(read, func(r rune) bool {
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
})
if i == -1 {
// We didn't find the end of the code, just remove the rest
i = len(read) - 1
}
// Strip off the end marker too
i = i + 1
// Skip the reader forward and reduce final length by that amount
read = read[i:]
input = input[:len(input)-i]
}
return input
}

View File

@ -49,48 +49,3 @@ func TestEnsureEmptyDirs(t *testing.T) {
ensureEmptyDirectoriesExist(ctx, filepath.Join(tmpDir, "a"))
}
func TestStripAnsiEscapes(t *testing.T) {
testcases := []struct {
input string
output string
}{
{
"",
"",
},
{
"This is a test",
"This is a test",
},
{
"interrupted: \x1b[12",
"interrupted: ",
},
{
"other \x1bescape \x1b",
"other \x1bescape \x1b",
},
{ // from pretty-error macro
"\x1b[1mart/Android.mk: \x1b[31merror:\x1b[0m\x1b[1m art: test error \x1b[0m",
"art/Android.mk: error: art: test error ",
},
{ // from envsetup.sh make wrapper
"\x1b[0;31m#### make failed to build some targets (2 seconds) ####\x1b[00m",
"#### make failed to build some targets (2 seconds) ####",
},
{ // from clang (via ninja testcase)
"\x1b[1maffixmgr.cxx:286:15: \x1b[0m\x1b[0;1;35mwarning: \x1b[0m\x1b[1musing the result... [-Wparentheses]\x1b[0m",
"affixmgr.cxx:286:15: warning: using the result... [-Wparentheses]",
},
}
for _, tc := range testcases {
got := string(stripAnsiEscapes([]byte(tc.input)))
if got != tc.output {
t.Errorf("output strings didn't match\n"+
"input: %#v\n"+
" want: %#v\n"+
" got: %#v", tc.input, tc.output, got)
}
}
}

42
ui/status/Android.bp Normal file
View File

@ -0,0 +1,42 @@
// 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.
bootstrap_go_package {
name: "soong-ui-status",
pkgPath: "android/soong/ui/status",
deps: [
"golang-protobuf-proto",
"soong-ui-logger",
"soong-ui-status-ninja_frontend",
],
srcs: [
"kati.go",
"log.go",
"ninja.go",
"status.go",
],
testSrcs: [
"kati_test.go",
"status_test.go",
],
}
bootstrap_go_package {
name: "soong-ui-status-ninja_frontend",
pkgPath: "android/soong/ui/status/ninja_frontend",
deps: ["golang-protobuf-proto"],
srcs: [
"ninja_frontend/frontend.pb.go",
],
}

138
ui/status/kati.go Normal file
View File

@ -0,0 +1,138 @@
// 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 status
import (
"bufio"
"fmt"
"io"
"regexp"
"strconv"
"strings"
)
var katiError = regexp.MustCompile(`^(\033\[1m)?[^ ]+:[0-9]+: (\033\[31m)?error:`)
var katiIncludeRe = regexp.MustCompile(`^(\[(\d+)/(\d+)] )?((including [^ ]+|initializing build system|finishing build rules|writing build rules) ...)$`)
var katiLogRe = regexp.MustCompile(`^\*kati\*: `)
var katiNinjaMissing = regexp.MustCompile("^[^ ]+ is missing, regenerating...$")
type katiOutputParser struct {
st ToolStatus
count int
total int
extra int
action *Action
buf strings.Builder
hasError bool
}
func (k *katiOutputParser) flushAction() {
if k.action == nil {
return
}
var err error
if k.hasError {
err = fmt.Errorf("makefile error")
}
k.st.FinishAction(ActionResult{
Action: k.action,
Output: k.buf.String(),
Error: err,
})
k.buf.Reset()
k.hasError = false
}
func (k *katiOutputParser) parseLine(line string) {
// Only put kati debug/stat lines in our verbose log
if katiLogRe.MatchString(line) {
k.st.Verbose(line)
return
}
if matches := katiIncludeRe.FindStringSubmatch(line); len(matches) > 0 {
k.flushAction()
k.count += 1
matches := katiIncludeRe.FindStringSubmatch(line)
if matches[2] != "" {
idx, err := strconv.Atoi(matches[2])
if err == nil && idx+k.extra != k.count {
k.extra = k.count - idx
k.st.SetTotalActions(k.total + k.extra)
}
} else {
k.extra += 1
k.st.SetTotalActions(k.total + k.extra)
}
if matches[3] != "" {
tot, err := strconv.Atoi(matches[3])
if err == nil && tot != k.total {
k.total = tot
k.st.SetTotalActions(k.total + k.extra)
}
}
k.action = &Action{
Description: matches[4],
}
k.st.StartAction(k.action)
} else if k.action != nil {
if katiError.MatchString(line) {
k.hasError = true
}
k.buf.WriteString(line)
k.buf.WriteString("\n")
} else {
// Before we've started executing actions from Kati
if line == "No need to regenerate ninja file" || katiNinjaMissing.MatchString(line) {
k.st.Status(line)
} else {
k.st.Print(line)
}
}
}
// KatiReader reads the output from Kati, and turns it into Actions and
// messages that are passed into the ToolStatus API.
func KatiReader(st ToolStatus, pipe io.ReadCloser) {
parser := &katiOutputParser{
st: st,
}
scanner := bufio.NewScanner(pipe)
for scanner.Scan() {
parser.parseLine(scanner.Text())
}
parser.flushAction()
if err := scanner.Err(); err != nil {
var buf strings.Builder
io.Copy(&buf, pipe)
st.Print(fmt.Sprintf("Error from kati parser: %s", err))
st.Print(buf.String())
}
st.Finish()
}

175
ui/status/kati_test.go Normal file
View File

@ -0,0 +1,175 @@
// 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 status
import (
"testing"
)
type lastOutput struct {
counterOutput
action *Action
result ActionResult
msgLevel MsgLevel
msg string
}
func (l *lastOutput) StartAction(a *Action, c Counts) {
l.action = a
l.counterOutput.StartAction(a, c)
}
func (l *lastOutput) FinishAction(r ActionResult, c Counts) {
l.result = r
l.counterOutput.FinishAction(r, c)
}
func (l *lastOutput) Message(level MsgLevel, msg string) {
l.msgLevel = level
l.msg = msg
}
func (l *lastOutput) Flush() {}
func TestKatiNormalCase(t *testing.T) {
status := &Status{}
output := &lastOutput{}
status.AddOutput(output)
parser := &katiOutputParser{
st: status.StartTool(),
}
msg := "*kati*: verbose msg"
parser.parseLine(msg)
output.Expect(t, Counts{})
if output.msgLevel != VerboseLvl {
t.Errorf("Expected verbose message, but got %d", output.msgLevel)
}
if output.msg != msg {
t.Errorf("unexpected message contents:\nwant: %q\n got: %q\n", msg, output.msg)
}
parser.parseLine("out/build-aosp_arm.ninja is missing, regenerating...")
output.Expect(t, Counts{})
parser.parseLine("[1/1] initializing build system ...")
output.Expect(t, Counts{
TotalActions: 1,
RunningActions: 1,
StartedActions: 1,
FinishedActions: 0,
})
parser.parseLine("[2/5] including out/soong/Android-aosp_arm.mk ...")
output.Expect(t, Counts{
TotalActions: 5,
RunningActions: 1,
StartedActions: 2,
FinishedActions: 1,
})
parser.parseLine("[3/5] including a ...")
msg = "a random message"
parser.parseLine(msg)
// Start the next line to flush the previous result
parser.parseLine("[4/5] finishing build rules ...")
msg += "\n"
if output.result.Output != msg {
t.Errorf("output for action did not match:\nwant: %q\n got: %q\n", msg, output.result.Output)
}
parser.parseLine("[5/5] writing build rules ...")
parser.parseLine("*kati*: verbose msg")
parser.flushAction()
if output.result.Output != "" {
t.Errorf("expected no output for last action, but got %q", output.result.Output)
}
output.Expect(t, Counts{
TotalActions: 5,
RunningActions: 0,
StartedActions: 5,
FinishedActions: 5,
})
}
func TestKatiExtraIncludes(t *testing.T) {
status := &Status{}
output := &lastOutput{}
status.AddOutput(output)
parser := &katiOutputParser{
st: status.StartTool(),
}
parser.parseLine("[1/1] initializing build system ...")
parser.parseLine("[2/5] including out/soong/Android-aosp_arm.mk ...")
output.Expect(t, Counts{
TotalActions: 5,
RunningActions: 1,
StartedActions: 2,
FinishedActions: 1,
})
parser.parseLine("including a ...")
output.Expect(t, Counts{
TotalActions: 6,
RunningActions: 1,
StartedActions: 3,
FinishedActions: 2,
})
parser.parseLine("including b ...")
output.Expect(t, Counts{
TotalActions: 7,
RunningActions: 1,
StartedActions: 4,
FinishedActions: 3,
})
parser.parseLine("[3/5] finishing build rules ...")
output.Expect(t, Counts{
TotalActions: 7,
RunningActions: 1,
StartedActions: 5,
FinishedActions: 4,
})
}
func TestKatiFailOnError(t *testing.T) {
status := &Status{}
output := &lastOutput{}
status.AddOutput(output)
parser := &katiOutputParser{
st: status.StartTool(),
}
parser.parseLine("[1/1] initializing build system ...")
parser.parseLine("[2/5] inclduing out/soong/Android-aosp_arm.mk ...")
parser.parseLine("build/make/tools/Android.mk:19: error: testing")
parser.flushAction()
if output.result.Error == nil {
t.Errorf("Expected the last action to be marked as an error")
}
}

136
ui/status/log.go Normal file
View File

@ -0,0 +1,136 @@
// 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 status
import (
"android/soong/ui/logger"
"compress/gzip"
"fmt"
"io"
"strings"
)
type verboseLog struct {
w io.WriteCloser
}
func NewVerboseLog(log logger.Logger, filename string) StatusOutput {
if !strings.HasSuffix(filename, ".gz") {
filename += ".gz"
}
f, err := logger.CreateFileWithRotation(filename, 5)
if err != nil {
log.Println("Failed to create verbose log file:", err)
return nil
}
w := gzip.NewWriter(f)
return &verboseLog{
w: w,
}
}
func (v *verboseLog) StartAction(action *Action, counts Counts) {}
func (v *verboseLog) FinishAction(result ActionResult, counts Counts) {
cmd := result.Command
if cmd == "" {
cmd = result.Description
}
fmt.Fprintf(v.w, "[%d/%d] %s\n", counts.FinishedActions, counts.TotalActions, cmd)
if result.Error != nil {
fmt.Fprintf(v.w, "FAILED: %s\n", strings.Join(result.Outputs, " "))
}
if result.Output != "" {
fmt.Fprintln(v.w, result.Output)
}
}
func (v *verboseLog) Flush() {
v.w.Close()
}
func (v *verboseLog) Message(level MsgLevel, message string) {
fmt.Fprintf(v.w, "%s%s\n", level.Prefix(), message)
}
type errorLog struct {
w io.WriteCloser
empty bool
}
func NewErrorLog(log logger.Logger, filename string) StatusOutput {
f, err := logger.CreateFileWithRotation(filename, 5)
if err != nil {
log.Println("Failed to create error log file:", err)
return nil
}
return &errorLog{
w: f,
empty: true,
}
}
func (e *errorLog) StartAction(action *Action, counts Counts) {}
func (e *errorLog) FinishAction(result ActionResult, counts Counts) {
if result.Error == nil {
return
}
cmd := result.Command
if cmd == "" {
cmd = result.Description
}
if !e.empty {
fmt.Fprintf(e.w, "\n\n")
}
e.empty = false
fmt.Fprintf(e.w, "FAILED: %s\n", result.Description)
if len(result.Outputs) > 0 {
fmt.Fprintf(e.w, "Outputs: %s\n", strings.Join(result.Outputs, " "))
}
fmt.Fprintf(e.w, "Error: %s\n", result.Error)
if result.Command != "" {
fmt.Fprintf(e.w, "Command: %s\n", result.Command)
}
fmt.Fprintf(e.w, "Output:\n%s\n", result.Output)
}
func (e *errorLog) Flush() {
e.w.Close()
}
func (e *errorLog) Message(level MsgLevel, message string) {
if level < ErrorLvl {
return
}
if !e.empty {
fmt.Fprintf(e.w, "\n\n")
}
e.empty = false
fmt.Fprintf(e.w, "error: %s\n", message)
}

153
ui/status/ninja.go Normal file
View File

@ -0,0 +1,153 @@
// 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 status
import (
"bufio"
"fmt"
"io"
"os"
"syscall"
"github.com/golang/protobuf/proto"
"android/soong/ui/logger"
"android/soong/ui/status/ninja_frontend"
)
// NinjaReader reads the protobuf frontend format from ninja and translates it
// into calls on the ToolStatus API.
func NinjaReader(ctx logger.Logger, status ToolStatus, fifo string) {
os.Remove(fifo)
err := syscall.Mkfifo(fifo, 0666)
if err != nil {
ctx.Fatalf("Failed to mkfifo(%q): %v", fifo, err)
}
go ninjaReader(ctx, status, fifo)
}
func ninjaReader(ctx logger.Logger, status ToolStatus, fifo string) {
defer os.Remove(fifo)
f, err := os.Open(fifo)
if err != nil {
ctx.Fatal("Failed to open fifo:", err)
}
defer f.Close()
r := bufio.NewReader(f)
running := map[uint32]*Action{}
for {
size, err := readVarInt(r)
if err != nil {
if err != io.EOF {
ctx.Println("Got error reading from ninja:", err)
}
return
}
buf := make([]byte, size)
_, err = io.ReadFull(r, buf)
if err != nil {
if err == io.EOF {
ctx.Printf("Missing message of size %d from ninja\n", size)
} else {
ctx.Fatal("Got error reading from ninja:", err)
}
return
}
msg := &ninja_frontend.Status{}
err = proto.Unmarshal(buf, msg)
if err != nil {
ctx.Printf("Error reading message from ninja: %v\n", err)
continue
}
// Ignore msg.BuildStarted
if msg.TotalEdges != nil {
status.SetTotalActions(int(msg.TotalEdges.GetTotalEdges()))
}
if msg.EdgeStarted != nil {
action := &Action{
Description: msg.EdgeStarted.GetDesc(),
Outputs: msg.EdgeStarted.Outputs,
Command: msg.EdgeStarted.GetCommand(),
}
status.StartAction(action)
running[msg.EdgeStarted.GetId()] = action
}
if msg.EdgeFinished != nil {
if started, ok := running[msg.EdgeFinished.GetId()]; ok {
delete(running, msg.EdgeFinished.GetId())
var err error
exitCode := int(msg.EdgeFinished.GetStatus())
if exitCode != 0 {
err = fmt.Errorf("exited with code: %d", exitCode)
}
status.FinishAction(ActionResult{
Action: started,
Output: msg.EdgeFinished.GetOutput(),
Error: err,
})
}
}
if msg.Message != nil {
message := "ninja: " + msg.Message.GetMessage()
switch msg.Message.GetLevel() {
case ninja_frontend.Status_Message_INFO:
status.Status(message)
case ninja_frontend.Status_Message_WARNING:
status.Print("warning: " + message)
case ninja_frontend.Status_Message_ERROR:
status.Error(message)
default:
status.Print(message)
}
}
if msg.BuildFinished != nil {
status.Finish()
}
}
}
func readVarInt(r *bufio.Reader) (int, error) {
ret := 0
shift := uint(0)
for {
b, err := r.ReadByte()
if err != nil {
return 0, err
}
ret += int(b&0x7f) << (shift * 7)
if b&0x80 == 0 {
break
}
shift += 1
if shift > 4 {
return 0, fmt.Errorf("Expected varint32 length-delimited message")
}
}
return ret, nil
}

View File

@ -0,0 +1,3 @@
This comes from https://android.googlesource.com/platform/external/ninja/+/master/src/frontend.proto
The only difference is the specification of a go_package. To regenerate frontend.pb.go, run regen.sh.

View File

@ -0,0 +1,510 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: frontend.proto
package ninja_frontend
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type Status_Message_Level int32
const (
Status_Message_INFO Status_Message_Level = 0
Status_Message_WARNING Status_Message_Level = 1
Status_Message_ERROR Status_Message_Level = 2
)
var Status_Message_Level_name = map[int32]string{
0: "INFO",
1: "WARNING",
2: "ERROR",
}
var Status_Message_Level_value = map[string]int32{
"INFO": 0,
"WARNING": 1,
"ERROR": 2,
}
func (x Status_Message_Level) Enum() *Status_Message_Level {
p := new(Status_Message_Level)
*p = x
return p
}
func (x Status_Message_Level) String() string {
return proto.EnumName(Status_Message_Level_name, int32(x))
}
func (x *Status_Message_Level) UnmarshalJSON(data []byte) error {
value, err := proto.UnmarshalJSONEnum(Status_Message_Level_value, data, "Status_Message_Level")
if err != nil {
return err
}
*x = Status_Message_Level(value)
return nil
}
func (Status_Message_Level) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 5, 0}
}
type Status struct {
TotalEdges *Status_TotalEdges `protobuf:"bytes,1,opt,name=total_edges,json=totalEdges" json:"total_edges,omitempty"`
BuildStarted *Status_BuildStarted `protobuf:"bytes,2,opt,name=build_started,json=buildStarted" json:"build_started,omitempty"`
BuildFinished *Status_BuildFinished `protobuf:"bytes,3,opt,name=build_finished,json=buildFinished" json:"build_finished,omitempty"`
EdgeStarted *Status_EdgeStarted `protobuf:"bytes,4,opt,name=edge_started,json=edgeStarted" json:"edge_started,omitempty"`
EdgeFinished *Status_EdgeFinished `protobuf:"bytes,5,opt,name=edge_finished,json=edgeFinished" json:"edge_finished,omitempty"`
Message *Status_Message `protobuf:"bytes,6,opt,name=message" json:"message,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Status) Reset() { *m = Status{} }
func (m *Status) String() string { return proto.CompactTextString(m) }
func (*Status) ProtoMessage() {}
func (*Status) Descriptor() ([]byte, []int) {
return fileDescriptor_frontend_5a49d9b15a642005, []int{0}
}
func (m *Status) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Status.Unmarshal(m, b)
}
func (m *Status) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Status.Marshal(b, m, deterministic)
}
func (dst *Status) XXX_Merge(src proto.Message) {
xxx_messageInfo_Status.Merge(dst, src)
}
func (m *Status) XXX_Size() int {
return xxx_messageInfo_Status.Size(m)
}
func (m *Status) XXX_DiscardUnknown() {
xxx_messageInfo_Status.DiscardUnknown(m)
}
var xxx_messageInfo_Status proto.InternalMessageInfo
func (m *Status) GetTotalEdges() *Status_TotalEdges {
if m != nil {
return m.TotalEdges
}
return nil
}
func (m *Status) GetBuildStarted() *Status_BuildStarted {
if m != nil {
return m.BuildStarted
}
return nil
}
func (m *Status) GetBuildFinished() *Status_BuildFinished {
if m != nil {
return m.BuildFinished
}
return nil
}
func (m *Status) GetEdgeStarted() *Status_EdgeStarted {
if m != nil {
return m.EdgeStarted
}
return nil
}
func (m *Status) GetEdgeFinished() *Status_EdgeFinished {
if m != nil {
return m.EdgeFinished
}
return nil
}
func (m *Status) GetMessage() *Status_Message {
if m != nil {
return m.Message
}
return nil
}
type Status_TotalEdges struct {
// New value for total edges in the build.
TotalEdges *uint32 `protobuf:"varint,1,opt,name=total_edges,json=totalEdges" json:"total_edges,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Status_TotalEdges) Reset() { *m = Status_TotalEdges{} }
func (m *Status_TotalEdges) String() string { return proto.CompactTextString(m) }
func (*Status_TotalEdges) ProtoMessage() {}
func (*Status_TotalEdges) Descriptor() ([]byte, []int) {
return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 0}
}
func (m *Status_TotalEdges) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Status_TotalEdges.Unmarshal(m, b)
}
func (m *Status_TotalEdges) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Status_TotalEdges.Marshal(b, m, deterministic)
}
func (dst *Status_TotalEdges) XXX_Merge(src proto.Message) {
xxx_messageInfo_Status_TotalEdges.Merge(dst, src)
}
func (m *Status_TotalEdges) XXX_Size() int {
return xxx_messageInfo_Status_TotalEdges.Size(m)
}
func (m *Status_TotalEdges) XXX_DiscardUnknown() {
xxx_messageInfo_Status_TotalEdges.DiscardUnknown(m)
}
var xxx_messageInfo_Status_TotalEdges proto.InternalMessageInfo
func (m *Status_TotalEdges) GetTotalEdges() uint32 {
if m != nil && m.TotalEdges != nil {
return *m.TotalEdges
}
return 0
}
type Status_BuildStarted struct {
// Number of jobs Ninja will run in parallel.
Parallelism *uint32 `protobuf:"varint,1,opt,name=parallelism" json:"parallelism,omitempty"`
// Verbose value passed to ninja.
Verbose *bool `protobuf:"varint,2,opt,name=verbose" json:"verbose,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Status_BuildStarted) Reset() { *m = Status_BuildStarted{} }
func (m *Status_BuildStarted) String() string { return proto.CompactTextString(m) }
func (*Status_BuildStarted) ProtoMessage() {}
func (*Status_BuildStarted) Descriptor() ([]byte, []int) {
return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 1}
}
func (m *Status_BuildStarted) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Status_BuildStarted.Unmarshal(m, b)
}
func (m *Status_BuildStarted) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Status_BuildStarted.Marshal(b, m, deterministic)
}
func (dst *Status_BuildStarted) XXX_Merge(src proto.Message) {
xxx_messageInfo_Status_BuildStarted.Merge(dst, src)
}
func (m *Status_BuildStarted) XXX_Size() int {
return xxx_messageInfo_Status_BuildStarted.Size(m)
}
func (m *Status_BuildStarted) XXX_DiscardUnknown() {
xxx_messageInfo_Status_BuildStarted.DiscardUnknown(m)
}
var xxx_messageInfo_Status_BuildStarted proto.InternalMessageInfo
func (m *Status_BuildStarted) GetParallelism() uint32 {
if m != nil && m.Parallelism != nil {
return *m.Parallelism
}
return 0
}
func (m *Status_BuildStarted) GetVerbose() bool {
if m != nil && m.Verbose != nil {
return *m.Verbose
}
return false
}
type Status_BuildFinished struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Status_BuildFinished) Reset() { *m = Status_BuildFinished{} }
func (m *Status_BuildFinished) String() string { return proto.CompactTextString(m) }
func (*Status_BuildFinished) ProtoMessage() {}
func (*Status_BuildFinished) Descriptor() ([]byte, []int) {
return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 2}
}
func (m *Status_BuildFinished) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Status_BuildFinished.Unmarshal(m, b)
}
func (m *Status_BuildFinished) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Status_BuildFinished.Marshal(b, m, deterministic)
}
func (dst *Status_BuildFinished) XXX_Merge(src proto.Message) {
xxx_messageInfo_Status_BuildFinished.Merge(dst, src)
}
func (m *Status_BuildFinished) XXX_Size() int {
return xxx_messageInfo_Status_BuildFinished.Size(m)
}
func (m *Status_BuildFinished) XXX_DiscardUnknown() {
xxx_messageInfo_Status_BuildFinished.DiscardUnknown(m)
}
var xxx_messageInfo_Status_BuildFinished proto.InternalMessageInfo
type Status_EdgeStarted struct {
// Edge identification number, unique to a Ninja run.
Id *uint32 `protobuf:"varint,1,opt,name=id" json:"id,omitempty"`
// Edge start time in milliseconds since Ninja started.
StartTime *uint32 `protobuf:"varint,2,opt,name=start_time,json=startTime" json:"start_time,omitempty"`
// List of edge inputs.
Inputs []string `protobuf:"bytes,3,rep,name=inputs" json:"inputs,omitempty"`
// List of edge outputs.
Outputs []string `protobuf:"bytes,4,rep,name=outputs" json:"outputs,omitempty"`
// Description field from the edge.
Desc *string `protobuf:"bytes,5,opt,name=desc" json:"desc,omitempty"`
// Command field from the edge.
Command *string `protobuf:"bytes,6,opt,name=command" json:"command,omitempty"`
// Edge uses console.
Console *bool `protobuf:"varint,7,opt,name=console" json:"console,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Status_EdgeStarted) Reset() { *m = Status_EdgeStarted{} }
func (m *Status_EdgeStarted) String() string { return proto.CompactTextString(m) }
func (*Status_EdgeStarted) ProtoMessage() {}
func (*Status_EdgeStarted) Descriptor() ([]byte, []int) {
return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 3}
}
func (m *Status_EdgeStarted) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Status_EdgeStarted.Unmarshal(m, b)
}
func (m *Status_EdgeStarted) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Status_EdgeStarted.Marshal(b, m, deterministic)
}
func (dst *Status_EdgeStarted) XXX_Merge(src proto.Message) {
xxx_messageInfo_Status_EdgeStarted.Merge(dst, src)
}
func (m *Status_EdgeStarted) XXX_Size() int {
return xxx_messageInfo_Status_EdgeStarted.Size(m)
}
func (m *Status_EdgeStarted) XXX_DiscardUnknown() {
xxx_messageInfo_Status_EdgeStarted.DiscardUnknown(m)
}
var xxx_messageInfo_Status_EdgeStarted proto.InternalMessageInfo
func (m *Status_EdgeStarted) GetId() uint32 {
if m != nil && m.Id != nil {
return *m.Id
}
return 0
}
func (m *Status_EdgeStarted) GetStartTime() uint32 {
if m != nil && m.StartTime != nil {
return *m.StartTime
}
return 0
}
func (m *Status_EdgeStarted) GetInputs() []string {
if m != nil {
return m.Inputs
}
return nil
}
func (m *Status_EdgeStarted) GetOutputs() []string {
if m != nil {
return m.Outputs
}
return nil
}
func (m *Status_EdgeStarted) GetDesc() string {
if m != nil && m.Desc != nil {
return *m.Desc
}
return ""
}
func (m *Status_EdgeStarted) GetCommand() string {
if m != nil && m.Command != nil {
return *m.Command
}
return ""
}
func (m *Status_EdgeStarted) GetConsole() bool {
if m != nil && m.Console != nil {
return *m.Console
}
return false
}
type Status_EdgeFinished struct {
// Edge identification number, unique to a Ninja run.
Id *uint32 `protobuf:"varint,1,opt,name=id" json:"id,omitempty"`
// Edge end time in milliseconds since Ninja started.
EndTime *uint32 `protobuf:"varint,2,opt,name=end_time,json=endTime" json:"end_time,omitempty"`
// Exit status (0 for success).
Status *int32 `protobuf:"zigzag32,3,opt,name=status" json:"status,omitempty"`
// Edge output, may contain ANSI codes.
Output *string `protobuf:"bytes,4,opt,name=output" json:"output,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Status_EdgeFinished) Reset() { *m = Status_EdgeFinished{} }
func (m *Status_EdgeFinished) String() string { return proto.CompactTextString(m) }
func (*Status_EdgeFinished) ProtoMessage() {}
func (*Status_EdgeFinished) Descriptor() ([]byte, []int) {
return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 4}
}
func (m *Status_EdgeFinished) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Status_EdgeFinished.Unmarshal(m, b)
}
func (m *Status_EdgeFinished) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Status_EdgeFinished.Marshal(b, m, deterministic)
}
func (dst *Status_EdgeFinished) XXX_Merge(src proto.Message) {
xxx_messageInfo_Status_EdgeFinished.Merge(dst, src)
}
func (m *Status_EdgeFinished) XXX_Size() int {
return xxx_messageInfo_Status_EdgeFinished.Size(m)
}
func (m *Status_EdgeFinished) XXX_DiscardUnknown() {
xxx_messageInfo_Status_EdgeFinished.DiscardUnknown(m)
}
var xxx_messageInfo_Status_EdgeFinished proto.InternalMessageInfo
func (m *Status_EdgeFinished) GetId() uint32 {
if m != nil && m.Id != nil {
return *m.Id
}
return 0
}
func (m *Status_EdgeFinished) GetEndTime() uint32 {
if m != nil && m.EndTime != nil {
return *m.EndTime
}
return 0
}
func (m *Status_EdgeFinished) GetStatus() int32 {
if m != nil && m.Status != nil {
return *m.Status
}
return 0
}
func (m *Status_EdgeFinished) GetOutput() string {
if m != nil && m.Output != nil {
return *m.Output
}
return ""
}
type Status_Message struct {
// Message priority level (INFO, WARNING, or ERROR).
Level *Status_Message_Level `protobuf:"varint,1,opt,name=level,enum=ninja.Status_Message_Level,def=0" json:"level,omitempty"`
// Info/warning/error message from Ninja.
Message *string `protobuf:"bytes,2,opt,name=message" json:"message,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Status_Message) Reset() { *m = Status_Message{} }
func (m *Status_Message) String() string { return proto.CompactTextString(m) }
func (*Status_Message) ProtoMessage() {}
func (*Status_Message) Descriptor() ([]byte, []int) {
return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 5}
}
func (m *Status_Message) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Status_Message.Unmarshal(m, b)
}
func (m *Status_Message) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Status_Message.Marshal(b, m, deterministic)
}
func (dst *Status_Message) XXX_Merge(src proto.Message) {
xxx_messageInfo_Status_Message.Merge(dst, src)
}
func (m *Status_Message) XXX_Size() int {
return xxx_messageInfo_Status_Message.Size(m)
}
func (m *Status_Message) XXX_DiscardUnknown() {
xxx_messageInfo_Status_Message.DiscardUnknown(m)
}
var xxx_messageInfo_Status_Message proto.InternalMessageInfo
const Default_Status_Message_Level Status_Message_Level = Status_Message_INFO
func (m *Status_Message) GetLevel() Status_Message_Level {
if m != nil && m.Level != nil {
return *m.Level
}
return Default_Status_Message_Level
}
func (m *Status_Message) GetMessage() string {
if m != nil && m.Message != nil {
return *m.Message
}
return ""
}
func init() {
proto.RegisterType((*Status)(nil), "ninja.Status")
proto.RegisterType((*Status_TotalEdges)(nil), "ninja.Status.TotalEdges")
proto.RegisterType((*Status_BuildStarted)(nil), "ninja.Status.BuildStarted")
proto.RegisterType((*Status_BuildFinished)(nil), "ninja.Status.BuildFinished")
proto.RegisterType((*Status_EdgeStarted)(nil), "ninja.Status.EdgeStarted")
proto.RegisterType((*Status_EdgeFinished)(nil), "ninja.Status.EdgeFinished")
proto.RegisterType((*Status_Message)(nil), "ninja.Status.Message")
proto.RegisterEnum("ninja.Status_Message_Level", Status_Message_Level_name, Status_Message_Level_value)
}
func init() { proto.RegisterFile("frontend.proto", fileDescriptor_frontend_5a49d9b15a642005) }
var fileDescriptor_frontend_5a49d9b15a642005 = []byte{
// 496 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x53, 0xd1, 0x6e, 0xd3, 0x30,
0x14, 0xa5, 0x69, 0xd3, 0x34, 0x37, 0x6d, 0x28, 0x96, 0x40, 0x59, 0x10, 0xa2, 0xda, 0xd3, 0x78,
0x20, 0x48, 0xbc, 0x20, 0x10, 0x12, 0xa2, 0xd2, 0x06, 0x43, 0xd0, 0x49, 0xde, 0x24, 0x24, 0x5e,
0xaa, 0x74, 0xf6, 0x86, 0x51, 0xe2, 0x54, 0xb1, 0xbb, 0x5f, 0xe0, 0x7f, 0x78, 0xe0, 0xfb, 0x90,
0xaf, 0xed, 0x2c, 0x65, 0x7b, 0xcb, 0xf1, 0x3d, 0xe7, 0xde, 0x73, 0x8f, 0x1d, 0x48, 0xaf, 0xda,
0x46, 0x6a, 0x2e, 0x59, 0xb1, 0x6d, 0x1b, 0xdd, 0x90, 0x50, 0x0a, 0xf9, 0xab, 0x3c, 0xfc, 0x13,
0xc1, 0xf8, 0x5c, 0x97, 0x7a, 0xa7, 0xc8, 0x5b, 0x48, 0x74, 0xa3, 0xcb, 0x6a, 0xcd, 0xd9, 0x35,
0x57, 0xd9, 0x60, 0x31, 0x38, 0x4a, 0x5e, 0x67, 0x05, 0xf2, 0x0a, 0xcb, 0x29, 0x2e, 0x0c, 0xe1,
0xd8, 0xd4, 0x29, 0xe8, 0xee, 0x9b, 0x7c, 0x80, 0xd9, 0x66, 0x27, 0x2a, 0xb6, 0x56, 0xba, 0x6c,
0x35, 0x67, 0x59, 0x80, 0xe2, 0x7c, 0x5f, 0xbc, 0x34, 0x94, 0x73, 0xcb, 0xa0, 0xd3, 0x4d, 0x0f,
0x91, 0x25, 0xa4, 0xb6, 0xc1, 0x95, 0x90, 0x42, 0xfd, 0xe4, 0x2c, 0x1b, 0x62, 0x87, 0xa7, 0xf7,
0x74, 0x38, 0x71, 0x14, 0x6a, 0x67, 0x7a, 0x48, 0xde, 0xc3, 0xd4, 0x38, 0xef, 0x3c, 0x8c, 0xb0,
0xc3, 0xc1, 0x7e, 0x07, 0xe3, 0xd7, 0x5b, 0x48, 0xf8, 0x2d, 0x30, 0x2b, 0xa0, 0xba, 0x33, 0x10,
0xde, 0xb7, 0x82, 0x91, 0x77, 0xf3, 0x71, 0x5c, 0x37, 0xfe, 0x15, 0x44, 0x35, 0x57, 0xaa, 0xbc,
0xe6, 0xd9, 0x18, 0xa5, 0x8f, 0xf7, 0xa5, 0xdf, 0x6c, 0x91, 0x7a, 0x56, 0xfe, 0x12, 0xe0, 0x36,
0x4e, 0xf2, 0xfc, 0x6e, 0xfa, 0xb3, 0x7e, 0xc6, 0xf9, 0x17, 0x98, 0xf6, 0x03, 0x24, 0x0b, 0x48,
0xb6, 0x65, 0x5b, 0x56, 0x15, 0xaf, 0x84, 0xaa, 0x9d, 0xa0, 0x7f, 0x44, 0x32, 0x88, 0x6e, 0x78,
0xbb, 0x69, 0x14, 0xc7, 0xfb, 0x98, 0x50, 0x0f, 0xf3, 0x87, 0x30, 0xdb, 0x8b, 0x32, 0xff, 0x3b,
0x80, 0xa4, 0x17, 0x0d, 0x49, 0x21, 0x10, 0xcc, 0xf5, 0x0c, 0x04, 0x23, 0xcf, 0x00, 0x30, 0xd6,
0xb5, 0x16, 0xb5, 0xed, 0x36, 0xa3, 0x31, 0x9e, 0x5c, 0x88, 0x9a, 0x93, 0x27, 0x30, 0x16, 0x72,
0xbb, 0xd3, 0x2a, 0x1b, 0x2e, 0x86, 0x47, 0x31, 0x75, 0xc8, 0x38, 0x68, 0x76, 0x1a, 0x0b, 0x23,
0x2c, 0x78, 0x48, 0x08, 0x8c, 0x18, 0x57, 0x97, 0x98, 0x72, 0x4c, 0xf1, 0xdb, 0xb0, 0x2f, 0x9b,
0xba, 0x2e, 0x25, 0xc3, 0x04, 0x63, 0xea, 0xa1, 0xad, 0x48, 0xd5, 0x54, 0x3c, 0x8b, 0xec, 0x26,
0x0e, 0xe6, 0x02, 0xa6, 0xfd, 0x3b, 0xb9, 0x63, 0xfc, 0x00, 0x26, 0x5c, 0xb2, 0xbe, 0xed, 0x88,
0x4b, 0xe6, 0x4d, 0x2b, 0xbc, 0x1a, 0x7c, 0x6b, 0x8f, 0xa8, 0x43, 0xe6, 0xdc, 0xba, 0xc4, 0x17,
0x14, 0x53, 0x87, 0xf2, 0xdf, 0x03, 0x88, 0xdc, 0x25, 0x92, 0x37, 0x10, 0x56, 0xfc, 0x86, 0x57,
0x38, 0x29, 0xfd, 0xff, 0x99, 0x3a, 0x56, 0xf1, 0xd5, 0x50, 0xde, 0x8d, 0x4e, 0x57, 0x27, 0x67,
0xd4, 0xf2, 0xcd, 0x26, 0xfe, 0x95, 0x04, 0x76, 0x47, 0x07, 0x0f, 0x5f, 0x40, 0x88, 0x7c, 0x32,
0x01, 0x54, 0xcc, 0x1f, 0x90, 0x04, 0xa2, 0xef, 0x1f, 0xe9, 0xea, 0x74, 0xf5, 0x69, 0x3e, 0x20,
0x31, 0x84, 0xc7, 0x94, 0x9e, 0xd1, 0x79, 0xb0, 0x24, 0x9f, 0x87, 0x3f, 0x52, 0x9c, 0xb8, 0xf6,
0x7f, 0xf5, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x2e, 0x8c, 0xef, 0xcb, 0xe0, 0x03, 0x00, 0x00,
}

View File

@ -0,0 +1,84 @@
// Copyright 2017 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.
syntax = "proto2";
option optimize_for = LITE_RUNTIME;
package ninja;
option go_package = "ninja_frontend";
message Status {
message TotalEdges {
// New value for total edges in the build.
optional uint32 total_edges = 1;
}
message BuildStarted {
// Number of jobs Ninja will run in parallel.
optional uint32 parallelism = 1;
// Verbose value passed to ninja.
optional bool verbose = 2;
}
message BuildFinished {
}
message EdgeStarted {
// Edge identification number, unique to a Ninja run.
optional uint32 id = 1;
// Edge start time in milliseconds since Ninja started.
optional uint32 start_time = 2;
// List of edge inputs.
repeated string inputs = 3;
// List of edge outputs.
repeated string outputs = 4;
// Description field from the edge.
optional string desc = 5;
// Command field from the edge.
optional string command = 6;
// Edge uses console.
optional bool console = 7;
}
message EdgeFinished {
// Edge identification number, unique to a Ninja run.
optional uint32 id = 1;
// Edge end time in milliseconds since Ninja started.
optional uint32 end_time = 2;
// Exit status (0 for success).
optional sint32 status = 3;
// Edge output, may contain ANSI codes.
optional string output = 4;
}
message Message {
enum Level {
INFO = 0;
WARNING = 1;
ERROR = 2;
}
// Message priority level (INFO, WARNING, or ERROR).
optional Level level = 1 [default = INFO];
// Info/warning/error message from Ninja.
optional string message = 2;
}
optional TotalEdges total_edges = 1;
optional BuildStarted build_started = 2;
optional BuildFinished build_finished = 3;
optional EdgeStarted edge_started = 4;
optional EdgeFinished edge_finished = 5;
optional Message message = 6;
}

View File

@ -0,0 +1,3 @@
#!/bin/bash
aprotoc --go_out=paths=source_relative:. frontend.proto

340
ui/status/status.go Normal file
View File

@ -0,0 +1,340 @@
// 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 status tracks actions run by various tools, combining the counts
// (total actions, currently running, started, finished), and giving that to
// multiple outputs.
package status
import (
"sync"
)
// Action describes an action taken (or as Ninja calls them, Edges).
type Action struct {
// Description is a shorter, more readable form of the command, meant
// for users. It's optional, but one of either Description or Command
// should be set.
Description string
// Outputs is the (optional) list of outputs. Usually these are files,
// but they can be any string.
Outputs []string
// Command is the actual command line executed to perform the action.
// It's optional, but one of either Description or Command should be
// set.
Command string
}
// ActionResult describes the result of running an Action.
type ActionResult struct {
// Action is a pointer to the original Action struct.
*Action
// Output is the output produced by the command (usually stdout&stderr
// for Actions that run commands)
Output string
// Error is nil if the Action succeeded, or set to an error if it
// failed.
Error error
}
// Counts describes the number of actions in each state
type Counts struct {
// TotalActions is the total number of expected changes. This can
// generally change up or down during a build, but it should never go
// below the number of StartedActions
TotalActions int
// RunningActions are the number of actions that are currently running
// -- the number that have called StartAction, but not FinishAction.
RunningActions int
// StartedActions are the number of actions that have been started with
// StartAction.
StartedActions int
// FinishedActions are the number of actions that have been finished
// with FinishAction.
FinishedActions int
}
// ToolStatus is the interface used by tools to report on their Actions, and to
// present other information through a set of messaging functions.
type ToolStatus interface {
// SetTotalActions sets the expected total number of actions that will
// be started by this tool.
//
// This call be will ignored if it sets a number that is less than the
// current number of started actions.
SetTotalActions(total int)
// StartAction specifies that the associated action has been started by
// the tool.
//
// A specific *Action should not be specified to StartAction more than
// once, even if the previous action has already been finished, and the
// contents rewritten.
//
// Do not re-use *Actions between different ToolStatus interfaces
// either.
StartAction(action *Action)
// FinishAction specifies the result of a particular Action.
//
// The *Action embedded in the ActionResult structure must have already
// been passed to StartAction (on this interface).
//
// Do not call FinishAction twice for the same *Action.
FinishAction(result ActionResult)
// Verbose takes a non-important message that is never printed to the
// screen, but is in the verbose build log, etc
Verbose(msg string)
// Status takes a less important message that may be printed to the
// screen, but overwritten by another status message. The full message
// will still appear in the verbose build log.
Status(msg string)
// Print takes an message and displays it to the screen and other
// output logs, etc.
Print(msg string)
// Error is similar to Print, but treats it similarly to a failed
// action, showing it in the error logs, etc.
Error(msg string)
// Finish marks the end of all Actions being run by this tool.
//
// SetTotalEdges, StartAction, and FinishAction should not be called
// after Finish.
Finish()
}
// MsgLevel specifies the importance of a particular log message. See the
// descriptions in ToolStatus: Verbose, Status, Print, Error.
type MsgLevel int
const (
VerboseLvl MsgLevel = iota
StatusLvl
PrintLvl
ErrorLvl
)
func (l MsgLevel) Prefix() string {
switch l {
case VerboseLvl:
return "verbose: "
case StatusLvl:
return "status: "
case PrintLvl:
return ""
case ErrorLvl:
return "error: "
default:
panic("Unknown message level")
}
}
// StatusOutput is the interface used to get status information as a Status
// output.
//
// All of the functions here are guaranteed to be called by Status while
// holding it's internal lock, so it's safe to assume a single caller at any
// time, and that the ordering of calls will be correct. It is not safe to call
// back into the Status, or one of its ToolStatus interfaces.
type StatusOutput interface {
// StartAction will be called once every time ToolStatus.StartAction is
// called. counts will include the current counters across all
// ToolStatus instances, including ones that have been finished.
StartAction(action *Action, counts Counts)
// FinishAction will be called once every time ToolStatus.FinishAction
// is called. counts will include the current counters across all
// ToolStatus instances, including ones that have been finished.
FinishAction(result ActionResult, counts Counts)
// Message is the equivalent of ToolStatus.Verbose/Status/Print/Error,
// but the level is specified as an argument.
Message(level MsgLevel, msg string)
// Flush is called when your outputs should be flushed / closed. No
// output is expected after this call.
Flush()
}
// Status is the multiplexer / accumulator between ToolStatus instances (via
// StartTool) and StatusOutputs (via AddOutput). There's generally one of these
// per build process (though tools like multiproduct_kati may have multiple
// independent versions).
type Status struct {
counts Counts
outputs []StatusOutput
// Protects counts and outputs, and allows each output to
// expect only a single caller at a time.
lock sync.Mutex
}
// AddOutput attaches an output to this object. It's generally expected that an
// output is attached to a single Status instance.
func (s *Status) AddOutput(output StatusOutput) {
if output == nil {
return
}
s.lock.Lock()
defer s.lock.Unlock()
s.outputs = append(s.outputs, output)
}
// StartTool returns a new ToolStatus instance to report the status of a tool.
func (s *Status) StartTool() ToolStatus {
return &toolStatus{
status: s,
}
}
// Finish will call Flush on all the outputs, generally flushing or closing all
// of their outputs. Do not call any other functions on this instance or any
// associated ToolStatus instances after this has been called.
func (s *Status) Finish() {
s.lock.Lock()
defer s.lock.Unlock()
for _, o := range s.outputs {
o.Flush()
}
}
func (s *Status) updateTotalActions(diff int) {
s.lock.Lock()
defer s.lock.Unlock()
s.counts.TotalActions += diff
}
func (s *Status) startAction(action *Action) {
s.lock.Lock()
defer s.lock.Unlock()
s.counts.RunningActions += 1
s.counts.StartedActions += 1
for _, o := range s.outputs {
o.StartAction(action, s.counts)
}
}
func (s *Status) finishAction(result ActionResult) {
s.lock.Lock()
defer s.lock.Unlock()
s.counts.RunningActions -= 1
s.counts.FinishedActions += 1
for _, o := range s.outputs {
o.FinishAction(result, s.counts)
}
}
func (s *Status) message(level MsgLevel, msg string) {
s.lock.Lock()
defer s.lock.Unlock()
for _, o := range s.outputs {
o.Message(level, msg)
}
}
type toolStatus struct {
status *Status
counts Counts
// Protects counts
lock sync.Mutex
}
var _ ToolStatus = (*toolStatus)(nil)
func (d *toolStatus) SetTotalActions(total int) {
diff := 0
d.lock.Lock()
if total >= d.counts.StartedActions && total != d.counts.TotalActions {
diff = total - d.counts.TotalActions
d.counts.TotalActions = total
}
d.lock.Unlock()
if diff != 0 {
d.status.updateTotalActions(diff)
}
}
func (d *toolStatus) StartAction(action *Action) {
totalDiff := 0
d.lock.Lock()
d.counts.RunningActions += 1
d.counts.StartedActions += 1
if d.counts.StartedActions > d.counts.TotalActions {
totalDiff = d.counts.StartedActions - d.counts.TotalActions
d.counts.TotalActions = d.counts.StartedActions
}
d.lock.Unlock()
if totalDiff != 0 {
d.status.updateTotalActions(totalDiff)
}
d.status.startAction(action)
}
func (d *toolStatus) FinishAction(result ActionResult) {
d.lock.Lock()
d.counts.RunningActions -= 1
d.counts.FinishedActions += 1
d.lock.Unlock()
d.status.finishAction(result)
}
func (d *toolStatus) Verbose(msg string) {
d.status.message(VerboseLvl, msg)
}
func (d *toolStatus) Status(msg string) {
d.status.message(StatusLvl, msg)
}
func (d *toolStatus) Print(msg string) {
d.status.message(PrintLvl, msg)
}
func (d *toolStatus) Error(msg string) {
d.status.message(ErrorLvl, msg)
}
func (d *toolStatus) Finish() {
d.lock.Lock()
defer d.lock.Unlock()
if d.counts.TotalActions != d.counts.StartedActions {
d.status.updateTotalActions(d.counts.StartedActions - d.counts.TotalActions)
}
// TODO: update status to correct running/finished edges?
d.counts.RunningActions = 0
d.counts.TotalActions = d.counts.StartedActions
}

166
ui/status/status_test.go Normal file
View File

@ -0,0 +1,166 @@
// 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 status
import "testing"
type counterOutput Counts
func (c *counterOutput) StartAction(action *Action, counts Counts) {
*c = counterOutput(counts)
}
func (c *counterOutput) FinishAction(result ActionResult, counts Counts) {
*c = counterOutput(counts)
}
func (c counterOutput) Message(level MsgLevel, msg string) {}
func (c counterOutput) Flush() {}
func (c counterOutput) Expect(t *testing.T, counts Counts) {
if Counts(c) == counts {
return
}
t.Helper()
if c.TotalActions != counts.TotalActions {
t.Errorf("Expected %d total edges, but got %d", counts.TotalActions, c.TotalActions)
}
if c.RunningActions != counts.RunningActions {
t.Errorf("Expected %d running edges, but got %d", counts.RunningActions, c.RunningActions)
}
if c.StartedActions != counts.StartedActions {
t.Errorf("Expected %d started edges, but got %d", counts.StartedActions, c.StartedActions)
}
if c.FinishedActions != counts.FinishedActions {
t.Errorf("Expected %d finished edges, but got %d", counts.FinishedActions, c.FinishedActions)
}
}
func TestBasicUse(t *testing.T) {
status := &Status{}
counts := &counterOutput{}
status.AddOutput(counts)
s := status.StartTool()
s.SetTotalActions(2)
a := &Action{}
s.StartAction(a)
counts.Expect(t, Counts{
TotalActions: 2,
RunningActions: 1,
StartedActions: 1,
FinishedActions: 0,
})
s.FinishAction(ActionResult{Action: a})
counts.Expect(t, Counts{
TotalActions: 2,
RunningActions: 0,
StartedActions: 1,
FinishedActions: 1,
})
a = &Action{}
s.StartAction(a)
counts.Expect(t, Counts{
TotalActions: 2,
RunningActions: 1,
StartedActions: 2,
FinishedActions: 1,
})
s.FinishAction(ActionResult{Action: a})
counts.Expect(t, Counts{
TotalActions: 2,
RunningActions: 0,
StartedActions: 2,
FinishedActions: 2,
})
}
// For when a tool claims to have 2 actions, but finishes after one.
func TestFinishEarly(t *testing.T) {
status := &Status{}
counts := &counterOutput{}
status.AddOutput(counts)
s := status.StartTool()
s.SetTotalActions(2)
a := &Action{}
s.StartAction(a)
s.FinishAction(ActionResult{Action: a})
s.Finish()
s = status.StartTool()
s.SetTotalActions(2)
a = &Action{}
s.StartAction(a)
counts.Expect(t, Counts{
TotalActions: 3,
RunningActions: 1,
StartedActions: 2,
FinishedActions: 1,
})
}
// For when a tool claims to have 1 action, but starts two.
func TestExtraActions(t *testing.T) {
status := &Status{}
counts := &counterOutput{}
status.AddOutput(counts)
s := status.StartTool()
s.SetTotalActions(1)
s.StartAction(&Action{})
s.StartAction(&Action{})
counts.Expect(t, Counts{
TotalActions: 2,
RunningActions: 2,
StartedActions: 2,
FinishedActions: 0,
})
}
// When a tool calls Finish() with a running Action
func TestRunningWhenFinished(t *testing.T) {
status := &Status{}
counts := &counterOutput{}
status.AddOutput(counts)
s := status.StartTool()
s.SetTotalActions(1)
s.StartAction(&Action{})
s.Finish()
s = status.StartTool()
s.SetTotalActions(1)
s.StartAction(&Action{})
counts.Expect(t, Counts{
TotalActions: 2,
RunningActions: 2,
StartedActions: 2,
FinishedActions: 0,
})
}

37
ui/terminal/Android.bp Normal file
View File

@ -0,0 +1,37 @@
// 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.
bootstrap_go_package {
name: "soong-ui-terminal",
pkgPath: "android/soong/ui/terminal",
deps: ["soong-ui-status"],
srcs: [
"status.go",
"writer.go",
"util.go",
],
testSrcs: [
"util_test.go",
],
darwin: {
srcs: [
"util_darwin.go",
],
},
linux: {
srcs: [
"util_linux.go",
],
},
}

142
ui/terminal/status.go Normal file
View File

@ -0,0 +1,142 @@
// 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 (
"fmt"
"strings"
"time"
"android/soong/ui/status"
)
type statusOutput struct {
writer Writer
format string
start time.Time
}
// 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) status.StatusOutput {
return &statusOutput{
writer: w,
format: statusFormat,
start: time.Now(),
}
}
func (s *statusOutput) Message(level status.MsgLevel, message string) {
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 {
hasCommand := ""
if result.Command != "" {
hasCommand = "\n"
}
s.writer.StatusAndMessage(progress, fmt.Sprintf("FAILED: %s\n%s%s%s",
strings.Join(result.Outputs, " "), result.Command, hasCommand, result.Output))
} else if result.Output != "" {
s.writer.StatusAndMessage(progress, result.Output)
} 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 buf.String()
}

101
ui/terminal/util.go Normal file
View File

@ -0,0 +1,101 @@
// Copyright 2017 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"
"io"
"os"
"syscall"
"unsafe"
)
func isTerminal(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
}
return false
}
func termWidth(w io.Writer) (int, bool) {
if f, ok := w.(*os.File); ok {
var winsize struct {
ws_row, ws_column uint16
ws_xpixel, ws_ypixel uint16
}
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, f.Fd(),
syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&winsize)),
0, 0, 0)
return int(winsize.ws_column), err == 0
}
return 0, false
}
// stripAnsiEscapes strips ANSI control codes from a byte array in place.
func stripAnsiEscapes(input []byte) []byte {
// read represents the remaining part of input that needs to be processed.
read := input
// write represents where we should be writing in input.
// It will share the same backing store as input so that we make our modifications
// in place.
write := input
// advance will copy count bytes from read to write and advance those slices
advance := func(write, read []byte, count int) ([]byte, []byte) {
copy(write, read[:count])
return write[count:], read[count:]
}
for {
// Find the next escape sequence
i := bytes.IndexByte(read, 0x1b)
// If it isn't found, or if there isn't room for <ESC>[, finish
if i == -1 || i+1 >= len(read) {
copy(write, read)
break
}
// Not a CSI code, continue searching
if read[i+1] != '[' {
write, read = advance(write, read, i+1)
continue
}
// Found a CSI code, advance up to the <ESC>
write, read = advance(write, read, i)
// Find the end of the CSI code
i = bytes.IndexFunc(read, func(r rune) bool {
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
})
if i == -1 {
// We didn't find the end of the code, just remove the rest
i = len(read) - 1
}
// Strip off the end marker too
i = i + 1
// Skip the reader forward and reduce final length by that amount
read = read[i:]
input = input[:len(input)-i]
}
return input
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package build
package terminal
import (
"syscall"

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package build
package terminal
import (
"syscall"

64
ui/terminal/util_test.go Normal file
View File

@ -0,0 +1,64 @@
// Copyright 2017 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 (
"testing"
)
func TestStripAnsiEscapes(t *testing.T) {
testcases := []struct {
input string
output string
}{
{
"",
"",
},
{
"This is a test",
"This is a test",
},
{
"interrupted: \x1b[12",
"interrupted: ",
},
{
"other \x1bescape \x1b",
"other \x1bescape \x1b",
},
{ // from pretty-error macro
"\x1b[1mart/Android.mk: \x1b[31merror:\x1b[0m\x1b[1m art: test error \x1b[0m",
"art/Android.mk: error: art: test error ",
},
{ // from envsetup.sh make wrapper
"\x1b[0;31m#### make failed to build some targets (2 seconds) ####\x1b[00m",
"#### make failed to build some targets (2 seconds) ####",
},
{ // from clang (via ninja testcase)
"\x1b[1maffixmgr.cxx:286:15: \x1b[0m\x1b[0;1;35mwarning: \x1b[0m\x1b[1musing the result... [-Wparentheses]\x1b[0m",
"affixmgr.cxx:286:15: warning: using the result... [-Wparentheses]",
},
}
for _, tc := range testcases {
got := string(stripAnsiEscapes([]byte(tc.input)))
if got != tc.output {
t.Errorf("output strings didn't match\n"+
"input: %#v\n"+
" want: %#v\n"+
" got: %#v", tc.input, tc.output, got)
}
}
}

229
ui/terminal/writer.go Normal file
View File

@ -0,0 +1,229 @@
// 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.stripEscapes = !isTerminal(stdio.Stderr())
w.smartTerminal = isTerminal(stdio.Stdout()) && !w.stripEscapes
}
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.Stderr(), str)
if len(str) == 0 || str[len(str)-1] != '\n' {
fmt.Fprint(w.stdio.Stderr(), "\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

@ -15,10 +15,13 @@
bootstrap_go_package {
name: "soong-ui-tracer",
pkgPath: "android/soong/ui/tracer",
deps: ["soong-ui-logger"],
deps: [
"soong-ui-logger",
"soong-ui-status",
],
srcs: [
"microfactory.go",
"ninja.go",
"status.go",
"tracer.go",
],
}

View File

@ -17,10 +17,48 @@ package tracer
import (
"bufio"
"os"
"sort"
"strconv"
"strings"
)
type eventEntry struct {
Name string
Begin uint64
End uint64
}
func (t *tracerImpl) importEvents(entries []*eventEntry) {
sort.Slice(entries, func(i, j int) bool {
return entries[i].Begin < entries[j].Begin
})
cpus := []uint64{}
for _, entry := range entries {
tid := -1
for cpu, endTime := range cpus {
if endTime <= entry.Begin {
tid = cpu
cpus[cpu] = entry.End
break
}
}
if tid == -1 {
tid = len(cpus)
cpus = append(cpus, entry.End)
}
t.writeEvent(&viewerEvent{
Name: entry.Name,
Phase: "X",
Time: entry.Begin,
Dur: entry.End - entry.Begin,
Pid: 1,
Tid: uint64(tid),
})
}
}
func (t *tracerImpl) ImportMicrofactoryLog(filename string) {
if _, err := os.Stat(filename); err != nil {
return

View File

@ -1,131 +0,0 @@
// Copyright 2016 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 tracer
import (
"bufio"
"os"
"sort"
"strconv"
"strings"
"time"
)
type eventEntry struct {
Name string
Begin uint64
End uint64
}
func (t *tracerImpl) importEvents(entries []*eventEntry) {
sort.Slice(entries, func(i, j int) bool {
return entries[i].Begin < entries[j].Begin
})
cpus := []uint64{}
for _, entry := range entries {
tid := -1
for cpu, endTime := range cpus {
if endTime <= entry.Begin {
tid = cpu
cpus[cpu] = entry.End
break
}
}
if tid == -1 {
tid = len(cpus)
cpus = append(cpus, entry.End)
}
t.writeEvent(&viewerEvent{
Name: entry.Name,
Phase: "X",
Time: entry.Begin,
Dur: entry.End - entry.Begin,
Pid: 1,
Tid: uint64(tid),
})
}
}
// ImportNinjaLog reads a .ninja_log file from ninja and writes the events out
// to the trace.
//
// startOffset is when the ninja process started, and is used to position the
// relative times from the ninja log into the trace. It's also used to skip
// reading the ninja log if nothing was run.
func (t *tracerImpl) ImportNinjaLog(thread Thread, filename string, startOffset time.Time) {
t.Begin("ninja log import", thread)
defer t.End(thread)
if stat, err := os.Stat(filename); err != nil {
t.log.Println("Missing ninja log:", err)
return
} else if stat.ModTime().Before(startOffset) {
t.log.Verboseln("Ninja log not modified, not importing any entries.")
return
}
f, err := os.Open(filename)
if err != nil {
t.log.Println("Error opening ninja log:", err)
return
}
defer f.Close()
s := bufio.NewScanner(f)
header := true
entries := []*eventEntry{}
prevEnd := 0
offset := uint64(startOffset.UnixNano()) / 1000
for s.Scan() {
if header {
hdr := s.Text()
if hdr != "# ninja log v5" {
t.log.Printf("Unknown ninja log header: %q", hdr)
return
}
header = false
continue
}
fields := strings.Split(s.Text(), "\t")
begin, err := strconv.Atoi(fields[0])
if err != nil {
t.log.Printf("Unable to parse ninja entry %q: %v", s.Text(), err)
return
}
end, err := strconv.Atoi(fields[1])
if err != nil {
t.log.Printf("Unable to parse ninja entry %q: %v", s.Text(), err)
return
}
if end < prevEnd {
entries = nil
}
prevEnd = end
entries = append(entries, &eventEntry{
Name: fields[3],
Begin: offset + uint64(begin)*1000,
End: offset + uint64(end)*1000,
})
}
if err := s.Err(); err != nil {
t.log.Println("Unable to parse ninja log:", err)
return
}
t.importEvents(entries)
}

87
ui/tracer/status.go Normal file
View File

@ -0,0 +1,87 @@
// 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 tracer
import (
"android/soong/ui/status"
"time"
)
func (t *tracerImpl) StatusTracer() status.StatusOutput {
return &statusOutput{
tracer: t,
running: map[*status.Action]actionStatus{},
}
}
type actionStatus struct {
cpu int
start time.Time
}
type statusOutput struct {
tracer *tracerImpl
cpus []bool
running map[*status.Action]actionStatus
}
func (s *statusOutput) StartAction(action *status.Action, counts status.Counts) {
cpu := -1
for i, busy := range s.cpus {
if !busy {
cpu = i
s.cpus[i] = true
break
}
}
if cpu == -1 {
cpu = len(s.cpus)
s.cpus = append(s.cpus, true)
}
s.running[action] = actionStatus{
cpu: cpu,
start: time.Now(),
}
}
func (s *statusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
start, ok := s.running[result.Action]
if !ok {
return
}
delete(s.running, result.Action)
s.cpus[start.cpu] = false
str := result.Action.Description
if len(result.Action.Outputs) > 0 {
str = result.Action.Outputs[0]
}
s.tracer.writeEvent(&viewerEvent{
Name: str,
Phase: "X",
Time: uint64(start.start.UnixNano()) / 1000,
Dur: uint64(time.Since(start.start).Nanoseconds()) / 1000,
Pid: 1,
Tid: uint64(start.cpu),
})
}
func (s *statusOutput) Flush() {}
func (s *statusOutput) Message(level status.MsgLevel, message string) {}

View File

@ -31,6 +31,7 @@ import (
"time"
"android/soong/ui/logger"
"android/soong/ui/status"
)
type Thread uint64
@ -46,7 +47,8 @@ type Tracer interface {
Complete(name string, thread Thread, begin, end uint64)
ImportMicrofactoryLog(filename string)
ImportNinjaLog(thread Thread, filename string, startOffset time.Time)
StatusTracer() status.StatusOutput
NewThread(name string) Thread
}