Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Android/build/build/soong/ui/terminal/   (Android Betriebssystem Version 17©)  Datei vom 26.5.2026 mit Größe 11 kB image not shown  

Quelle  smart_status.go   Sprache: unbekannt

 
Spracherkennung für: .go vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

// Copyright 2019 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package terminal

import (
 "fmt"
 "io"
 "os"
 "os/signal"
 "strconv"
 "strings"
 "sync"
 "syscall"
 "time"

 "android/soong/ui/status"
)

const tableHeightEnVar = "SOONG_UI_TABLE_HEIGHT"

type actionTableEntry struct {
 action    *status.Action
 startTime time.Time
}

type smartStatusOutput struct {
 writer         io.Writer
 formatter      formatter
 suppressOutput bool

 lock sync.Mutex

 haveBlankLine bool

 tableMode             bool
 tableHeight           int
 requestedTableHeight  int
 termWidth, termHeight int

 runningActions  []actionTableEntry
 ticker          *time.Ticker
 done            chan bool
 sigwinch        chan os.Signal
 sigwinchHandled chan bool

 // Once there is a failure, we stop printing command output so the error
 // is easier to find
 haveFailures bool
 // If we are dropping errors, then at the end, we report a message to go
 // look in the verbose log if you want that command output.
 postFailureActionCount int
}

// NewSmartStatusOutput returns a StatusOutput that represents the
// current build status similarly to Ninja's built-in terminal
// output.
func NewSmartStatusOutput(w io.Writer,
 formatter formatter,
 suppressOutput bool) status.StatusOutput {
 s := &smartStatusOutput{
  writer:         w,
  formatter:      formatter,
  suppressOutput: suppressOutput,

  haveBlankLine: true,

  tableMode: true,

  done:     make(chan bool),
  sigwinch: make(chan os.Signal),
 }

 if env, ok := os.LookupEnv(tableHeightEnVar); ok {
  h, _ := strconv.Atoi(env)
  s.tableMode = h > 0
  s.requestedTableHeight = h
 }

 if w, h, ok := termSize(s.writer); ok {
  s.termWidth, s.termHeight = w, h
  s.computeTableHeight()
 } else {
  s.tableMode = false
 }

 if s.tableMode {
  // Add empty lines at the bottom of the screen to scroll back the existing history
  // and make room for the action table.
  // TODO: read the cursor position to see if the empty lines are necessary?
  for i := 0; i < s.tableHeight; i++ {
   fmt.Fprintln(w)
  }

  // Hide the cursor to prevent seeing it bouncing around
  fmt.Fprintf(s.writer, ansi.hideCursor())

  // Configure the empty action table
  s.actionTable()

  // Start a tick to update the action table periodically
  s.startActionTableTick()
 }

 s.startSigwinch()

 return s
}

func (s *smartStatusOutput) Message(level status.MsgLevel, message string) {
 if level < status.StatusLvl {
  return
 }

 str := s.formatter.message(level, message)

 s.lock.Lock()
 defer s.lock.Unlock()

 if level > status.StatusLvl {
  s.print(str)
 } else {
  s.statusLine(str)
 }
}

func (s *smartStatusOutput) StartAction(action *status.Action, counts status.Counts) {
 startTime := time.Now()

 str := action.Description
 if str == "" {
  str = action.Command
 }

 progress := s.formatter.progress(counts)

 s.lock.Lock()
 defer s.lock.Unlock()

 s.runningActions = append(s.runningActions, actionTableEntry{
  action:    action,
  startTime: startTime,
 })

 s.statusLine(progress + str)
}

func (s *smartStatusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
 str := result.Description
 if str == "" {
  str = result.Command
 }

 progress := s.formatter.progress(counts) + str

 output := s.formatter.result(result)

 s.lock.Lock()
 defer s.lock.Unlock()

 for i, runningAction := range s.runningActions {
  if runningAction.action == result.Action {
   s.runningActions = append(s.runningActions[:i], s.runningActions[i+1:]...)
   break
  }
 }

 s.statusLine(progress)

 // Stop printing when there are failures, but don't skip actions that also have their own errors.
 if output != "" {
  if (!s.haveFailures && !s.suppressOutput) || result.Error != nil {
   s.requestLine()
   s.print(output)
  } else {
   s.postFailureActionCount++
  }
 }

 if result.Error != nil {
  s.haveFailures = true
 }
}

func (s *smartStatusOutput) Flush() {
 if s.tableMode {
  // Stop the action table tick outside of the lock to avoid lock ordering issues between s.done and
  // s.lock, the goroutine in startActionTableTick can get blocked on the lock and be unable to read
  // from the channel.
  s.stopActionTableTick()
 }

 s.lock.Lock()
 defer s.lock.Unlock()

 s.stopSigwinch()

 if s.postFailureActionCount > 0 {
  s.requestLine()
  if s.postFailureActionCount == 1 {
   s.print(fmt.Sprintf("There was 1 action that completed after the action that failed. See verbose.log.gz for its output."))
  } else {
   s.print(fmt.Sprintf("There were %d actions that completed after the action that failed. See verbose.log.gz for their output.", s.postFailureActionCount))
  }
 }

 s.requestLine()

 s.runningActions = nil

 if s.tableMode {
  // Update the table after clearing runningActions to clear it
  s.actionTable()

  // Reset the scrolling region to the whole terminal
  fmt.Fprintf(s.writer, ansi.resetScrollingMargins())
  _, height, _ := termSize(s.writer)
  // Move the cursor to the top of the now-blank, previously non-scrolling region
  fmt.Fprintf(s.writer, ansi.setCursor(height-s.tableHeight, 1))
  // Turn the cursor back on
  fmt.Fprintf(s.writer, ansi.showCursor())
 }
}

func (s *smartStatusOutput) Write(p []byte) (int, error) {
 s.lock.Lock()
 defer s.lock.Unlock()
 s.print(string(p))
 return len(p), nil
}

func (s *smartStatusOutput) requestLine() {
 if !s.haveBlankLine {
  fmt.Fprintln(s.writer)
  s.haveBlankLine = true
 }
}

func (s *smartStatusOutput) print(str string) {
 if !s.haveBlankLine {
  fmt.Fprint(s.writer, "\r", ansi.clearToEndOfLine())
  s.haveBlankLine = true
 }
 fmt.Fprint(s.writer, str)
 if len(str) == 0 || str[len(str)-1] != '\n' {
  fmt.Fprint(s.writer, "\n")
 }
}

func (s *smartStatusOutput) statusLine(str string) {
 idx := strings.IndexRune(str, '\n')
 if idx != -1 {
  str = str[0:idx]
 }

 // Limit line width to the terminal width, otherwise we'll wrap onto
 // another line and we won't delete the previous line.
 str = elide(str, s.termWidth)

 // Move to the beginning on the line, turn on bold, print the output,
 // turn off bold, then clear the rest of the line.
 start := "\r" + ansi.bold()
 end := ansi.regular() + ansi.clearToEndOfLine()
 fmt.Fprint(s.writer, start, str, end)
 s.haveBlankLine = false
}

func elide(str string, width int) string {
 if width > 0 && len(str) > width {
  // TODO: Just do a max. Ninja elides the middle, but that's
  // more complicated and these lines aren't that important.
  str = str[:width]
 }

 return str
}

func (s *smartStatusOutput) startActionTableTick() {
 s.ticker = time.NewTicker(time.Second)
 go func() {
  for {
   select {
   case <-s.ticker.C:
    s.lock.Lock()
    s.actionTable()
    s.lock.Unlock()
   case <-s.done:
    return
   }
  }
 }()
}

func (s *smartStatusOutput) stopActionTableTick() {
 s.ticker.Stop()
 s.done <- true
}

func (s *smartStatusOutput) startSigwinch() {
 signal.Notify(s.sigwinch, syscall.SIGWINCH)
 go func() {
  for _ = range s.sigwinch {
   s.lock.Lock()
   s.updateTermSize()
   if s.tableMode {
    s.actionTable()
   }
   s.lock.Unlock()
   if s.sigwinchHandled != nil {
    s.sigwinchHandled <- true
   }
  }
 }()
}

func (s *smartStatusOutput) stopSigwinch() {
 signal.Stop(s.sigwinch)
 close(s.sigwinch)
}

// computeTableHeight recomputes s.tableHeight based on s.termHeight and s.requestedTableHeight.
func (s *smartStatusOutput) computeTableHeight() {
 tableHeight := s.requestedTableHeight
 if tableHeight == 0 {
  tableHeight = s.termHeight / 4
  if tableHeight < 1 {
   tableHeight = 1
  } else if tableHeight > 10 {
   tableHeight = 10
  }
 }
 if tableHeight > s.termHeight-1 {
  tableHeight = s.termHeight - 1
 }
 s.tableHeight = tableHeight
}

// updateTermSize recomputes the table height after a SIGWINCH and pans any existing text if
// necessary.
func (s *smartStatusOutput) updateTermSize() {
 if w, h, ok := termSize(s.writer); ok {
  oldScrollingHeight := s.termHeight - s.tableHeight

  s.termWidth, s.termHeight = w, h

  if s.tableMode {
   s.computeTableHeight()

   scrollingHeight := s.termHeight - s.tableHeight

   // If the scrolling region has changed, attempt to pan the existing text so that it is
   // not overwritten by the table.
   if scrollingHeight < oldScrollingHeight {
    pan := oldScrollingHeight - scrollingHeight
    if pan > s.tableHeight {
     pan = s.tableHeight
    }
    fmt.Fprint(s.writer, ansi.panDown(pan))
   }
  }
 }
}

func (s *smartStatusOutput) actionTable() {
 scrollingHeight := s.termHeight - s.tableHeight

 // Update the scrolling region in case the height of the terminal changed

 fmt.Fprint(s.writer, ansi.setScrollingMargins(1, scrollingHeight))

 // Write as many status lines as fit in the table
 for tableLine := 0; tableLine < s.tableHeight; tableLine++ {
  if tableLine >= s.tableHeight {
   break
  }
  // Move the cursor to the correct line of the non-scrolling region
  fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight+1+tableLine, 1))

  if tableLine < len(s.runningActions) {
   runningAction := s.runningActions[tableLine]

   seconds := int(time.Since(runningAction.startTime).Round(time.Second).Seconds())

   desc := runningAction.action.Description
   if desc == "" {
    desc = runningAction.action.Command
   }

   color := ""
   if seconds >= 60 {
    color = ansi.red() + ansi.bold()
   } else if seconds >= 30 {
    color = ansi.yellow() + ansi.bold()
   }

   durationStr := fmt.Sprintf("   %2d:%02d ", seconds/60, seconds%60)
   desc = elide(desc, s.termWidth-len(durationStr))
   durationStr = color + durationStr + ansi.regular()
   fmt.Fprint(s.writer, durationStr, desc)
  }
  fmt.Fprint(s.writer, ansi.clearToEndOfLine())
 }

 // Move the cursor back to the last line of the scrolling region
 fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight, 1))
}

var ansi = ansiImpl{}

type ansiImpl struct{}

func (ansiImpl) clearToEndOfLine() string {
 return "\x1b[K"
}

func (ansiImpl) setCursor(row, column int) string {
 // Direct cursor address
 return fmt.Sprintf("\x1b[%d;%dH", row, column)
}

func (ansiImpl) setScrollingMargins(top, bottom int) string {
 // Set Top and Bottom Margins DECSTBM
 return fmt.Sprintf("\x1b[%d;%dr", top, bottom)
}

func (ansiImpl) resetScrollingMargins() string {
 // Set Top and Bottom Margins DECSTBM
 return fmt.Sprintf("\x1b[r")
}

func (ansiImpl) red() string {
 return "\x1b[31m"
}

func (ansiImpl) yellow() string {
 return "\x1b[33m"
}

func (ansiImpl) bold() string {
 return "\x1b[1m"
}

func (ansiImpl) regular() string {
 return "\x1b[0m"
}

func (ansiImpl) showCursor() string {
 return "\x1b[?25h"
}

func (ansiImpl) hideCursor() string {
 return "\x1b[?25l"
}

func (ansiImpl) panDown(lines int) string {
 return fmt.Sprintf("\x1b[%dS", lines)
}

func (ansiImpl) panUp(lines int) string {
 return fmt.Sprintf("\x1b[%dT", lines)
}

[Dauer der Verarbeitung: 0.26 Sekunden, vorverarbeitet 2026-06-28]